efaimo 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,77 @@ All notable changes to efaimo are documented here. Format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project uses
5
5
  [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.6.0] - 2026-11-02
8
+
9
+ ### Added
10
+
11
+ - **S108: a reference file nothing points at**. A file under
12
+ `references/` that no markdown link or backticked path resolves to, whose
13
+ path never appears in `SKILL.md`, whose directory is never named there, and
14
+ that no reference file which IS reached mentions either. It ships in the
15
+ package, installs byte-perfect, and an agent has no route to it.
16
+
17
+ Found in this project's own skills on 2026-09-04: `read-back` had shipped a
18
+ 4.6 KB failure gallery that grew for a month with nothing linking to it, and
19
+ `unreleased-guard` had shipped `the-gap.md` the same way. The installer
20
+ tests, the house style guard and an A(100) were all green, because none of
21
+ them is about whether a shipped file is reachable. The workspace that
22
+ publishes those skills got a checker the same day; this is the rule for
23
+ everyone else's `references/`.
24
+
25
+ Reported and **not scored**, the way S105 is. Measured before it was
26
+ written, on the public corpus at the commits `research/skills-index/manifest.json`
27
+ pins: 2 of 36 skills carry a `references/` directory at all, 4 files between
28
+ them, no orphan, so there is no population to calibrate a penalty on and no
29
+ published grade moves. Scoring it is a separate decision. It fires under
30
+ `--strict`, which is where our own seven repositories run it.
31
+
32
+ Transitive on purpose: a reader who opens `references/api.md` can follow a
33
+ link inside it, so a file reached that way is not an orphan. Scoped to
34
+ `references/` on purpose: `scripts/` is executed rather than read and
35
+ `assets/` is consumed by scripts, so an unmentioned file there is a
36
+ different claim.
37
+
38
+ ### Changed
39
+
40
+ - **`rulesVersion` is `3`**. No grade moves with S108, but a
41
+ report from this ruleset carries a rule id that ruleset `2` could not have
42
+ produced, and `check --diff` across the two would attribute that to the
43
+ subject rather than to the ruler.
44
+
45
+ ### Security
46
+
47
+ - **Seven advisories against the MCP SDK and zod, fixed by upgrading**.
48
+ `@modelcontextprotocol/client` and
49
+ `@modelcontextprotocol/server` move from 2.0.0 to 2.3.1, the first release
50
+ that carries every patch, and still pinned exactly. zod's floor moves from
51
+ `^4.5.4` to `^4.6.6`. `pnpm audit --prod` went from 7 findings (5 high, 2
52
+ moderate) to none.
53
+
54
+ What they reach in this tool, read from the call sites: `efaimo mcp` serves
55
+ over stdio only, so the four server advisories about the HTTP transport
56
+ (GHSA-f897-62hf-6gxq body size, GHSA-jm63-cq47-j5jp shared transport,
57
+ GHSA-46jr-4wx8-v453 memory, GHSA-rvq5-wwqv-78pq bearer auth) describe code it
58
+ never runs. The OAuth advisory on the client (GHSA-6qxp-vccf-f47h) needs an
59
+ auth provider, and efaimo passes none. The one with a plausible path is
60
+ GHSA-6prh-2h8m-c8cw, headers re-sent across an origin redirect: `--header`
61
+ values go to Streamable HTTP and legacy SSE targets, and `requestInit` sets
62
+ `redirect: "error"`, but nobody has shown that every request the SSE
63
+ fallback makes inherits it. zod (GHSA-mrhx-c7xc-m3w5) has no known path here.
64
+
65
+ Who gets it when: zod's range floats, so a fresh `npx efaimo@0.5.0` already
66
+ resolves a patched zod. The SDK pins are exact, so that half reaches users
67
+ with this release and not before.
68
+
69
+ Measured after the upgrade with the same command as 0.5.0's entry: `check
70
+ --mcp "node dist/cli.js mcp"` still reports `grade A (100)` and `clean,
71
+ nothing to migrate`.
72
+
73
+ - **Ten dev-only advisories, fixed by moving vitest from 4.1.10 to 4.1.12**,
74
+ which re-resolves vite from 8.2.0 to 8.3.5 and source-map-js
75
+ to 1.2.2. None of them ships in the package; `pnpm audit` over every
76
+ dependency now finds nothing.
77
+
7
78
  ## [0.5.0] - 2026-09-04
8
79
 
9
80
  ### Changed
@@ -455,6 +526,7 @@ First release.
455
526
  Documented rule set (`docs/RULES.md`), token methodology (`docs/METHODOLOGY.md`),
456
527
  and integration guide (`docs/INTEGRATIONS.md`).
457
528
 
529
+ [0.6.0]: https://github.com/efaimo-ai/efaimo/releases/tag/v0.6.0
458
530
  [0.5.0]: https://github.com/efaimo-ai/efaimo/releases/tag/v0.5.0
459
531
  [0.4.0]: https://github.com/efaimo-ai/efaimo/releases/tag/v0.4.0
460
532
  [0.3.0]: https://github.com/efaimo-ai/efaimo/releases/tag/v0.3.0
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="assets/banner.png" alt="efaimo. The audit CLI for Agent Skills and MCP servers." width="820">
2
+ <img src="https://raw.githubusercontent.com/efaimo-ai/efaimo/main/assets/banner.png" alt="efaimo. The audit CLI for Agent Skills and MCP servers." width="820">
3
3
  </p>
4
4
 
5
5
  <p align="center">
@@ -87,7 +87,7 @@ csv-cleanup skill that a capable model does not need, and a skill that encodes a
87
87
  convention the model cannot guess.
88
88
 
89
89
  <p align="center">
90
- <img src="assets/test-ab.png" alt="efaimo test A/B results: csv-cleanup measures +0 points (no measurable effect) while contoso-crm-import measures +100 points (helps); both skills lint clean at grade A" width="880">
90
+ <img src="https://raw.githubusercontent.com/efaimo-ai/efaimo/main/assets/test-ab.png" alt="efaimo test A/B results: csv-cleanup measures +0 points (no measurable effect) while contoso-crm-import measures +100 points (helps); both skills lint clean at grade A" width="880">
91
91
  </p>
92
92
 
93
93
  <details>
@@ -137,7 +137,7 @@ npx efaimo check --mcp "npx -y my-server" # quality grade + 2026-07-28 migrat
137
137
  per tool, with an optional `--anthropic` exact Claude count.
138
138
 
139
139
  <p align="center">
140
- <img src="assets/demo.png" alt="efaimo weigh example output" width="720">
140
+ <img src="https://raw.githubusercontent.com/efaimo-ai/efaimo/main/assets/demo.png" alt="efaimo weigh example output" width="720">
141
141
  </p>
142
142
 
143
143
  `check --mcp` connects live (it speaks both the legacy handshake and the new
@@ -155,7 +155,7 @@ surface that is an A. Pass `--strict-readiness` when you want CI to fail on
155
155
  the diff. Full list: [docs/RULES.md](./docs/RULES.md).
156
156
 
157
157
  <p align="center">
158
- <img src="assets/check.png" alt="efaimo check example output: quality grade A (95) plus a four-item 2026-07-28 migration diff for the reference server" width="800">
158
+ <img src="https://raw.githubusercontent.com/efaimo-ai/efaimo/main/assets/check.png" alt="efaimo check example output: quality grade A (95) plus a four-item 2026-07-28 migration diff for the reference server" width="800">
159
159
  </p>
160
160
 
161
161
  *(That is the official reference server: solid quality, four things left to
@@ -253,7 +253,7 @@ your host actually serializes it, and grades the quality, before any of it reach
253
253
  your context window.
254
254
 
255
255
  <p align="center">
256
- <img src="assets/architecture.png" alt="efaimo weighs, checks, and tests what your agent loads" width="880">
256
+ <img src="https://raw.githubusercontent.com/efaimo-ai/efaimo/main/assets/architecture.png" alt="efaimo weighs, checks, and tests what your agent loads" width="880">
257
257
  </p>
258
258
 
259
259
  ## How it compares
@@ -349,7 +349,7 @@ npx efaimo weigh "npx -y my-server" --diff base.json --allow-increase 10
349
349
  `--badge badge.svg` writes an SVG plus a shields.io endpoint JSON for your
350
350
  README. These two are real efaimo output for the reference server:
351
351
 
352
- <img src="assets/badge-weigh.svg" alt="context cost: 1120 tok (o200k)"> <img src="assets/badge-grade.svg" alt="efaimo: A (95)">
352
+ <img src="https://raw.githubusercontent.com/efaimo-ai/efaimo/main/assets/badge-weigh.svg" alt="context cost: 1120 tok (o200k)"> <img src="https://raw.githubusercontent.com/efaimo-ai/efaimo/main/assets/badge-grade.svg" alt="efaimo: A (95)">
353
353
 
354
354
  More recipes (pre-commit, GitLab, editor audit, programmatic use):
355
355
  [docs/INTEGRATIONS.md](./docs/INTEGRATIONS.md).
@@ -358,7 +358,7 @@ More recipes (pre-commit, GitLab, editor audit, programmatic use):
358
358
 
359
359
  | family | covers |
360
360
  |---|---|
361
- | **S101 to S106** | skills: frontmatter and trigger quality, trigger collisions, context budget, reference integrity, injection hygiene |
361
+ | **S101 to S108** | skills: frontmatter and trigger quality, trigger collisions, context budget, reference integrity, injection hygiene, a filename one capitalisation away from `SKILL.md`, and a reference file nothing points at |
362
362
  | **E101 to E118** | MCP 2026-07-28 readiness: deprecated primitives, statelessness, `server/discover`, `resultType`, cache fields, transport |
363
363
  | **E121 to E128, E130** | MCP quality: description quality, annotations, schema hygiene, tool-count and token-cost budgets |
364
364
  | **E141 to E146** | MCP findability: exclusive vocabulary, whether a search returns the tool, indistinguishable pairs, names that carry no domain word |
package/SKILL.md CHANGED
@@ -3,8 +3,8 @@ name: efaimo
3
3
  description: Audit MCP servers and Agent Skills for quality and context-window cost. Use when the user wants to check an MCP server's token cost, lint a skill, verify 2026-07-28 MCP spec readiness, find out whether a search can single out a server's tools under deferred tool loading, or add an efaimo badge or CI gate.
4
4
  license: Apache-2.0
5
5
  metadata:
6
- version: "0.5.0"
7
- homepage: "https://efaimo.ai"
6
+ version: "0.6.0"
7
+ homepage: "https://efaimo.com"
8
8
  ---
9
9
 
10
10
  # efaimo
@@ -74,3 +74,14 @@ methodology doc for the full method.
74
74
  efaimo is a linter and cost profiler, not a security scanner: its injection checks
75
75
  are surface heuristics. For supply-chain security use a dedicated scanner. Full
76
76
  rule reference: https://github.com/efaimo-ai/efaimo/blob/main/docs/RULES.md
77
+
78
+ <!-- generated:siblings -->
79
+
80
+ ## Siblings
81
+
82
+ Every skill in this set is about a report that was true about the wrong thing. The set: https://efaimo.com/skills
83
+
84
+ - `denominator` - the counts efaimo prints are only worth as much as the sets they were taken over.
85
+ - `mcp-stateless-migration` - when `check --mcp` prints a readiness list and you want to work it.
86
+
87
+ <!-- /generated:siblings -->
@@ -1,7 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { scanTextForInjection } from "../injection.js";
3
3
  import { tokenize, QUERY_STOPWORDS } from "../../find/tokenize.js";
4
- import { formatTokens } from "../../util/misc.js";
4
+ import { formatTokens, readTextSafe, isBinaryPath } from "../../util/misc.js";
5
5
  const NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
6
6
  const STANDARD_FIELDS = new Set(["name", "description", "license", "compatibility", "metadata", "allowed-tools"]);
7
7
  function skillLabel(s) {
@@ -309,4 +309,116 @@ const s106 = {
309
309
  return findings;
310
310
  },
311
311
  };
312
- export const SKILL_RULES = [s101, s102, s103, s104, s105, s106];
312
+ // S108. A file under references/ that nothing points at.
313
+ //
314
+ // Found on 2026-09-04 in two of this project's own seven published skills:
315
+ // read-back shipped a 4.6 KB failure gallery that had been growing for a
316
+ // month, unreleased-guard shipped the-gap.md, and no SKILL.md linked to
317
+ // either. Both were packaged, both installed byte-perfect into a user's skills
318
+ // directory, and an agent had no route to them. The agentskills.io layout puts
319
+ // material "loaded into context as needed" under references/, and the only
320
+ // thing that can cause a load is a path in the instructions. Installer tests,
321
+ // house style and an A(100) were all green about it, because none of them is
322
+ // about whether a shipped file is reachable.
323
+ //
324
+ // Reachable means: a reference the parser already resolved (a markdown link
325
+ // or a backticked path), the file's skill-relative path appearing anywhere in
326
+ // the body, or an ancestor directory named with its trailing slash ("see
327
+ // references/"); and transitively the same from any reference file that is
328
+ // itself reached, because a reader who opens references/api.md can follow a
329
+ // link inside it. The lenient direction is deliberate: a false red here
330
+ // teaches the reader to skip the rule.
331
+ //
332
+ // Scoped to references/ on purpose. scripts/ is executed rather than read and
333
+ // assets/ is consumed by scripts, so an unmentioned file there is a different
334
+ // claim. Measured on the public corpus at the pinned commits before this was
335
+ // written (research/skills-index/manifest.json): 2 of 36 skills carry a
336
+ // references/ directory at all, 4 files between them, no orphan. Our own set
337
+ // had two on 2026-09-04 and has none since. There is no population to
338
+ // calibrate a penalty on, so this is reported without being scored, the way
339
+ // S105 is; scoring it is a separate decision that has to move grades with
340
+ // evidence behind it.
341
+ const S108_MAX = 10;
342
+ const s108 = {
343
+ id: "S108",
344
+ title: "reference file nothing points at",
345
+ surface: "skill",
346
+ check({ skill }) {
347
+ const refDir = path.join(skill.dir, "references");
348
+ const prefix = refDir + path.sep;
349
+ const candidates = skill.files.filter((f) => f.path.startsWith(prefix));
350
+ if (!candidates.length)
351
+ return [];
352
+ const rel = (abs) => path.relative(skill.dir, abs).split(path.sep).join("/");
353
+ // The file's own path, or any ancestor directory of it named on its own
354
+ // with its trailing slash, appearing in the text.
355
+ //
356
+ // "On its own" is load-bearing and the first version did not have it: it
357
+ // asked whether the text contained "references/", and every link to a
358
+ // sibling file under references/ contains exactly that, so linking ONE
359
+ // reference file marked every other file in the directory as reached and
360
+ // the rule could not fire on the fixture written to make it fire. The
361
+ // slash has to be followed by something that cannot continue a path.
362
+ const dirNamed = (text, dir) => new RegExp(dir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "/(?![A-Za-z0-9_.-])").test(text);
363
+ const mentions = (text, r) => {
364
+ if (text.includes(r))
365
+ return true;
366
+ const parts = r.split("/");
367
+ for (let i = 1; i < parts.length; i++) {
368
+ if (dirNamed(text, parts.slice(0, i).join("/")))
369
+ return true;
370
+ }
371
+ return false;
372
+ };
373
+ const reached = new Set();
374
+ const resolvedRefs = new Set(skill.referencedPaths.filter((x) => x.exists && !x.escapes).map((x) => path.resolve(x.resolved)));
375
+ for (const c of candidates) {
376
+ if (resolvedRefs.has(path.resolve(c.path)) || mentions(skill.body, rel(c.path)))
377
+ reached.add(c.path);
378
+ }
379
+ // Transitive: a reached text file can point at its siblings, by the
380
+ // skill-relative path or by a path relative to itself.
381
+ const scanned = new Set();
382
+ let grew = true;
383
+ while (grew) {
384
+ grew = false;
385
+ for (const r of [...reached]) {
386
+ if (scanned.has(r) || isBinaryPath(r))
387
+ continue;
388
+ scanned.add(r);
389
+ const text = readTextSafe(r);
390
+ if (text === undefined)
391
+ continue;
392
+ const from = path.dirname(r);
393
+ for (const c of candidates) {
394
+ if (reached.has(c.path))
395
+ continue;
396
+ const own = path.relative(from, c.path).split(path.sep).join("/");
397
+ if (mentions(text, rel(c.path)) || text.includes(own)) {
398
+ reached.add(c.path);
399
+ grew = true;
400
+ }
401
+ }
402
+ }
403
+ }
404
+ const label = skillLabel(skill);
405
+ const findings = [];
406
+ for (const c of candidates) {
407
+ if (reached.has(c.path))
408
+ continue;
409
+ if (findings.length >= S108_MAX)
410
+ break;
411
+ findings.push({
412
+ ruleId: "S108",
413
+ severity: "warn",
414
+ title: "reference file nothing points at",
415
+ message: `"${rel(c.path)}" ships with the skill and nothing in SKILL.md, or in any reference it does reach, names it; an agent has no path to it, so it costs the reader disk and the publisher maintenance and is never loaded`,
416
+ target: label,
417
+ fixHint: "link it from SKILL.md at the point where it applies, or delete it",
418
+ graded: false,
419
+ });
420
+ }
421
+ return findings;
422
+ },
423
+ };
424
+ export const SKILL_RULES = [s101, s102, s103, s104, s105, s106, s108];
@@ -12,6 +12,11 @@
12
12
  * no `rulesVersion` field at all; the absence IS the value.
13
13
  * "2" the first ruleset that declares itself. Adds the findability family
14
14
  * (E141-E145), which no earlier version had.
15
+ * "3" adds S108, a reference file nothing points at. Reported and not
16
+ * scored, so no grade moves with it; the number moves anyway, because a
17
+ * report from this ruleset carries a rule id that "2" could not have
18
+ * produced, and a diff across the two would attribute that to the
19
+ * subject.
15
20
  *
16
21
  * Bump it when a rule is added, removed, renumbered, or changed in what it
17
22
  * fires on. `test/meta.test.ts` pins the rule inventory, so an added or
@@ -23,4 +28,4 @@
23
28
  * would make it move on every release whether or not a rule moved, which is
24
29
  * the thing it exists to distinguish.
25
30
  */
26
- export declare const RULES_VERSION = "2";
31
+ export declare const RULES_VERSION = "3";
@@ -12,6 +12,11 @@
12
12
  * no `rulesVersion` field at all; the absence IS the value.
13
13
  * "2" the first ruleset that declares itself. Adds the findability family
14
14
  * (E141-E145), which no earlier version had.
15
+ * "3" adds S108, a reference file nothing points at. Reported and not
16
+ * scored, so no grade moves with it; the number moves anyway, because a
17
+ * report from this ruleset carries a rule id that "2" could not have
18
+ * produced, and a diff across the two would attribute that to the
19
+ * subject.
15
20
  *
16
21
  * Bump it when a rule is added, removed, renumbered, or changed in what it
17
22
  * fires on. `test/meta.test.ts` pins the rule inventory, so an added or
@@ -23,4 +28,4 @@
23
28
  * would make it move on every release whether or not a rule moved, which is
24
29
  * the thing it exists to distinguish.
25
30
  */
26
- export const RULES_VERSION = "2";
31
+ export const RULES_VERSION = "3";
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** Kept in sync with package.json by test/meta.test.ts. */
2
- export declare const VERSION = "0.5.0";
2
+ export declare const VERSION = "0.6.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /** Kept in sync with package.json by test/meta.test.ts. */
2
- export const VERSION = "0.5.0";
2
+ export const VERSION = "0.6.0";
package/docs/RULES.md CHANGED
@@ -65,7 +65,7 @@ never reused, so a gap is permanent and is better stated than smoothed over.
65
65
  | E128 | warn | total tool-definition tax over ~10k / ~25k tokens, estimated (needs `weigh` data); hard CI failure is left to the `--max-tokens`/`--diff` gate |
66
66
  | E130 | info | instruction-injection patterns in a tool description or server instructions (shallow heuristic, never a security verdict; use a dedicated scanner for depth). **Reported but never scored** |
67
67
 
68
- ## Skills (S101-S107)
68
+ ## Skills (S101-S108)
69
69
 
70
70
  | id | sev | what it catches | spec |
71
71
  |---|---|---|---|
@@ -76,6 +76,7 @@ never reused, so a gap is permanent and is better stated than smoothed over.
76
76
  | S105 | info | instruction-injection patterns in the skill body or description (shallow heuristic, never a security verdict). **Reported but never scored** | internal |
77
77
  | S106 | error/warn/info | referenced file missing, escaping the skill dir (`..`), or nested more than one level deep | agentskills.io |
78
78
  | S107 | warn | a file one capitalisation away from a skill, such as `skill.md`. Set-level and never scored: the spec names `SKILL.md` exactly, so it is not a skill here on any platform, but a case-insensitive filesystem (the macOS and Windows default) may hand it to a host, which makes a repository work on a laptop and have no skill at all in Linux CI | internal |
79
+ | S108 | warn | a file under `references/` that nothing points at: no markdown link or backticked path resolves to it, its path does not appear in `SKILL.md`, no ancestor directory of it is named there, and no reference file that IS reached mentions it either. It ships and installs perfectly and an agent has no route to it. **Reported but never scored** _(ruleset 3)_: at the pinned public corpus only 2 of 36 skills carry a `references/` directory and neither has an orphan, so there is no population to calibrate a penalty on; the two real instances were in this project's own skills | internal |
79
80
 
80
81
  ## Findability (E141-E145)
81
82
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "efaimo",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Audit what your agent loads: quality and context cost for MCP servers and Agent Skills",
5
5
  "keywords": [
6
6
  "mcp",
@@ -65,19 +65,19 @@
65
65
  "prepublishOnly": "npm run typecheck && npm run build && npm run test"
66
66
  },
67
67
  "dependencies": {
68
- "@modelcontextprotocol/client": "2.0.0",
69
- "@modelcontextprotocol/server": "2.0.0",
68
+ "@modelcontextprotocol/client": "2.3.1",
69
+ "@modelcontextprotocol/server": "2.3.1",
70
70
  "commander": "^15.0.0",
71
71
  "cross-spawn": "^7.0.6",
72
72
  "gpt-tokenizer": "^3.4.0",
73
73
  "picocolors": "^1.1.1",
74
74
  "yaml": "^2.9.0",
75
- "zod": "^4.5.4"
75
+ "zod": "^4.6.6"
76
76
  },
77
77
  "devDependencies": {
78
78
  "@types/cross-spawn": "^6.0.6",
79
79
  "@types/node": "^26.1.1",
80
80
  "typescript": "^7.0.2",
81
- "vitest": "^4.1.10"
81
+ "vitest": "^4.1.12"
82
82
  }
83
83
  }