tdoms-mcp 0.6.1

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,571 @@
1
+ import { execFile } from "node:child_process";
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { promisify } from "node:util";
7
+ import { applyEdits, modify, parse } from "jsonc-parser";
8
+ import { atomicWriteFile, withFileLock } from "./storage-utils.js";
9
+
10
+ const execFileAsync = promisify(execFile);
11
+ const SERVER_NAME = "tdoms";
12
+ const DEFAULT_SERVER_ENTRY = fileURLToPath(new URL("./index.js", import.meta.url));
13
+
14
+ export class ClientManager {
15
+ constructor({
16
+ env = process.env,
17
+ platform = process.platform,
18
+ homedir = os.homedir(),
19
+ nodeCommand = process.execPath,
20
+ serverEntry = DEFAULT_SERVER_ENTRY,
21
+ runner = runExecutable
22
+ } = {}) {
23
+ this.env = env;
24
+ this.platform = platform;
25
+ this.homedir = homedir;
26
+ this.nodeCommand = nodeCommand;
27
+ this.serverEntry = serverEntry;
28
+ this.runner = runner;
29
+ this.runtime = describeRuntime(platform, env);
30
+ }
31
+
32
+ async listClients() {
33
+ const definitions = await this.#definitions();
34
+ return Promise.all(definitions.map(async (definition) => {
35
+ const executable = await firstExistingFile(definition.executableCandidates);
36
+ const detectedByConfig = await anyPathExists(definition.configPaths);
37
+ const configured = await isConfigured(definition);
38
+ const detected = Boolean(executable || detectedByConfig || await anyPathExists(definition.appCandidates));
39
+ return {
40
+ id: definition.id,
41
+ name: definition.name,
42
+ description: definition.description,
43
+ detected,
44
+ configured,
45
+ installable: detected && (definition.strategy === "json" || Boolean(executable)),
46
+ executable,
47
+ configPath: definition.configPaths[0],
48
+ strategy: definition.strategy,
49
+ environment: this.runtime,
50
+ restartRequired: definition.restartRequired,
51
+ status: configured ? "configured" : detected ? "detected" : "not-detected"
52
+ };
53
+ }));
54
+ }
55
+
56
+ async installDetected({ confirm = false } = {}) {
57
+ if (confirm !== true) {
58
+ throw new Error("Refusing to modify MCP client configurations without confirm: true.");
59
+ }
60
+ const clients = await this.listClients();
61
+ const targets = clients.filter((client) => client.detected && client.installable && !client.configured);
62
+ const results = [];
63
+ for (const client of targets) {
64
+ try {
65
+ results.push(await this.installClient(client.id, { confirm: true }));
66
+ } catch (error) {
67
+ results.push({ ok: false, clientId: client.id, clientName: client.name, error: error.message });
68
+ }
69
+ }
70
+ return {
71
+ ok: results.every((result) => result.ok),
72
+ environment: this.runtime,
73
+ configured: results.filter((result) => result.ok).length,
74
+ failed: results.filter((result) => !result.ok).length,
75
+ results
76
+ };
77
+ }
78
+
79
+ async installClient(id, { confirm = false } = {}) {
80
+ if (confirm !== true) {
81
+ throw new Error("Refusing to modify an MCP client configuration without confirm: true.");
82
+ }
83
+
84
+ const definition = (await this.#definitions()).find((item) => item.id === id);
85
+ if (!definition) throw new Error(`Unsupported MCP client: ${id}`);
86
+ const executable = await firstExistingFile(definition.executableCandidates);
87
+ const detected = Boolean(executable || await anyPathExists(definition.configPaths) || await anyPathExists(definition.appCandidates));
88
+ if (!detected) {
89
+ throw new Error(`${definition.name} is not installed in ${this.runtime.label}.`);
90
+ }
91
+ if (definition.strategy !== "json" && !executable) {
92
+ throw new Error(`${definition.name} was not detected with an installable CLI.`);
93
+ }
94
+
95
+ const snapshot = await snapshotFiles(definition.configPaths);
96
+ const backupPaths = await writeBackups(snapshot);
97
+ try {
98
+ if (definition.strategy === "json") {
99
+ await installJsonClient(
100
+ definition.configPaths[0],
101
+ definition.container,
102
+ formatServerDefinition(this.#serverDefinition(), definition.format),
103
+ definition.seedConfigPaths
104
+ );
105
+ } else {
106
+ await this.#installWithCli(definition, executable, await isConfigured(definition));
107
+ }
108
+ if (!await isConfigured(definition)) {
109
+ throw new Error("The client did not persist the TD/OMS MCP server.");
110
+ }
111
+ } catch (error) {
112
+ await restoreSnapshot(snapshot);
113
+ throw new Error(`${definition.name} installation failed and its configuration was restored: ${error.message}`);
114
+ }
115
+
116
+ return {
117
+ ok: true,
118
+ clientId: definition.id,
119
+ clientName: definition.name,
120
+ backupPaths,
121
+ restartRequired: definition.restartRequired,
122
+ configured: true
123
+ };
124
+ }
125
+
126
+ #serverDefinition() {
127
+ return {
128
+ type: "stdio",
129
+ command: this.nodeCommand,
130
+ args: [this.serverEntry, "mcp"],
131
+ env: { TDOMS_MCP_PROFILE: "core" }
132
+ };
133
+ }
134
+
135
+ async #installWithCli(definition, executable, configured) {
136
+ const server = this.#serverDefinition();
137
+ if (definition.strategy === "codex") {
138
+ if (configured) await this.runner(executable, ["mcp", "remove", SERVER_NAME]);
139
+ await this.runner(executable, ["mcp", "add", "--env", "TDOMS_MCP_PROFILE=core", SERVER_NAME, "--", server.command, ...server.args]);
140
+ return;
141
+ }
142
+
143
+ if (definition.strategy === "claude") {
144
+ if (configured) await this.runner(executable, ["mcp", "remove", "--scope", "user", SERVER_NAME]);
145
+ await this.runner(executable, ["mcp", "add", "--scope", "user", SERVER_NAME, "--env", "TDOMS_MCP_PROFILE=core", "--", server.command, ...server.args]);
146
+ return;
147
+ }
148
+
149
+ const payload = JSON.stringify({ name: SERVER_NAME, type: "stdio", ...server });
150
+ await this.runner(executable, ["--add-mcp", payload]);
151
+ }
152
+
153
+ async #definitions() {
154
+ const paths = platformPaths(this.platform, this.env, this.homedir);
155
+ const vscodeExtensions = await extensionDirectories(paths.vscodeExtensions);
156
+ const windowsPackages = await extensionDirectories(paths.windowsPackages);
157
+ const claudeDesktopCodeVersions = await extensionDirectories(paths.claudeDesktopCodeRoot);
158
+ const codexExtension = newestMatching(vscodeExtensions, "openai.chatgpt-");
159
+ const claudeExtension = newestMatching(vscodeExtensions, "anthropic.claude-code-");
160
+ const claudeStorePackage = newestMatching(windowsPackages, "Claude_");
161
+ const claudeDesktopCodeVersion = newestMatching(claudeDesktopCodeVersions, "");
162
+ const claudeDesktopConfig = claudeStorePackage
163
+ ? path.join(claudeStorePackage, "LocalCache", "Roaming", "Claude", "claude_desktop_config.json")
164
+ : paths.claudeDesktopConfig;
165
+ const commandCandidates = async (name, fixed = []) => compact([...fixed, await findOnPath(name, this.platform, this.env)]);
166
+
167
+ return [
168
+ {
169
+ id: "ibm-bob",
170
+ name: "IBM Bob",
171
+ description: "IBM Bob global MCP configuration",
172
+ strategy: "json",
173
+ executableCandidates: await commandCandidates("bobide"),
174
+ appCandidates: compact([paths.bobApp]),
175
+ configPaths: [paths.bobMcp],
176
+ seedConfigPaths: compact([paths.bobLegacyMcp, paths.bobExtensionLegacyMcp]),
177
+ container: "mcpServers",
178
+ format: "mcp",
179
+ containers: ["mcpServers"],
180
+ restartRequired: false
181
+ },
182
+ {
183
+ id: "codex",
184
+ name: "Codex",
185
+ description: "Codex app and CLI",
186
+ strategy: "codex",
187
+ executableCandidates: compact([
188
+ codexExtension && path.join(codexExtension, "bin", platformBinaryDir(this.platform), executableName("codex", this.platform)),
189
+ await findOnPath("codex", this.platform, this.env)
190
+ ]),
191
+ appCandidates: [paths.codexHome],
192
+ configPaths: [paths.codexConfig],
193
+ tomlTable: "mcp_servers.tdoms",
194
+ restartRequired: true
195
+ },
196
+ {
197
+ id: "claude-code",
198
+ name: "Claude Code sessions",
199
+ description: "Code sessions in Claude Desktop and the VS Code extension",
200
+ strategy: "claude",
201
+ executableCandidates: compact([
202
+ claudeDesktopCodeVersion && path.join(claudeDesktopCodeVersion, executableName("claude", this.platform)),
203
+ claudeExtension && path.join(claudeExtension, "resources", "native-binary", executableName("claude", this.platform)),
204
+ await findOnPath("claude", this.platform, this.env)
205
+ ]),
206
+ appCandidates: compact([claudeExtension]),
207
+ configPaths: [paths.claudeCodeConfig],
208
+ containers: ["mcpServers"],
209
+ restartRequired: true
210
+ },
211
+ {
212
+ id: "vscode",
213
+ name: "Visual Studio Code",
214
+ description: "VS Code user profile",
215
+ strategy: "json",
216
+ executableCandidates: await commandCandidates("code", compact([paths.vscodeExecutable])),
217
+ appCandidates: compact([paths.vscodeApp]),
218
+ configPaths: [paths.vscodeMcp],
219
+ container: "servers",
220
+ format: "vscode",
221
+ containers: ["servers"],
222
+ restartRequired: true
223
+ },
224
+ {
225
+ id: "claude-desktop",
226
+ name: "Claude Desktop chat",
227
+ description: "Desktop chat MCP configuration; Code sessions use the separate row above",
228
+ strategy: "json",
229
+ executableCandidates: [],
230
+ appCandidates: compact([paths.claudeDesktopApp, claudeStorePackage]),
231
+ configPaths: [claudeDesktopConfig],
232
+ container: "mcpServers",
233
+ format: "mcp",
234
+ containers: ["mcpServers"],
235
+ restartRequired: true
236
+ },
237
+ {
238
+ id: "cursor",
239
+ name: "Cursor",
240
+ description: "Cursor global MCP configuration",
241
+ strategy: "json",
242
+ executableCandidates: await commandCandidates("cursor"),
243
+ appCandidates: compact([paths.cursorApp]),
244
+ configPaths: [paths.cursorMcp],
245
+ container: "mcpServers",
246
+ format: "mcp",
247
+ containers: ["mcpServers"],
248
+ restartRequired: true
249
+ },
250
+ {
251
+ id: "windsurf",
252
+ name: "Windsurf",
253
+ description: "Windsurf global MCP configuration",
254
+ strategy: "json",
255
+ executableCandidates: await commandCandidates("windsurf"),
256
+ appCandidates: compact([paths.windsurfApp]),
257
+ configPaths: [paths.windsurfMcp],
258
+ container: "mcpServers",
259
+ format: "mcp",
260
+ containers: ["mcpServers"],
261
+ restartRequired: true
262
+ },
263
+ {
264
+ id: "kiro",
265
+ name: "Kiro",
266
+ description: "Kiro IDE and CLI user configuration",
267
+ strategy: "json",
268
+ executableCandidates: await commandCandidates("kiro", compact([paths.kiroExecutable])),
269
+ appCandidates: compact([paths.kiroApp, paths.kiroHome]),
270
+ configPaths: [paths.kiroMcp],
271
+ container: "mcpServers",
272
+ format: "mcp",
273
+ containers: ["mcpServers"],
274
+ restartRequired: false
275
+ },
276
+ {
277
+ id: "gemini-cli",
278
+ name: "Gemini CLI",
279
+ description: "Gemini CLI user settings",
280
+ strategy: "json",
281
+ executableCandidates: await commandCandidates("gemini"),
282
+ appCandidates: compact([paths.geminiHome]),
283
+ configPaths: [paths.geminiConfig],
284
+ container: "mcpServers",
285
+ format: "mcp",
286
+ containers: ["mcpServers"],
287
+ restartRequired: true
288
+ },
289
+ {
290
+ id: "zed",
291
+ name: "Zed",
292
+ description: "Zed Agent user settings",
293
+ strategy: "json",
294
+ executableCandidates: await commandCandidates("zed"),
295
+ appCandidates: compact([paths.zedApp, paths.zedHome]),
296
+ configPaths: [paths.zedConfig],
297
+ container: "context_servers",
298
+ format: "zed",
299
+ containers: ["context_servers"],
300
+ restartRequired: true
301
+ },
302
+ {
303
+ id: "opencode",
304
+ name: "OpenCode",
305
+ description: "OpenCode global configuration",
306
+ strategy: "json",
307
+ executableCandidates: await commandCandidates("opencode"),
308
+ appCandidates: compact([paths.openCodeHome]),
309
+ configPaths: [paths.openCodeConfig],
310
+ container: "mcp",
311
+ format: "opencode",
312
+ containers: ["mcp"],
313
+ restartRequired: true
314
+ }
315
+ ];
316
+ }
317
+ }
318
+
319
+ export async function runExecutable(executable, args) {
320
+ const { stdout, stderr } = await execFileAsync(executable, args, {
321
+ windowsHide: true,
322
+ shell: process.platform === "win32" && /\.(?:bat|cmd)$/i.test(executable),
323
+ timeout: 30000,
324
+ maxBuffer: 1024 * 1024
325
+ });
326
+ return { stdout: stdout.trim(), stderr: stderr.trim() };
327
+ }
328
+
329
+ async function installJsonClient(configPath, container, server, seedConfigPaths = []) {
330
+ await withFileLock(configPath, async () => {
331
+ let existing = await readText(configPath, "");
332
+ if (!existing.trim()) {
333
+ for (const seedPath of seedConfigPaths) {
334
+ existing = await readText(seedPath, "");
335
+ if (existing.trim()) break;
336
+ }
337
+ }
338
+ const original = existing.trim() ? existing : "{}";
339
+ const errors = [];
340
+ parse(original, errors, { allowTrailingComma: true, disallowComments: false });
341
+ if (errors.length) throw new Error(`Cannot safely parse ${configPath}; no changes were made.`);
342
+ const edits = modify(original, [container, SERVER_NAME], server, {
343
+ formattingOptions: { insertSpaces: true, tabSize: 2, eol: "\n" }
344
+ });
345
+ await atomicWriteFile(configPath, `${applyEdits(original, edits).trimEnd()}\n`);
346
+ });
347
+ }
348
+
349
+ function formatServerDefinition(server, format = "mcp") {
350
+ if (format === "vscode") return server;
351
+ if (format === "opencode") {
352
+ return {
353
+ type: "local",
354
+ command: [server.command, ...server.args],
355
+ enabled: true,
356
+ environment: server.env
357
+ };
358
+ }
359
+ const { type: _type, ...standard } = server;
360
+ return standard;
361
+ }
362
+
363
+ async function isConfigured(definition) {
364
+ if (definition.tomlTable) {
365
+ const text = await readText(definition.configPaths[0], "");
366
+ const escaped = definition.tomlTable.replaceAll(".", "\\.");
367
+ return new RegExp(`^\\s*\\[${escaped}\\]\\s*$`, "m").test(text);
368
+ }
369
+
370
+ for (const configPath of definition.configPaths) {
371
+ const text = await readText(configPath, "");
372
+ if (!text.trim()) continue;
373
+ const errors = [];
374
+ const parsed = parse(text, errors, { allowTrailingComma: true, disallowComments: false });
375
+ if (errors.length || !parsed) continue;
376
+ for (const container of definition.containers || []) {
377
+ if (parsed[container]?.[SERVER_NAME]) return true;
378
+ }
379
+ }
380
+ return false;
381
+ }
382
+
383
+ async function snapshotFiles(paths) {
384
+ return Promise.all(paths.map(async (filePath) => ({
385
+ filePath,
386
+ existed: await pathExists(filePath),
387
+ content: await readText(filePath, "")
388
+ })));
389
+ }
390
+
391
+ async function writeBackups(snapshot) {
392
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
393
+ const backups = [];
394
+ for (const file of snapshot) {
395
+ if (!file.existed) continue;
396
+ const backupPath = `${file.filePath}.tdoms-backup-${stamp}`;
397
+ await atomicWriteFile(backupPath, file.content);
398
+ backups.push(backupPath);
399
+ }
400
+ return backups;
401
+ }
402
+
403
+ async function restoreSnapshot(snapshot) {
404
+ for (const file of snapshot) {
405
+ if (file.existed) await atomicWriteFile(file.filePath, file.content);
406
+ else await fs.rm(file.filePath, { force: true }).catch(() => {});
407
+ }
408
+ }
409
+
410
+ async function firstExistingFile(paths) {
411
+ for (const candidate of paths || []) {
412
+ if (!candidate) continue;
413
+ try {
414
+ if ((await fs.stat(candidate)).isFile()) return candidate;
415
+ } catch (error) {
416
+ if (error.code !== "ENOENT") continue;
417
+ }
418
+ }
419
+ return undefined;
420
+ }
421
+
422
+ async function anyPathExists(paths) {
423
+ for (const candidate of paths || []) if (candidate && await pathExists(candidate)) return true;
424
+ return false;
425
+ }
426
+
427
+ async function pathExists(candidate) {
428
+ try {
429
+ await fs.access(candidate);
430
+ return true;
431
+ } catch {
432
+ return false;
433
+ }
434
+ }
435
+
436
+ async function readText(filePath, fallback) {
437
+ try {
438
+ return await fs.readFile(filePath, "utf8");
439
+ } catch (error) {
440
+ if (error.code === "ENOENT") return fallback;
441
+ throw error;
442
+ }
443
+ }
444
+
445
+ async function extensionDirectories(root) {
446
+ if (!root) return [];
447
+ try {
448
+ return (await fs.readdir(root, { withFileTypes: true }))
449
+ .filter((entry) => entry.isDirectory())
450
+ .map((entry) => path.join(root, entry.name));
451
+ } catch {
452
+ return [];
453
+ }
454
+ }
455
+
456
+ function newestMatching(directories, prefix) {
457
+ return directories.filter((directory) => path.basename(directory).startsWith(prefix)).sort().at(-1);
458
+ }
459
+
460
+ async function findOnPath(name, platform, env) {
461
+ const extensions = platform === "win32" ? [".exe", ".cmd", ".bat", ""] : [""];
462
+ for (const directory of String(env.PATH || "").split(path.delimiter)) {
463
+ if (!directory) continue;
464
+ for (const extension of extensions) {
465
+ const candidate = path.join(directory.replace(/^"|"$/g, ""), `${name}${extension}`);
466
+ if (await pathExists(candidate)) return candidate;
467
+ }
468
+ }
469
+ return undefined;
470
+ }
471
+
472
+ function platformPaths(platform, env, home) {
473
+ if (platform === "win32") {
474
+ const appData = env.APPDATA || path.join(home, "AppData", "Roaming");
475
+ const local = env.LOCALAPPDATA || path.join(home, "AppData", "Local");
476
+ return {
477
+ vscodeExtensions: path.join(home, ".vscode", "extensions"),
478
+ windowsPackages: path.join(local, "Packages"),
479
+ codexHome: env.CODEX_HOME || path.join(home, ".codex"),
480
+ codexConfig: path.join(env.CODEX_HOME || path.join(home, ".codex"), "config.toml"),
481
+ claudeCodeConfig: path.join(home, ".claude.json"),
482
+ claudeDesktopCodeRoot: path.join(appData, "Claude", "claude-code"),
483
+ bobExecutable: path.join(local, "Programs", "IBM Bob", "IBM Bob.exe"),
484
+ bobApp: path.join(local, "Programs", "IBM Bob"),
485
+ bobMcp: path.join(home, ".bob", "settings", "mcp.json"),
486
+ bobLegacyMcp: path.join(home, ".bob", "settings", "mcp_settings.json"),
487
+ bobExtensionLegacyMcp: path.join(appData, "IBM Bob", "User", "globalStorage", "ibm.bob-code", "settings", "mcp_settings.json"),
488
+ vscodeExecutable: path.join(local, "Programs", "Microsoft VS Code", "Code.exe"),
489
+ vscodeApp: path.join(local, "Programs", "Microsoft VS Code"),
490
+ vscodeMcp: path.join(appData, "Code", "User", "mcp.json"),
491
+ claudeDesktopApp: path.join(local, "Programs", "Claude"),
492
+ claudeDesktopConfig: path.join(appData, "Claude", "claude_desktop_config.json"),
493
+ cursorApp: path.join(local, "Programs", "cursor"),
494
+ cursorMcp: path.join(home, ".cursor", "mcp.json"),
495
+ windsurfApp: path.join(local, "Programs", "Windsurf"),
496
+ windsurfMcp: path.join(home, ".codeium", "windsurf", "mcp_config.json"),
497
+ kiroExecutable: path.join(local, "Programs", "Kiro", "Kiro.exe"),
498
+ kiroApp: path.join(local, "Programs", "Kiro"),
499
+ kiroHome: path.join(home, ".kiro"),
500
+ kiroMcp: path.join(home, ".kiro", "settings", "mcp.json"),
501
+ geminiHome: path.join(home, ".gemini"),
502
+ geminiConfig: path.join(home, ".gemini", "settings.json"),
503
+ zedApp: undefined,
504
+ zedHome: path.join(appData, "Zed"),
505
+ zedConfig: path.join(appData, "Zed", "settings.json"),
506
+ openCodeHome: path.join(home, ".config", "opencode"),
507
+ openCodeConfig: path.join(home, ".config", "opencode", "opencode.json")
508
+ };
509
+ }
510
+
511
+ const config = env.XDG_CONFIG_HOME || path.join(home, ".config");
512
+ const applications = platform === "darwin" ? "/Applications" : undefined;
513
+ const bobUserData = platform === "darwin" ? path.join(home, "Library", "Application Support", "IBM Bob") : path.join(config, "IBM Bob");
514
+ return {
515
+ vscodeExtensions: path.join(home, ".vscode", "extensions"),
516
+ windowsPackages: undefined,
517
+ codexHome: env.CODEX_HOME || path.join(home, ".codex"),
518
+ codexConfig: path.join(env.CODEX_HOME || path.join(home, ".codex"), "config.toml"),
519
+ claudeCodeConfig: path.join(home, ".claude.json"),
520
+ claudeDesktopCodeRoot: undefined,
521
+ bobExecutable: undefined,
522
+ bobApp: applications && path.join(applications, "IBM Bob.app"),
523
+ bobMcp: path.join(home, ".bob", "settings", "mcp.json"),
524
+ bobLegacyMcp: path.join(home, ".bob", "settings", "mcp_settings.json"),
525
+ bobExtensionLegacyMcp: path.join(bobUserData, "User", "globalStorage", "ibm.bob-code", "settings", "mcp_settings.json"),
526
+ vscodeExecutable: undefined,
527
+ vscodeApp: applications && path.join(applications, "Visual Studio Code.app"),
528
+ vscodeMcp: platform === "darwin" ? path.join(home, "Library", "Application Support", "Code", "User", "mcp.json") : path.join(config, "Code", "User", "mcp.json"),
529
+ claudeDesktopApp: applications && path.join(applications, "Claude.app"),
530
+ claudeDesktopConfig: platform === "darwin" ? path.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json") : path.join(config, "Claude", "claude_desktop_config.json"),
531
+ cursorApp: applications && path.join(applications, "Cursor.app"),
532
+ cursorMcp: path.join(home, ".cursor", "mcp.json"),
533
+ windsurfApp: applications && path.join(applications, "Windsurf.app"),
534
+ windsurfMcp: path.join(home, ".codeium", "windsurf", "mcp_config.json"),
535
+ kiroExecutable: undefined,
536
+ kiroApp: applications && path.join(applications, "Kiro.app"),
537
+ kiroHome: path.join(home, ".kiro"),
538
+ kiroMcp: path.join(home, ".kiro", "settings", "mcp.json"),
539
+ geminiHome: path.join(home, ".gemini"),
540
+ geminiConfig: path.join(home, ".gemini", "settings.json"),
541
+ zedApp: applications && path.join(applications, "Zed.app"),
542
+ zedHome: platform === "darwin" ? path.join(home, "Library", "Application Support", "Zed") : path.join(config, "zed"),
543
+ zedConfig: platform === "darwin" ? path.join(home, "Library", "Application Support", "Zed", "settings.json") : path.join(config, "zed", "settings.json"),
544
+ openCodeHome: path.join(config, "opencode"),
545
+ openCodeConfig: path.join(config, "opencode", "opencode.json")
546
+ };
547
+ }
548
+
549
+ function describeRuntime(platform, env) {
550
+ const wsl = platform === "linux" && Boolean(env.WSL_DISTRO_NAME || env.WSL_INTEROP);
551
+ return {
552
+ kind: wsl ? "wsl" : platform === "win32" ? "windows" : platform === "darwin" ? "macos" : "linux",
553
+ label: wsl ? `WSL${env.WSL_DISTRO_NAME ? ` (${env.WSL_DISTRO_NAME})` : ""}` : platform === "win32" ? "Windows" : platform === "darwin" ? "macOS" : "Linux",
554
+ isolated: wsl,
555
+ note: wsl ? "Configures agents installed inside this WSL distribution, not Windows applications." : "Configures agents installed in this operating-system environment."
556
+ };
557
+ }
558
+
559
+ function platformBinaryDir(platform) {
560
+ if (platform === "win32") return "windows-x86_64";
561
+ if (platform === "darwin") return process.arch === "arm64" ? "aarch64-apple-darwin" : "x86_64-apple-darwin";
562
+ return process.arch === "arm64" ? "aarch64-unknown-linux-musl" : "x86_64-unknown-linux-musl";
563
+ }
564
+
565
+ function executableName(name, platform) {
566
+ return platform === "win32" ? `${name}.exe` : name;
567
+ }
568
+
569
+ function compact(values) {
570
+ return values.filter(Boolean);
571
+ }
package/src/config.js ADDED
@@ -0,0 +1,49 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+
4
+ export const DEFAULT_HOST = "127.0.0.1";
5
+ export const DEFAULT_PORT = Number.parseInt(process.env.TDOMS_MCP_UI_PORT || "3737", 10);
6
+
7
+ export function dataDir({ env = process.env, platform = process.platform, homedir = os.homedir() } = {}) {
8
+ if (env.TDOMS_MCP_DATA_DIR) {
9
+ return path.resolve(env.TDOMS_MCP_DATA_DIR);
10
+ }
11
+
12
+ const platformPath = platform === "win32" ? path.win32 : path.posix;
13
+
14
+ if (platform === "win32" && env.APPDATA) {
15
+ return platformPath.join(env.APPDATA, "tdoms-mcp");
16
+ }
17
+
18
+ if (platform === "darwin") {
19
+ return platformPath.join(homedir, "Library", "Application Support", "tdoms-mcp");
20
+ }
21
+
22
+ return platformPath.join(env.XDG_CONFIG_HOME || platformPath.join(homedir, ".config"), "tdoms-mcp");
23
+ }
24
+
25
+ export function redactConnection(connection) {
26
+ if (!connection) {
27
+ return connection;
28
+ }
29
+
30
+ const { token, password, ...safeConnection } = connection;
31
+ return safeConnection;
32
+ }
33
+
34
+ export function normalizeBaseUrl({ protocol, host, port, library }) {
35
+ const cleanProtocol = protocol || "https";
36
+ const cleanHost = String(host || "").trim();
37
+ const cleanPort = String(port || "").trim();
38
+ const cleanLibrary = String(library || "OMS").trim().replace(/^\/+|\/+$/g, "");
39
+
40
+ if (!cleanHost) {
41
+ throw new Error("TD/OMS host is required.");
42
+ }
43
+
44
+ if (!cleanPort) {
45
+ throw new Error("TD/OMS REST port is required.");
46
+ }
47
+
48
+ return `${cleanProtocol}://${cleanHost}:${cleanPort}/${cleanLibrary}`;
49
+ }