@4da/mcp-server 5.0.4 → 6.0.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.
- package/CHANGELOG.md +698 -0
- package/README.md +70 -39
- package/dist/build-staleness.d.ts +35 -0
- package/dist/build-staleness.js +95 -0
- package/dist/db.d.ts +53 -58
- package/dist/db.js +226 -299
- package/dist/doctor.js +27 -7
- package/dist/embeddings.js +7 -1
- package/dist/freshness-markers.d.ts +34 -0
- package/dist/freshness-markers.js +74 -0
- package/dist/gitignore.d.ts +37 -0
- package/dist/gitignore.js +130 -0
- package/dist/index.js +127 -44
- package/dist/live/bun-lockfile.d.ts +26 -0
- package/dist/live/bun-lockfile.js +123 -0
- package/dist/live/cargo-platform.d.ts +38 -0
- package/dist/live/cargo-platform.js +178 -0
- package/dist/live/changelog-classify.d.ts +41 -0
- package/dist/live/changelog-classify.js +207 -0
- package/dist/live/changelog.d.ts +64 -0
- package/dist/live/changelog.js +318 -0
- package/dist/live/crates-registry.js +3 -1
- package/dist/live/crates-versions.d.ts +47 -0
- package/dist/live/crates-versions.js +201 -0
- package/dist/live/dependency-set.d.ts +13 -0
- package/dist/live/dependency-set.js +95 -0
- package/dist/live/dev-scope.d.ts +32 -0
- package/dist/live/dev-scope.js +102 -0
- package/dist/live/file-signature.d.ts +18 -0
- package/dist/live/file-signature.js +43 -0
- package/dist/live/go-registry.js +3 -1
- package/dist/live/hn-fetcher.js +9 -3
- package/dist/live/index.d.ts +105 -2
- package/dist/live/index.js +266 -139
- package/dist/live/install-state.d.ts +43 -0
- package/dist/live/install-state.js +150 -0
- package/dist/live/js-lockfile-readers.d.ts +46 -0
- package/dist/live/js-lockfile-readers.js +181 -0
- package/dist/live/lockfile-parsers-pygo.d.ts +60 -0
- package/dist/live/lockfile-parsers-pygo.js +274 -0
- package/dist/live/lockfile-parsers.d.ts +69 -0
- package/dist/live/lockfile-parsers.js +311 -0
- package/dist/live/lockfile-types.d.ts +34 -0
- package/dist/live/lockfile-types.js +42 -0
- package/dist/live/maintenance.d.ts +33 -0
- package/dist/live/maintenance.js +37 -0
- package/dist/live/npm-packument.d.ts +47 -0
- package/dist/live/npm-packument.js +150 -0
- package/dist/live/npm-registry.js +67 -53
- package/dist/live/osv-scanner.d.ts +12 -1
- package/dist/live/osv-scanner.js +50 -15
- package/dist/live/package-archive.d.ts +49 -0
- package/dist/live/package-archive.js +186 -0
- package/dist/live/pypi-registry.js +3 -1
- package/dist/live/registry-health.d.ts +16 -0
- package/dist/live/registry-health.js +74 -0
- package/dist/live/release-metadata.d.ts +67 -0
- package/dist/live/release-metadata.js +16 -0
- package/dist/live/resolution.d.ts +43 -0
- package/dist/live/resolution.js +62 -0
- package/dist/live/semver-precedence.d.ts +30 -0
- package/dist/live/semver-precedence.js +83 -0
- package/dist/live/severity-scope.d.ts +57 -0
- package/dist/live/severity-scope.js +89 -0
- package/dist/live/types.d.ts +83 -0
- package/dist/live/upgrade-sources.d.ts +71 -0
- package/dist/live/upgrade-sources.js +198 -0
- package/dist/live/version-compare.d.ts +39 -0
- package/dist/live/version-compare.js +130 -0
- package/dist/live/version-resolver.d.ts +14 -1
- package/dist/live/version-resolver.js +93 -303
- package/dist/native-bindings.d.ts +19 -0
- package/dist/native-bindings.js +39 -0
- package/dist/project-manifests.d.ts +40 -0
- package/dist/project-manifests.js +193 -0
- package/dist/project-scanner.js +38 -48
- package/dist/project-tree.d.ts +41 -0
- package/dist/project-tree.js +110 -0
- package/dist/prompts.d.ts +28 -0
- package/dist/prompts.js +54 -0
- package/dist/schema-registry.d.ts +41 -29
- package/dist/schema-registry.js +98 -105
- package/dist/server-instructions.d.ts +11 -0
- package/dist/server-instructions.js +36 -0
- package/dist/tool-args.d.ts +28 -0
- package/dist/tool-args.js +93 -0
- package/dist/tool-dispatch.d.ts +1 -1
- package/dist/tool-dispatch.js +42 -8
- package/dist/tools/app-plan.d.ts +92 -0
- package/dist/tools/app-plan.js +155 -0
- package/dist/tools/briefing-stored-advisories.d.ts +20 -0
- package/dist/tools/briefing-stored-advisories.js +58 -0
- package/dist/tools/briefing-task-scope.d.ts +73 -0
- package/dist/tools/briefing-task-scope.js +197 -0
- package/dist/tools/briefing-wisdom.d.ts +31 -0
- package/dist/tools/briefing-wisdom.js +148 -0
- package/dist/tools/decision-enforcement.d.ts +0 -5
- package/dist/tools/decision-enforcement.js +0 -4
- package/dist/tools/decision-memory.js +18 -66
- package/dist/tools/dependency-check-signals.d.ts +74 -0
- package/dist/tools/dependency-check-signals.js +264 -0
- package/dist/tools/dependency-check.d.ts +98 -0
- package/dist/tools/dependency-check.js +295 -0
- package/dist/tools/dependency-health.js +74 -21
- package/dist/tools/developer-dna.d.ts +3 -0
- package/dist/tools/developer-dna.js +16 -15
- package/dist/tools/ecosystem-pulse.d.ts +6 -2
- package/dist/tools/ecosystem-pulse.js +53 -14
- package/dist/tools/get-actionable-signals.d.ts +26 -4
- package/dist/tools/get-actionable-signals.js +214 -251
- package/dist/tools/get-context.d.ts +2 -0
- package/dist/tools/get-context.js +9 -1
- package/dist/tools/get-relevant-content.d.ts +3 -1
- package/dist/tools/get-relevant-content.js +34 -3
- package/dist/tools/index.d.ts +4 -2
- package/dist/tools/index.js +4 -2
- package/dist/tools/install-drift-notes.d.ts +51 -0
- package/dist/tools/install-drift-notes.js +172 -0
- package/dist/tools/knowledge-gap-citations.d.ts +55 -0
- package/dist/tools/knowledge-gap-citations.js +132 -0
- package/dist/tools/knowledge-gap-exposure.d.ts +72 -0
- package/dist/tools/knowledge-gap-exposure.js +160 -0
- package/dist/tools/knowledge-gap-grading.d.ts +91 -0
- package/dist/tools/knowledge-gap-grading.js +157 -0
- package/dist/tools/knowledge-gap-installs.d.ts +65 -0
- package/dist/tools/knowledge-gap-installs.js +134 -0
- package/dist/tools/knowledge-gap-ranges.d.ts +51 -0
- package/dist/tools/knowledge-gap-ranges.js +146 -0
- package/dist/tools/knowledge-gap-scope.d.ts +63 -0
- package/dist/tools/knowledge-gap-scope.js +118 -0
- package/dist/tools/knowledge-gaps.d.ts +12 -3
- package/dist/tools/knowledge-gaps.js +172 -121
- package/dist/tools/package-presence.d.ts +24 -0
- package/dist/tools/package-presence.js +35 -0
- package/dist/tools/record-feedback.d.ts +0 -1
- package/dist/tools/record-feedback.js +7 -8
- package/dist/tools/signal-classifier.d.ts +25 -0
- package/dist/tools/signal-classifier.js +40 -0
- package/dist/tools/upgrade-impact-callsites.d.ts +52 -0
- package/dist/tools/upgrade-impact-callsites.js +226 -0
- package/dist/tools/upgrade-impact-literals.d.ts +45 -0
- package/dist/tools/upgrade-impact-literals.js +118 -0
- package/dist/tools/upgrade-impact-report.d.ts +80 -0
- package/dist/tools/upgrade-impact-report.js +178 -0
- package/dist/tools/upgrade-impact.d.ts +84 -0
- package/dist/tools/upgrade-impact.js +305 -0
- package/dist/tools/upgrade-planner.d.ts +41 -8
- package/dist/tools/upgrade-planner.js +201 -53
- package/dist/tools/vulnerability-scan-concise.d.ts +72 -0
- package/dist/tools/vulnerability-scan-concise.js +97 -0
- package/dist/tools/vulnerability-scan-format.d.ts +137 -0
- package/dist/tools/vulnerability-scan-format.js +276 -0
- package/dist/tools/vulnerability-scan.d.ts +338 -18
- package/dist/tools/vulnerability-scan.js +84 -125
- package/dist/tools/what-should-i-know.d.ts +86 -19
- package/dist/tools/what-should-i-know.js +277 -283
- package/dist/types.d.ts +4 -136
- package/package.json +35 -35
- package/dist/schemas/agent-memory.json +0 -49
- package/dist/schemas/decision-enforcement.json +0 -22
- package/dist/schemas/decision-memory.json +0 -54
- package/dist/schemas/dependency-health.json +0 -27
- package/dist/schemas/developer-dna.json +0 -219
- package/dist/schemas/ecosystem-pulse.json +0 -17
- package/dist/schemas/get-actionable-signals.json +0 -29
- package/dist/schemas/get-context.json +0 -17
- package/dist/schemas/get-relevant-content.json +0 -26
- package/dist/schemas/knowledge-gaps.json +0 -14
- package/dist/schemas/record-feedback.json +0 -24
- package/dist/schemas/upgrade-planner.json +0 -22
- package/dist/schemas/vulnerability-scan.json +0 -26
- package/dist/schemas/what-should-i-know.json +0 -19
package/dist/doctor.js
CHANGED
|
@@ -13,14 +13,18 @@
|
|
|
13
13
|
* 4da-mcp --doctor
|
|
14
14
|
*/
|
|
15
15
|
import { existsSync } from "node:fs";
|
|
16
|
-
import { FourDADatabase } from "./db.js";
|
|
16
|
+
import { FourDADatabase, nativeBindingProblem } from "./db.js";
|
|
17
|
+
import { checkBuildStaleness } from "./build-staleness.js";
|
|
17
18
|
export function runDoctor() {
|
|
18
19
|
console.log("\n 4DA MCP Server — Doctor\n");
|
|
19
20
|
const checks = [];
|
|
20
21
|
// 1. Node.js version
|
|
21
22
|
const nodeVersion = process.version;
|
|
22
23
|
const major = parseInt(nodeVersion.slice(1).split(".")[0], 10);
|
|
23
|
-
|
|
24
|
+
// 22, matching package.json engines: better-sqlite3 >= 12.10 ships no Node 20
|
|
25
|
+
// prebuild, so a Node 20 install compiles from source and fails without a
|
|
26
|
+
// C++ toolchain (measured 2026-10-03 on node:20-bookworm-slim and alpine).
|
|
27
|
+
const nodeRequired = 22;
|
|
24
28
|
checks.push({
|
|
25
29
|
name: "Node.js version",
|
|
26
30
|
status: major >= nodeRequired ? "pass" : "fail",
|
|
@@ -28,12 +32,14 @@ export function runDoctor() {
|
|
|
28
32
|
? `${nodeVersion} (>= ${nodeRequired} required)`
|
|
29
33
|
: `${nodeVersion} — Node.js ${nodeRequired}+ required`,
|
|
30
34
|
});
|
|
31
|
-
// 2. Native bindings —
|
|
32
|
-
//
|
|
35
|
+
// 2. Native bindings — opening a database, not just importing the package:
|
|
36
|
+
// the import succeeds without the compiled module (npm 12 blocking its
|
|
37
|
+
// install script left none, and this check used to say "loaded").
|
|
38
|
+
const bindingProblem = nativeBindingProblem();
|
|
33
39
|
checks.push({
|
|
34
40
|
name: "SQLite native bindings",
|
|
35
|
-
status: "pass",
|
|
36
|
-
detail: "better-sqlite3
|
|
41
|
+
status: bindingProblem ? "fail" : "pass",
|
|
42
|
+
detail: bindingProblem ?? "better-sqlite3 opened an in-memory database",
|
|
37
43
|
});
|
|
38
44
|
// 3. Database discovery — deep integrity_check is appropriate here: --doctor is
|
|
39
45
|
// an explicit, user-invoked diagnostic where a slow full scan is expected. The
|
|
@@ -70,7 +76,21 @@ export function runDoctor() {
|
|
|
70
76
|
detail: envExists ? envDb : `Set to ${envDb} but file does not exist`,
|
|
71
77
|
});
|
|
72
78
|
}
|
|
73
|
-
// 5.
|
|
79
|
+
// 5. Build freshness — repo checkouts only. `.mcp.json` runs dist/, and
|
|
80
|
+
// nothing rebuilds it on a merge; a stale dist served two-day-old behavior
|
|
81
|
+
// on 2026-08-30 with zero symptoms. The published npm package ships no src/
|
|
82
|
+
// (check returns null there) so users never see a false alarm.
|
|
83
|
+
const staleness = checkBuildStaleness();
|
|
84
|
+
if (staleness) {
|
|
85
|
+
checks.push({
|
|
86
|
+
name: "Server build freshness",
|
|
87
|
+
status: staleness.stale ? "fail" : "pass",
|
|
88
|
+
detail: staleness.stale
|
|
89
|
+
? staleness.note ?? "dist/ is older than src/ — rebuild"
|
|
90
|
+
: "dist/ is at least as new as src/",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
// 6. Semantic recall provider (optional)
|
|
74
94
|
const embedProvider = process.env.FOURDA_EMBED_PROVIDER;
|
|
75
95
|
const hasOpenAI = !!process.env.OPENAI_API_KEY;
|
|
76
96
|
const hasOllama = process.env.FOURDA_EMBED_PROVIDER === "ollama" ||
|
package/dist/embeddings.js
CHANGED
|
@@ -146,7 +146,13 @@ export async function embedText(text, config, signal) {
|
|
|
146
146
|
const res = await fetch(`${base}/api/embeddings`, {
|
|
147
147
|
method: "POST",
|
|
148
148
|
headers: { "Content-Type": "application/json" },
|
|
149
|
-
|
|
149
|
+
// The embedder runs on the CPU, as the 4DA app's does: on the GPU,
|
|
150
|
+
// Ollama evicts and reloads the local judge model around each call.
|
|
151
|
+
body: JSON.stringify({
|
|
152
|
+
model: config.model,
|
|
153
|
+
prompt: input,
|
|
154
|
+
options: { num_gpu: 0 },
|
|
155
|
+
}),
|
|
150
156
|
signal,
|
|
151
157
|
});
|
|
152
158
|
if (!res.ok)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marker files the Rust engine leaves beside `4da.db` when it cannot record
|
|
3
|
+
* an event in the database itself. `data_freshness` reads them so a
|
|
4
|
+
* DB-backed answer can say what happened to the data it was read from.
|
|
5
|
+
*
|
|
6
|
+
* Read-only by design: the app owns each marker's lifecycle. The engine
|
|
7
|
+
* clears `.engine-blocked` the moment a cycle opens the database again; the
|
|
8
|
+
* desktop app shows `.db-recovered` once and deletes it. Nothing here deletes
|
|
9
|
+
* either one, so every MCP answer keeps saying it until the app has.
|
|
10
|
+
*/
|
|
11
|
+
export interface EngineBlockMarker {
|
|
12
|
+
at: string;
|
|
13
|
+
error: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* `data/.engine-blocked`: written by the Rust engine (`engine_block.rs`) when
|
|
17
|
+
* a scheduled refresh is refused by a newer database schema.
|
|
18
|
+
*/
|
|
19
|
+
export declare function readEngineBlockMarker(dbFile: string): EngineBlockMarker | null;
|
|
20
|
+
/** The kinds the engine writes today. A newer engine's unknown kind is still surfaced. */
|
|
21
|
+
export type DbRecoveryKind = "restored_from_backup" | "quarantined_no_backup" | "recovery_failed";
|
|
22
|
+
export interface DbRecoveredMarker {
|
|
23
|
+
at: string;
|
|
24
|
+
kind: DbRecoveryKind | string;
|
|
25
|
+
detail: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* `data/.db-recovered`: written by the headless refresh engine when it
|
|
29
|
+
* restored the database from a backup or quarantined it.
|
|
30
|
+
* `{"at": "<ISO UTC>", "kind": "...", "detail": "<path or reason>"}`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function readDbRecoveredMarker(dbFile: string): DbRecoveredMarker | null;
|
|
33
|
+
/** The sentence `data_freshness.note` carries for a recovery marker. */
|
|
34
|
+
export declare function dbRecoveryNote(marker: DbRecoveredMarker): string;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Marker files the Rust engine leaves beside `4da.db` when it cannot record
|
|
4
|
+
* an event in the database itself. `data_freshness` reads them so a
|
|
5
|
+
* DB-backed answer can say what happened to the data it was read from.
|
|
6
|
+
*
|
|
7
|
+
* Read-only by design: the app owns each marker's lifecycle. The engine
|
|
8
|
+
* clears `.engine-blocked` the moment a cycle opens the database again; the
|
|
9
|
+
* desktop app shows `.db-recovered` once and deletes it. Nothing here deletes
|
|
10
|
+
* either one, so every MCP answer keeps saying it until the app has.
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
function markerPath(dbFile, name) {
|
|
15
|
+
// An in-memory database has no directory; do not read a marker out of the cwd.
|
|
16
|
+
if (!dbFile || dbFile === ":memory:")
|
|
17
|
+
return null;
|
|
18
|
+
return path.join(path.dirname(dbFile), name);
|
|
19
|
+
}
|
|
20
|
+
function readJsonMarker(dbFile, name) {
|
|
21
|
+
const file = markerPath(dbFile, name);
|
|
22
|
+
if (!file)
|
|
23
|
+
return null;
|
|
24
|
+
try {
|
|
25
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
26
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
27
|
+
? parsed
|
|
28
|
+
: null;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* `data/.engine-blocked`: written by the Rust engine (`engine_block.rs`) when
|
|
36
|
+
* a scheduled refresh is refused by a newer database schema.
|
|
37
|
+
*/
|
|
38
|
+
export function readEngineBlockMarker(dbFile) {
|
|
39
|
+
const v = readJsonMarker(dbFile, ".engine-blocked");
|
|
40
|
+
if (v && typeof v.at === "string" && typeof v.error === "string") {
|
|
41
|
+
return { at: v.at, error: v.error };
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* `data/.db-recovered`: written by the headless refresh engine when it
|
|
47
|
+
* restored the database from a backup or quarantined it.
|
|
48
|
+
* `{"at": "<ISO UTC>", "kind": "...", "detail": "<path or reason>"}`.
|
|
49
|
+
*/
|
|
50
|
+
export function readDbRecoveredMarker(dbFile) {
|
|
51
|
+
const v = readJsonMarker(dbFile, ".db-recovered");
|
|
52
|
+
if (!v)
|
|
53
|
+
return null;
|
|
54
|
+
const { at, kind, detail } = v;
|
|
55
|
+
if (typeof at !== "string" || typeof kind !== "string" || typeof detail !== "string")
|
|
56
|
+
return null;
|
|
57
|
+
if (!at.trim() || !kind.trim())
|
|
58
|
+
return null;
|
|
59
|
+
return { at, kind, detail };
|
|
60
|
+
}
|
|
61
|
+
/** The sentence `data_freshness.note` carries for a recovery marker. */
|
|
62
|
+
export function dbRecoveryNote(marker) {
|
|
63
|
+
const { at, kind, detail } = marker;
|
|
64
|
+
switch (kind) {
|
|
65
|
+
case "restored_from_backup":
|
|
66
|
+
return `The database was restored from a backup by the background refresh at ${at} — results may be incomplete or empty; the preserved file is ${detail}.`;
|
|
67
|
+
case "quarantined_no_backup":
|
|
68
|
+
return `The database was replaced with a fresh empty database by the background refresh at ${at} — results may be incomplete or empty; the preserved file is ${detail}.`;
|
|
69
|
+
case "recovery_failed":
|
|
70
|
+
return `The background refresh failed to recover the database at ${at} — results may be incomplete or empty; ${detail}.`;
|
|
71
|
+
default:
|
|
72
|
+
return `The background refresh recorded a database recovery (${kind}) at ${at} — results may be incomplete or empty; ${detail}.`;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enough of .gitignore to decide whether a directory or lockfile belongs to
|
|
3
|
+
* the project, for the standalone project-tree walk.
|
|
4
|
+
*
|
|
5
|
+
* A directory the repository ignores is not part of the project: scratch
|
|
6
|
+
* clones, generated output, a sibling tool checked out inside the tree.
|
|
7
|
+
* Measured 2026-10-02: walking 4DA's root without this scanned the ignored
|
|
8
|
+
* `victauri-gauntlet/` and `cli/` and reported their advisories (rustls, h2,
|
|
9
|
+
* scc) as the project's, where osv-scanner, which honours .gitignore, did not.
|
|
10
|
+
*
|
|
11
|
+
* Read from the files, never by running git: `git check-ignore` in a cloned
|
|
12
|
+
* repository honours that repository's config, and a config can name programs
|
|
13
|
+
* for git to run (core.fsmonitor). Reading a scanned folder must not execute
|
|
14
|
+
* anything it names.
|
|
15
|
+
*
|
|
16
|
+
* Supported: comments, blank lines, `dir/` (directory only), leading `/` and
|
|
17
|
+
* inner `/` anchoring, `*`, `?`, `**`, nested .gitignore files and
|
|
18
|
+
* .git/info/exclude. A negation (`!pattern`) is treated conservatively: a
|
|
19
|
+
* path that any negation could re-include is never skipped.
|
|
20
|
+
*/
|
|
21
|
+
/** Ignore rules gathered while walking down from a root. */
|
|
22
|
+
export declare class IgnoreRules {
|
|
23
|
+
private readonly root;
|
|
24
|
+
private rules;
|
|
25
|
+
private loaded;
|
|
26
|
+
constructor(root: string);
|
|
27
|
+
/** Read the .gitignore of a directory (relative to the root) once. */
|
|
28
|
+
load(relDir: string): void;
|
|
29
|
+
/**
|
|
30
|
+
* Whether an absolute directory below the root lies in, or is, an ignored
|
|
31
|
+
* directory. Every ancestor is checked, loading nested .gitignore files on
|
|
32
|
+
* the way down. A directory outside the root is never "ignored" here.
|
|
33
|
+
*/
|
|
34
|
+
ignoresDirectory(absDir: string): boolean;
|
|
35
|
+
/** Whether a path (relative to the root, `/`-separated) is ignored. */
|
|
36
|
+
ignores(relPath: string, isDir: boolean): boolean;
|
|
37
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Enough of .gitignore to decide whether a directory or lockfile belongs to
|
|
4
|
+
* the project, for the standalone project-tree walk.
|
|
5
|
+
*
|
|
6
|
+
* A directory the repository ignores is not part of the project: scratch
|
|
7
|
+
* clones, generated output, a sibling tool checked out inside the tree.
|
|
8
|
+
* Measured 2026-10-02: walking 4DA's root without this scanned the ignored
|
|
9
|
+
* `victauri-gauntlet/` and `cli/` and reported their advisories (rustls, h2,
|
|
10
|
+
* scc) as the project's, where osv-scanner, which honours .gitignore, did not.
|
|
11
|
+
*
|
|
12
|
+
* Read from the files, never by running git: `git check-ignore` in a cloned
|
|
13
|
+
* repository honours that repository's config, and a config can name programs
|
|
14
|
+
* for git to run (core.fsmonitor). Reading a scanned folder must not execute
|
|
15
|
+
* anything it names.
|
|
16
|
+
*
|
|
17
|
+
* Supported: comments, blank lines, `dir/` (directory only), leading `/` and
|
|
18
|
+
* inner `/` anchoring, `*`, `?`, `**`, nested .gitignore files and
|
|
19
|
+
* .git/info/exclude. A negation (`!pattern`) is treated conservatively: a
|
|
20
|
+
* path that any negation could re-include is never skipped.
|
|
21
|
+
*/
|
|
22
|
+
import * as fs from "node:fs";
|
|
23
|
+
import * as path from "node:path";
|
|
24
|
+
const caseless = process.platform === "win32" || process.platform === "darwin";
|
|
25
|
+
function globToRegex(glob, anchored) {
|
|
26
|
+
let re = "";
|
|
27
|
+
for (let i = 0; i < glob.length; i++) {
|
|
28
|
+
const ch = glob[i];
|
|
29
|
+
if (ch === "*") {
|
|
30
|
+
if (glob[i + 1] === "*") {
|
|
31
|
+
re += ".*";
|
|
32
|
+
i++;
|
|
33
|
+
if (glob[i + 1] === "/")
|
|
34
|
+
i++;
|
|
35
|
+
}
|
|
36
|
+
else
|
|
37
|
+
re += "[^/]*";
|
|
38
|
+
}
|
|
39
|
+
else if (ch === "?")
|
|
40
|
+
re += "[^/]";
|
|
41
|
+
else
|
|
42
|
+
re += ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
43
|
+
}
|
|
44
|
+
// Unanchored patterns match at any depth below their file's directory.
|
|
45
|
+
return new RegExp(`^${anchored ? "" : "(?:.*/)?"}${re}(?:/.*)?$`, caseless ? "i" : "");
|
|
46
|
+
}
|
|
47
|
+
function parseRules(content, base) {
|
|
48
|
+
const rules = [];
|
|
49
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
50
|
+
let line = raw.replace(/\s+$/, "");
|
|
51
|
+
if (!line || line.startsWith("#"))
|
|
52
|
+
continue;
|
|
53
|
+
const negated = line.startsWith("!");
|
|
54
|
+
if (negated)
|
|
55
|
+
line = line.slice(1);
|
|
56
|
+
const dirOnly = line.endsWith("/");
|
|
57
|
+
if (dirOnly)
|
|
58
|
+
line = line.slice(0, -1);
|
|
59
|
+
const anchored = line.startsWith("/") || line.includes("/");
|
|
60
|
+
line = line.replace(/^\//, "");
|
|
61
|
+
if (!line)
|
|
62
|
+
continue;
|
|
63
|
+
rules.push({ base, regex: globToRegex(line, anchored), dirOnly, negated });
|
|
64
|
+
}
|
|
65
|
+
return rules;
|
|
66
|
+
}
|
|
67
|
+
/** Ignore rules gathered while walking down from a root. */
|
|
68
|
+
export class IgnoreRules {
|
|
69
|
+
root;
|
|
70
|
+
rules = [];
|
|
71
|
+
loaded = new Set();
|
|
72
|
+
constructor(root) {
|
|
73
|
+
this.root = root;
|
|
74
|
+
this.load("");
|
|
75
|
+
try {
|
|
76
|
+
this.rules.push(...parseRules(fs.readFileSync(path.join(root, ".git", "info", "exclude"), "utf-8"), ""));
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Not a git checkout, or no exclude file.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Read the .gitignore of a directory (relative to the root) once. */
|
|
83
|
+
load(relDir) {
|
|
84
|
+
if (this.loaded.has(relDir))
|
|
85
|
+
return;
|
|
86
|
+
this.loaded.add(relDir);
|
|
87
|
+
try {
|
|
88
|
+
this.rules.push(...parseRules(fs.readFileSync(path.join(this.root, relDir, ".gitignore"), "utf-8"), relDir));
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// No .gitignore here.
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Whether an absolute directory below the root lies in, or is, an ignored
|
|
96
|
+
* directory. Every ancestor is checked, loading nested .gitignore files on
|
|
97
|
+
* the way down. A directory outside the root is never "ignored" here.
|
|
98
|
+
*/
|
|
99
|
+
ignoresDirectory(absDir) {
|
|
100
|
+
const rel = path.relative(this.root, absDir).replace(/\\/g, "/");
|
|
101
|
+
if (!rel || rel.startsWith("..") || path.isAbsolute(rel))
|
|
102
|
+
return false;
|
|
103
|
+
const segments = rel.split("/");
|
|
104
|
+
for (let i = 1; i <= segments.length; i++) {
|
|
105
|
+
this.load(segments.slice(0, i - 1).join("/"));
|
|
106
|
+
if (this.ignores(segments.slice(0, i).join("/"), true))
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
/** Whether a path (relative to the root, `/`-separated) is ignored. */
|
|
112
|
+
ignores(relPath, isDir) {
|
|
113
|
+
let ignored = false;
|
|
114
|
+
let reincludable = false;
|
|
115
|
+
for (const rule of this.rules) {
|
|
116
|
+
if (rule.base && !relPath.startsWith(`${rule.base}/`))
|
|
117
|
+
continue;
|
|
118
|
+
const local = rule.base ? relPath.slice(rule.base.length + 1) : relPath;
|
|
119
|
+
if (rule.dirOnly && !isDir && !local.includes("/"))
|
|
120
|
+
continue;
|
|
121
|
+
if (!rule.regex.test(local))
|
|
122
|
+
continue;
|
|
123
|
+
if (rule.negated)
|
|
124
|
+
reincludable = true;
|
|
125
|
+
else
|
|
126
|
+
ignored = true;
|
|
127
|
+
}
|
|
128
|
+
return ignored && !reincludable;
|
|
129
|
+
}
|
|
130
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -15,11 +15,13 @@
|
|
|
15
15
|
* secret (MCP_AUTH_SECRET) and then verifies an HMAC-SHA256-signed Bearer
|
|
16
16
|
* token on every request, enforcing the token's role at tool dispatch.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
18
|
+
* 16 tools across 5 categories, plus the `deps` prompt. Upgrade impact,
|
|
19
|
+
* live vulnerability scanning (OSV.dev), pre-install dependency checks,
|
|
19
20
|
* ecosystem news, persistent memory, and tech stack awareness for any MCP host.
|
|
20
21
|
*
|
|
21
22
|
* Categories (canonical — matches schema-registry.ts `ToolCategory`):
|
|
22
|
-
* Security (
|
|
23
|
+
* Security (5) — vulnerability scanning, dependency health, upgrade planning,
|
|
24
|
+
* upgrade impact, dependency check
|
|
23
25
|
* Intelligence (7) — briefing, ecosystem pulse, context, content feed,
|
|
24
26
|
* actionable signals, knowledge gaps, feedback
|
|
25
27
|
* Decisions (2) — decision memory, alignment checking
|
|
@@ -40,14 +42,18 @@ import { homedir } from "node:os";
|
|
|
40
42
|
import { startHttpServer } from "./http-transport.js";
|
|
41
43
|
import { runSetup } from "./setup.js";
|
|
42
44
|
import { runDoctor } from "./doctor.js";
|
|
43
|
-
import {
|
|
45
|
+
import { scanProjectTree, treeResolutionGroups } from "./project-tree.js";
|
|
46
|
+
import { IgnoreRules } from "./gitignore.js";
|
|
44
47
|
import { LiveIntelligence } from "./live/index.js";
|
|
45
|
-
import { deriveTechStackForHeadlines } from "./tools/ecosystem-pulse.js";
|
|
46
48
|
import { setLiveIntelligence } from "./live-singleton.js";
|
|
47
|
-
|
|
48
|
-
import {
|
|
49
|
+
import { SERVER_INSTRUCTIONS } from "./server-instructions.js";
|
|
50
|
+
import { validateToolArgs } from "./tool-args.js";
|
|
51
|
+
// Tool registry: listing, schema resources, category metadata
|
|
52
|
+
import { getCategoryManifest, getSchemaResources, getSlimToolList, getToolSchemaDocument, hasToolSchema, TOOL_REGISTRY, } from "./schema-registry.js";
|
|
49
53
|
// Map-based tool dispatch (replaces per-tool imports + switch statement)
|
|
50
54
|
import { dispatchTool } from "./tool-dispatch.js";
|
|
55
|
+
import { getPrompt, listPrompts } from "./prompts.js";
|
|
56
|
+
import { checkBuildStaleness } from "./build-staleness.js";
|
|
51
57
|
const __filename = fileURLToPath(import.meta.url);
|
|
52
58
|
const __dirname = dirname(__filename);
|
|
53
59
|
// Single source of truth for the server version — read from package.json so
|
|
@@ -99,29 +105,30 @@ function getDatabase() {
|
|
|
99
105
|
// the user's code) — the .mcpb bundle wires its directory picker to this.
|
|
100
106
|
if (db.isStandalone) {
|
|
101
107
|
const cwd = resolveProjectDir();
|
|
102
|
-
|
|
108
|
+
// The root and every independently-locked project below it
|
|
109
|
+
// (project-tree.ts): a repo root's own lockfile is often not the only one.
|
|
110
|
+
const tree = scanProjectTree(cwd);
|
|
111
|
+
const scan = tree[0].scan;
|
|
103
112
|
db.populateFromScan(scan);
|
|
113
|
+
const groups = treeResolutionGroups(tree);
|
|
104
114
|
const detected = [
|
|
105
115
|
...scan.languages,
|
|
106
116
|
...scan.frameworks,
|
|
107
117
|
].filter(Boolean);
|
|
108
|
-
console.error(`[4DA] Standalone mode: scanned ${scan.projectPath}`);
|
|
109
|
-
if (
|
|
110
|
-
console.error(`[4DA] Detected: ${detected.join(", ")} | ${scan.dependencies.length} deps, ${scan.devDependencies.length} dev deps`);
|
|
118
|
+
console.error(`[4DA] Standalone mode: scanned ${scan.projectPath} (${tree.length} project${tree.length === 1 ? "" : "s"})`);
|
|
119
|
+
if (groups.length > 0) {
|
|
120
|
+
console.error(`[4DA] Detected: ${detected.join(", ") || "lockfiles"} | ${scan.dependencies.length} deps, ${scan.devDependencies.length} dev deps at the root`);
|
|
111
121
|
// Initialize live intelligence with per-ecosystem resolved versions
|
|
112
|
-
// (
|
|
113
|
-
//
|
|
114
|
-
liveIntel.
|
|
122
|
+
// (targets carry platform-gated dep info so advisories can be flagged
|
|
123
|
+
// platform-relevant for the host).
|
|
124
|
+
liveIntel.initFromProjectTree(cwd, groups);
|
|
115
125
|
if (liveIntel.isEnabled()) {
|
|
116
|
-
console.error(`[4DA] Live intelligence: enabled (OSV.dev
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
liveIntel.fetchHeadlines(techStack).catch((err) => {
|
|
123
|
-
console.error(`[4DA] Headline prefetch failed: ${err instanceof Error ? err.message : String(err)}.`);
|
|
124
|
-
});
|
|
126
|
+
console.error(`[4DA] Live intelligence: enabled (OSV.dev)`);
|
|
127
|
+
// The vulnerability scan goes through the warmup so a briefing that
|
|
128
|
+
// arrives before it finishes can await it instead of reading nothing.
|
|
129
|
+
// Headlines are fetched only when ecosystem_pulse is called.
|
|
130
|
+
liveIntel.startVulnerabilityWarmup(cwd);
|
|
131
|
+
console.error(`[4DA] Vulnerability scan warming in background (OSV.dev).`);
|
|
125
132
|
}
|
|
126
133
|
}
|
|
127
134
|
else {
|
|
@@ -144,6 +151,10 @@ function getDatabase() {
|
|
|
144
151
|
// into this project's vulnerability scan.
|
|
145
152
|
const norm = (p) => p.replace(/\\/g, "/").toLowerCase().replace(/\/+$/, "");
|
|
146
153
|
const rootNorm = norm(resolveProjectDir());
|
|
154
|
+
// The app indexes every project it can see, including directories the
|
|
155
|
+
// repository ignores (4DA's victauri-gauntlet/ and cli/); their
|
|
156
|
+
// advisories are not this project's. Same rule as standalone mode.
|
|
157
|
+
const ignore = new IgnoreRules(resolveProjectDir());
|
|
147
158
|
const groups = new Map();
|
|
148
159
|
for (const row of rows) {
|
|
149
160
|
const projectPath = row.project_path || process.cwd();
|
|
@@ -152,6 +163,8 @@ function getDatabase() {
|
|
|
152
163
|
continue;
|
|
153
164
|
if (pp.includes("/.claude/worktrees/") || pp.includes("/.codex/worktrees/"))
|
|
154
165
|
continue;
|
|
166
|
+
if (rootNorm && ignore.ignoresDirectory(projectPath))
|
|
167
|
+
continue;
|
|
155
168
|
const language = row.language || "npm";
|
|
156
169
|
const key = `${projectPath}::${language}`;
|
|
157
170
|
let group = groups.get(key);
|
|
@@ -166,15 +179,19 @@ function getDatabase() {
|
|
|
166
179
|
if (groups.size > 0) {
|
|
167
180
|
liveIntel.initFromDependencyGroups([...groups.values()]);
|
|
168
181
|
}
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
// cache forever. Non-blocking; the tool also fetches on demand now.
|
|
182
|
+
// Headlines are fetched only when ecosystem_pulse is called: the
|
|
183
|
+
// search terms come from the user's projects.
|
|
172
184
|
if (liveIntel.isEnabled()) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
185
|
+
// Warm the vulnerability scan too. This branch never scanned: the
|
|
186
|
+
// dependency set was initialised, headlines were prefetched, and
|
|
187
|
+
// `lastVulnScan` stayed null until some tool called
|
|
188
|
+
// vulnerability_scan — so the first what_should_i_know of a session
|
|
189
|
+
// read an empty cache and answered "safe_to_delegate" for a task
|
|
190
|
+
// that named a package with an open advisory (verified live
|
|
191
|
+
// 2026-09-07: 0 advisories at t+0, 11 advisories at t+7min).
|
|
192
|
+
if (liveIntel.isInitialized()) {
|
|
193
|
+
liveIntel.startVulnerabilityWarmup(resolveProjectDir());
|
|
194
|
+
console.error(`[4DA] Vulnerability scan warming in background (OSV.dev) for ${liveIntel.getAuditDeps().length} resolved dependencies.`);
|
|
178
195
|
}
|
|
179
196
|
}
|
|
180
197
|
}
|
|
@@ -189,6 +206,37 @@ function getDatabase() {
|
|
|
189
206
|
}
|
|
190
207
|
return db;
|
|
191
208
|
}
|
|
209
|
+
/** Standalone vs desktop-app database, decided once by the cheap probe (no scan, no resolution). */
|
|
210
|
+
let standaloneMode = null;
|
|
211
|
+
function isStandaloneMode() {
|
|
212
|
+
if (db)
|
|
213
|
+
return db.isStandalone;
|
|
214
|
+
if (standaloneMode === null) {
|
|
215
|
+
const probe = FourDADatabase.validateDatabase(process.env.FOURDA_DB_PATH || undefined);
|
|
216
|
+
standaloneMode = probe.standalone === true;
|
|
217
|
+
}
|
|
218
|
+
return standaloneMode;
|
|
219
|
+
}
|
|
220
|
+
let backgroundInitScheduled = false;
|
|
221
|
+
/**
|
|
222
|
+
* Start the full init (database, project scan, lockfile resolution, scan
|
|
223
|
+
* warmup) after the current response has been written. It is synchronous
|
|
224
|
+
* work, so it runs on a later tick; a tool call that arrives first simply
|
|
225
|
+
* performs it itself, exactly as before.
|
|
226
|
+
*/
|
|
227
|
+
function scheduleBackgroundInit() {
|
|
228
|
+
if (backgroundInitScheduled || db)
|
|
229
|
+
return;
|
|
230
|
+
backgroundInitScheduled = true;
|
|
231
|
+
setTimeout(() => {
|
|
232
|
+
try {
|
|
233
|
+
getDatabase();
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
console.error(`[4DA] Background init failed (tools will retry on first call): ${err instanceof Error ? err.message : String(err)}`);
|
|
237
|
+
}
|
|
238
|
+
}, 50).unref?.();
|
|
239
|
+
}
|
|
192
240
|
// =============================================================================
|
|
193
241
|
// Server Factory
|
|
194
242
|
// =============================================================================
|
|
@@ -205,17 +253,23 @@ export function buildServer() {
|
|
|
205
253
|
name: "4da-server",
|
|
206
254
|
version: SERVER_VERSION,
|
|
207
255
|
}, {
|
|
256
|
+
// `listChanged` was advertised and never sent: the tool set is fixed for
|
|
257
|
+
// the life of the process, so the honest capability is none.
|
|
208
258
|
capabilities: {
|
|
209
|
-
tools: {
|
|
259
|
+
tools: {},
|
|
210
260
|
resources: {},
|
|
261
|
+
prompts: {},
|
|
211
262
|
},
|
|
263
|
+
instructions: SERVER_INSTRUCTIONS,
|
|
212
264
|
});
|
|
213
|
-
//
|
|
214
|
-
//
|
|
265
|
+
// tools/list answers from a cheap mode probe, never from the full init. It
|
|
266
|
+
// used to open the database AND resolve every lockfile synchronously first
|
|
267
|
+
// (measured 6.6 s on a 2,063-dependency tree), stalling the host's handshake.
|
|
268
|
+
// The full init is started right after the first listing is sent.
|
|
215
269
|
server.setRequestHandler("tools/list", async () => {
|
|
216
|
-
|
|
270
|
+
scheduleBackgroundInit();
|
|
217
271
|
return {
|
|
218
|
-
tools: getSlimToolList(
|
|
272
|
+
tools: getSlimToolList(isStandaloneMode() ? true : undefined),
|
|
219
273
|
};
|
|
220
274
|
});
|
|
221
275
|
// List schema resources: full tool schemas as MCP Resources for lazy
|
|
@@ -278,29 +332,45 @@ export function buildServer() {
|
|
|
278
332
|
throw new Error(`Invalid resource URI: ${uri}`);
|
|
279
333
|
}
|
|
280
334
|
const toolName = match[1];
|
|
281
|
-
|
|
335
|
+
const document = hasToolSchema(toolName) ? getToolSchemaDocument(toolName) : null;
|
|
336
|
+
if (!document) {
|
|
282
337
|
throw new Error(`Unknown tool: ${toolName}`);
|
|
283
338
|
}
|
|
284
|
-
const schemaFile = getSchemaFilename(toolName);
|
|
285
|
-
if (!schemaFile) {
|
|
286
|
-
throw new Error(`No schema file for tool: ${toolName}`);
|
|
287
|
-
}
|
|
288
|
-
// Read schema from file
|
|
289
|
-
const schemaPath = join(__dirname, "schemas", schemaFile);
|
|
290
|
-
const schemaContent = readFileSync(schemaPath, "utf-8");
|
|
291
339
|
return {
|
|
292
340
|
contents: [
|
|
293
341
|
{
|
|
294
342
|
uri,
|
|
295
343
|
mimeType: "application/json",
|
|
296
|
-
text:
|
|
344
|
+
text: JSON.stringify(document, null, 2),
|
|
297
345
|
},
|
|
298
346
|
],
|
|
299
347
|
};
|
|
300
348
|
});
|
|
349
|
+
// Prompts: user-invoked workflows (surfaced as slash commands by some hosts).
|
|
350
|
+
server.setRequestHandler("prompts/list", async () => ({ prompts: listPrompts() }));
|
|
351
|
+
server.setRequestHandler("prompts/get", async (request) => {
|
|
352
|
+
const prompt = getPrompt(request.params.name, request.params.arguments);
|
|
353
|
+
if (!prompt) {
|
|
354
|
+
throw new Error(`Unknown prompt: ${request.params.name}`);
|
|
355
|
+
}
|
|
356
|
+
return prompt;
|
|
357
|
+
});
|
|
301
358
|
// Execute a tool
|
|
302
359
|
server.setRequestHandler("tools/call", async (request) => {
|
|
303
360
|
const { name, arguments: args } = request.params;
|
|
361
|
+
// Arguments are checked against the published schema before anything
|
|
362
|
+
// runs, so a wrong type or a misspelled parameter comes back as a
|
|
363
|
+
// correctable error rather than a Node exception or a silent no-op.
|
|
364
|
+
const entry = TOOL_REGISTRY[name];
|
|
365
|
+
if (entry) {
|
|
366
|
+
const problem = validateToolArgs(name, entry.definition.inputSchema, (args ?? {}));
|
|
367
|
+
if (problem) {
|
|
368
|
+
return {
|
|
369
|
+
content: [{ type: "text", text: JSON.stringify({ error: problem }) }],
|
|
370
|
+
isError: true,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
}
|
|
304
374
|
try {
|
|
305
375
|
const database = getDatabase();
|
|
306
376
|
return await dispatchTool(name, database, args);
|
|
@@ -333,6 +403,19 @@ export function buildServer() {
|
|
|
333
403
|
*/
|
|
334
404
|
async function main() {
|
|
335
405
|
const args = process.argv.slice(2);
|
|
406
|
+
// Say it before anything else: a stale dist answers with code the repo has
|
|
407
|
+
// already replaced (the 2026-08-30 server was two days behind its src and
|
|
408
|
+
// its already-fixed bugs were nearly re-diagnosed as live). stderr only —
|
|
409
|
+
// stdout is the MCP protocol.
|
|
410
|
+
try {
|
|
411
|
+
const staleness = checkBuildStaleness();
|
|
412
|
+
if (staleness?.stale && staleness.note) {
|
|
413
|
+
console.error(`[4DA] WARNING: ${staleness.note}`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
catch {
|
|
417
|
+
// The self-check must never block startup.
|
|
418
|
+
}
|
|
336
419
|
// Version
|
|
337
420
|
if (args.includes("--version") || args.includes("-v")) {
|
|
338
421
|
console.log(`@4da/mcp-server ${SERVER_VERSION}`);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bun.lock reader (Bun 1.2+ text lockfile).
|
|
3
|
+
*
|
|
4
|
+
* Measured 2026-10-03 on the pre-publish held-out benchmark: the FastAPI
|
|
5
|
+
* full-stack template's frontend is locked by bun.lock only, and the server
|
|
6
|
+
* read nothing from it — 52 advisories osv-scanner reports (axios 1.18.0,
|
|
7
|
+
* next 16.2.6, @babel/core 7.28.6, ...) were never scanned.
|
|
8
|
+
*
|
|
9
|
+
* Format: JSONC (trailing commas). `workspaces` lists each workspace's
|
|
10
|
+
* declared dependencies; `packages` maps a resolution key to
|
|
11
|
+
* `["name@version", registry, { dependencies, optionalDependencies, ... }, integrity]`.
|
|
12
|
+
* The key is the package name for the hoisted copy and a path for a nested
|
|
13
|
+
* one ("next/postcss" is the postcss next resolves to; scoped names keep their
|
|
14
|
+
* slash: "@tanstack/router-plugin/@babel/core"). Workspace, link, file, git and
|
|
15
|
+
* github entries do not name a registry version and are skipped.
|
|
16
|
+
*/
|
|
17
|
+
import type { InstanceSet } from "./lockfile-types.js";
|
|
18
|
+
/** JSONC to JSON: drops trailing commas outside strings (bun.lock has no comments). */
|
|
19
|
+
export declare function stripTrailingCommas(text: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Reads a bun.lock into `versions` (the hoisted copy of each name) and
|
|
22
|
+
* `instances` (every copy). Dev scope is known for every copy: dev when only
|
|
23
|
+
* the workspaces' devDependencies reach it, runtime when a dependency or
|
|
24
|
+
* optionalDependency does.
|
|
25
|
+
*/
|
|
26
|
+
export declare function readBunLock(content: string, versions: Map<string, string>, instances?: InstanceSet): void;
|