@proofofwork-agency/toolpin 0.2.3
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/CONTRIBUTING.md +117 -0
- package/LICENSE +183 -0
- package/README.md +323 -0
- package/SECURITY.md +61 -0
- package/action.yml +134 -0
- package/dist/canonicalJson.js +38 -0
- package/dist/capabilities.js +139 -0
- package/dist/ci.js +26 -0
- package/dist/cli.js +1843 -0
- package/dist/clientSupport.js +76 -0
- package/dist/codexToml.js +213 -0
- package/dist/config.js +337 -0
- package/dist/constants.js +3 -0
- package/dist/continueYaml.js +76 -0
- package/dist/doctor.js +163 -0
- package/dist/install.js +191 -0
- package/dist/installed.js +405 -0
- package/dist/integrity.js +14 -0
- package/dist/inventory.js +169 -0
- package/dist/packageIntegrity.js +153 -0
- package/dist/plan.js +595 -0
- package/dist/policy.js +310 -0
- package/dist/registry.js +1610 -0
- package/dist/runtimeAdvisory.js +80 -0
- package/dist/safeFetch.js +157 -0
- package/dist/sarif.js +162 -0
- package/dist/scan.js +113 -0
- package/dist/search.js +44 -0
- package/dist/secrets.js +165 -0
- package/dist/signing.js +146 -0
- package/dist/tester.js +240 -0
- package/dist/trust.js +528 -0
- package/dist/tui/app.js +1731 -0
- package/dist/tui/command.js +50 -0
- package/dist/tui/configSnippet.js +11 -0
- package/dist/tui/constants.js +37 -0
- package/dist/tui/format.js +31 -0
- package/dist/tui/installedState.js +23 -0
- package/dist/tui/layout.js +65 -0
- package/dist/tui/selectors.js +282 -0
- package/dist/tui/types.js +1 -0
- package/dist/tui/ui/trust.js +77 -0
- package/dist/tui/views/installed.js +82 -0
- package/dist/tui/views/panels.js +637 -0
- package/dist/tui.js +12 -0
- package/dist/types.js +1 -0
- package/dist/verificationTrust.js +103 -0
- package/dist/verify.js +537 -0
- package/dist/version.js +1 -0
- package/dist/versions.js +127 -0
- package/docs/assets/readme/terminal-demo.svg +174 -0
- package/docs/assets/readme/tui-browse-overview.jpg +0 -0
- package/docs/assets/readme/tui-config-preview.jpg +0 -0
- package/docs/assets/readme/tui-help.jpg +0 -0
- package/docs/assets/readme/tui-installed-inventory.jpg +0 -0
- package/docs/how-to/catch-drift-in-ci.md +189 -0
- package/docs/how-to/custom-registries.md +156 -0
- package/docs/how-to/toolpin-curated-registry.md +153 -0
- package/package.json +76 -0
- package/registry/README.md +92 -0
- package/registry/v0/servers +115 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { parseDocument, stringify } from "yaml";
|
|
2
|
+
const DEFAULT_CONTINUE_CONFIG = {
|
|
3
|
+
name: "ToolPin Config",
|
|
4
|
+
version: "1.0.0",
|
|
5
|
+
schema: "v1",
|
|
6
|
+
mcpServers: [],
|
|
7
|
+
};
|
|
8
|
+
export function continueYamlFromClientConfig(config) {
|
|
9
|
+
return `${stringify(normalizeContinueConfig(config))}`;
|
|
10
|
+
}
|
|
11
|
+
export function mergeContinueYaml(existing, incoming) {
|
|
12
|
+
const root = normalizeContinueConfig(parseYamlObject(existing));
|
|
13
|
+
const incomingRoot = normalizeContinueConfig(incoming);
|
|
14
|
+
const nextServers = mergeServers(root.mcpServers, incomingRoot.mcpServers);
|
|
15
|
+
return continueYamlFromClientConfig({ ...root, mcpServers: nextServers });
|
|
16
|
+
}
|
|
17
|
+
export function removeContinueServerYaml(existing, serverName) {
|
|
18
|
+
const parsed = parseYamlObject(existing);
|
|
19
|
+
if (!parsed)
|
|
20
|
+
return existing;
|
|
21
|
+
const root = normalizeContinueConfig(parsed);
|
|
22
|
+
const nextServers = root.mcpServers.filter((server) => asRecord(server).name !== serverName);
|
|
23
|
+
if (nextServers.length === root.mcpServers.length)
|
|
24
|
+
return existing;
|
|
25
|
+
return continueYamlFromClientConfig({ ...root, mcpServers: nextServers });
|
|
26
|
+
}
|
|
27
|
+
export function readContinueServerConfig(raw, serverName) {
|
|
28
|
+
const parsed = parseYamlObject(raw);
|
|
29
|
+
if (!parsed)
|
|
30
|
+
return undefined;
|
|
31
|
+
const root = normalizeContinueConfig(parsed);
|
|
32
|
+
return root.mcpServers.find((server) => asRecord(server).name === serverName);
|
|
33
|
+
}
|
|
34
|
+
function mergeServers(existing, incoming) {
|
|
35
|
+
const next = [...existing];
|
|
36
|
+
for (const server of incoming) {
|
|
37
|
+
const name = asRecord(server).name;
|
|
38
|
+
if (typeof name !== "string")
|
|
39
|
+
continue;
|
|
40
|
+
const index = next.findIndex((candidate) => asRecord(candidate).name === name);
|
|
41
|
+
if (index >= 0) {
|
|
42
|
+
next[index] = server;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
next.push(server);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return next;
|
|
49
|
+
}
|
|
50
|
+
function normalizeContinueConfig(config) {
|
|
51
|
+
const root = asRecord(config);
|
|
52
|
+
const mcpServers = Array.isArray(root.mcpServers) ? root.mcpServers : [];
|
|
53
|
+
return {
|
|
54
|
+
...root,
|
|
55
|
+
name: typeof root.name === "string" ? root.name : DEFAULT_CONTINUE_CONFIG.name,
|
|
56
|
+
version: typeof root.version === "string" ? root.version : DEFAULT_CONTINUE_CONFIG.version,
|
|
57
|
+
schema: typeof root.schema === "string" ? root.schema : DEFAULT_CONTINUE_CONFIG.schema,
|
|
58
|
+
mcpServers,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function parseYamlObject(raw) {
|
|
62
|
+
if (!raw.trim())
|
|
63
|
+
return undefined;
|
|
64
|
+
const document = parseDocument(raw);
|
|
65
|
+
if (document.errors.length) {
|
|
66
|
+
throw new Error(document.errors.map((error) => error.message).join("; "));
|
|
67
|
+
}
|
|
68
|
+
const parsed = document.toJS();
|
|
69
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
70
|
+
}
|
|
71
|
+
function asRecord(value) {
|
|
72
|
+
return isRecord(value) ? value : {};
|
|
73
|
+
}
|
|
74
|
+
function isRecord(value) {
|
|
75
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
76
|
+
}
|
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { canonicalJson } from "./canonicalJson.js";
|
|
3
|
+
import { readCodexServerConfig } from "./codexToml.js";
|
|
4
|
+
import { clientConfigRootKey } from "./config.js";
|
|
5
|
+
import { readContinueServerConfig } from "./continueYaml.js";
|
|
6
|
+
import { resolveConfigTarget } from "./install.js";
|
|
7
|
+
import { readLockfile } from "./plan.js";
|
|
8
|
+
export async function doctorLockfile(lockfilePath = "mcp-lock.json", scope = "all") {
|
|
9
|
+
const lockfile = await readLockfile(lockfilePath);
|
|
10
|
+
const issues = [];
|
|
11
|
+
const entries = Object.entries(lockfile.servers);
|
|
12
|
+
for (const [key, plan] of entries) {
|
|
13
|
+
const expected = expectedServerConfig(plan);
|
|
14
|
+
if (!expected) {
|
|
15
|
+
issues.push({
|
|
16
|
+
key,
|
|
17
|
+
kind: "invalid",
|
|
18
|
+
client: plan.client,
|
|
19
|
+
serverName: plan.name,
|
|
20
|
+
file: "",
|
|
21
|
+
message: "locked plan does not contain a comparable client config entry",
|
|
22
|
+
});
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const missing = [];
|
|
26
|
+
const invalidScopes = [];
|
|
27
|
+
let foundConfig = false;
|
|
28
|
+
let foundUnreadable = false;
|
|
29
|
+
for (const currentScope of scopesToCheck(scope, plan)) {
|
|
30
|
+
let target;
|
|
31
|
+
try {
|
|
32
|
+
target = resolveConfigTarget(plan.client, currentScope);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
invalidScopes.push(`${currentScope}: ${error instanceof Error ? error.message : String(error)}`);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const actual = await readInstalledServerConfig(target.file, plan.name, plan.client);
|
|
39
|
+
if (actual.kind === "missing") {
|
|
40
|
+
missing.push({ scope: currentScope, file: target.file });
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (actual.kind === "unreadable") {
|
|
44
|
+
foundUnreadable = true;
|
|
45
|
+
issues.push({
|
|
46
|
+
key,
|
|
47
|
+
kind: "unreadable",
|
|
48
|
+
client: plan.client,
|
|
49
|
+
serverName: plan.name,
|
|
50
|
+
file: target.file,
|
|
51
|
+
scope: currentScope,
|
|
52
|
+
message: actual.message,
|
|
53
|
+
});
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
foundConfig = true;
|
|
57
|
+
if (stableJson(actual.config) !== stableJson(expected)) {
|
|
58
|
+
issues.push({
|
|
59
|
+
key,
|
|
60
|
+
kind: "drift",
|
|
61
|
+
client: plan.client,
|
|
62
|
+
serverName: plan.name,
|
|
63
|
+
file: target.file,
|
|
64
|
+
scope: currentScope,
|
|
65
|
+
message: `client config entry differs from ${lockfilePath}`,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (!foundConfig && !foundUnreadable && missing.length > 0) {
|
|
70
|
+
issues.push({
|
|
71
|
+
key,
|
|
72
|
+
kind: "missing",
|
|
73
|
+
client: plan.client,
|
|
74
|
+
serverName: plan.name,
|
|
75
|
+
file: missing.map((entry) => entry.file).join(", "),
|
|
76
|
+
scope: scope === "all" ? undefined : missing[0]?.scope,
|
|
77
|
+
message: scope === "all"
|
|
78
|
+
? `missing ${plan.client} config entry for ${plan.name} in checked scopes: ${missing.map((entry) => entry.scope).join(", ")}`
|
|
79
|
+
: `missing ${plan.client} config entry for ${plan.name}`,
|
|
80
|
+
});
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (!foundConfig && !foundUnreadable && missing.length === 0 && invalidScopes.length > 0) {
|
|
84
|
+
issues.push({
|
|
85
|
+
key,
|
|
86
|
+
kind: "invalid",
|
|
87
|
+
client: plan.client,
|
|
88
|
+
serverName: plan.name,
|
|
89
|
+
file: "",
|
|
90
|
+
message: `cannot check ${plan.client} at ${scope} scope: ${invalidScopes.join("; ")}`,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
ok: issues.length === 0,
|
|
96
|
+
checked: entries.length,
|
|
97
|
+
issues,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function scopesToCheck(scope, plan) {
|
|
101
|
+
if (scope !== "all")
|
|
102
|
+
return [scope];
|
|
103
|
+
return plan.scope ? [plan.scope] : ["project", "global"];
|
|
104
|
+
}
|
|
105
|
+
function expectedServerConfig(plan) {
|
|
106
|
+
return serverConfigFromWrapped(plan.locked?.config ?? plan.config, plan.name, plan.client);
|
|
107
|
+
}
|
|
108
|
+
export async function readInstalledServerConfig(file, serverName, client) {
|
|
109
|
+
let raw;
|
|
110
|
+
try {
|
|
111
|
+
raw = await readFile(file, "utf8");
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if (error.code === "ENOENT")
|
|
115
|
+
return { kind: "missing" };
|
|
116
|
+
return { kind: "unreadable", message: error instanceof Error ? error.message : String(error) };
|
|
117
|
+
}
|
|
118
|
+
if (client === "codex") {
|
|
119
|
+
const config = readCodexServerConfig(raw, serverName);
|
|
120
|
+
return config ? { kind: "ok", config } : { kind: "missing" };
|
|
121
|
+
}
|
|
122
|
+
if (client === "continue") {
|
|
123
|
+
try {
|
|
124
|
+
const config = readContinueServerConfig(raw, serverName);
|
|
125
|
+
return config ? { kind: "ok", config } : { kind: "missing" };
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
return {
|
|
129
|
+
kind: "unreadable",
|
|
130
|
+
message: `invalid YAML in ${file}: ${error instanceof Error ? error.message : String(error)}`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
const parsed = JSON.parse(raw);
|
|
136
|
+
const config = serverConfigFromWrapped(parsed, serverName, client);
|
|
137
|
+
return config ? { kind: "ok", config } : { kind: "missing" };
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
return {
|
|
141
|
+
kind: "unreadable",
|
|
142
|
+
message: `invalid JSON in ${file}: ${error instanceof Error ? error.message : String(error)}`,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function serverConfigFromWrapped(config, serverName, client) {
|
|
147
|
+
if (client === "continue") {
|
|
148
|
+
const servers = asRecord(config).mcpServers;
|
|
149
|
+
return Array.isArray(servers) ? servers.find((server) => asRecord(server).name === serverName) : undefined;
|
|
150
|
+
}
|
|
151
|
+
const root = asRecord(config);
|
|
152
|
+
const section = clientConfigRootKey(client);
|
|
153
|
+
return asRecord(root[section])[serverName];
|
|
154
|
+
}
|
|
155
|
+
function stableJson(value) {
|
|
156
|
+
return canonicalJson(value, { pruneEmptyObjects: true });
|
|
157
|
+
}
|
|
158
|
+
function asRecord(value) {
|
|
159
|
+
return isRecord(value) ? value : {};
|
|
160
|
+
}
|
|
161
|
+
function isRecord(value) {
|
|
162
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
163
|
+
}
|
package/dist/install.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { clientConfigRootKey, exportClientConfig } from "./config.js";
|
|
5
|
+
import { mergeCodexToml, removeCodexServerToml } from "./codexToml.js";
|
|
6
|
+
import { mergeContinueYaml, removeContinueServerYaml } from "./continueYaml.js";
|
|
7
|
+
export async function installServerConfig(server, client, scope) {
|
|
8
|
+
const exported = exportClientConfig(server, client);
|
|
9
|
+
const target = resolveConfigTarget(client, scope);
|
|
10
|
+
if (client === "codex") {
|
|
11
|
+
const existing = await readText(target.file);
|
|
12
|
+
const next = mergeCodexToml(existing, exported.config);
|
|
13
|
+
await mkdir(path.dirname(target.file), { recursive: true });
|
|
14
|
+
await writeFile(target.file, next, "utf8");
|
|
15
|
+
return {
|
|
16
|
+
client,
|
|
17
|
+
scope,
|
|
18
|
+
file: target.file,
|
|
19
|
+
serverName: server.name,
|
|
20
|
+
action: existing.trim().length === 0 ? "created" : "updated",
|
|
21
|
+
notes: [...target.notes, ...exported.notes],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (client === "continue") {
|
|
25
|
+
const existing = await readText(target.file);
|
|
26
|
+
const next = mergeContinueYaml(existing, exported.config);
|
|
27
|
+
await mkdir(path.dirname(target.file), { recursive: true });
|
|
28
|
+
await writeFile(target.file, next, "utf8");
|
|
29
|
+
return {
|
|
30
|
+
client,
|
|
31
|
+
scope,
|
|
32
|
+
file: target.file,
|
|
33
|
+
serverName: server.name,
|
|
34
|
+
action: existing.trim().length === 0 ? "created" : "updated",
|
|
35
|
+
notes: [...target.notes, ...exported.notes],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const existing = await readJsonObject(target.file);
|
|
39
|
+
const next = mergeClientConfig(existing, exported.config, client);
|
|
40
|
+
await mkdir(path.dirname(target.file), { recursive: true });
|
|
41
|
+
await writeFile(target.file, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
42
|
+
return {
|
|
43
|
+
client,
|
|
44
|
+
scope,
|
|
45
|
+
file: target.file,
|
|
46
|
+
serverName: server.name,
|
|
47
|
+
action: Object.keys(existing).length === 0 ? "created" : "updated",
|
|
48
|
+
notes: [...target.notes, ...exported.notes],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export async function removeServerConfig(serverName, client, scope) {
|
|
52
|
+
const target = resolveConfigTarget(client, scope);
|
|
53
|
+
if (client === "codex") {
|
|
54
|
+
const existing = await readText(target.file);
|
|
55
|
+
const next = removeCodexServerToml(existing, serverName);
|
|
56
|
+
if (next === existing) {
|
|
57
|
+
return { client, scope, file: target.file, serverName, action: "missing", notes: target.notes };
|
|
58
|
+
}
|
|
59
|
+
await mkdir(path.dirname(target.file), { recursive: true });
|
|
60
|
+
await writeFile(target.file, next, "utf8");
|
|
61
|
+
return { client, scope, file: target.file, serverName, action: "removed", notes: target.notes };
|
|
62
|
+
}
|
|
63
|
+
if (client === "continue") {
|
|
64
|
+
const existing = await readText(target.file);
|
|
65
|
+
const next = removeContinueServerYaml(existing, serverName);
|
|
66
|
+
if (next === existing) {
|
|
67
|
+
return { client, scope, file: target.file, serverName, action: "missing", notes: target.notes };
|
|
68
|
+
}
|
|
69
|
+
await mkdir(path.dirname(target.file), { recursive: true });
|
|
70
|
+
await writeFile(target.file, next, "utf8");
|
|
71
|
+
return { client, scope, file: target.file, serverName, action: "removed", notes: target.notes };
|
|
72
|
+
}
|
|
73
|
+
const existing = await readJsonObject(target.file);
|
|
74
|
+
const { config: next, removed } = removeClientConfig(existing, serverName, client);
|
|
75
|
+
if (!removed) {
|
|
76
|
+
return { client, scope, file: target.file, serverName, action: "missing", notes: target.notes };
|
|
77
|
+
}
|
|
78
|
+
await mkdir(path.dirname(target.file), { recursive: true });
|
|
79
|
+
await writeFile(target.file, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
80
|
+
return { client, scope, file: target.file, serverName, action: "removed", notes: target.notes };
|
|
81
|
+
}
|
|
82
|
+
export function resolveConfigTarget(client, scope) {
|
|
83
|
+
const cwd = process.cwd();
|
|
84
|
+
const home = os.homedir();
|
|
85
|
+
if (scope === "project") {
|
|
86
|
+
switch (client) {
|
|
87
|
+
case "vscode":
|
|
88
|
+
return { file: path.join(cwd, ".vscode", "mcp.json"), notes: ["Project VS Code MCP config written."] };
|
|
89
|
+
case "codex":
|
|
90
|
+
return { file: path.join(cwd, ".codex", "config.toml"), notes: ["Project Codex config.toml written. Project must be trusted by Codex before this layer loads."] };
|
|
91
|
+
case "opencode":
|
|
92
|
+
return { file: path.join(cwd, "opencode.json"), notes: ["Project opencode config written. Restart opencode to load it."] };
|
|
93
|
+
case "cursor":
|
|
94
|
+
return { file: path.join(cwd, ".cursor", "mcp.json"), notes: ["Project Cursor MCP config written. Restart Cursor or reload MCP servers to load it."] };
|
|
95
|
+
case "gemini":
|
|
96
|
+
return { file: path.join(cwd, ".gemini", "settings.json"), notes: ["Project Gemini CLI settings.json written."] };
|
|
97
|
+
case "roo":
|
|
98
|
+
return { file: path.join(cwd, ".roo", "mcp.json"), notes: ["Project Roo Code MCP config written."] };
|
|
99
|
+
case "windsurf":
|
|
100
|
+
throw new Error("Project Windsurf/Cascade MCP config path is not documented; use --scope global.");
|
|
101
|
+
case "cline":
|
|
102
|
+
throw new Error("Project Cline MCP config path is not documented; use --scope global.");
|
|
103
|
+
case "continue":
|
|
104
|
+
throw new Error("Project Continue config path is not documented; use --scope global.");
|
|
105
|
+
case "zed":
|
|
106
|
+
throw new Error("Zed settings path is not verified yet; export the config snippet and add it through Zed settings.");
|
|
107
|
+
case "generic":
|
|
108
|
+
default:
|
|
109
|
+
return { file: path.join(cwd, ".mcp.json"), notes: ["Project MCP config written. Import it into clients that support project MCP config."] };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
switch (client) {
|
|
113
|
+
case "opencode":
|
|
114
|
+
return { file: path.join(home, ".config", "opencode", "opencode.json"), notes: ["Global opencode config written. Restart opencode to load it."] };
|
|
115
|
+
case "cursor":
|
|
116
|
+
return { file: path.join(home, ".cursor", "mcp.json"), notes: ["Global Cursor MCP config written. Restart Cursor or reload MCP servers to load it."] };
|
|
117
|
+
case "vscode":
|
|
118
|
+
return { file: vsCodeGlobalConfigFile(home), notes: ["Global VS Code user MCP config path written."] };
|
|
119
|
+
case "codex":
|
|
120
|
+
return { file: path.join(home, ".codex", "config.toml"), notes: ["Global Codex config.toml written."] };
|
|
121
|
+
case "windsurf":
|
|
122
|
+
return { file: path.join(home, ".codeium", "windsurf", "mcp_config.json"), notes: ["Global Windsurf/Cascade MCP config written. Restart Windsurf to load it."] };
|
|
123
|
+
case "cline":
|
|
124
|
+
return { file: path.join(home, ".cline", "mcp.json"), notes: ["Global Cline CLI MCP config written. Reload Cline to load it."] };
|
|
125
|
+
case "continue":
|
|
126
|
+
return { file: path.join(home, ".continue", "config.yaml"), notes: ["Global Continue config.yaml written. Continue reloads config on save."] };
|
|
127
|
+
case "gemini":
|
|
128
|
+
return { file: path.join(home, ".gemini", "settings.json"), notes: ["Global Gemini CLI settings.json written."] };
|
|
129
|
+
case "zed":
|
|
130
|
+
throw new Error("Zed settings path is not verified yet; export the config snippet and add it through Zed settings.");
|
|
131
|
+
case "roo":
|
|
132
|
+
throw new Error("Global Roo Code mcp_settings.json path is not verified yet; use --scope project.");
|
|
133
|
+
case "claude":
|
|
134
|
+
throw new Error("Claude global MCP config is managed by the Claude CLI and is not written directly by ToolPin; use `toolpin export-config ... --client claude` with `claude mcp add-json --scope user`, or use --scope project.");
|
|
135
|
+
case "generic":
|
|
136
|
+
default:
|
|
137
|
+
return { file: path.join(home, ".config", "toolpin", `${client}-mcp.json`), notes: ["Generic global MCP config written; client-specific import may still be required."] };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function removeClientConfig(existing, serverName, client) {
|
|
141
|
+
const next = { ...existing };
|
|
142
|
+
const key = clientConfigRootKey(client);
|
|
143
|
+
const servers = { ...asObject(next[key]) };
|
|
144
|
+
if (!(serverName in servers))
|
|
145
|
+
return { config: existing, removed: false };
|
|
146
|
+
delete servers[serverName];
|
|
147
|
+
next[key] = servers;
|
|
148
|
+
return { config: next, removed: true };
|
|
149
|
+
}
|
|
150
|
+
function mergeClientConfig(existing, incoming, client) {
|
|
151
|
+
const incomingObject = asObject(incoming);
|
|
152
|
+
const key = clientConfigRootKey(client);
|
|
153
|
+
const next = { ...existing };
|
|
154
|
+
if (client === "opencode" && typeof next.$schema !== "string" && typeof incomingObject.$schema === "string") {
|
|
155
|
+
next.$schema = incomingObject.$schema;
|
|
156
|
+
}
|
|
157
|
+
next[key] = {
|
|
158
|
+
...asObject(existing[key]),
|
|
159
|
+
...asObject(incomingObject[key]),
|
|
160
|
+
};
|
|
161
|
+
return next;
|
|
162
|
+
}
|
|
163
|
+
export function vsCodeGlobalConfigFile(home = os.homedir(), platform = process.platform, appData = process.env.APPDATA) {
|
|
164
|
+
if (platform === "darwin") {
|
|
165
|
+
return path.join(home, "Library", "Application Support", "Code", "User", "mcp.json");
|
|
166
|
+
}
|
|
167
|
+
if (platform === "win32") {
|
|
168
|
+
return path.join(appData || path.join(home, "AppData", "Roaming"), "Code", "User", "mcp.json");
|
|
169
|
+
}
|
|
170
|
+
return path.join(home, ".config", "Code", "User", "mcp.json");
|
|
171
|
+
}
|
|
172
|
+
async function readJsonObject(file) {
|
|
173
|
+
try {
|
|
174
|
+
const raw = await readFile(file, "utf8");
|
|
175
|
+
return asObject(JSON.parse(raw));
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return {};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async function readText(file) {
|
|
182
|
+
try {
|
|
183
|
+
return await readFile(file, "utf8");
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return "";
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function asObject(value) {
|
|
190
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
191
|
+
}
|