@4da/mcp-server 6.2.2 → 6.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## 6.2.3 — 2026-10-11
4
+
5
+ ### Yarn 1 refresh instruction covers alias entries
6
+
7
+ The yarn 1 refresh step said "delete the `<name>@` entries from yarn.lock". A
8
+ yarn 1 block is keyed by every spec that resolved to it, and an alias block's
9
+ first key names the alias (`"string-width-cjs@npm:string-width@^4.2.0"`), so
10
+ following the step left those blocks and kept the vulnerable version. It now
11
+ names both forms: keys `"<name>@…"` and alias keys `"…@npm:<name>@…"`.
12
+
13
+ ### Fix-path parents name the real package
14
+
15
+ The parent reader behind the fix-path rule ("a lockfile refresh is enough"
16
+ vs "the parent must move") parsed lockfiles with its own code and still had
17
+ the two naming defects 6.2.2 fixed in the package readers. It now names
18
+ packages with the readers' helpers.
19
+
20
+ - **yarn (v1 and berry): npm aliases.** A parent installed under an alias was
21
+ reported by the alias: on hono's benchmarks/routers lockfile, string-width
22
+ 4.2.3 and wrap-ansi 7.0.0 were named `string-width-cjs` and `wrap-ansi-cjs`.
23
+ An aliased dependency edge (`string-width-cjs: "npm:string-width@^4.2.0"`)
24
+ was never matched, so @isaacs/cliui was missing from string-width 4.2.3's
25
+ parents, and its range is now the real package's `^4.2.0`.
26
+ - **pnpm 5: entries keyed by a tarball URL.** Such an entry was dropped as a
27
+ parent and as a dependency: trpc's tarball-keyed @prisma/client 4.7.1 was
28
+ missing from prisma 4.7.1's parents, and the four workspace projects that
29
+ depend on it were missing from @prisma/client's. A dependency value that
30
+ points at another entry (that tarball key, or a pnpm alias's
31
+ `/real/1.2.3`) is now an edge to the package it installs.
32
+
3
33
  ## 6.2.2 — 2026-10-11
4
34
 
5
35
  ### Lockfile readers agree with the 4DA conformance corpus
@@ -312,7 +312,10 @@ export function refreshCommand(format, name, installed, target) {
312
312
  case "yarn-berry":
313
313
  return `yarn up -R ${name}`;
314
314
  case "yarn":
315
- return `delete the ${name}@ entries from yarn.lock, then run yarn install`;
315
+ // Yarn 1 has no command that refreshes a transitive copy. Its blocks are keyed
316
+ // by every spec that resolved to them, and an alias block's first key names
317
+ // the alias ("string-width-cjs@npm:string-width@^4.2.0"), so name both forms.
318
+ return `delete every yarn.lock entry that resolves to ${name} (keys "${name}@…" and alias keys "…@npm:${name}@…"), then run yarn install`;
316
319
  case "bun":
317
320
  return `delete the ${name} entries from bun.lock, then run bun install`;
318
321
  case "cargo":
@@ -17,12 +17,30 @@
17
17
  * file, link) is dropped instead of being queried as a package name.
18
18
  */
19
19
  import type { InstanceSet } from "./lockfile-types.js";
20
+ /** An exact version with pnpm's peer suffixes removed: v6/v9 `1.2.3(react@18)`, v5 `1.2.3_react@18`. */
21
+ export declare function exactVersion(raw: string): string | null;
20
22
  /**
21
23
  * Splits one `packages:` or `snapshots:` key into [name, version]:
22
24
  * v9 `'@scope/pkg@1.2.3(peer@4.5.6)'`, v6 `/pkg@1.2.3`, v5 `/@scope/pkg/1.2.3_peer@4.5.6`.
23
25
  * Returns null for a key that does not pin a registry version.
24
26
  */
25
27
  export declare function parsePnpmKey(rawKey: string): [string, string] | null;
28
+ /** A `packages:`/`snapshots:` key, or a dependency value that points at one, in one spelling: unquoted, no leading `/`. */
29
+ export declare function pnpmKeyRef(raw: string): string;
30
+ export interface PnpmEntryReader {
31
+ /** One line inside a `packages:` or `snapshots:` section (indent > 0). */
32
+ line(indent: number, text: string): void;
33
+ /** Ends the entry being read: call at every section change and at the end of the file. */
34
+ finish(): void;
35
+ }
36
+ /**
37
+ * Reads `packages:`/`snapshots:` entries line by line and reports the
38
+ * registry (name, version) each installs (see pnpmEntryIdentity); entries
39
+ * that install no registry release are not reported.
40
+ */
41
+ export declare function pnpmEntryReader(onEntry: (key: string, id: [string, string], dev: boolean | undefined) => void): PnpmEntryReader;
42
+ /** Every `packages:`/`snapshots:` entry's registry (name, version), by pnpmKeyRef of its key. */
43
+ export declare function pnpmEntryIdentities(content: string): Map<string, [string, string]>;
26
44
  /**
27
45
  * Reads a pnpm lockfile (v5.x, v6 or v9) into `versions`; the first version seen wins.
28
46
  *
@@ -37,6 +55,15 @@ export declare function parsePnpmKey(rawKey: string): [string, string] | null;
37
55
  * package key. pnpm 9 dropped that field, so its copies have unknown scope.
38
56
  */
39
57
  export declare function readPnpmLock(content: string, versions: Map<string, string>, instances?: InstanceSet): void;
58
+ /**
59
+ * The registry package a yarn block installs. An npm alias
60
+ * (`"string-width-cjs": "npm:string-width@^4.2.0"`) keys the block by the
61
+ * alias, which is not a package: OSV knows `string-width`, so the alias was
62
+ * queried for nothing and the real package went unscanned (hono's
63
+ * benchmarks/routers yarn.lock, 4DA conformance corpus, 2026-10-12). berry
64
+ * names the real package in `resolution:`; v1 only in the alias spec.
65
+ */
66
+ export declare function yarnPackageName(block: string, firstLine: string, headerName: string): string;
40
67
  /**
41
68
  * Reads a yarn lockfile (v1 or berry) into `versions`.
42
69
  * v1: `"@scope/pkg@^1.0.0", "@scope/pkg@^1.1.0":` then ` version "1.2.3"`.
@@ -24,7 +24,7 @@ function unquote(s) {
24
24
  return s.length >= 2 && (s[0] === "'" || s[0] === '"') && s[s.length - 1] === s[0] ? s.slice(1, -1) : s;
25
25
  }
26
26
  /** An exact version with pnpm's peer suffixes removed: v6/v9 `1.2.3(react@18)`, v5 `1.2.3_react@18`. */
27
- function exactVersion(raw) {
27
+ export function exactVersion(raw) {
28
28
  const version = unquote(raw.trim()).split("(")[0].split("_")[0].trim();
29
29
  return /^\d/.test(version) ? version : null;
30
30
  }
@@ -116,6 +116,60 @@ function pnpmEntryIdentity(entry) {
116
116
  }
117
117
  return parsed;
118
118
  }
119
+ /** A `packages:`/`snapshots:` key, or a dependency value that points at one, in one spelling: unquoted, no leading `/`. */
120
+ export function pnpmKeyRef(raw) {
121
+ return unquote(raw.trim()).replace(/^\//, "");
122
+ }
123
+ /**
124
+ * Reads `packages:`/`snapshots:` entries line by line and reports the
125
+ * registry (name, version) each installs (see pnpmEntryIdentity); entries
126
+ * that install no registry release are not reported.
127
+ */
128
+ export function pnpmEntryReader(onEntry) {
129
+ let entry = null;
130
+ const finish = () => {
131
+ const id = entry ? pnpmEntryIdentity(entry) : null;
132
+ if (entry && id)
133
+ onEntry(entry.key, id, entry.dev);
134
+ entry = null;
135
+ };
136
+ return {
137
+ line(indent, text) {
138
+ if (indent === 2 && text.endsWith(":")) {
139
+ finish();
140
+ entry = { key: text.slice(0, -1), inResolution: false, local: false };
141
+ }
142
+ else if (entry && indent === 4) {
143
+ pnpmEntryField(entry, text);
144
+ }
145
+ else if (entry?.inResolution && indent > 4) {
146
+ pnpmResolutionText(entry, text);
147
+ }
148
+ },
149
+ finish,
150
+ };
151
+ }
152
+ /** Every `packages:`/`snapshots:` entry's registry (name, version), by pnpmKeyRef of its key. */
153
+ export function pnpmEntryIdentities(content) {
154
+ const ids = new Map();
155
+ const reader = pnpmEntryReader((key, id) => ids.set(pnpmKeyRef(key), id));
156
+ let inEntries = false;
157
+ for (const line of content.split(/\r?\n/)) {
158
+ const text = line.trim();
159
+ if (text === "" || text.startsWith("#"))
160
+ continue;
161
+ const indent = line.length - line.trimStart().length;
162
+ if (indent === 0) {
163
+ reader.finish();
164
+ inEntries = text === "packages:" || text === "snapshots:";
165
+ }
166
+ else if (inEntries) {
167
+ reader.line(indent, text);
168
+ }
169
+ }
170
+ reader.finish();
171
+ return ids;
172
+ }
119
173
  /**
120
174
  * Reads a pnpm lockfile (v5.x, v6 or v9) into `versions`; the first version seen wins.
121
175
  *
@@ -139,39 +193,25 @@ export function readPnpmLock(content, versions, instances) {
139
193
  let section = "";
140
194
  let blockIndent = -1; // column of the dependency block being read, or -1
141
195
  let pending = null; // v6/v9: a name whose `version:` line follows
142
- let entry = null; // the packages/snapshots entry whose fields are being read
143
- const finish = () => {
144
- const id = entry ? pnpmEntryIdentity(entry) : null;
145
- if (id) {
146
- transitive.push(id);
147
- if (entry.dev !== undefined)
148
- devByKey.set(`${id[0]}\0${id[1]}`, entry.dev);
149
- }
150
- entry = null;
151
- };
196
+ const entries = pnpmEntryReader((_key, id, dev) => {
197
+ transitive.push(id);
198
+ if (dev !== undefined)
199
+ devByKey.set(`${id[0]}\0${id[1]}`, dev);
200
+ });
152
201
  for (const line of content.split(/\r?\n/)) {
153
202
  const text = line.trim();
154
203
  if (text === "" || text.startsWith("#"))
155
204
  continue;
156
205
  const indent = line.length - line.trimStart().length;
157
206
  if (indent === 0) {
158
- finish();
207
+ entries.finish();
159
208
  section = text.endsWith(":") ? text.slice(0, -1) : "";
160
209
  blockIndent = DEPENDENCY_BLOCKS.has(text) ? 0 : -1;
161
210
  pending = null;
162
211
  continue;
163
212
  }
164
213
  if (section === "packages" || section === "snapshots") {
165
- if (indent === 2 && text.endsWith(":")) {
166
- finish();
167
- entry = { key: text.slice(0, -1), inResolution: false, local: false };
168
- }
169
- else if (entry && indent === 4) {
170
- pnpmEntryField(entry, text);
171
- }
172
- else if (entry?.inResolution && indent > 4) {
173
- pnpmResolutionText(entry, text);
174
- }
214
+ entries.line(indent, text);
175
215
  continue;
176
216
  }
177
217
  if (section === "importers" && indent <= 4) {
@@ -204,7 +244,7 @@ export function readPnpmLock(content, versions, instances) {
204
244
  pending = null;
205
245
  }
206
246
  }
207
- finish();
247
+ entries.finish();
208
248
  for (const [name, version] of transitive)
209
249
  setFirst(name, version);
210
250
  if (!instances)
@@ -224,7 +264,7 @@ export function readPnpmLock(content, versions, instances) {
224
264
  * benchmarks/routers yarn.lock, 4DA conformance corpus, 2026-10-12). berry
225
265
  * names the real package in `resolution:`; v1 only in the alias spec.
226
266
  */
227
- function yarnPackageName(block, firstLine, headerName) {
267
+ export function yarnPackageName(block, firstLine, headerName) {
228
268
  const resolution = /^\s+resolution:\s+"?((?:@[^\s/@"]+\/)?[^\s/@"]+)@npm:/m.exec(block);
229
269
  if (resolution)
230
270
  return resolution[1];
@@ -45,8 +45,20 @@ export declare function packageLockParents(lock: {
45
45
  * dependency blocks resolve `name` to `version`. pnpm records resolved
46
46
  * versions only, so `range` is null. Importers (the project's own manifests)
47
47
  * become local edges with their `specifier` when the lockfile records one.
48
+ *
49
+ * Entries and dependency values are named the way the package reader names
50
+ * them (pnpmEntryIdentities): a pnpm 5 entry keyed by its tarball URL is the
51
+ * package its `name:`/`version:` fields say, and a dependency value that is a
52
+ * key rather than a version (that tarball key, or an alias's
53
+ * `/real/1.2.3`, `/real@1.2.3`, `real@1.2.3`) is an edge to the package it points at.
48
54
  */
49
55
  export declare function pnpmParents(content: string, name: string, version: string): ParentEdge[];
56
+ /**
57
+ * Blocks whose dependencies resolve `name` to `version`. An npm alias edge
58
+ * (`string-width-cjs: "npm:string-width@^4.2.0"`) is an edge to the real
59
+ * package, with the real package's range; it resolves through the block whose
60
+ * spec is `string-width-cjs@npm:string-width@^4.2.0`.
61
+ */
50
62
  export declare function yarnParents(content: string, name: string, version: string): ParentEdge[];
51
63
  export declare function bunParents(lock: {
52
64
  packages?: Record<string, unknown>;
@@ -11,7 +11,7 @@
11
11
  import * as fs from "node:fs";
12
12
  import * as path from "node:path";
13
13
  import { stripTrailingCommas } from "./bun-lockfile.js";
14
- import { parsePnpmKey } from "./js-lockfile-readers.js";
14
+ import { exactVersion, parsePnpmKey, pnpmEntryIdentities, pnpmKeyRef, yarnPackageName } from "./js-lockfile-readers.js";
15
15
  import { packageNameFromNodeModulesPath, parseCargoLockPackages } from "./lockfile-parsers.js";
16
16
  const DEP_FIELDS = ["dependencies", "optionalDependencies", "peerDependencies"];
17
17
  /** The lockfile format of a path, or null for a file this module does not read. */
@@ -123,18 +123,32 @@ export function packageLockParents(lock, name, version) {
123
123
  // ---------------------------------------------------------------------------
124
124
  // pnpm-lock.yaml (v5, v6, v9)
125
125
  // ---------------------------------------------------------------------------
126
- function exactVersion(raw) {
127
- const unquoted = raw.trim().replace(/^['"]|['"]$/g, "");
128
- const v = unquoted.split("(")[0].split("_")[0].trim();
129
- return /^\d/.test(v) ? v : null;
130
- }
131
126
  /**
132
127
  * Parents from `packages:` (v5/v6) or `snapshots:` (v9) entries whose
133
128
  * dependency blocks resolve `name` to `version`. pnpm records resolved
134
129
  * versions only, so `range` is null. Importers (the project's own manifests)
135
130
  * become local edges with their `specifier` when the lockfile records one.
131
+ *
132
+ * Entries and dependency values are named the way the package reader names
133
+ * them (pnpmEntryIdentities): a pnpm 5 entry keyed by its tarball URL is the
134
+ * package its `name:`/`version:` fields say, and a dependency value that is a
135
+ * key rather than a version (that tarball key, or an alias's
136
+ * `/real/1.2.3`, `/real@1.2.3`, `real@1.2.3`) is an edge to the package it points at.
136
137
  */
137
138
  export function pnpmParents(content, name, version) {
139
+ const ids = pnpmEntryIdentities(content);
140
+ /** The (name, version) a dependency line installs: `dep: 1.2.3`, or a value that points at an entry. */
141
+ const installs = (dep, value) => {
142
+ const exact = exactVersion(value);
143
+ if (exact)
144
+ return [dep, exact];
145
+ const ref = pnpmKeyRef(value);
146
+ return ids.get(ref) ?? parsePnpmKey(ref);
147
+ };
148
+ const isTarget = (dep, value) => {
149
+ const id = installs(dep, value);
150
+ return id !== null && id[0] === name && id[1] === version;
151
+ };
138
152
  const edges = [];
139
153
  const seen = new Set();
140
154
  let section = "";
@@ -142,7 +156,7 @@ export function pnpmParents(content, name, version) {
142
156
  let importer = null;
143
157
  let inDeps = false;
144
158
  let depIndent = -1;
145
- let pendingImporterDep = false;
159
+ let pendingDep = null;
146
160
  let importerSpecifier = null;
147
161
  const add = (edge) => {
148
162
  const key = `${edge.parent}\0${edge.parentVersion}`;
@@ -166,7 +180,7 @@ export function pnpmParents(content, name, version) {
166
180
  }
167
181
  if (section === "packages" || section === "snapshots") {
168
182
  if (indent === 2 && text.endsWith(":")) {
169
- parent = parsePnpmKey(text.slice(0, -1));
183
+ parent = ids.get(pnpmKeyRef(text.slice(0, -1))) ?? null;
170
184
  inDeps = false;
171
185
  }
172
186
  else if (indent === 4 && parent) {
@@ -174,7 +188,7 @@ export function pnpmParents(content, name, version) {
174
188
  }
175
189
  else if (indent === 6 && parent && inDeps) {
176
190
  const m = /^(?:'([^']+)'|"([^"]+)"|([^\s:'"]+)):\s*(.+)$/.exec(text);
177
- if (m && (m[1] ?? m[2] ?? m[3]) === name && exactVersion(m[4]) === version) {
191
+ if (m && isTarget(m[1] ?? m[2] ?? m[3], m[4])) {
178
192
  add({ parent: parent[0], parentVersion: parent[1], range: null, local: false });
179
193
  }
180
194
  }
@@ -197,21 +211,23 @@ export function pnpmParents(content, name, version) {
197
211
  // A dependency of the project itself: v5 `name: 1.2.3`, v6/v9 `name:` + `specifier:` + `version:`.
198
212
  if (indent === depIndent + 2) {
199
213
  const m = /^(?:'([^']+)'|"([^"]+)"|([^\s:'"]+)):(?:\s+(.*))?$/.exec(text);
200
- pendingImporterDep = !!m && (m[1] ?? m[2] ?? m[3]) === name;
214
+ pendingDep = m ? m[1] ?? m[2] ?? m[3] : null;
201
215
  importerSpecifier = null;
202
- if (pendingImporterDep && m?.[4] && exactVersion(m[4]) === version) {
203
- add({ parent: importer, parentVersion: null, range: null, local: true });
204
- pendingImporterDep = false;
216
+ if (pendingDep && m?.[4]) {
217
+ if (isTarget(pendingDep, m[4]))
218
+ add({ parent: importer, parentVersion: null, range: null, local: true });
219
+ pendingDep = null;
205
220
  }
206
221
  }
207
- else if (indent === depIndent + 4 && pendingImporterDep) {
222
+ else if (indent === depIndent + 4 && pendingDep) {
208
223
  const spec = /^specifier:\s*(.+)$/.exec(text);
209
224
  if (spec)
210
225
  importerSpecifier = spec[1].replace(/^['"]|['"]$/g, "");
211
226
  const ver = /^version:\s*(.+)$/.exec(text);
212
- if (ver && exactVersion(ver[1]) === version) {
213
- add({ parent: importer, parentVersion: null, range: importerSpecifier, local: true });
214
- pendingImporterDep = false;
227
+ if (ver) {
228
+ if (isTarget(pendingDep, ver[1]))
229
+ add({ parent: importer, parentVersion: null, range: importerSpecifier, local: true });
230
+ pendingDep = null;
215
231
  }
216
232
  }
217
233
  }
@@ -226,9 +242,11 @@ function parseYarnBlocks(content) {
226
242
  for (const block of content.split(/\r?\n(?=\S)/)) {
227
243
  const lines = block.split(/\r?\n/);
228
244
  const header = lines[0];
229
- const name = /^"?(@?[^@\s"]+)@/.exec(header)?.[1];
230
- if (!name || name.startsWith("__"))
245
+ const headerName = /^"?(@?[^@\s"]+)@/.exec(header)?.[1];
246
+ if (!headerName || headerName.startsWith("__"))
231
247
  continue;
248
+ // An npm alias block (`string-width-cjs@npm:string-width@^4.2.0`) installs the real package.
249
+ const name = yarnPackageName(block, header, headerName);
232
250
  const version = /^\s+version:?\s+"?([^"\s]+)"?\s*$/m.exec(block)?.[1];
233
251
  if (!version)
234
252
  continue;
@@ -260,21 +278,27 @@ function parseYarnBlocks(content) {
260
278
  }
261
279
  return blocks;
262
280
  }
281
+ /**
282
+ * Blocks whose dependencies resolve `name` to `version`. An npm alias edge
283
+ * (`string-width-cjs: "npm:string-width@^4.2.0"`) is an edge to the real
284
+ * package, with the real package's range; it resolves through the block whose
285
+ * spec is `string-width-cjs@npm:string-width@^4.2.0`.
286
+ */
263
287
  export function yarnParents(content, name, version) {
264
288
  const blocks = parseYarnBlocks(content);
265
289
  const resolved = new Map();
266
290
  for (const b of blocks)
267
291
  for (const spec of b.specs)
268
- resolved.set(spec, b.version);
292
+ resolved.set(spec, b);
269
293
  const edges = [];
270
294
  for (const b of blocks) {
271
295
  for (const [dep, rawRange] of b.deps) {
272
- if (dep !== name)
273
- continue;
274
- const range = rawRange.replace(/^npm:/, "");
275
- if (resolved.get(`${name}@${range}`) !== version)
296
+ const spec = rawRange.replace(/^npm:/, "");
297
+ const target = resolved.get(`${dep}@${spec}`);
298
+ if (!target || target.name !== name || target.version !== version)
276
299
  continue;
277
- edges.push({ parent: b.name, parentVersion: b.version, range, local: false });
300
+ const alias = /^npm:((?:@[^\s/@]+\/)?[^\s/@]+)@(.+)$/.exec(rawRange);
301
+ edges.push({ parent: b.name, parentVersion: b.version, range: alias ? alias[2] : spec, local: false });
278
302
  }
279
303
  }
280
304
  return edges;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4da/mcp-server",
3
- "version": "6.2.2",
3
+ "version": "6.2.3",
4
4
  "mcpName": "io.github.4DA-Systems/4da-mcp-server",
5
5
  "description": "Upgrade intelligence for AI coding agents: what changes between your installed and target version (changelog, breaking changes, the files you import it from), a pre-install verdict on each bump, vulnerability scanning at osv-scanner parity across npm/pnpm/yarn, Cargo, Python and Go lockfiles, ranked upgrade plans, task-scoped briefings, decision memory. Local, no account; works standalone, and adds the 4DA desktop app's signals, knowledge gaps and stack-judged news when it is installed.",
6
6
  "type": "module",