@4da/mcp-server 6.0.2 → 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.
Files changed (91) hide show
  1. package/CHANGELOG.md +265 -0
  2. package/README.md +13 -10
  3. package/dist/db.d.ts +73 -8
  4. package/dist/db.js +180 -43
  5. package/dist/doctor.d.ts +1 -1
  6. package/dist/doctor.js +15 -9
  7. package/dist/embeddings.js +19 -7
  8. package/dist/gitignore.js +9 -3
  9. package/dist/index.js +43 -4
  10. package/dist/live/cache.d.ts +31 -2
  11. package/dist/live/cache.js +113 -1
  12. package/dist/live/changelog-classify.js +4 -1
  13. package/dist/live/changelog.d.ts +15 -0
  14. package/dist/live/changelog.js +67 -11
  15. package/dist/live/dev-scope.d.ts +2 -2
  16. package/dist/live/fix-paths.d.ts +118 -0
  17. package/dist/live/fix-paths.js +498 -0
  18. package/dist/live/go-registry.d.ts +5 -0
  19. package/dist/live/go-registry.js +1 -1
  20. package/dist/live/index.d.ts +30 -3
  21. package/dist/live/index.js +59 -6
  22. package/dist/live/lockfile-parents.d.ts +61 -0
  23. package/dist/live/lockfile-parents.js +360 -0
  24. package/dist/live/lockfile-parsers-pygo.d.ts +31 -0
  25. package/dist/live/lockfile-parsers-pygo.js +178 -28
  26. package/dist/live/lockfile-parsers.d.ts +4 -1
  27. package/dist/live/lockfile-parsers.js +57 -24
  28. package/dist/live/lockfile-types.d.ts +12 -3
  29. package/dist/live/lockfile-types.js +2 -1
  30. package/dist/live/osv-ranges.d.ts +79 -0
  31. package/dist/live/osv-ranges.js +133 -0
  32. package/dist/live/osv-scanner.d.ts +1 -0
  33. package/dist/live/osv-scanner.js +85 -78
  34. package/dist/live/range-satisfies.d.ts +21 -0
  35. package/dist/live/range-satisfies.js +190 -0
  36. package/dist/live/rate-limiter.d.ts +6 -0
  37. package/dist/live/rate-limiter.js +5 -0
  38. package/dist/live/registry-health.js +2 -0
  39. package/dist/live/resolution.d.ts +11 -1
  40. package/dist/live/resolution.js +16 -6
  41. package/dist/live/semver-utils.d.ts +8 -0
  42. package/dist/live/semver-utils.js +10 -2
  43. package/dist/live/types.d.ts +17 -1
  44. package/dist/live/upgrade-sources.d.ts +2 -0
  45. package/dist/live/upgrade-sources.js +7 -2
  46. package/dist/memory-store.d.ts +19 -0
  47. package/dist/memory-store.js +8 -0
  48. package/dist/native-bindings.d.ts +7 -2
  49. package/dist/native-bindings.js +4 -0
  50. package/dist/project-tree.d.ts +50 -8
  51. package/dist/project-tree.js +121 -29
  52. package/dist/schema-registry.js +2 -2
  53. package/dist/sqlite-driver.d.ts +134 -0
  54. package/dist/sqlite-driver.js +359 -0
  55. package/dist/tool-dispatch.d.ts +8 -1
  56. package/dist/tool-dispatch.js +17 -2
  57. package/dist/tools/agent-memory.js +10 -2
  58. package/dist/tools/app-plan.d.ts +11 -1
  59. package/dist/tools/app-plan.js +32 -6
  60. package/dist/tools/briefing-task-scope.d.ts +21 -2
  61. package/dist/tools/briefing-task-scope.js +97 -4
  62. package/dist/tools/decision-memory.js +9 -3
  63. package/dist/tools/dependency-check-signals.d.ts +7 -0
  64. package/dist/tools/dependency-check-signals.js +17 -2
  65. package/dist/tools/dependency-check.d.ts +1 -1
  66. package/dist/tools/dependency-check.js +1 -1
  67. package/dist/tools/dependency-health.d.ts +1 -1
  68. package/dist/tools/dependency-health.js +5 -0
  69. package/dist/tools/knowledge-gap-ranges.d.ts +1 -1
  70. package/dist/tools/knowledge-gap-ranges.js +1 -1
  71. package/dist/tools/knowledge-gap-scope.d.ts +2 -2
  72. package/dist/tools/project-scope.d.ts +45 -0
  73. package/dist/tools/project-scope.js +119 -0
  74. package/dist/tools/record-feedback.js +1 -1
  75. package/dist/tools/upgrade-impact-literals.js +1 -1
  76. package/dist/tools/upgrade-impact-report.d.ts +10 -0
  77. package/dist/tools/upgrade-impact-report.js +32 -2
  78. package/dist/tools/upgrade-impact.js +1 -1
  79. package/dist/tools/upgrade-planner-fixpaths.d.ts +40 -0
  80. package/dist/tools/upgrade-planner-fixpaths.js +0 -0
  81. package/dist/tools/upgrade-planner.d.ts +31 -3
  82. package/dist/tools/upgrade-planner.js +74 -19
  83. package/dist/tools/vulnerability-scan-concise.d.ts +13 -1
  84. package/dist/tools/vulnerability-scan-concise.js +15 -3
  85. package/dist/tools/vulnerability-scan-format.d.ts +58 -1
  86. package/dist/tools/vulnerability-scan-format.js +107 -11
  87. package/dist/tools/vulnerability-scan.d.ts +104 -13
  88. package/dist/tools/vulnerability-scan.js +27 -3
  89. package/dist/tools/what-should-i-know.d.ts +22 -2
  90. package/dist/tools/what-should-i-know.js +121 -11
  91. package/package.json +5 -27
@@ -19,7 +19,7 @@
19
19
  */
20
20
  import { compareVersionPrecedence, parseSemverPrecedence } from "./semver-precedence.js";
21
21
  import { ArchiveError, defaultFetch, fetchArchiveRootFiles } from "./package-archive.js";
22
- import { findChangelogFile, isChangelogName, parseChangelog } from "./changelog.js";
22
+ import { CHANGELOG_PARSER_VERSION, findChangelogFile, isChangelogName, parseChangelog } from "./changelog.js";
23
23
  /** crates.io refuses API requests without a descriptive User-Agent. */
24
24
  export const CRATES_USER_AGENT = "4da-mcp-server (https://4da.ai; upgrade_impact)";
25
25
  const INDEX_TTL = 6 * 3600;
@@ -124,13 +124,18 @@ export function archiveUrl(index, version) {
124
124
  return null;
125
125
  }
126
126
  }
127
+ /** Cache key of one version's parsed changelog; it carries CHANGELOG_PARSER_VERSION. */
128
+ export function changelogCacheKey(ecosystem, name, version) {
129
+ return `upgrade-impact:changelog:p${CHANGELOG_PARSER_VERSION}:${ecosystem}:${name}:${version}`;
130
+ }
127
131
  /** Parsed changelog shipped inside one version's archive (cached 7 days per version). */
128
132
  export async function getChangelog(net, index, version) {
129
133
  const url = archiveUrl(index, version);
130
134
  if (!url) {
131
135
  return { found: false, reason: "the version's archive is not hosted on the package's own registry" };
132
136
  }
133
- const key = `upgrade-impact:changelog:${index.ecosystem}:${index.name}:${version.version}`;
137
+ // The parser version is in the key: a parser fix must not wait out the 7-day TTL.
138
+ const key = changelogCacheKey(index.ecosystem, index.name, version.version);
134
139
  const hit = net.cache?.get(key);
135
140
  if (hit)
136
141
  return hit;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Where a decision, agent memory or feedback record is written.
3
+ *
4
+ * Reads go through the database's main connection (read-only for the desktop
5
+ * app's database). A write into one of the app-owned tables the tools exist to
6
+ * write goes through FourDADatabase.writerFor: the app's own table via a
7
+ * separate write connection, or, when the app's schema lacks the table or a
8
+ * column, the server's store with a note. A stand-in object without writerFor
9
+ * (some tests hand tools a minimal double) writes to its own connection.
10
+ */
11
+ import type { AppWritableTable, FourDADatabase, WriteTarget } from "./db.js";
12
+ export declare function writerOf(db: FourDADatabase, table: AppWritableTable, columns: readonly string[]): WriteTarget;
13
+ /** `_meta` for a tool result when the record could not go where it normally goes. */
14
+ export declare function writeMeta(target: WriteTarget): {
15
+ _meta?: {
16
+ written_to: string;
17
+ note: string;
18
+ };
19
+ };
@@ -0,0 +1,8 @@
1
+ export function writerOf(db, table, columns) {
2
+ const writerFor = db.writerFor;
3
+ return typeof writerFor === "function" ? writerFor.call(db, table, columns) : { raw: db.getRawDb(), target: "database" };
4
+ }
5
+ /** `_meta` for a tool result when the record could not go where it normally goes. */
6
+ export function writeMeta(target) {
7
+ return target.note ? { _meta: { written_to: target.target, note: target.note } } : {};
8
+ }
@@ -8,12 +8,17 @@
8
8
  * (local or -g) left no better_sqlite3.node, `--doctor` still reported the
9
9
  * bindings as loaded, and every tool failed with "Could not locate the
10
10
  * bindings file" and no hint why. `npx @4da/mcp-server` was unaffected.
11
+ *
12
+ * Since 6.1.0 the server uses Node's built-in node:sqlite when the runtime
13
+ * has it (sqlite-driver.ts), so this matters only on Node 22.0-22.12, where
14
+ * better-sqlite3 is the one driver.
11
15
  */
12
- import type BetterSqlite3 from "better-sqlite3";
13
16
  export declare const NATIVE_BINDING_FIX: string;
14
17
  /** Errors that mean the compiled module is absent or built for another Node/platform. */
15
18
  export declare function isNativeBindingError(err: unknown): boolean;
16
19
  /** The first line of a native-binding error, with the fix appended. */
17
20
  export declare function nativeBindingMessage(err: unknown): string;
18
21
  /** Null when an in-memory database opens; otherwise why not, with the fix. */
19
- export declare function checkNativeBindings(Database: typeof BetterSqlite3): string | null;
22
+ export declare function checkNativeBindings(Database: new (file: string) => {
23
+ close(): unknown;
24
+ }): string | null;
@@ -9,6 +9,10 @@
9
9
  * (local or -g) left no better_sqlite3.node, `--doctor` still reported the
10
10
  * bindings as loaded, and every tool failed with "Could not locate the
11
11
  * bindings file" and no hint why. `npx @4da/mcp-server` was unaffected.
12
+ *
13
+ * Since 6.1.0 the server uses Node's built-in node:sqlite when the runtime
14
+ * has it (sqlite-driver.ts), so this matters only on Node 22.0-22.12, where
15
+ * better-sqlite3 is the one driver.
12
16
  */
13
17
  // Each remedy measured on npm 12.2.0 (node:24-bookworm-slim), 2026-10-03.
14
18
  export const NATIVE_BINDING_FIX = "npm 12+ blocks dependency install scripts unless allowed. With npx: allow it once with " +
@@ -9,23 +9,65 @@
9
9
  * app does not have this problem — it records each dependency's own manifest
10
10
  * directory — so this is the standalone counterpart of that inventory.
11
11
  *
12
- * A directory joins when it holds a lockfile (or go.mod / requirements.txt,
13
- * which carry exact versions themselves). Workspace members that share their
14
- * root's lockfile are covered by the root. Bounded: depth 2 below the root,
15
- * at most 64 projects, and directories that hold installed code, build
16
- * output, test fixtures or tool worktrees are skipped.
12
+ * A directory joins when it holds a lockfile (or go.mod / a requirements
13
+ * file, which carry exact versions themselves). Workspace members that share
14
+ * their root's lockfile are covered by the root. Directories that hold
15
+ * installed code, build output, test fixtures or tool worktrees are skipped.
16
+ *
17
+ * Bounded (MAX_DEPTH levels below the root, MAX_PROJECTS projects), and the
18
+ * bound is never silent. langchain keeps 108 lockfiles up to three levels
19
+ * down; the old bounds (depth 2, 64 projects) read 64 and dropped 44 with no
20
+ * trace in any answer (2026-10-10). Past a bound the walk keeps going without
21
+ * scanning, up to MAX_WALK_DIRS directories, so `discovery` can name every
22
+ * lockfile that was left out and say why.
17
23
  */
18
24
  import { type ProjectScanResult } from "./project-scanner.js";
25
+ /**
26
+ * Measured on langchain (108 lockfiles, up to 3 levels down; 2026-10-10):
27
+ * the walk plus every project scan takes about 2 s. Depth 4 covers it with a
28
+ * level to spare; 256 projects is 2.4x its count.
29
+ */
30
+ export declare const MAX_DEPTH = 4;
31
+ export declare const MAX_PROJECTS = 256;
19
32
  export interface ProjectTreeEntry {
20
33
  dir: string;
21
34
  scan: ProjectScanResult;
22
35
  }
36
+ /** What the walk left out, so an answer can say it is partial. */
37
+ export interface TreeDiscovery {
38
+ /** Projects scanned (the root included). */
39
+ projects: number;
40
+ maxDepth: number;
41
+ maxProjects: number;
42
+ /** Lockfiles found but not scanned, relative to the root (the first MAX_LISTED_SKIPS). */
43
+ skipped: Array<{
44
+ path: string;
45
+ reason: "depth" | "project_limit";
46
+ }>;
47
+ /** Every lockfile found but not scanned (`skipped` may list fewer). */
48
+ skippedCount: number;
49
+ /** True when the walk itself stopped at MAX_WALK_DIRS: lockfiles beyond it were not even counted. */
50
+ walkTruncated: boolean;
51
+ }
52
+ /** Python requirement files: requirements.txt, requirements-dev.txt, dev_requirements.txt, _test_minimum_requirements.txt. */
53
+ export declare function isRequirementsFileName(name: string): boolean;
54
+ /** The lockfiles (and exact-pin files) that make a directory a project, by name. */
55
+ export declare function lockfilesIn(dir: string, listing?: string[]): string[];
23
56
  /**
24
57
  * The root (always) and every locked project below it, each with its own
25
- * scan. Directories and lockfiles the repository's .gitignore excludes are not
26
- * part of the project and are skipped (gitignore.ts).
58
+ * scan, plus what the bounds left out. Directories and lockfiles the
59
+ * repository's .gitignore excludes are not part of the project and are
60
+ * skipped (gitignore.ts); they are not reported as left out either.
27
61
  */
28
- export declare function scanProjectTree(root: string): ProjectTreeEntry[];
62
+ export declare function scanProjectTree(root: string, limits?: {
63
+ maxDepth: number;
64
+ maxProjects: number;
65
+ }): {
66
+ entries: ProjectTreeEntry[];
67
+ discovery: TreeDiscovery;
68
+ };
69
+ /** One sentence for an answer, or null when nothing was left out. */
70
+ export declare function discoveryNote(discovery: TreeDiscovery | null): string | null;
29
71
  /**
30
72
  * Resolution groups for the live layer: one per (project directory, ecosystem).
31
73
  * A lockfile whose manifest declared nothing the scanner could read still gets
@@ -10,11 +10,17 @@
10
10
  * app does not have this problem — it records each dependency's own manifest
11
11
  * directory — so this is the standalone counterpart of that inventory.
12
12
  *
13
- * A directory joins when it holds a lockfile (or go.mod / requirements.txt,
14
- * which carry exact versions themselves). Workspace members that share their
15
- * root's lockfile are covered by the root. Bounded: depth 2 below the root,
16
- * at most 64 projects, and directories that hold installed code, build
17
- * output, test fixtures or tool worktrees are skipped.
13
+ * A directory joins when it holds a lockfile (or go.mod / a requirements
14
+ * file, which carry exact versions themselves). Workspace members that share
15
+ * their root's lockfile are covered by the root. Directories that hold
16
+ * installed code, build output, test fixtures or tool worktrees are skipped.
17
+ *
18
+ * Bounded (MAX_DEPTH levels below the root, MAX_PROJECTS projects), and the
19
+ * bound is never silent. langchain keeps 108 lockfiles up to three levels
20
+ * down; the old bounds (depth 2, 64 projects) read 64 and dropped 44 with no
21
+ * trace in any answer (2026-10-10). Past a bound the walk keeps going without
22
+ * scanning, up to MAX_WALK_DIRS directories, so `discovery` can name every
23
+ * lockfile that was left out and say why.
18
24
  */
19
25
  import * as fs from "node:fs";
20
26
  import * as path from "node:path";
@@ -29,60 +35,143 @@ const LOCK_MARKERS = [
29
35
  "Cargo.lock",
30
36
  "poetry.lock",
31
37
  "uv.lock",
38
+ "pdm.lock",
32
39
  "Pipfile.lock",
33
40
  "go.mod",
34
- "requirements.txt",
35
41
  ];
36
42
  const SKIP_DIRS = new Set([
37
43
  "node_modules", "target", ".git", "dist", "build", "out", "vendor", ".venv", "venv",
38
44
  "__pycache__", ".next", ".nuxt", "coverage", ".claude", ".codex", ".cursor", "fixtures",
39
45
  "__fixtures__", "testdata", "test-fixtures", "__tests__", ".turbo", ".cache", ".pnpm-store",
40
46
  ]);
41
- const MAX_DEPTH = 2;
42
- const MAX_PROJECTS = 64;
47
+ /**
48
+ * Measured on langchain (108 lockfiles, up to 3 levels down; 2026-10-10):
49
+ * the walk plus every project scan takes about 2 s. Depth 4 covers it with a
50
+ * level to spare; 256 projects is 2.4x its count.
51
+ */
52
+ export const MAX_DEPTH = 4;
53
+ export const MAX_PROJECTS = 256;
54
+ /** Directories visited in total, scanned or not: the hard cap on the walk itself. */
55
+ const MAX_WALK_DIRS = 20_000;
56
+ /** Lockfile paths listed in `discovery.skipped`; the count covers the rest. */
57
+ const MAX_LISTED_SKIPS = 50;
58
+ /** Python requirement files: requirements.txt, requirements-dev.txt, dev_requirements.txt, _test_minimum_requirements.txt. */
59
+ export function isRequirementsFileName(name) {
60
+ return /requirements[\w.-]*\.txt$/i.test(name);
61
+ }
62
+ /** The lockfiles (and exact-pin files) that make a directory a project, by name. */
63
+ export function lockfilesIn(dir, listing) {
64
+ let names;
65
+ try {
66
+ names = listing ?? fs.readdirSync(dir);
67
+ }
68
+ catch {
69
+ return [];
70
+ }
71
+ const found = names.filter((n) => LOCK_MARKERS.includes(n) || isRequirementsFileName(n));
72
+ // pip-compile-multi and similar keep the pins in a requirements/ folder (superset: requirements/base.txt).
73
+ if (names.includes("requirements")) {
74
+ try {
75
+ for (const n of fs.readdirSync(path.join(dir, "requirements")))
76
+ if (/\.txt$/i.test(n))
77
+ found.push(`requirements/${n}`);
78
+ }
79
+ catch {
80
+ // a file named "requirements"
81
+ }
82
+ }
83
+ return found;
84
+ }
43
85
  /**
44
86
  * The root (always) and every locked project below it, each with its own
45
- * scan. Directories and lockfiles the repository's .gitignore excludes are not
46
- * part of the project and are skipped (gitignore.ts).
87
+ * scan, plus what the bounds left out. Directories and lockfiles the
88
+ * repository's .gitignore excludes are not part of the project and are
89
+ * skipped (gitignore.ts); they are not reported as left out either.
47
90
  */
48
- export function scanProjectTree(root) {
91
+ export function scanProjectTree(root, limits = { maxDepth: MAX_DEPTH, maxProjects: MAX_PROJECTS }) {
49
92
  const entries = [{ dir: root, scan: scanCurrentProject(root) }];
50
93
  const ignore = new IgnoreRules(root);
51
- const walk = (dir, rel, depth) => {
52
- if (depth > MAX_DEPTH || entries.length >= MAX_PROJECTS)
53
- return;
54
- ignore.load(rel);
55
- let children;
94
+ const skipped = [];
95
+ let skippedCount = 0;
96
+ let visited = 0;
97
+ let walkTruncated = false;
98
+ // One directory listing per directory: it decides both whether the
99
+ // directory is a project and which children to descend into.
100
+ const listing = (dir) => {
56
101
  try {
57
- children = fs.readdirSync(dir, { withFileTypes: true });
102
+ return fs.readdirSync(dir, { withFileTypes: true });
58
103
  }
59
104
  catch {
60
- return;
105
+ return null;
61
106
  }
107
+ };
108
+ const walk = (dir, rel, depth, children) => {
62
109
  for (const child of children) {
63
- if (entries.length >= MAX_PROJECTS)
64
- return;
65
110
  if (!child.isDirectory() || SKIP_DIRS.has(child.name) || child.name.startsWith("."))
66
111
  continue;
112
+ if (++visited > MAX_WALK_DIRS) {
113
+ walkTruncated = true;
114
+ return;
115
+ }
67
116
  const childRel = rel ? `${rel}/${child.name}` : child.name;
68
117
  if (ignore.ignores(childRel, true))
69
118
  continue;
70
119
  const sub = path.join(dir, child.name);
71
- ignore.load(childRel);
72
- const locked = LOCK_MARKERS.some((marker) => fs.existsSync(path.join(sub, marker)) && !ignore.ignores(`${childRel}/${marker}`, false));
73
- if (locked)
74
- entries.push({ dir: sub, scan: scanCurrentProject(sub) });
75
- walk(sub, childRel, depth + 1);
120
+ const grandchildren = listing(sub);
121
+ if (!grandchildren)
122
+ continue;
123
+ // A .gitignore is only read where one exists.
124
+ if (grandchildren.some((g) => g.name === ".gitignore"))
125
+ ignore.load(childRel);
126
+ const names = grandchildren.map((g) => g.name);
127
+ const locks = lockfilesIn(sub, names).filter((name) => !ignore.ignores(`${childRel}/${name}`, false));
128
+ if (locks.length > 0) {
129
+ const reason = depth > limits.maxDepth ? "depth" : entries.length >= limits.maxProjects ? "project_limit" : null;
130
+ if (reason === null) {
131
+ entries.push({ dir: sub, scan: scanCurrentProject(sub) });
132
+ }
133
+ else {
134
+ skippedCount += locks.length;
135
+ for (const name of locks)
136
+ if (skipped.length < MAX_LISTED_SKIPS)
137
+ skipped.push({ path: `${childRel}/${name}`, reason });
138
+ }
139
+ }
140
+ walk(sub, childRel, depth + 1, grandchildren);
141
+ if (walkTruncated)
142
+ return;
76
143
  }
77
144
  };
78
- walk(root, "", 1);
79
- return entries;
145
+ walk(root, "", 1, listing(root) ?? []);
146
+ return {
147
+ entries,
148
+ discovery: { projects: entries.length, maxDepth: limits.maxDepth, maxProjects: limits.maxProjects, skipped, skippedCount, walkTruncated },
149
+ };
150
+ }
151
+ /** One sentence for an answer, or null when nothing was left out. */
152
+ export function discoveryNote(discovery) {
153
+ if (!discovery || (discovery.skippedCount === 0 && !discovery.walkTruncated))
154
+ return null;
155
+ const parts = [];
156
+ if (discovery.skippedCount > 0) {
157
+ const depth = discovery.skipped.some((s) => s.reason === "depth");
158
+ const limit = discovery.skipped.some((s) => s.reason === "project_limit");
159
+ const why = [
160
+ depth ? `deeper than ${discovery.maxDepth} levels` : null,
161
+ limit ? `past the ${discovery.maxProjects}-project limit` : null,
162
+ ].filter(Boolean).join(" or ");
163
+ const listed = discovery.skipped.slice(0, 5).map((s) => s.path).join(", ");
164
+ parts.push(`${discovery.skippedCount} lockfile${discovery.skippedCount === 1 ? " was" : "s were"} found but NOT scanned (${why}): ${listed}${discovery.skippedCount > 5 ? ", ..." : ""}; pass project_path to scan one of them`);
165
+ }
166
+ if (discovery.walkTruncated)
167
+ parts.push(`the directory walk stopped after ${MAX_WALK_DIRS} directories, so lockfiles beyond that were not counted`);
168
+ return `Partial scan: ${parts.join("; ")}.`;
80
169
  }
81
170
  /** Lockfiles that make an ecosystem scannable in a directory even when no direct dependency was parsed. */
82
171
  const LOCKS_BY_LANGUAGE = {
83
172
  npm: ["package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock"],
84
173
  rust: ["Cargo.lock"],
85
- python: ["poetry.lock", "uv.lock", "Pipfile.lock", "requirements.txt"],
174
+ python: ["poetry.lock", "uv.lock", "pdm.lock", "Pipfile.lock"],
86
175
  go: ["go.mod"],
87
176
  };
88
177
  /**
@@ -94,8 +183,11 @@ const LOCKS_BY_LANGUAGE = {
94
183
  export function treeResolutionGroups(entries) {
95
184
  return entries.flatMap(({ dir, scan }) => {
96
185
  const byLanguage = { ...scan.depsByEcosystem };
186
+ const present = lockfilesIn(dir);
187
+ const pins = present.some((n) => isRequirementsFileName(n) || n.startsWith("requirements/"));
97
188
  for (const [language, locks] of Object.entries(LOCKS_BY_LANGUAGE)) {
98
- if (!byLanguage[language] && locks.some((lock) => fs.existsSync(path.join(dir, lock)))) {
189
+ const pinned = language === "python" && pins;
190
+ if (!byLanguage[language] && (pinned || locks.some((lock) => present.includes(lock)))) {
99
191
  byLanguage[language] = { deps: [], devDeps: [] };
100
192
  }
101
193
  }
@@ -47,7 +47,7 @@ export const TOOL_REGISTRY = {
47
47
  annotations: { title: "Dependency health", ...READ_NETWORK },
48
48
  },
49
49
  upgrade_planner: {
50
- summary: "Prioritized upgrade plan: the 4DA app's work order when computed (per-line targets, manifest vs lockfile fix), else the smallest version that fixes each vulnerability from the lockfiles. Call when the user asks what to upgrade.",
50
+ summary: "Prioritized upgrade plan for one project (project_path): the 4DA app's work order when computed (per-line targets, manifest vs lockfile fix), else the smallest version that fixes each vulnerability. Call when the user asks what to upgrade.",
51
51
  definition: upgradePlannerTool,
52
52
  category: "security",
53
53
  tags: ["upgrade", "dependencies", "recommendations", "versions"],
@@ -72,7 +72,7 @@ export const TOOL_REGISTRY = {
72
72
  },
73
73
  // --- Intelligence (mixed) ---
74
74
  what_should_i_know: {
75
- summary: "Pre-task briefing scoped to the task: the dependencies it touches, their installed versions, version-confirmed vulnerabilities, releases since, your recorded decisions, and a delegation verdict. Call BEFORE starting a non-trivial task, especially one that changes dependencies.",
75
+ summary: "Pre-task briefing for a task in one project: the dependencies and package families it names, installed versions, confirmed vulnerabilities, releases since, your decisions, a delegation verdict. Call BEFORE a non-trivial task, especially one that changes dependencies.",
76
76
  definition: whatShouldIKnowTool,
77
77
  category: "intelligence",
78
78
  tags: ["briefing", "advisories", "pre-task", "delegation"],
@@ -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 {};