@4da/mcp-server 6.0.1 → 6.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.
- package/CHANGELOG.md +316 -0
- package/README.md +19 -13
- package/dist/db.d.ts +88 -7
- package/dist/db.js +248 -63
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +15 -9
- package/dist/embeddings.js +19 -7
- package/dist/gitignore.js +9 -3
- package/dist/index.js +54 -7
- package/dist/live/cache.d.ts +31 -2
- package/dist/live/cache.js +113 -1
- package/dist/live/changelog-classify.js +4 -1
- package/dist/live/changelog.d.ts +15 -0
- package/dist/live/changelog.js +67 -11
- package/dist/live/dev-scope.d.ts +2 -2
- package/dist/live/fix-paths.d.ts +118 -0
- package/dist/live/fix-paths.js +498 -0
- package/dist/live/go-registry.d.ts +5 -0
- package/dist/live/go-registry.js +1 -1
- package/dist/live/index.d.ts +30 -3
- package/dist/live/index.js +59 -6
- package/dist/live/lockfile-parents.d.ts +61 -0
- package/dist/live/lockfile-parents.js +360 -0
- package/dist/live/lockfile-parsers-pygo.d.ts +31 -0
- package/dist/live/lockfile-parsers-pygo.js +178 -28
- package/dist/live/lockfile-parsers.d.ts +4 -1
- package/dist/live/lockfile-parsers.js +57 -24
- package/dist/live/lockfile-types.d.ts +12 -3
- package/dist/live/lockfile-types.js +2 -1
- package/dist/live/osv-ranges.d.ts +79 -0
- package/dist/live/osv-ranges.js +133 -0
- package/dist/live/osv-scanner.d.ts +1 -0
- package/dist/live/osv-scanner.js +85 -78
- package/dist/live/range-satisfies.d.ts +21 -0
- package/dist/live/range-satisfies.js +190 -0
- package/dist/live/rate-limiter.d.ts +6 -0
- package/dist/live/rate-limiter.js +5 -0
- package/dist/live/registry-health.js +2 -0
- package/dist/live/resolution.d.ts +11 -1
- package/dist/live/resolution.js +16 -6
- package/dist/live/semver-utils.d.ts +8 -0
- package/dist/live/semver-utils.js +10 -2
- package/dist/live/types.d.ts +17 -1
- package/dist/live/upgrade-sources.d.ts +2 -0
- package/dist/live/upgrade-sources.js +7 -2
- package/dist/memory-store.d.ts +19 -0
- package/dist/memory-store.js +8 -0
- package/dist/native-bindings.d.ts +7 -2
- package/dist/native-bindings.js +4 -0
- package/dist/project-tree.d.ts +50 -8
- package/dist/project-tree.js +121 -29
- package/dist/schema-registry.js +2 -2
- package/dist/setup.d.ts +76 -3
- package/dist/setup.js +313 -130
- package/dist/sqlite-driver.d.ts +134 -0
- package/dist/sqlite-driver.js +359 -0
- package/dist/tool-dispatch.d.ts +10 -1
- package/dist/tool-dispatch.js +37 -2
- package/dist/tools/agent-memory.js +10 -2
- package/dist/tools/app-plan.d.ts +11 -1
- package/dist/tools/app-plan.js +32 -6
- package/dist/tools/briefing-task-scope.d.ts +21 -2
- package/dist/tools/briefing-task-scope.js +97 -4
- package/dist/tools/decision-memory.js +9 -3
- package/dist/tools/dependency-check-signals.d.ts +7 -0
- package/dist/tools/dependency-check-signals.js +17 -2
- package/dist/tools/dependency-check.d.ts +1 -1
- package/dist/tools/dependency-check.js +1 -1
- package/dist/tools/dependency-health.d.ts +1 -1
- package/dist/tools/dependency-health.js +5 -0
- package/dist/tools/knowledge-gap-ranges.d.ts +1 -1
- package/dist/tools/knowledge-gap-ranges.js +1 -1
- package/dist/tools/knowledge-gap-scope.d.ts +2 -2
- package/dist/tools/project-scope.d.ts +45 -0
- package/dist/tools/project-scope.js +119 -0
- package/dist/tools/record-feedback.js +1 -1
- package/dist/tools/upgrade-impact-literals.js +1 -1
- package/dist/tools/upgrade-impact-report.d.ts +10 -0
- package/dist/tools/upgrade-impact-report.js +32 -2
- package/dist/tools/upgrade-impact.js +1 -1
- package/dist/tools/upgrade-planner-fixpaths.d.ts +40 -0
- package/dist/tools/upgrade-planner-fixpaths.js +0 -0
- package/dist/tools/upgrade-planner.d.ts +31 -3
- package/dist/tools/upgrade-planner.js +74 -19
- package/dist/tools/vulnerability-scan-concise.d.ts +13 -1
- package/dist/tools/vulnerability-scan-concise.js +15 -3
- package/dist/tools/vulnerability-scan-format.d.ts +58 -1
- package/dist/tools/vulnerability-scan-format.js +107 -11
- package/dist/tools/vulnerability-scan.d.ts +104 -13
- package/dist/tools/vulnerability-scan.js +27 -3
- package/dist/tools/what-should-i-know.d.ts +22 -2
- package/dist/tools/what-should-i-know.js +121 -11
- package/package.json +10 -31
package/dist/setup.js
CHANGED
|
@@ -7,160 +7,343 @@
|
|
|
7
7
|
* the user can start using 4DA tools immediately after install.
|
|
8
8
|
*
|
|
9
9
|
* Usage:
|
|
10
|
-
* npx @4da/mcp-server --setup
|
|
11
|
-
* 4da-mcp-setup
|
|
10
|
+
* npx @4da/mcp-server --setup [--dry-run]
|
|
11
|
+
* 4da-mcp-setup [--dry-run]
|
|
12
12
|
* pnpm run setup
|
|
13
|
+
*
|
|
14
|
+
* Audited 2026-10-05: three of the four editors were configured in files the
|
|
15
|
+
* editor never reads (`~/.vscode/mcp.json`, `~/.windsurf/mcp.json`, and
|
|
16
|
+
* `mcpServers` in a project's `.claude/settings.json`), and any config file
|
|
17
|
+
* that failed `JSON.parse` (VS Code's `mcp.json` is JSONC: comments and
|
|
18
|
+
* trailing commas are legal) was replaced by `{}` plus our entry, deleting
|
|
19
|
+
* every other server the user had. Each path below cites the doc it was
|
|
20
|
+
* checked against, and a file that cannot be parsed is never written.
|
|
13
21
|
*/
|
|
14
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
15
|
-
import
|
|
22
|
+
import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
23
|
+
import path from "node:path";
|
|
16
24
|
import { homedir } from "node:os";
|
|
25
|
+
import jsonc from "jsonc-parser";
|
|
26
|
+
/** The command every editor runs. */
|
|
27
|
+
const SERVER_COMMAND = { command: "npx", args: ["@4da/mcp-server"] };
|
|
17
28
|
/** MCP config snippet for editors that use the standard mcpServers format */
|
|
18
29
|
const MCP_CONFIG = {
|
|
19
|
-
"4da":
|
|
20
|
-
command: "npx",
|
|
21
|
-
args: ["@4da/mcp-server"],
|
|
22
|
-
},
|
|
30
|
+
"4da": SERVER_COMMAND,
|
|
23
31
|
};
|
|
32
|
+
/** The Claude Code command printed instead of editing `~/.claude.json`. */
|
|
33
|
+
export const CLAUDE_CODE_COMMAND = "claude mcp add --scope user 4da -- npx @4da/mcp-server";
|
|
34
|
+
export function currentContext() {
|
|
35
|
+
return { platform: process.platform, env: process.env, home: homedir() };
|
|
36
|
+
}
|
|
37
|
+
function pathFor(ctx) {
|
|
38
|
+
return ctx.platform === "win32" ? path.win32 : path.posix;
|
|
39
|
+
}
|
|
40
|
+
/** `%APPDATA%` on Windows, the Electron-style config base elsewhere. */
|
|
41
|
+
function roamingDir(ctx) {
|
|
42
|
+
const p = pathFor(ctx);
|
|
43
|
+
return ctx.env.APPDATA?.trim() || p.join(ctx.home, "AppData", "Roaming");
|
|
44
|
+
}
|
|
45
|
+
function xdgConfigDir(ctx) {
|
|
46
|
+
return ctx.env.XDG_CONFIG_HOME?.trim() || pathFor(ctx).join(ctx.home, ".config");
|
|
47
|
+
}
|
|
24
48
|
/**
|
|
25
|
-
*
|
|
49
|
+
* The per-platform user-data dir of a desktop app (Electron `appData`):
|
|
50
|
+
* `%APPDATA%\<name>`, `~/Library/Application Support/<name>`,
|
|
51
|
+
* `$XDG_CONFIG_HOME/<name>` (default `~/.config/<name>`).
|
|
26
52
|
*/
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return {};
|
|
35
|
-
}
|
|
53
|
+
function appDataDir(ctx, name) {
|
|
54
|
+
const p = pathFor(ctx);
|
|
55
|
+
if (ctx.platform === "win32")
|
|
56
|
+
return p.join(roamingDir(ctx), name);
|
|
57
|
+
if (ctx.platform === "darwin")
|
|
58
|
+
return p.join(ctx.home, "Library", "Application Support", name);
|
|
59
|
+
return p.join(xdgConfigDir(ctx), name);
|
|
36
60
|
}
|
|
37
61
|
/**
|
|
38
|
-
*
|
|
62
|
+
* Every editor this setup can write, with its config path on `ctx.platform`.
|
|
63
|
+
* Pure: nothing is read from disk here.
|
|
39
64
|
*/
|
|
40
|
-
function
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
name: "Cursor",
|
|
70
|
-
detect: () => {
|
|
71
|
-
const cursorDir = join(homedir(), ".cursor");
|
|
72
|
-
if (existsSync(cursorDir)) {
|
|
73
|
-
return join(cursorDir, "mcp.json");
|
|
74
|
-
}
|
|
75
|
-
return null;
|
|
76
|
-
},
|
|
77
|
-
write: (configPath) => {
|
|
78
|
-
const config = readJsonSafe(configPath);
|
|
79
|
-
const mcpServers = (config.mcpServers || {});
|
|
80
|
-
mcpServers["4da"] = MCP_CONFIG["4da"];
|
|
81
|
-
config.mcpServers = mcpServers;
|
|
82
|
-
writeJsonFile(configPath, config);
|
|
83
|
-
console.log(` Written: ${configPath}`);
|
|
84
|
-
},
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
name: "VS Code (Copilot)",
|
|
88
|
-
detect: () => {
|
|
89
|
-
const vscodePath = join(homedir(), ".vscode");
|
|
90
|
-
if (existsSync(vscodePath)) {
|
|
91
|
-
return join(vscodePath, "mcp.json");
|
|
92
|
-
}
|
|
93
|
-
return null;
|
|
65
|
+
export function editorTargets(ctx = currentContext()) {
|
|
66
|
+
const p = pathFor(ctx);
|
|
67
|
+
const vscodeEntry = { type: "stdio", ...SERVER_COMMAND };
|
|
68
|
+
const vscode = (name, product) => {
|
|
69
|
+
const userDir = p.join(appDataDir(ctx, product), "User");
|
|
70
|
+
return {
|
|
71
|
+
name,
|
|
72
|
+
// The default profile's user-level file; it sits beside settings.json.
|
|
73
|
+
// A non-default profile keeps its own under User/profiles/<id>/, which
|
|
74
|
+
// VS Code does not name anywhere setup could find it.
|
|
75
|
+
configPath: p.join(userDir, "mcp.json"),
|
|
76
|
+
detectDir: userDir,
|
|
77
|
+
key: "servers",
|
|
78
|
+
entry: vscodeEntry,
|
|
79
|
+
docs: "https://code.visualstudio.com/docs/copilot/reference/mcp-configuration",
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
const devinDir = ctx.platform === "win32" ? p.join(roamingDir(ctx), "devin") : p.join(xdgConfigDir(ctx), "devin");
|
|
83
|
+
const windsurfDir = p.join(ctx.home, ".codeium", "windsurf");
|
|
84
|
+
return [
|
|
85
|
+
{
|
|
86
|
+
name: "Cursor",
|
|
87
|
+
configPath: p.join(ctx.home, ".cursor", "mcp.json"),
|
|
88
|
+
detectDir: p.join(ctx.home, ".cursor"),
|
|
89
|
+
key: "mcpServers",
|
|
90
|
+
entry: SERVER_COMMAND,
|
|
91
|
+
docs: "https://cursor.com/docs/mcp",
|
|
94
92
|
},
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
config.servers = servers;
|
|
105
|
-
writeJsonFile(configPath, config);
|
|
106
|
-
console.log(` Written: ${configPath}`);
|
|
93
|
+
vscode("VS Code (Copilot)", "Code"),
|
|
94
|
+
vscode("VS Code Insiders (Copilot)", "Code - Insiders"),
|
|
95
|
+
{
|
|
96
|
+
name: "Windsurf",
|
|
97
|
+
configPath: p.join(windsurfDir, "mcp_config.json"),
|
|
98
|
+
detectDir: windsurfDir,
|
|
99
|
+
key: "mcpServers",
|
|
100
|
+
entry: SERVER_COMMAND,
|
|
101
|
+
docs: "https://docs.devin.ai/cli/reference/configuration/read-config-from",
|
|
107
102
|
},
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
103
|
+
{
|
|
104
|
+
// Windsurf's successor. Its Cascade agent and the Devin CLI read the
|
|
105
|
+
// same user-level file.
|
|
106
|
+
name: "Devin Desktop / Devin CLI",
|
|
107
|
+
configPath: p.join(devinDir, "mcp_config.json"),
|
|
108
|
+
detectDir: devinDir,
|
|
109
|
+
key: "mcpServers",
|
|
110
|
+
entry: SERVER_COMMAND,
|
|
111
|
+
docs: "https://docs.devin.ai/desktop/cascade/mcp",
|
|
117
112
|
},
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
113
|
+
{
|
|
114
|
+
name: "Claude Desktop",
|
|
115
|
+
configPath: p.join(appDataDir(ctx, "Claude"), "claude_desktop_config.json"),
|
|
116
|
+
detectDir: appDataDir(ctx, "Claude"),
|
|
117
|
+
key: "mcpServers",
|
|
118
|
+
entry: SERVER_COMMAND,
|
|
119
|
+
docs: "https://modelcontextprotocol.io/docs/develop/connect-local-servers",
|
|
125
120
|
},
|
|
126
|
-
|
|
127
|
-
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Claude Code keeps user- and local-scope servers in `~/.claude.json`
|
|
125
|
+
* (`$CLAUDE_CONFIG_DIR/.claude.json` when set), a file it rewrites itself and
|
|
126
|
+
* that also holds the sign-in session and per-project state. Setup never
|
|
127
|
+
* edits it: it prints the `claude mcp add` command, which does.
|
|
128
|
+
* https://code.claude.com/docs/en/mcp
|
|
129
|
+
*/
|
|
130
|
+
export function claudeCodeConfigPath(ctx = currentContext()) {
|
|
131
|
+
const p = pathFor(ctx);
|
|
132
|
+
const configDir = ctx.env.CLAUDE_CONFIG_DIR?.trim();
|
|
133
|
+
return configDir ? p.join(configDir, ".claude.json") : p.join(ctx.home, ".claude.json");
|
|
134
|
+
}
|
|
135
|
+
function lineOf(text, offset) {
|
|
136
|
+
return text.slice(0, offset).split("\n").length;
|
|
137
|
+
}
|
|
138
|
+
function isPlainObject(value) {
|
|
139
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
140
|
+
}
|
|
141
|
+
function sameJson(a, b) {
|
|
142
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Decide what writing `entry` under `<key>["4da"]` does to a config file whose
|
|
146
|
+
* current text is `existing` (null when it does not exist).
|
|
147
|
+
*
|
|
148
|
+
* The file is parsed as JSONC (comments and trailing commas allowed) and
|
|
149
|
+
* edited in place with `jsonc-parser`'s `modify`, so other servers, other keys,
|
|
150
|
+
* comments and formatting all survive. A file that does not parse is refused,
|
|
151
|
+
* never replaced. An existing `4da` entry keeps its other fields (`env`, say);
|
|
152
|
+
* only the fields setup owns are set.
|
|
153
|
+
*/
|
|
154
|
+
export function planConfig(existing, key, entry) {
|
|
155
|
+
if (existing === null || existing.trim() === "") {
|
|
156
|
+
return {
|
|
157
|
+
action: existing === null ? "create" : "update",
|
|
158
|
+
content: JSON.stringify({ [key]: { "4da": entry } }, null, 2) + "\n",
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const errors = [];
|
|
162
|
+
const root = jsonc.parse(existing, errors, { allowTrailingComma: true, disallowComments: false });
|
|
163
|
+
if (errors.length > 0) {
|
|
164
|
+
const first = errors[0];
|
|
165
|
+
return {
|
|
166
|
+
action: "refuse",
|
|
167
|
+
reason: `it does not parse (${jsonc.printParseErrorCode(first.error)} at line ${lineOf(existing, first.offset)})`,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (!isPlainObject(root)) {
|
|
171
|
+
return { action: "refuse", reason: "its top level is not a JSON object" };
|
|
172
|
+
}
|
|
173
|
+
const servers = root[key];
|
|
174
|
+
if (servers !== undefined && !isPlainObject(servers)) {
|
|
175
|
+
return { action: "refuse", reason: `its "${key}" is not an object` };
|
|
176
|
+
}
|
|
177
|
+
const current = servers?.["4da"];
|
|
178
|
+
if (current !== undefined && !isPlainObject(current)) {
|
|
179
|
+
return { action: "refuse", reason: `its "${key}"."4da" is not an object` };
|
|
180
|
+
}
|
|
181
|
+
if (current && Object.entries(entry).every(([field, value]) => sameJson(current[field], value))) {
|
|
182
|
+
return { action: "unchanged" };
|
|
183
|
+
}
|
|
184
|
+
const eol = existing.includes("\r\n") ? "\r\n" : "\n";
|
|
185
|
+
const indent = /^([ \t]+)\S/m.exec(existing)?.[1] ?? " ";
|
|
186
|
+
const formattingOptions = {
|
|
187
|
+
insertSpaces: !indent.startsWith("\t"),
|
|
188
|
+
tabSize: indent.startsWith("\t") ? 1 : indent.length,
|
|
189
|
+
eol,
|
|
190
|
+
};
|
|
191
|
+
let content = existing;
|
|
192
|
+
// Field by field when the entry exists, so its other fields and any
|
|
193
|
+
// comments inside it stay; whole when it does not.
|
|
194
|
+
const edits = current
|
|
195
|
+
? Object.entries(entry).map(([field, value]) => [[key, "4da", field], value])
|
|
196
|
+
: [[[key, "4da"], entry]];
|
|
197
|
+
for (const [jsonPath, value] of edits) {
|
|
198
|
+
content = jsonc.applyEdits(content, jsonc.modify(content, jsonPath, value, { formattingOptions }));
|
|
199
|
+
}
|
|
200
|
+
return { action: "update", content };
|
|
201
|
+
}
|
|
202
|
+
/** Read a config file's text; null when it does not exist. */
|
|
203
|
+
function readConfig(filePath) {
|
|
204
|
+
return existsSync(filePath) ? readFileSync(filePath, "utf-8") : null;
|
|
205
|
+
}
|
|
206
|
+
/** `<file>.bak`, or a timestamped name when that is taken: an earlier backup is never overwritten. */
|
|
207
|
+
function backupPath(filePath) {
|
|
208
|
+
const plain = `${filePath}.bak`;
|
|
209
|
+
if (!existsSync(plain))
|
|
210
|
+
return plain;
|
|
211
|
+
const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..*$/, "");
|
|
212
|
+
for (let n = 0;; n++) {
|
|
213
|
+
const candidate = `${filePath}.${stamp}${n === 0 ? "" : `-${n}`}.bak`;
|
|
214
|
+
if (!existsSync(candidate))
|
|
215
|
+
return candidate;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Write `content` to `filePath`: back up an existing file first, then write a
|
|
220
|
+
* temp file beside it and rename it over the original, so an interrupted run
|
|
221
|
+
* leaves either the old file or the new one, never half of one. A symlinked
|
|
222
|
+
* config (a dotfiles repo) is written through to its target.
|
|
223
|
+
* @returns The backup path, when one was made.
|
|
224
|
+
*/
|
|
225
|
+
export function writeConfigAtomic(filePath, content) {
|
|
226
|
+
// Only a symlinked file is resolved: resolving the whole path would turn
|
|
227
|
+
// macOS's /var/... into /private/var/... in every path reported back.
|
|
228
|
+
const target = existsSync(filePath) && lstatSync(filePath).isSymbolicLink() ? realpathSync(filePath) : filePath;
|
|
229
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
230
|
+
let backup = null;
|
|
231
|
+
if (existsSync(target)) {
|
|
232
|
+
backup = backupPath(target);
|
|
233
|
+
copyFileSync(target, backup);
|
|
234
|
+
}
|
|
235
|
+
const temp = `${target}.4da-${process.pid}.tmp`;
|
|
236
|
+
try {
|
|
237
|
+
writeFileSync(temp, content);
|
|
238
|
+
renameSync(temp, target);
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
rmSync(temp, { force: true });
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
return backup;
|
|
245
|
+
}
|
|
246
|
+
/** The snippet an editor gets, as printed in a dry run and for manual setup. */
|
|
247
|
+
function snippet(target) {
|
|
248
|
+
return JSON.stringify({ [target.key]: { "4da": target.entry } }, null, 2)
|
|
249
|
+
.split("\n")
|
|
250
|
+
.map((line) => ` ${line}`)
|
|
251
|
+
.join("\n");
|
|
252
|
+
}
|
|
253
|
+
/** Whether `~/.claude.json` already has a user-scope `4da` server. Read-only. */
|
|
254
|
+
function claudeCodeHas4da(configPath) {
|
|
255
|
+
try {
|
|
256
|
+
const root = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
257
|
+
return isPlainObject(root) && isPlainObject(root.mcpServers) && "4da" in root.mcpServers;
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
128
263
|
/**
|
|
129
264
|
* Run the setup wizard: detect editors and write MCP configurations.
|
|
130
265
|
* @param dryRun If true, only report what would be written without modifying files.
|
|
131
266
|
*/
|
|
132
|
-
export function runSetup(dryRun = false) {
|
|
133
|
-
|
|
267
|
+
export function runSetup(dryRun = false, ctx = currentContext(), log = console.log) {
|
|
268
|
+
log("\n 4DA MCP Server — Setup\n");
|
|
134
269
|
if (dryRun)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
let
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
270
|
+
log(" [DRY RUN] No files will be modified.\n");
|
|
271
|
+
log(" Detecting editors...\n");
|
|
272
|
+
let found = 0;
|
|
273
|
+
let refused = 0;
|
|
274
|
+
// Claude Code: print the command, never edit ~/.claude.json.
|
|
275
|
+
const claudeJson = claudeCodeConfigPath(ctx);
|
|
276
|
+
const claudeDir = pathFor(ctx).join(ctx.home, ".claude");
|
|
277
|
+
if (existsSync(claudeJson) || existsSync(claudeDir)) {
|
|
278
|
+
found++;
|
|
279
|
+
log(" Found: Claude Code");
|
|
280
|
+
if (claudeCodeHas4da(claudeJson)) {
|
|
281
|
+
log(` Already configured (user scope, ${claudeJson}).`);
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
log(" Claude Code keeps its MCP servers in ~/.claude.json, which it rewrites");
|
|
285
|
+
log(" itself; setup does not edit it. Add the server with:");
|
|
286
|
+
log(` ${CLAUDE_CODE_COMMAND}`);
|
|
287
|
+
}
|
|
288
|
+
log("");
|
|
289
|
+
}
|
|
290
|
+
for (const target of editorTargets(ctx)) {
|
|
291
|
+
if (!existsSync(target.detectDir))
|
|
292
|
+
continue;
|
|
293
|
+
found++;
|
|
294
|
+
log(` Found: ${target.name}`);
|
|
295
|
+
let plan;
|
|
296
|
+
try {
|
|
297
|
+
plan = planConfig(readConfig(target.configPath), target.key, target.entry);
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
plan = { action: "refuse", reason: `it could not be read (${error instanceof Error ? error.message : String(error)})` };
|
|
301
|
+
}
|
|
302
|
+
if (plan.action === "unchanged") {
|
|
303
|
+
log(` Already configured: ${target.configPath}`);
|
|
304
|
+
}
|
|
305
|
+
else if (plan.action === "refuse") {
|
|
306
|
+
refused++;
|
|
307
|
+
log(` Not modified: ${target.configPath}`);
|
|
308
|
+
log(` Setup left it untouched because ${plan.reason}. Add this by hand:`);
|
|
309
|
+
log(snippet(target));
|
|
310
|
+
}
|
|
311
|
+
else if (dryRun) {
|
|
312
|
+
log(` Would ${plan.action === "create" ? "create" : "update"}: ${target.configPath}`);
|
|
313
|
+
log(` Adding under "${target.key}":`);
|
|
314
|
+
log(snippet(target));
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
try {
|
|
318
|
+
const backup = writeConfigAtomic(target.configPath, plan.content ?? "");
|
|
319
|
+
log(` Written: ${target.configPath}`);
|
|
320
|
+
if (backup)
|
|
321
|
+
log(` Backup: ${backup}`);
|
|
144
322
|
}
|
|
145
|
-
|
|
146
|
-
|
|
323
|
+
catch (error) {
|
|
324
|
+
refused++;
|
|
325
|
+
log(` Could not write ${target.configPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
326
|
+
log(" Add this by hand:");
|
|
327
|
+
log(snippet(target));
|
|
147
328
|
}
|
|
148
|
-
configured++;
|
|
149
|
-
console.log("");
|
|
150
329
|
}
|
|
330
|
+
log("");
|
|
331
|
+
}
|
|
332
|
+
if (found === 0) {
|
|
333
|
+
log(" No supported editors detected.\n");
|
|
334
|
+
log(" Manual setup — add to your editor's MCP config:\n");
|
|
335
|
+
log(` ${JSON.stringify({ mcpServers: MCP_CONFIG }, null, 2)}\n`);
|
|
336
|
+
log(` Claude Code: ${CLAUDE_CODE_COMMAND}\n`);
|
|
151
337
|
}
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
console.log(" https://github.com/4DA-Systems/4DA/releases/latest");
|
|
162
|
-
console.log("");
|
|
163
|
-
console.log(" Run: npx @4da/mcp-server --doctor to verify everything works.\n");
|
|
338
|
+
if (refused > 0)
|
|
339
|
+
log(` ${refused} config file${refused === 1 ? " needs" : "s need"} the entry added by hand (above).`);
|
|
340
|
+
log(" Done. Restart your editor to activate 4DA.");
|
|
341
|
+
log("");
|
|
342
|
+
log(" The server works on its own; with the 4DA desktop app installed it");
|
|
343
|
+
log(" also reads the app's scored feed:");
|
|
344
|
+
log(" https://github.com/4DA-Systems/4DA/releases/latest");
|
|
345
|
+
log("");
|
|
346
|
+
log(" Run: npx @4da/mcp-server --doctor to verify everything works.\n");
|
|
164
347
|
}
|
|
165
348
|
// Run directly if invoked as a script (bin entry or pnpm run setup)
|
|
166
349
|
const isDirectRun = process.argv[1]?.endsWith("setup.js") || process.argv[1]?.endsWith("setup.ts");
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SQLite driver: Node's built-in `node:sqlite` when the runtime has it,
|
|
3
|
+
* otherwise better-sqlite3, behind one interface.
|
|
4
|
+
*
|
|
5
|
+
* Why: npm 12 blocks dependency install scripts unless allowed, so a default
|
|
6
|
+
* `npx @4da/mcp-server` or `npm install` left better-sqlite3's native module
|
|
7
|
+
* unbuilt and every tool failed (measured 2026-10-03 on npm 12.2.0, see
|
|
8
|
+
* native-bindings.ts). `node:sqlite` ships inside Node: no install script, no
|
|
9
|
+
* native download. It is available without a flag from Node 22.13.0 (and
|
|
10
|
+
* 23.4.0); npm 12 itself requires Node ^22.22.2 || ^24.15.0 || >=26, so every
|
|
11
|
+
* npm 12 install has it. better-sqlite3 is an optional dependency now, used on
|
|
12
|
+
* Node 22.0-22.12 where it builds (npm 10 and 11 run its install script).
|
|
13
|
+
*
|
|
14
|
+
* The interface is the subset of better-sqlite3 this server uses: open
|
|
15
|
+
* options `readonly` / `fileMustExist`, prepare().get/all/run (positional
|
|
16
|
+
* parameters, arrays flattened, or one object of named parameters), exec,
|
|
17
|
+
* pragma (incl. `{ simple: true }`), transaction (nested calls become
|
|
18
|
+
* savepoints), close, and the `open` / `readonly` / `name` properties.
|
|
19
|
+
* better-sqlite3's Database satisfies it as is; node:sqlite is adapted
|
|
20
|
+
* (NodeSqliteDatabase) so that rows, BLOBs, error codes and the 5 s busy
|
|
21
|
+
* timeout match what the server's code already expects.
|
|
22
|
+
*
|
|
23
|
+
* `FOURDA_SQLITE_DRIVER=node|better` forces one driver (tests, diagnostics):
|
|
24
|
+
* a forced driver that cannot load is reported, never silently replaced.
|
|
25
|
+
* `none` simulates a host where neither works.
|
|
26
|
+
*/
|
|
27
|
+
export interface SqliteRunResult {
|
|
28
|
+
changes: number;
|
|
29
|
+
lastInsertRowid: number | bigint;
|
|
30
|
+
}
|
|
31
|
+
export interface SqliteStatement {
|
|
32
|
+
run(...params: unknown[]): SqliteRunResult;
|
|
33
|
+
get(...params: unknown[]): unknown;
|
|
34
|
+
all(...params: unknown[]): unknown[];
|
|
35
|
+
}
|
|
36
|
+
export interface SqliteOpenOptions {
|
|
37
|
+
readonly?: boolean;
|
|
38
|
+
fileMustExist?: boolean;
|
|
39
|
+
}
|
|
40
|
+
/** The subset of better-sqlite3's Database the server uses; both drivers provide it. */
|
|
41
|
+
export interface SqliteDatabase {
|
|
42
|
+
prepare(source: string): SqliteStatement;
|
|
43
|
+
exec(source: string): unknown;
|
|
44
|
+
pragma(source: string, options?: {
|
|
45
|
+
simple?: boolean;
|
|
46
|
+
}): unknown;
|
|
47
|
+
transaction<F extends (...args: any[]) => unknown>(fn: F): (...args: Parameters<F>) => ReturnType<F>;
|
|
48
|
+
close(): unknown;
|
|
49
|
+
readonly open: boolean;
|
|
50
|
+
readonly readonly: boolean;
|
|
51
|
+
readonly name: string;
|
|
52
|
+
}
|
|
53
|
+
export type SqliteDriverName = "node:sqlite" | "better-sqlite3";
|
|
54
|
+
export interface SqliteDriver {
|
|
55
|
+
name: SqliteDriverName;
|
|
56
|
+
open(file: string, options?: SqliteOpenOptions): SqliteDatabase;
|
|
57
|
+
}
|
|
58
|
+
/** Thrown by sqliteDriver() when neither driver can open a database here. */
|
|
59
|
+
export declare class SqliteUnavailableError extends Error {
|
|
60
|
+
readonly code = "FOURDA_NO_SQLITE";
|
|
61
|
+
}
|
|
62
|
+
/** The first Node release with `node:sqlite` unflagged on the 22 line. */
|
|
63
|
+
export declare const NODE_SQLITE_MIN = "22.13.0";
|
|
64
|
+
/** better-sqlite3's error code for a SQLite result code (extended when known, else primary). */
|
|
65
|
+
export declare function sqliteCodeName(errcode: number): string;
|
|
66
|
+
interface NativeStatement {
|
|
67
|
+
run(...params: unknown[]): {
|
|
68
|
+
changes: number | bigint;
|
|
69
|
+
lastInsertRowid: number | bigint;
|
|
70
|
+
};
|
|
71
|
+
get(...params: unknown[]): unknown;
|
|
72
|
+
all(...params: unknown[]): unknown[];
|
|
73
|
+
setReadBigInts(enabled: boolean): void;
|
|
74
|
+
}
|
|
75
|
+
interface NativeDatabase {
|
|
76
|
+
prepare(sql: string): NativeStatement;
|
|
77
|
+
exec(sql: string): void;
|
|
78
|
+
close(): void;
|
|
79
|
+
}
|
|
80
|
+
type NativeDatabaseCtor = new (path: string, options?: Record<string, unknown>) => NativeDatabase;
|
|
81
|
+
export declare class NodeSqliteDatabase implements SqliteDatabase {
|
|
82
|
+
readonly name: string;
|
|
83
|
+
readonly readonly: boolean;
|
|
84
|
+
private db;
|
|
85
|
+
private isOpen;
|
|
86
|
+
private depth;
|
|
87
|
+
private savepoints;
|
|
88
|
+
constructor(Ctor: NativeDatabaseCtor, file: string, options?: SqliteOpenOptions);
|
|
89
|
+
get open(): boolean;
|
|
90
|
+
private assertOpen;
|
|
91
|
+
prepare(source: string): SqliteStatement;
|
|
92
|
+
exec(source: string): this;
|
|
93
|
+
pragma(source: string, options?: {
|
|
94
|
+
simple?: boolean;
|
|
95
|
+
}): unknown;
|
|
96
|
+
transaction<F extends (...args: any[]) => unknown>(fn: F): (...args: Parameters<F>) => ReturnType<F>;
|
|
97
|
+
close(): this;
|
|
98
|
+
}
|
|
99
|
+
/** Load node:sqlite without its one-time "SQLite is an experimental feature" warning; every other warning passes. */
|
|
100
|
+
export declare function loadNodeSqlite(): NativeDatabaseCtor;
|
|
101
|
+
type BetterCtor = new (file: string, options?: {
|
|
102
|
+
readonly?: boolean;
|
|
103
|
+
fileMustExist?: boolean;
|
|
104
|
+
}) => SqliteDatabase;
|
|
105
|
+
export declare function loadBetterSqlite3(): BetterCtor;
|
|
106
|
+
export interface DriverLoaders {
|
|
107
|
+
node: () => NativeDatabaseCtor;
|
|
108
|
+
better: () => BetterCtor;
|
|
109
|
+
}
|
|
110
|
+
export interface DriverResolution {
|
|
111
|
+
driver: SqliteDriver | null;
|
|
112
|
+
/** Why each driver tried was not used (empty when the first one worked). */
|
|
113
|
+
rejected: {
|
|
114
|
+
name: SqliteDriverName;
|
|
115
|
+
reason: string;
|
|
116
|
+
}[];
|
|
117
|
+
/** The actionable message when no driver is usable. */
|
|
118
|
+
problem: string | null;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Pick the driver: node:sqlite, then better-sqlite3 if its native module
|
|
122
|
+
* actually opens a database (importing it succeeds without one). Pure apart
|
|
123
|
+
* from the loaders, so tests can make either one fail.
|
|
124
|
+
*/
|
|
125
|
+
export declare function resolveSqliteDriver(loaders?: DriverLoaders, env?: NodeJS.ProcessEnv): DriverResolution;
|
|
126
|
+
/** The resolution for this process (computed once). */
|
|
127
|
+
export declare function sqliteDriverStatus(): DriverResolution;
|
|
128
|
+
/** The driver in use; throws SqliteUnavailableError with the fix when there is none. */
|
|
129
|
+
export declare function sqliteDriver(): SqliteDriver;
|
|
130
|
+
/** Open a database with the driver in use. */
|
|
131
|
+
export declare function openDatabase(file: string, options?: SqliteOpenOptions): SqliteDatabase;
|
|
132
|
+
/** Forget the cached resolution (tests that change FOURDA_SQLITE_DRIVER). */
|
|
133
|
+
export declare function resetSqliteDriver(): void;
|
|
134
|
+
export {};
|