@synsci/thesis 0.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.
@@ -0,0 +1,441 @@
1
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { load as loadYaml, dump as dumpYaml } from "js-yaml";
4
+
5
+ function stripJsonComments(text) {
6
+ let result = "";
7
+ let i = 0;
8
+ while (i < text.length) {
9
+ if (text[i] === '"') {
10
+ const start = i++;
11
+ while (i < text.length && text[i] !== '"') {
12
+ if (text[i] === "\\") i += 1;
13
+ i += 1;
14
+ }
15
+ result += text.slice(start, ++i);
16
+ } else if (text[i] === "/" && text[i + 1] === "/") {
17
+ i += 2;
18
+ while (i < text.length && text[i] !== "\n") i += 1;
19
+ } else if (text[i] === "/" && text[i + 1] === "*") {
20
+ i += 2;
21
+ while (i < text.length && !(text[i] === "*" && text[i + 1] === "/"))
22
+ i += 1;
23
+ i += 2;
24
+ } else {
25
+ result += text[i];
26
+ i += 1;
27
+ }
28
+ }
29
+ return result;
30
+ }
31
+
32
+ function normalizeLineEndings(text) {
33
+ return text.replace(/\r\n/g, "\n");
34
+ }
35
+
36
+ function stripTomlInlineComment(valueText) {
37
+ let inSingleQuotedString = false;
38
+ let inDoubleQuotedString = false;
39
+ let escapeNextChar = false;
40
+
41
+ for (let index = 0; index < valueText.length; index += 1) {
42
+ const char = valueText[index];
43
+
44
+ if (escapeNextChar) {
45
+ escapeNextChar = false;
46
+ continue;
47
+ }
48
+
49
+ if (inDoubleQuotedString && char === "\\") {
50
+ escapeNextChar = true;
51
+ continue;
52
+ }
53
+
54
+ if (!inDoubleQuotedString && char === "'") {
55
+ inSingleQuotedString = !inSingleQuotedString;
56
+ continue;
57
+ }
58
+
59
+ if (!inSingleQuotedString && char === '"') {
60
+ inDoubleQuotedString = !inDoubleQuotedString;
61
+ continue;
62
+ }
63
+
64
+ if (!inSingleQuotedString && !inDoubleQuotedString && char === "#") {
65
+ return valueText.slice(0, index).trim();
66
+ }
67
+ }
68
+
69
+ return valueText.trim();
70
+ }
71
+
72
+ function parseTomlStringValue(valueText) {
73
+ const stripped = stripTomlInlineComment(valueText);
74
+ if (!stripped) {
75
+ return null;
76
+ }
77
+
78
+ if (
79
+ stripped.startsWith('"""') &&
80
+ stripped.endsWith('"""') &&
81
+ stripped.length >= 6
82
+ ) {
83
+ return stripped.slice(3, -3);
84
+ }
85
+
86
+ if (
87
+ stripped.startsWith("'''") &&
88
+ stripped.endsWith("'''") &&
89
+ stripped.length >= 6
90
+ ) {
91
+ return stripped.slice(3, -3);
92
+ }
93
+
94
+ if (stripped.startsWith('"') && stripped.endsWith('"')) {
95
+ try {
96
+ return JSON.parse(stripped);
97
+ } catch {
98
+ return stripped.slice(1, -1);
99
+ }
100
+ }
101
+
102
+ if (stripped.startsWith("'") && stripped.endsWith("'")) {
103
+ return stripped.slice(1, -1);
104
+ }
105
+
106
+ return stripped;
107
+ }
108
+
109
+ function parseTomlSectionName(lineText) {
110
+ const withoutComment = stripTomlInlineComment(
111
+ String(lineText || "").trim(),
112
+ );
113
+ const sectionMatch = withoutComment.match(/^\[(.+)\]$/);
114
+ return sectionMatch ? sectionMatch[1] : null;
115
+ }
116
+
117
+ // --- JSON config ---
118
+
119
+ export async function readJsonConfig(filePath) {
120
+ let raw;
121
+ try {
122
+ raw = await readFile(filePath, "utf8");
123
+ } catch {
124
+ return {};
125
+ }
126
+ const trimmed = raw.trim();
127
+ if (!trimmed) return {};
128
+ return JSON.parse(stripJsonComments(trimmed));
129
+ }
130
+
131
+ export async function writeJsonConfig(filePath, config) {
132
+ await mkdir(path.dirname(filePath), { recursive: true });
133
+ await writeFile(filePath, `${JSON.stringify(config, null, 2)}\n`, {
134
+ encoding: "utf8",
135
+ mode: 0o600,
136
+ });
137
+ }
138
+
139
+ // --- YAML config ---
140
+
141
+ export async function readYamlConfig(filePath) {
142
+ let raw;
143
+ try {
144
+ raw = await readFile(filePath, "utf8");
145
+ } catch {
146
+ return {};
147
+ }
148
+ const trimmed = raw.trim();
149
+ if (!trimmed) return {};
150
+
151
+ try {
152
+ const parsed = loadYaml(trimmed);
153
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
154
+ return parsed;
155
+ }
156
+ return {};
157
+ } catch {
158
+ return {};
159
+ }
160
+ }
161
+
162
+ export async function writeYamlConfig(filePath, config) {
163
+ await mkdir(path.dirname(filePath), { recursive: true });
164
+ await writeFile(filePath, dumpYaml(config, { noRefs: true }), {
165
+ encoding: "utf8",
166
+ mode: 0o600,
167
+ });
168
+ }
169
+
170
+ // --- Path resolution ---
171
+
172
+ export async function resolveMcpPath(candidates) {
173
+ for (const candidate of candidates) {
174
+ try {
175
+ // eslint-disable-next-line no-await-in-loop
176
+ await access(candidate);
177
+ return candidate;
178
+ } catch {
179
+ // keep searching
180
+ }
181
+ }
182
+ return candidates[0];
183
+ }
184
+
185
+ // --- Config section helpers ---
186
+
187
+ function keyPath(configKey) {
188
+ return Array.isArray(configKey) ? configKey : [configKey];
189
+ }
190
+
191
+ function readConfigSection(config, configKey) {
192
+ const pathParts = keyPath(configKey);
193
+ let current = config && typeof config === "object" ? config : {};
194
+ for (const part of pathParts) {
195
+ if (
196
+ !current ||
197
+ typeof current !== "object" ||
198
+ Array.isArray(current) ||
199
+ !(part in current)
200
+ ) {
201
+ return {};
202
+ }
203
+ const next = current[part];
204
+ if (!next || typeof next !== "object" || Array.isArray(next)) {
205
+ return {};
206
+ }
207
+ current = next;
208
+ }
209
+ return current;
210
+ }
211
+
212
+ export function readNamedConfigEntry(config, configKey, serverName) {
213
+ const section = readConfigSection(config, configKey);
214
+ const entry = section[serverName];
215
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
216
+ return null;
217
+ }
218
+ return entry;
219
+ }
220
+
221
+ function writeConfigSection(config, configKey, section) {
222
+ const pathParts = keyPath(configKey);
223
+ if (pathParts.length === 0) return config || {};
224
+
225
+ const root =
226
+ config && typeof config === "object" && !Array.isArray(config)
227
+ ? { ...config }
228
+ : {};
229
+ let cursor = root;
230
+
231
+ for (let index = 0; index < pathParts.length - 1; index += 1) {
232
+ const part = pathParts[index];
233
+ const existing = cursor[part];
234
+ cursor[part] =
235
+ existing && typeof existing === "object" && !Array.isArray(existing)
236
+ ? { ...existing }
237
+ : {};
238
+ cursor = cursor[part];
239
+ }
240
+ cursor[pathParts[pathParts.length - 1]] = section;
241
+ return root;
242
+ }
243
+
244
+ // --- Merge / Remove for JSON + YAML ---
245
+
246
+ export function mergeServerEntry(existing, configKey, serverName, entry) {
247
+ const section = readConfigSection(existing, configKey);
248
+
249
+ if (serverName in section) {
250
+ return { config: existing, alreadyExists: true };
251
+ }
252
+
253
+ return {
254
+ config: writeConfigSection(existing, configKey, {
255
+ ...section,
256
+ [serverName]: entry,
257
+ }),
258
+ alreadyExists: false,
259
+ };
260
+ }
261
+
262
+ export function removeJsonServerEntry({ config, configKey, serverName }) {
263
+ const section = { ...readConfigSection(config, configKey) };
264
+
265
+ if (!(serverName in section)) {
266
+ return { config: config || {}, changed: false };
267
+ }
268
+
269
+ delete section[serverName];
270
+ return {
271
+ config: writeConfigSection(config, configKey, section),
272
+ changed: true,
273
+ };
274
+ }
275
+
276
+ // --- TOML read helpers ---
277
+
278
+ export async function readTomlServerExists(filePath, serverName) {
279
+ try {
280
+ const raw = await readFile(filePath, "utf8");
281
+ return raw.includes(`[mcp_servers.${serverName}]`);
282
+ } catch {
283
+ return false;
284
+ }
285
+ }
286
+
287
+ export async function readTomlServerEntry(filePath, serverName) {
288
+ let raw = "";
289
+ try {
290
+ raw = await readFile(filePath, "utf8");
291
+ } catch {
292
+ return { exists: false, url: null };
293
+ }
294
+
295
+ const normalized = normalizeLineEndings(raw);
296
+ const lines = normalized.split("\n");
297
+ const targetSection = `mcp_servers.${serverName}`;
298
+ let inTargetSection = false;
299
+ let exists = false;
300
+ let url = null;
301
+
302
+ for (const line of lines) {
303
+ const trimmed = line.trim();
304
+ const sectionName = parseTomlSectionName(trimmed);
305
+
306
+ if (sectionName) {
307
+ if (sectionName === targetSection) {
308
+ exists = true;
309
+ inTargetSection = true;
310
+ continue;
311
+ }
312
+
313
+ if (exists && !sectionName.startsWith(`${targetSection}.`)) {
314
+ break;
315
+ }
316
+
317
+ inTargetSection = false;
318
+ continue;
319
+ }
320
+
321
+ if (!inTargetSection) continue;
322
+
323
+ const withoutComment = stripTomlInlineComment(trimmed);
324
+ const urlMatch = withoutComment.match(/^url\s*=\s*(.+)$/);
325
+ if (!urlMatch) continue;
326
+
327
+ url = parseTomlStringValue(urlMatch[1]);
328
+ }
329
+
330
+ return { exists, url };
331
+ }
332
+
333
+ // --- TOML write helpers ---
334
+
335
+ export function buildTomlServerBlock(serverName, entry) {
336
+ const lines = [`[mcp_servers.${serverName}]`];
337
+ const headers =
338
+ entry && typeof entry.headers === "object" && entry.headers !== null
339
+ ? entry.headers
340
+ : null;
341
+
342
+ for (const [key, value] of Object.entries(entry || {})) {
343
+ if (key === "headers") continue;
344
+ lines.push(`${key} = ${JSON.stringify(value)}`);
345
+ }
346
+
347
+ if (headers && Object.keys(headers).length > 0) {
348
+ lines.push("");
349
+ lines.push(`[mcp_servers.${serverName}.http_headers]`);
350
+ for (const [headerKey, headerValue] of Object.entries(headers)) {
351
+ lines.push(`${headerKey} = ${JSON.stringify(String(headerValue))}`);
352
+ }
353
+ }
354
+
355
+ return `${lines.join("\n")}\n`;
356
+ }
357
+
358
+ export async function appendTomlServer(filePath, serverName, entry) {
359
+ if (await readTomlServerExists(filePath, serverName)) {
360
+ return { alreadyExists: true };
361
+ }
362
+
363
+ const block = buildTomlServerBlock(serverName, entry);
364
+
365
+ let existing = "";
366
+ try {
367
+ existing = await readFile(filePath, "utf8");
368
+ } catch {
369
+ existing = "";
370
+ }
371
+
372
+ const separator =
373
+ existing.length > 0 && !existing.endsWith("\n")
374
+ ? "\n\n"
375
+ : existing.length > 0
376
+ ? "\n"
377
+ : "";
378
+ await mkdir(path.dirname(filePath), { recursive: true });
379
+ await writeFile(filePath, `${existing}${separator}${block}`, {
380
+ encoding: "utf8",
381
+ mode: 0o600,
382
+ });
383
+ return { alreadyExists: false };
384
+ }
385
+
386
+ // --- TOML removal ---
387
+
388
+ function stripCodexTomlServerBlock({ tomlText, serverName }) {
389
+ const normalized = normalizeLineEndings(tomlText || "");
390
+ const lines = normalized.split("\n");
391
+ const output = [];
392
+ let skipping = false;
393
+ let removed = false;
394
+
395
+ for (const line of lines) {
396
+ const trimmed = line.trim();
397
+ const sectionName = parseTomlSectionName(trimmed);
398
+
399
+ if (sectionName) {
400
+ const isTargetSection =
401
+ sectionName === `mcp_servers.${serverName}` ||
402
+ sectionName.startsWith(`mcp_servers.${serverName}.`);
403
+ if (isTargetSection) {
404
+ skipping = true;
405
+ removed = true;
406
+ continue;
407
+ }
408
+ if (skipping) {
409
+ skipping = false;
410
+ }
411
+ }
412
+
413
+ if (!skipping) {
414
+ output.push(line);
415
+ }
416
+ }
417
+
418
+ let stripped = output.join("\n");
419
+ stripped = stripped.replace(/\n{3,}/g, "\n\n").trimEnd();
420
+ if (stripped.length > 0) stripped += "\n";
421
+ return { stripped, removed };
422
+ }
423
+
424
+ export async function removeCodexTomlServer({ filePath, serverName }) {
425
+ let existing = "";
426
+ try {
427
+ existing = await readFile(filePath, "utf8");
428
+ } catch {
429
+ return { changed: false };
430
+ }
431
+
432
+ const { stripped, removed } = stripCodexTomlServerBlock({
433
+ tomlText: existing,
434
+ serverName,
435
+ });
436
+ if (!removed) return { changed: false };
437
+
438
+ await mkdir(path.dirname(filePath), { recursive: true });
439
+ await writeFile(filePath, stripped, { encoding: "utf8", mode: 0o600 });
440
+ return { changed: true };
441
+ }