@4da/mcp-server 6.2.4 → 6.2.5
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 +484 -0
- package/README.md +22 -6
- package/dist/db.d.ts +38 -0
- package/dist/db.js +78 -23
- package/dist/doctor.js +6 -3
- package/dist/embeddings.d.ts +1 -1
- package/dist/embeddings.js +16 -14
- package/dist/index.js +73 -103
- package/dist/live/advisory-groups.d.ts +68 -0
- package/dist/live/advisory-groups.js +131 -0
- package/dist/live/cache.js +11 -3
- package/dist/live/cargo-platform.d.ts +6 -0
- package/dist/live/cargo-platform.js +8 -2
- package/dist/live/changelog.js +38 -6
- package/dist/live/dependency-set.d.ts +9 -1
- package/dist/live/dependency-set.js +10 -2
- package/dist/live/fix-paths.d.ts +258 -9
- package/dist/live/fix-paths.js +790 -45
- package/dist/live/github-releases.js +3 -0
- package/dist/live/held-dependencies.d.ts +93 -0
- package/dist/live/held-dependencies.js +212 -0
- package/dist/live/held-targets.d.ts +30 -0
- package/dist/live/held-targets.js +107 -0
- package/dist/live/http-utils.js +13 -3
- package/dist/live/index.d.ts +7 -4
- package/dist/live/index.js +24 -19
- package/dist/live/install-state.js +6 -2
- package/dist/live/js-lockfile-readers.d.ts +8 -1
- package/dist/live/js-lockfile-readers.js +43 -6
- package/dist/live/lockfile-parents.d.ts +15 -0
- package/dist/live/lockfile-parents.js +85 -10
- package/dist/live/lockfile-parsers-pygo.d.ts +47 -2
- package/dist/live/lockfile-parsers-pygo.js +230 -12
- package/dist/live/lockfile-parsers-rbphp.d.ts +38 -0
- package/dist/live/lockfile-parsers-rbphp.js +123 -0
- package/dist/live/lockfile-parsers.d.ts +6 -0
- package/dist/live/lockfile-parsers.js +116 -4
- package/dist/live/lockfile-types.d.ts +28 -1
- package/dist/live/maintenance.d.ts +20 -4
- package/dist/live/maintenance.js +31 -2
- package/dist/live/malware.d.ts +20 -0
- package/dist/live/malware.js +31 -0
- package/dist/live/npm-packument.d.ts +4 -1
- package/dist/live/npm-packument.js +5 -2
- package/dist/live/offered-release.d.ts +53 -0
- package/dist/live/offered-release.js +117 -0
- package/dist/live/osv-ranges.d.ts +111 -0
- package/dist/live/osv-ranges.js +173 -14
- package/dist/live/osv-scanner.d.ts +87 -5
- package/dist/live/osv-scanner.js +310 -59
- package/dist/live/pinned-advisories.d.ts +2 -2
- package/dist/live/pinned-advisories.js +3 -3
- package/dist/live/pypi-advice.d.ts +36 -0
- package/dist/live/pypi-advice.js +158 -0
- package/dist/live/rate-limiter.d.ts +7 -0
- package/dist/live/rate-limiter.js +22 -1
- package/dist/live/release-metadata.d.ts +5 -0
- package/dist/live/resolution.d.ts +3 -0
- package/dist/live/resolution.js +6 -3
- package/dist/live/scan-coverage.d.ts +26 -0
- package/dist/live/scan-coverage.js +70 -0
- package/dist/live/semver-utils.d.ts +8 -0
- package/dist/live/semver-utils.js +26 -3
- package/dist/live/severity-scope.d.ts +26 -1
- package/dist/live/severity-scope.js +32 -0
- package/dist/live/shell-safe.d.ts +19 -0
- package/dist/live/shell-safe.js +36 -0
- package/dist/live/types.d.ts +134 -8
- package/dist/live/version-compare.d.ts +49 -0
- package/dist/live/version-compare.js +131 -0
- package/dist/live/withdrawn-releases.d.ts +50 -0
- package/dist/live/withdrawn-releases.js +125 -0
- package/dist/project-manifests.d.ts +7 -0
- package/dist/project-manifests.js +45 -5
- package/dist/project-scanner.js +6 -4
- package/dist/project-tree.d.ts +56 -0
- package/dist/project-tree.js +174 -5
- package/dist/schema-registry.d.ts +47 -4
- package/dist/schema-registry.js +76 -9
- package/dist/server-instructions.js +4 -2
- package/dist/setup.d.ts +8 -0
- package/dist/setup.js +21 -2
- package/dist/tool-dispatch.js +3 -3
- package/dist/tools/agent-memory.d.ts +5 -0
- package/dist/tools/agent-memory.js +9 -0
- package/dist/tools/briefing-task-scope.js +5 -2
- package/dist/tools/decision-memory.js +2 -0
- package/dist/tools/dependency-check-held.d.ts +44 -0
- package/dist/tools/dependency-check-held.js +98 -0
- package/dist/tools/dependency-check-signals.d.ts +19 -2
- package/dist/tools/dependency-check-signals.js +84 -13
- package/dist/tools/dependency-check-versions.d.ts +37 -4
- package/dist/tools/dependency-check-versions.js +48 -13
- package/dist/tools/dependency-check.d.ts +11 -1
- package/dist/tools/dependency-check.js +81 -29
- package/dist/tools/dependency-health.js +209 -45
- package/dist/tools/get-context.d.ts +3 -0
- package/dist/tools/get-context.js +4 -0
- package/dist/tools/project-scope.d.ts +8 -0
- package/dist/tools/project-scope.js +21 -0
- package/dist/tools/upgrade-impact-features.d.ts +37 -0
- package/dist/tools/upgrade-impact-features.js +176 -0
- package/dist/tools/upgrade-impact-literals.js +26 -4
- package/dist/tools/upgrade-impact-report.d.ts +5 -1
- package/dist/tools/upgrade-impact-report.js +26 -5
- package/dist/tools/upgrade-impact.js +10 -3
- package/dist/tools/upgrade-planner-fixpaths.d.ts +162 -5
- package/dist/tools/upgrade-planner-fixpaths.js +0 -0
- package/dist/tools/upgrade-planner.d.ts +59 -4
- package/dist/tools/upgrade-planner.js +237 -61
- package/dist/tools/vulnerability-scan-concise.d.ts +47 -2
- package/dist/tools/vulnerability-scan-concise.js +37 -3
- package/dist/tools/vulnerability-scan-format.d.ts +126 -24
- package/dist/tools/vulnerability-scan-format.js +193 -26
- package/dist/tools/vulnerability-scan.d.ts +224 -63
- package/dist/tools/vulnerability-scan.js +50 -15
- package/dist/tools/what-should-i-know.d.ts +6 -1
- package/dist/tools/what-should-i-know.js +72 -15
- package/package.json +2 -2
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import * as fs from "node:fs";
|
|
17
17
|
import * as path from "node:path";
|
|
18
|
-
import { normalizePythonName, parseGoMod } from "./live/lockfile-parsers-pygo.js";
|
|
18
|
+
import { compiledIndirect, compiledRequirements, confinedInclude, normalizePythonName, parseGoMod, requirementOptions } from "./live/lockfile-parsers-pygo.js";
|
|
19
19
|
/** The body of a TOML table (`[name]`) up to the next table header, or null. */
|
|
20
20
|
export function tomlTable(content, name) {
|
|
21
21
|
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -130,6 +130,37 @@ export function parseRequirementNames(content) {
|
|
|
130
130
|
}
|
|
131
131
|
return [...new Set(names)];
|
|
132
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Requirement names from a requirements file and every file it includes with `-r` (confined to
|
|
135
|
+
* `root`, as the version reader confines them): an included file's pins are declared too. A pin
|
|
136
|
+
* in `-r sub/inner.txt` read as "Transitive dependency, not declared in your manifest" (final
|
|
137
|
+
* release audit of 6.2.5, 7g: jinja2==2.10).
|
|
138
|
+
*/
|
|
139
|
+
export function requirementNamesWithIncludes(file, root = path.dirname(path.resolve(file)), visited = new Set()) {
|
|
140
|
+
const key = path.resolve(file).toLowerCase();
|
|
141
|
+
if (visited.has(key))
|
|
142
|
+
return [];
|
|
143
|
+
visited.add(key);
|
|
144
|
+
let content;
|
|
145
|
+
try {
|
|
146
|
+
content = fs.readFileSync(file, "utf-8");
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
// A compiled file pins its whole tree: a pin there only because other packages require it
|
|
152
|
+
// (`# via requests`) is transitive, as go.mod's `// indirect` is (audit 6.2.5 A-4).
|
|
153
|
+
const compiled = compiledRequirements(content);
|
|
154
|
+
const names = parseRequirementNames(content).filter((n) => !compiled || !compiledIndirect(compiled, n));
|
|
155
|
+
for (const { option, target } of requirementOptions(content)) {
|
|
156
|
+
if (option !== "r")
|
|
157
|
+
continue;
|
|
158
|
+
const included = confinedInclude(path.dirname(file), target, root);
|
|
159
|
+
if (included)
|
|
160
|
+
names.push(...requirementNamesWithIncludes(included, root, visited));
|
|
161
|
+
}
|
|
162
|
+
return [...new Set(names)];
|
|
163
|
+
}
|
|
133
164
|
/** Direct Go dependencies: go.mod requirements not marked `// indirect`. */
|
|
134
165
|
export function parseGoModDirectDeps(content) {
|
|
135
166
|
return parseGoMod(content)
|
|
@@ -162,12 +193,15 @@ export function cargoWorkspaceDeps(root, content) {
|
|
|
162
193
|
const name = /^name\s*=\s*"([^"]+)"/m.exec(tomlTable(manifest, "package") ?? "")?.[1];
|
|
163
194
|
if (name)
|
|
164
195
|
memberNames.add(name);
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
196
|
+
// build-dependencies run at build time on every machine that builds the crate: they are not
|
|
197
|
+
// dev-only (the app reads them as normal), and discounting them hid memoffset with
|
|
198
|
+
// include_dev: false (audit 6.2.5 F-3).
|
|
199
|
+
for (const section of ["dependencies", "build-dependencies"]) {
|
|
168
200
|
for (const dep of tomlKeys(tomlTable(manifest, section)))
|
|
169
|
-
|
|
201
|
+
deps.add(dep);
|
|
170
202
|
}
|
|
203
|
+
for (const dep of tomlKeys(tomlTable(manifest, "dev-dependencies")))
|
|
204
|
+
devDeps.add(dep);
|
|
171
205
|
}
|
|
172
206
|
}
|
|
173
207
|
const strip = (set) => [...set].filter((d) => !memberNames.has(d));
|
|
@@ -176,6 +210,12 @@ export function cargoWorkspaceDeps(root, content) {
|
|
|
176
210
|
}
|
|
177
211
|
/** Member directories for one `members` entry: a path, or a path whose last segment is `*`. */
|
|
178
212
|
function expandMemberGlob(root, pattern) {
|
|
213
|
+
// `members = ["../outside/*"]` was followed out of the project: manifests outside it were read
|
|
214
|
+
// and their dependency names sent to the registries (audit 6.2.5 C-4). Members must resolve inside.
|
|
215
|
+
const inside = (dir) => confinedInclude(root, path.relative(root, dir), root) !== null;
|
|
216
|
+
return expandMemberGlobUnconfined(root, pattern).filter(inside);
|
|
217
|
+
}
|
|
218
|
+
function expandMemberGlobUnconfined(root, pattern) {
|
|
179
219
|
if (!pattern.includes("*"))
|
|
180
220
|
return [path.join(root, pattern)];
|
|
181
221
|
const star = pattern.indexOf("*");
|
package/dist/project-scanner.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import * as fs from "node:fs";
|
|
15
15
|
import * as path from "node:path";
|
|
16
|
-
import { cargoWorkspaceDeps, parseGoModDirectDeps, parsePyprojectDependencies,
|
|
16
|
+
import { cargoWorkspaceDeps, parseGoModDirectDeps, parsePyprojectDependencies, requirementNamesWithIncludes, } from "./project-manifests.js";
|
|
17
17
|
// =============================================================================
|
|
18
18
|
// Framework Detection Maps
|
|
19
19
|
// =============================================================================
|
|
@@ -190,7 +190,8 @@ export function scanCurrentProject(cwd) {
|
|
|
190
190
|
const rustDevDeps = [];
|
|
191
191
|
parseTOMLDependencies(content, "[dependencies]", rustDeps, fwSet, RUST_FRAMEWORK_MAP);
|
|
192
192
|
parseTOMLDependencies(content, "[dev-dependencies]", rustDevDeps, fwSet, RUST_FRAMEWORK_MAP);
|
|
193
|
-
|
|
193
|
+
// build-dependencies run at build time on every machine that builds the crate: normal, not dev (audit 6.2.5 F-3).
|
|
194
|
+
parseTOMLDependencies(content, "[build-dependencies]", rustDeps, fwSet, RUST_FRAMEWORK_MAP);
|
|
194
195
|
// Platform-gated deps: [target.'cfg(...)'.dependencies] / [target.<triple>.dependencies].
|
|
195
196
|
// Previously skipped entirely (e.g. windows-sys, winreg, libc were invisible).
|
|
196
197
|
parseTargetDependencies(content, rustDeps, rustDevDeps, result.depTargets, fwSet, RUST_FRAMEWORK_MAP);
|
|
@@ -243,7 +244,8 @@ export function scanCurrentProject(cwd) {
|
|
|
243
244
|
if (fs.existsSync(requirementsPath)) {
|
|
244
245
|
langSet.add("python");
|
|
245
246
|
try {
|
|
246
|
-
|
|
247
|
+
// Included files (`-r sub/inner.txt`) declare their pins too (audit 7g).
|
|
248
|
+
requirementNamesWithIncludes(requirementsPath, cwd).forEach((d) => pyDeps.add(d));
|
|
247
249
|
}
|
|
248
250
|
catch {
|
|
249
251
|
// Skip
|
|
@@ -364,7 +366,7 @@ function parseTargetDependencies(content, deps, devDeps, depTargets, frameworkSe
|
|
|
364
366
|
let m;
|
|
365
367
|
while ((m = headerRegex.exec(content)) !== null) {
|
|
366
368
|
const spec = m[1].trim().replace(/^['"]|['"]$/g, ""); // strip surrounding quotes
|
|
367
|
-
const isDev = m[2]
|
|
369
|
+
const isDev = m[2] === "dev-dependencies"; // build-dependencies are normal (audit 6.2.5 F-3)
|
|
368
370
|
// Body runs from the end of this header line to the next section header.
|
|
369
371
|
const rest = content.slice(m.index + m[0].length);
|
|
370
372
|
const nextHeader = rest.search(/^\[/m);
|
package/dist/project-tree.d.ts
CHANGED
|
@@ -22,6 +22,13 @@
|
|
|
22
22
|
* lockfile that was left out and say why.
|
|
23
23
|
*/
|
|
24
24
|
import { type ProjectScanResult } from "./project-scanner.js";
|
|
25
|
+
/**
|
|
26
|
+
* Manifests and lockfiles of ecosystems this server does not read, by file
|
|
27
|
+
* name: reported as `not_covered` so an empty or clean answer is never read
|
|
28
|
+
* as "no dependencies" (audit A2: a Maven or .NET project was told "No
|
|
29
|
+
* manifest files found... run from your project root").
|
|
30
|
+
*/
|
|
31
|
+
export declare function unsupportedEcosystem(name: string, names: string[]): string | null;
|
|
25
32
|
/**
|
|
26
33
|
* Measured on langchain (108 lockfiles, up to 3 levels down; 2026-10-10):
|
|
27
34
|
* the walk plus every project scan takes about 2 s. Depth 4 covers it with a
|
|
@@ -48,6 +55,18 @@ export interface TreeDiscovery {
|
|
|
48
55
|
skippedCount: number;
|
|
49
56
|
/** True when the walk itself stopped at MAX_WALK_DIRS: lockfiles beyond it were not even counted. */
|
|
50
57
|
walkTruncated: boolean;
|
|
58
|
+
/** Manifests and lockfiles of ecosystems this server does not read (the first MAX_LISTED_NOT_COVERED), relative to the root. */
|
|
59
|
+
notCovered?: Array<{
|
|
60
|
+
path: string;
|
|
61
|
+
ecosystem: string;
|
|
62
|
+
}>;
|
|
63
|
+
/** Every such file found (`notCovered` may list fewer). */
|
|
64
|
+
notCoveredCount?: number;
|
|
65
|
+
/** Lockfiles under test-fixture directories, left out unless include_test_fixtures was set. */
|
|
66
|
+
fixtures?: {
|
|
67
|
+
count: number;
|
|
68
|
+
dirs: string[];
|
|
69
|
+
};
|
|
51
70
|
}
|
|
52
71
|
/** Python requirement files: requirements.txt, requirements-dev.txt, dev_requirements.txt, _test_minimum_requirements.txt. */
|
|
53
72
|
export declare function isRequirementsFileName(name: string): boolean;
|
|
@@ -62,10 +81,47 @@ export declare function lockfilesIn(dir: string, listing?: string[]): string[];
|
|
|
62
81
|
export declare function scanProjectTree(root: string, limits?: {
|
|
63
82
|
maxDepth: number;
|
|
64
83
|
maxProjects: number;
|
|
84
|
+
}, options?: {
|
|
85
|
+
includeFixtures?: boolean;
|
|
65
86
|
}): {
|
|
66
87
|
entries: ProjectTreeEntry[];
|
|
67
88
|
discovery: TreeDiscovery;
|
|
68
89
|
};
|
|
90
|
+
/**
|
|
91
|
+
* The walk's report for `root`, taken once per process. For answers that
|
|
92
|
+
* had no walk recorded and must not claim "nothing here" unchecked (H3):
|
|
93
|
+
* vulnerability_scan's "nothing found" message and dependency_health's
|
|
94
|
+
* nothing-checked answer. Re-walking on every call cost a full tree walk per
|
|
95
|
+
* call (review of H3, 2026-10-18).
|
|
96
|
+
*/
|
|
97
|
+
export declare function discoveryFor(root: string): TreeDiscovery;
|
|
98
|
+
/** A lockfile problem as the notes read it (lockfile-types.ts LockfileProblem). */
|
|
99
|
+
export type LockfileProblemLike = {
|
|
100
|
+
file: string;
|
|
101
|
+
error: string;
|
|
102
|
+
skipped?: number;
|
|
103
|
+
refused?: true;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* The three kinds, kept apart: a file that could not be read (UNREADABLE),
|
|
107
|
+
* one read with entries skipped (`skipped`, PARTIAL: a yarn.lock or
|
|
108
|
+
* requirements line past the length cap, audit L5), and a requirements
|
|
109
|
+
* include refused because it points outside the project (`refused`, B-11).
|
|
110
|
+
*/
|
|
111
|
+
export declare function splitLockfileProblems<P extends LockfileProblemLike>(problems: P[]): {
|
|
112
|
+
unreadable: P[];
|
|
113
|
+
partial: P[];
|
|
114
|
+
refused: P[];
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* One wording for every tool (vulnerability_scan, dependency_health,
|
|
118
|
+
* upgrade_planner). `show` renders a path (the scan shows it relative).
|
|
119
|
+
*/
|
|
120
|
+
export declare function lockfileProblemNotes(problems: LockfileProblemLike[], show?: (file: string) => string): string[];
|
|
121
|
+
/** Kept for its importers (upgrade_planner): lockfileProblemNotes. */
|
|
122
|
+
export declare function unreadableNotes(problems: LockfileProblemLike[]): string[];
|
|
123
|
+
/** What a scan left out on purpose or cannot read: ecosystems it does not cover, and test fixtures. */
|
|
124
|
+
export declare function exclusionNotes(discovery: TreeDiscovery | null): string[];
|
|
69
125
|
/** One sentence for an answer, or null when nothing was left out. */
|
|
70
126
|
export declare function discoveryNote(discovery: TreeDiscovery | null): string | null;
|
|
71
127
|
/**
|
package/dist/project-tree.js
CHANGED
|
@@ -38,12 +38,57 @@ const LOCK_MARKERS = [
|
|
|
38
38
|
"pdm.lock",
|
|
39
39
|
"Pipfile.lock",
|
|
40
40
|
"go.mod",
|
|
41
|
+
"Gemfile.lock",
|
|
42
|
+
"composer.lock",
|
|
41
43
|
];
|
|
42
44
|
const SKIP_DIRS = new Set([
|
|
43
45
|
"node_modules", "target", ".git", "dist", "build", "out", "vendor", ".venv", "venv",
|
|
44
|
-
"__pycache__", ".next", ".nuxt", "coverage", ".claude", ".codex", ".cursor",
|
|
45
|
-
"
|
|
46
|
+
"__pycache__", ".next", ".nuxt", "coverage", ".claude", ".codex", ".cursor",
|
|
47
|
+
".turbo", ".cache", ".pnpm-store",
|
|
46
48
|
]);
|
|
49
|
+
/**
|
|
50
|
+
* Directories whose lockfiles are test data, not the project's dependencies:
|
|
51
|
+
* a scan of D:\4DA\src-tauri read the 4DA conformance corpus
|
|
52
|
+
* (tests/conformance/cases, 10,000+ vendored packages) as the app's own
|
|
53
|
+
* (audit A3, 2026-10-12). Left out by default, counted and named in the
|
|
54
|
+
* answer (`not_scanned`), and scanned on request (include_test_fixtures).
|
|
55
|
+
*/
|
|
56
|
+
const FIXTURE_DIRS = new Set([
|
|
57
|
+
"fixtures", "__fixtures__", "testdata", "test-data", "test-fixtures", "__tests__", "tests", "test", "conformance",
|
|
58
|
+
]);
|
|
59
|
+
/**
|
|
60
|
+
* Manifests and lockfiles of ecosystems this server does not read, by file
|
|
61
|
+
* name: reported as `not_covered` so an empty or clean answer is never read
|
|
62
|
+
* as "no dependencies" (audit A2: a Maven or .NET project was told "No
|
|
63
|
+
* manifest files found... run from your project root").
|
|
64
|
+
*/
|
|
65
|
+
export function unsupportedEcosystem(name, names) {
|
|
66
|
+
if (name === "pom.xml")
|
|
67
|
+
return "Maven";
|
|
68
|
+
if (/^build\.gradle(\.kts)?$/.test(name) || name === "gradle.lockfile")
|
|
69
|
+
return "Maven (Gradle)";
|
|
70
|
+
if (/\.(cs|fs|vb)proj$/i.test(name) || name === "packages.lock.json" || name === "packages.config" || name === "Directory.Packages.props")
|
|
71
|
+
return "NuGet";
|
|
72
|
+
if (name === "pubspec.lock")
|
|
73
|
+
return "Pub";
|
|
74
|
+
if (name === "mix.lock")
|
|
75
|
+
return "Hex";
|
|
76
|
+
if (name === "Package.resolved")
|
|
77
|
+
return "SwiftPM";
|
|
78
|
+
if (name === "Podfile.lock")
|
|
79
|
+
return "CocoaPods";
|
|
80
|
+
if (name === "conan.lock")
|
|
81
|
+
return "ConanCenter";
|
|
82
|
+
if (name === "renv.lock")
|
|
83
|
+
return "CRAN";
|
|
84
|
+
if (name === "Gemfile" && !names.includes("Gemfile.lock"))
|
|
85
|
+
return "RubyGems (Gemfile with no Gemfile.lock)";
|
|
86
|
+
if (name === "composer.json" && !names.includes("composer.lock"))
|
|
87
|
+
return "Packagist (composer.json with no composer.lock)";
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
/** Entries listed in `notCovered`; the count covers the rest. */
|
|
91
|
+
const MAX_LISTED_NOT_COVERED = 25;
|
|
47
92
|
/**
|
|
48
93
|
* Measured on langchain (108 lockfiles, up to 3 levels down; 2026-10-10):
|
|
49
94
|
* the walk plus every project scan takes about 2 s. Depth 4 covers it with a
|
|
@@ -88,13 +133,26 @@ export function lockfilesIn(dir, listing) {
|
|
|
88
133
|
* repository's .gitignore excludes are not part of the project and are
|
|
89
134
|
* skipped (gitignore.ts); they are not reported as left out either.
|
|
90
135
|
*/
|
|
91
|
-
export function scanProjectTree(root, limits = { maxDepth: MAX_DEPTH, maxProjects: MAX_PROJECTS }) {
|
|
136
|
+
export function scanProjectTree(root, limits = { maxDepth: MAX_DEPTH, maxProjects: MAX_PROJECTS }, options = {}) {
|
|
92
137
|
const entries = [{ dir: root, scan: scanCurrentProject(root) }];
|
|
93
138
|
const ignore = new IgnoreRules(root);
|
|
94
139
|
const skipped = [];
|
|
95
140
|
let skippedCount = 0;
|
|
96
141
|
let visited = 0;
|
|
97
142
|
let walkTruncated = false;
|
|
143
|
+
const notCovered = [];
|
|
144
|
+
let notCoveredCount = 0;
|
|
145
|
+
const fixtures = { count: 0, dirs: [] };
|
|
146
|
+
const noteUnsupported = (rel, names) => {
|
|
147
|
+
for (const name of names) {
|
|
148
|
+
const ecosystem = unsupportedEcosystem(name, names);
|
|
149
|
+
if (!ecosystem)
|
|
150
|
+
continue;
|
|
151
|
+
notCoveredCount++;
|
|
152
|
+
if (notCovered.length < MAX_LISTED_NOT_COVERED)
|
|
153
|
+
notCovered.push({ path: rel ? `${rel}/${name}` : name, ecosystem });
|
|
154
|
+
}
|
|
155
|
+
};
|
|
98
156
|
// One directory listing per directory: it decides both whether the
|
|
99
157
|
// directory is a project and which children to descend into.
|
|
100
158
|
const listing = (dir) => {
|
|
@@ -105,6 +163,25 @@ export function scanProjectTree(root, limits = { maxDepth: MAX_DEPTH, maxProject
|
|
|
105
163
|
return null;
|
|
106
164
|
}
|
|
107
165
|
};
|
|
166
|
+
// Test-fixture lockfiles: counted, never scanned (unless asked for).
|
|
167
|
+
const countFixtures = (dir, children) => {
|
|
168
|
+
for (const child of children) {
|
|
169
|
+
if (!child.isDirectory() || SKIP_DIRS.has(child.name) || child.name.startsWith("."))
|
|
170
|
+
continue;
|
|
171
|
+
if (++visited > MAX_WALK_DIRS) {
|
|
172
|
+
walkTruncated = true;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const sub = path.join(dir, child.name);
|
|
176
|
+
const grandchildren = listing(sub);
|
|
177
|
+
if (!grandchildren)
|
|
178
|
+
continue;
|
|
179
|
+
fixtures.count += lockfilesIn(sub, grandchildren.map((g) => g.name)).length;
|
|
180
|
+
countFixtures(sub, grandchildren);
|
|
181
|
+
if (walkTruncated)
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
};
|
|
108
185
|
const walk = (dir, rel, depth, children) => {
|
|
109
186
|
for (const child of children) {
|
|
110
187
|
if (!child.isDirectory() || SKIP_DIRS.has(child.name) || child.name.startsWith("."))
|
|
@@ -120,10 +197,21 @@ export function scanProjectTree(root, limits = { maxDepth: MAX_DEPTH, maxProject
|
|
|
120
197
|
const grandchildren = listing(sub);
|
|
121
198
|
if (!grandchildren)
|
|
122
199
|
continue;
|
|
200
|
+
if (!options.includeFixtures && FIXTURE_DIRS.has(child.name.toLowerCase())) {
|
|
201
|
+
const before = fixtures.count;
|
|
202
|
+
fixtures.count += lockfilesIn(sub, grandchildren.map((g) => g.name)).length;
|
|
203
|
+
countFixtures(sub, grandchildren);
|
|
204
|
+
if (fixtures.count > before && fixtures.dirs.length < 10)
|
|
205
|
+
fixtures.dirs.push(childRel);
|
|
206
|
+
if (walkTruncated)
|
|
207
|
+
return;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
123
210
|
// A .gitignore is only read where one exists.
|
|
124
211
|
if (grandchildren.some((g) => g.name === ".gitignore"))
|
|
125
212
|
ignore.load(childRel);
|
|
126
213
|
const names = grandchildren.map((g) => g.name);
|
|
214
|
+
noteUnsupported(childRel, names.filter((n) => !ignore.ignores(`${childRel}/${n}`, false)));
|
|
127
215
|
const locks = lockfilesIn(sub, names).filter((name) => !ignore.ignores(`${childRel}/${name}`, false));
|
|
128
216
|
if (locks.length > 0) {
|
|
129
217
|
const reason = depth > limits.maxDepth ? "depth" : entries.length >= limits.maxProjects ? "project_limit" : null;
|
|
@@ -142,12 +230,91 @@ export function scanProjectTree(root, limits = { maxDepth: MAX_DEPTH, maxProject
|
|
|
142
230
|
return;
|
|
143
231
|
}
|
|
144
232
|
};
|
|
145
|
-
|
|
233
|
+
const rootListing = listing(root) ?? [];
|
|
234
|
+
noteUnsupported("", rootListing.filter((d) => d.isFile()).map((d) => d.name));
|
|
235
|
+
walk(root, "", 1, rootListing);
|
|
146
236
|
return {
|
|
147
237
|
entries,
|
|
148
|
-
discovery: {
|
|
238
|
+
discovery: {
|
|
239
|
+
projects: entries.length,
|
|
240
|
+
maxDepth: limits.maxDepth,
|
|
241
|
+
maxProjects: limits.maxProjects,
|
|
242
|
+
skipped,
|
|
243
|
+
skippedCount,
|
|
244
|
+
walkTruncated,
|
|
245
|
+
...(notCoveredCount > 0 ? { notCovered, notCoveredCount } : {}),
|
|
246
|
+
...(fixtures.count > 0 ? { fixtures } : {}),
|
|
247
|
+
},
|
|
149
248
|
};
|
|
150
249
|
}
|
|
250
|
+
const walkedDiscovery = new Map();
|
|
251
|
+
/**
|
|
252
|
+
* The walk's report for `root`, taken once per process. For answers that
|
|
253
|
+
* had no walk recorded and must not claim "nothing here" unchecked (H3):
|
|
254
|
+
* vulnerability_scan's "nothing found" message and dependency_health's
|
|
255
|
+
* nothing-checked answer. Re-walking on every call cost a full tree walk per
|
|
256
|
+
* call (review of H3, 2026-10-18).
|
|
257
|
+
*/
|
|
258
|
+
export function discoveryFor(root) {
|
|
259
|
+
const key = path.resolve(root).toLowerCase();
|
|
260
|
+
let found = walkedDiscovery.get(key);
|
|
261
|
+
if (!found) {
|
|
262
|
+
found = scanProjectTree(root).discovery;
|
|
263
|
+
walkedDiscovery.set(key, found);
|
|
264
|
+
}
|
|
265
|
+
return found;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* The three kinds, kept apart: a file that could not be read (UNREADABLE),
|
|
269
|
+
* one read with entries skipped (`skipped`, PARTIAL: a yarn.lock or
|
|
270
|
+
* requirements line past the length cap, audit L5), and a requirements
|
|
271
|
+
* include refused because it points outside the project (`refused`, B-11).
|
|
272
|
+
*/
|
|
273
|
+
export function splitLockfileProblems(problems) {
|
|
274
|
+
return {
|
|
275
|
+
unreadable: problems.filter((p) => !p.skipped && !p.refused),
|
|
276
|
+
partial: problems.filter((p) => Boolean(p.skipped) && !p.refused),
|
|
277
|
+
refused: problems.filter((p) => p.refused === true),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* One wording for every tool (vulnerability_scan, dependency_health,
|
|
282
|
+
* upgrade_planner). `show` renders a path (the scan shows it relative).
|
|
283
|
+
*/
|
|
284
|
+
export function lockfileProblemNotes(problems, show = (f) => f) {
|
|
285
|
+
const listed = (ps) => `${ps.slice(0, 3).map((p) => `${show(p.file)} (${p.error})`).join("; ")}${ps.length > 3 ? "; ..." : ""}`;
|
|
286
|
+
const { unreadable, partial, refused } = splitLockfileProblems(problems);
|
|
287
|
+
const notes = [];
|
|
288
|
+
if (unreadable.length > 0) {
|
|
289
|
+
notes.push(`UNREADABLE: ${unreadable.length} lockfile${unreadable.length === 1 ? "" : "s"} could not be read, so ${unreadable.length === 1 ? "its" : "their"} dependencies were not checked: ${listed(unreadable)}.`);
|
|
290
|
+
}
|
|
291
|
+
if (partial.length > 0) {
|
|
292
|
+
notes.push(`PARTIAL: ${partial.length} lockfile${partial.length === 1 ? " was" : "s were"} read with entries skipped: ${listed(partial)}.`);
|
|
293
|
+
}
|
|
294
|
+
if (refused.length > 0) {
|
|
295
|
+
notes.push(`REFUSED: ${refused.length} requirements include${refused.length === 1 ? " was" : "s were"} not read: ${listed(refused)}.`);
|
|
296
|
+
}
|
|
297
|
+
return notes;
|
|
298
|
+
}
|
|
299
|
+
/** Kept for its importers (upgrade_planner): lockfileProblemNotes. */
|
|
300
|
+
export function unreadableNotes(problems) {
|
|
301
|
+
return lockfileProblemNotes(problems);
|
|
302
|
+
}
|
|
303
|
+
/** What a scan left out on purpose or cannot read: ecosystems it does not cover, and test fixtures. */
|
|
304
|
+
export function exclusionNotes(discovery) {
|
|
305
|
+
const notes = [];
|
|
306
|
+
const unsupported = discovery?.notCoveredCount ?? 0;
|
|
307
|
+
if (unsupported > 0) {
|
|
308
|
+
const ecosystems = [...new Set((discovery?.notCovered ?? []).map((n) => n.ecosystem.split(" ")[0]))].join(", ");
|
|
309
|
+
const listed = (discovery?.notCovered ?? []).slice(0, 3).map((n) => n.path).join(", ");
|
|
310
|
+
notes.push(`NOT COVERED: ${unsupported} manifest/lockfile${unsupported === 1 ? "" : "s"} of ecosystems this server does not read (${ecosystems}): ${listed}${unsupported > 3 ? ", ..." : ""}. Their dependencies were not checked at all; see not_covered.`);
|
|
311
|
+
}
|
|
312
|
+
const fixtures = discovery?.fixtures;
|
|
313
|
+
if (fixtures && fixtures.count > 0) {
|
|
314
|
+
notes.push(`Not scanned: ${fixtures.count} test-fixture lockfile${fixtures.count === 1 ? "" : "s"} under ${fixtures.dirs.slice(0, 3).join(", ")}${fixtures.dirs.length > 3 ? ", ..." : ""} (pass include_test_fixtures: true to scan them).`);
|
|
315
|
+
}
|
|
316
|
+
return notes;
|
|
317
|
+
}
|
|
151
318
|
/** One sentence for an answer, or null when nothing was left out. */
|
|
152
319
|
export function discoveryNote(discovery) {
|
|
153
320
|
if (!discovery || (discovery.skippedCount === 0 && !discovery.walkTruncated))
|
|
@@ -173,6 +340,8 @@ const LOCKS_BY_LANGUAGE = {
|
|
|
173
340
|
rust: ["Cargo.lock"],
|
|
174
341
|
python: ["poetry.lock", "uv.lock", "pdm.lock", "Pipfile.lock"],
|
|
175
342
|
go: ["go.mod"],
|
|
343
|
+
ruby: ["Gemfile.lock"],
|
|
344
|
+
php: ["composer.lock"],
|
|
176
345
|
};
|
|
177
346
|
/**
|
|
178
347
|
* Resolution groups for the live layer: one per (project directory, ecosystem).
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* tool output told agents to "pass include_dev". The full list costs about
|
|
19
19
|
* 4k tokens, inside the 3-5k band of well-regarded servers.
|
|
20
20
|
*/
|
|
21
|
+
import { ProtocolError } from "@modelcontextprotocol/server";
|
|
21
22
|
/** Tool categories — maps to the functional groupings in the MCP server */
|
|
22
23
|
export type ToolCategory = "security" | "intelligence" | "decisions" | "agent" | "identity";
|
|
23
24
|
/**
|
|
@@ -73,12 +74,19 @@ export declare function getSlimToolList(standaloneOnly?: boolean): Array<{
|
|
|
73
74
|
inputSchema: ToolInputSchema;
|
|
74
75
|
annotations: ToolAnnotations;
|
|
75
76
|
}>;
|
|
77
|
+
/**
|
|
78
|
+
* The registry entry for a tool NAME a client sent, or undefined. Own
|
|
79
|
+
* properties only: `TOOL_REGISTRY["constructor"]` is Object's constructor,
|
|
80
|
+
* and `"toString" in TOOL_REGISTRY` is true, so a read of
|
|
81
|
+
* `4da://schema/__proto__` used to throw a TypeError (audit 2026-10-12 B-14).
|
|
82
|
+
*/
|
|
83
|
+
export declare function toolEntry(name: string): ToolRegistryEntry | undefined;
|
|
76
84
|
/** The full schema document a `4da://schema/{tool}` resource serves. */
|
|
77
85
|
export declare function getToolSchemaDocument(toolName: string): object | null;
|
|
78
86
|
/**
|
|
79
87
|
* Get list of schema resources for ListResources
|
|
80
88
|
*/
|
|
81
|
-
export declare function getSchemaResources(): Array<{
|
|
89
|
+
export declare function getSchemaResources(standaloneOnly?: boolean): Array<{
|
|
82
90
|
uri: string;
|
|
83
91
|
name: string;
|
|
84
92
|
description: string;
|
|
@@ -86,10 +94,35 @@ export declare function getSchemaResources(): Array<{
|
|
|
86
94
|
}>;
|
|
87
95
|
/** Check if a tool exists */
|
|
88
96
|
export declare function hasToolSchema(toolName: string): boolean;
|
|
89
|
-
/**
|
|
90
|
-
|
|
97
|
+
/**
|
|
98
|
+
* MCP Resources: every tool's full schema, and the category manifest.
|
|
99
|
+
* `4da://skills` was advertised until 6.2.5, but it read a file nothing ever
|
|
100
|
+
* wrote, so every read failed (audit 2026-10-12 B-15). `standaloneOnly` is
|
|
101
|
+
* tools/list's filter: in standalone mode the 5 desktop-only schemas were
|
|
102
|
+
* advertised beside a tool list that leaves them out (audit L6).
|
|
103
|
+
*/
|
|
104
|
+
export declare function listResources(standaloneOnly?: boolean): Array<{
|
|
105
|
+
uri: string;
|
|
106
|
+
name: string;
|
|
107
|
+
description: string;
|
|
108
|
+
mimeType: string;
|
|
109
|
+
}>;
|
|
110
|
+
/** `resources/read`: the resource, or a resource-not-found protocol error naming the URI. */
|
|
111
|
+
export declare function readResource(uri: string, standaloneOnly?: boolean): {
|
|
112
|
+
contents: Array<{
|
|
113
|
+
uri: string;
|
|
114
|
+
mimeType: string;
|
|
115
|
+
text: string;
|
|
116
|
+
}>;
|
|
117
|
+
};
|
|
118
|
+
/** tools/call for a name no tool has: JSON-RPC Invalid Params (-32602), as the MCP spec says. */
|
|
119
|
+
export declare function unknownToolError(name: string): ProtocolError;
|
|
120
|
+
/** prompts/get for a name no prompt has: JSON-RPC Invalid Params (-32602). */
|
|
121
|
+
export declare function unknownPromptError(name: string): ProtocolError;
|
|
122
|
+
/** Get tool names grouped by category (only the standalone ones, or only the desktop ones, when asked). */
|
|
123
|
+
export declare function getToolsByCategory(standaloneOnly?: boolean): Record<ToolCategory, string[]>;
|
|
91
124
|
/** Structured category manifest for the 4da://categories resource */
|
|
92
|
-
export declare function getCategoryManifest(): {
|
|
125
|
+
export declare function getCategoryManifest(standaloneOnly?: boolean): {
|
|
93
126
|
version: string;
|
|
94
127
|
total_tools: number;
|
|
95
128
|
categories: Record<ToolCategory, {
|
|
@@ -97,5 +130,15 @@ export declare function getCategoryManifest(): {
|
|
|
97
130
|
count: number;
|
|
98
131
|
}>;
|
|
99
132
|
};
|
|
133
|
+
/**
|
|
134
|
+
* The tool count the startup line states, for what this start will serve.
|
|
135
|
+
* With FOURDA_DB_PATH refused or the database unreadable, every call returns
|
|
136
|
+
* that error, so "16 tools" (6.2.5) was not true (audit L6); with no SQLite
|
|
137
|
+
* driver only the dependency tools run.
|
|
138
|
+
*/
|
|
139
|
+
export declare function startupToolsLine(validation: {
|
|
140
|
+
valid: boolean;
|
|
141
|
+
standalone?: boolean;
|
|
142
|
+
}, hasSqliteDriver: boolean, databaseFreeTools?: number): string;
|
|
100
143
|
/** Find tools matching any of the given tags */
|
|
101
144
|
export declare function getToolsByTags(tags: string[]): string[];
|
package/dist/schema-registry.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* tool output told agents to "pass include_dev". The full list costs about
|
|
20
20
|
* 4k tokens, inside the 3-5k band of well-regarded servers.
|
|
21
21
|
*/
|
|
22
|
+
import { ProtocolError, ProtocolErrorCode, ResourceNotFoundError } from "@modelcontextprotocol/server";
|
|
22
23
|
import { agentMemoryTool, checkDecisionAlignmentTool, decisionMemoryTool, dependencyCheckTool, dependencyHealthTool, developerDnaTool, ecosystemPulseTool, getActionableSignalsTool, getContextTool, getRelevantContentTool, knowledgeGapsTool, recordFeedbackTool, upgradeImpactTool, upgradePlannerTool, vulnerabilityScanTool, whatShouldIKnowTool, } from "./tools/index.js";
|
|
23
24
|
const READ_LOCAL = { readOnlyHint: true, openWorldHint: false };
|
|
24
25
|
const READ_NETWORK = { readOnlyHint: true, openWorldHint: true };
|
|
@@ -178,9 +179,18 @@ export function getSlimToolList(standaloneOnly) {
|
|
|
178
179
|
annotations: info.annotations,
|
|
179
180
|
}));
|
|
180
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* The registry entry for a tool NAME a client sent, or undefined. Own
|
|
184
|
+
* properties only: `TOOL_REGISTRY["constructor"]` is Object's constructor,
|
|
185
|
+
* and `"toString" in TOOL_REGISTRY` is true, so a read of
|
|
186
|
+
* `4da://schema/__proto__` used to throw a TypeError (audit 2026-10-12 B-14).
|
|
187
|
+
*/
|
|
188
|
+
export function toolEntry(name) {
|
|
189
|
+
return Object.hasOwn(TOOL_REGISTRY, name) ? TOOL_REGISTRY[name] : undefined;
|
|
190
|
+
}
|
|
181
191
|
/** The full schema document a `4da://schema/{tool}` resource serves. */
|
|
182
192
|
export function getToolSchemaDocument(toolName) {
|
|
183
|
-
const entry =
|
|
193
|
+
const entry = toolEntry(toolName);
|
|
184
194
|
if (!entry)
|
|
185
195
|
return null;
|
|
186
196
|
return {
|
|
@@ -196,8 +206,10 @@ export function getToolSchemaDocument(toolName) {
|
|
|
196
206
|
/**
|
|
197
207
|
* Get list of schema resources for ListResources
|
|
198
208
|
*/
|
|
199
|
-
export function getSchemaResources() {
|
|
200
|
-
return Object.entries(TOOL_REGISTRY)
|
|
209
|
+
export function getSchemaResources(standaloneOnly) {
|
|
210
|
+
return Object.entries(TOOL_REGISTRY)
|
|
211
|
+
.filter(([, info]) => standaloneOnly == null || info.standalone === standaloneOnly)
|
|
212
|
+
.map(([name]) => ({
|
|
201
213
|
uri: `4da://schema/${name}`,
|
|
202
214
|
name: `${name} schema`,
|
|
203
215
|
description: `Full JSON Schema for the ${name} tool`,
|
|
@@ -206,12 +218,51 @@ export function getSchemaResources() {
|
|
|
206
218
|
}
|
|
207
219
|
/** Check if a tool exists */
|
|
208
220
|
export function hasToolSchema(toolName) {
|
|
209
|
-
return toolName
|
|
221
|
+
return toolEntry(toolName) !== undefined;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* MCP Resources: every tool's full schema, and the category manifest.
|
|
225
|
+
* `4da://skills` was advertised until 6.2.5, but it read a file nothing ever
|
|
226
|
+
* wrote, so every read failed (audit 2026-10-12 B-15). `standaloneOnly` is
|
|
227
|
+
* tools/list's filter: in standalone mode the 5 desktop-only schemas were
|
|
228
|
+
* advertised beside a tool list that leaves them out (audit L6).
|
|
229
|
+
*/
|
|
230
|
+
export function listResources(standaloneOnly) {
|
|
231
|
+
return [
|
|
232
|
+
...getSchemaResources(standaloneOnly),
|
|
233
|
+
{
|
|
234
|
+
uri: "4da://categories",
|
|
235
|
+
name: "Tool categories",
|
|
236
|
+
description: "Tool groupings by category with tag metadata",
|
|
237
|
+
mimeType: "application/json",
|
|
238
|
+
},
|
|
239
|
+
];
|
|
210
240
|
}
|
|
211
|
-
/**
|
|
212
|
-
export function
|
|
241
|
+
/** `resources/read`: the resource, or a resource-not-found protocol error naming the URI. */
|
|
242
|
+
export function readResource(uri, standaloneOnly) {
|
|
243
|
+
const json = (value) => ({ contents: [{ uri, mimeType: "application/json", text: JSON.stringify(value, null, 2) }] });
|
|
244
|
+
if (uri === "4da://categories")
|
|
245
|
+
return json(getCategoryManifest(standaloneOnly));
|
|
246
|
+
const match = /^4da:\/\/schema\/(.+)$/.exec(uri);
|
|
247
|
+
const document = match ? getToolSchemaDocument(match[1]) : null;
|
|
248
|
+
if (!document)
|
|
249
|
+
throw new ResourceNotFoundError(uri, `Resource not found: ${uri}`);
|
|
250
|
+
return json(document);
|
|
251
|
+
}
|
|
252
|
+
/** tools/call for a name no tool has: JSON-RPC Invalid Params (-32602), as the MCP spec says. */
|
|
253
|
+
export function unknownToolError(name) {
|
|
254
|
+
return new ProtocolError(ProtocolErrorCode.InvalidParams, `Unknown tool: ${name}`);
|
|
255
|
+
}
|
|
256
|
+
/** prompts/get for a name no prompt has: JSON-RPC Invalid Params (-32602). */
|
|
257
|
+
export function unknownPromptError(name) {
|
|
258
|
+
return new ProtocolError(ProtocolErrorCode.InvalidParams, `Unknown prompt: ${name}`);
|
|
259
|
+
}
|
|
260
|
+
/** Get tool names grouped by category (only the standalone ones, or only the desktop ones, when asked). */
|
|
261
|
+
export function getToolsByCategory(standaloneOnly) {
|
|
213
262
|
const result = {};
|
|
214
263
|
for (const [name, entry] of Object.entries(TOOL_REGISTRY)) {
|
|
264
|
+
if (standaloneOnly != null && entry.standalone !== standaloneOnly)
|
|
265
|
+
continue;
|
|
215
266
|
if (!result[entry.category]) {
|
|
216
267
|
result[entry.category] = [];
|
|
217
268
|
}
|
|
@@ -220,18 +271,34 @@ export function getToolsByCategory() {
|
|
|
220
271
|
return result;
|
|
221
272
|
}
|
|
222
273
|
/** Structured category manifest for the 4da://categories resource */
|
|
223
|
-
export function getCategoryManifest() {
|
|
224
|
-
const grouped = getToolsByCategory();
|
|
274
|
+
export function getCategoryManifest(standaloneOnly) {
|
|
275
|
+
const grouped = getToolsByCategory(standaloneOnly);
|
|
225
276
|
const categories = {};
|
|
226
277
|
for (const [cat, tools] of Object.entries(grouped)) {
|
|
227
278
|
categories[cat] = { tools, count: tools.length };
|
|
228
279
|
}
|
|
229
280
|
return {
|
|
230
281
|
version: "1.0.0",
|
|
231
|
-
total_tools: Object.
|
|
282
|
+
total_tools: Object.values(grouped).reduce((n, tools) => n + tools.length, 0),
|
|
232
283
|
categories,
|
|
233
284
|
};
|
|
234
285
|
}
|
|
286
|
+
/**
|
|
287
|
+
* The tool count the startup line states, for what this start will serve.
|
|
288
|
+
* With FOURDA_DB_PATH refused or the database unreadable, every call returns
|
|
289
|
+
* that error, so "16 tools" (6.2.5) was not true (audit L6); with no SQLite
|
|
290
|
+
* driver only the dependency tools run.
|
|
291
|
+
*/
|
|
292
|
+
export function startupToolsLine(validation, hasSqliteDriver, databaseFreeTools = 0) {
|
|
293
|
+
const standalone = validation.standalone === true;
|
|
294
|
+
if (!hasSqliteDriver)
|
|
295
|
+
return `${databaseFreeTools} dependency tools run without a database (every other tool returns the driver problem above)`;
|
|
296
|
+
if (validation.valid || standalone) {
|
|
297
|
+
const count = getSlimToolList(standalone ? true : undefined).length;
|
|
298
|
+
return `${count} ${standalone ? "standalone tools" : "tools"}`;
|
|
299
|
+
}
|
|
300
|
+
return `no tool can run until the database problem above is fixed (${getSlimToolList().length} tools are listed; each call returns that error)`;
|
|
301
|
+
}
|
|
235
302
|
/** Find tools matching any of the given tags */
|
|
236
303
|
export function getToolsByTags(tags) {
|
|
237
304
|
const tagSet = new Set(tags.map((t) => t.toLowerCase()));
|