efaimo 0.3.0 → 0.4.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 +57 -3
- package/README.md +4 -0
- package/SKILL.md +1 -1
- package/dist/check/check.js +15 -0
- package/dist/core/types.d.ts +18 -0
- package/dist/skills/parse.d.ts +6 -0
- package/dist/skills/parse.js +47 -3
- package/dist/util/misc.d.ts +20 -2
- package/dist/util/misc.js +8 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/RULES.md +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,51 @@ 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.4.0] - 2026-09-03
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **`check --skill <dir>` finds skills it used to walk past**.
|
|
12
|
+
Two causes, both silent. The walk never entered a directory whose name starts
|
|
13
|
+
with a dot, so `.claude/skills/<name>/`, which is where a project keeps its
|
|
14
|
+
own skills, was invisible; and its depth bound of 3 missed layouts one level
|
|
15
|
+
deeper such as `skills/custom_skills/<name>/`. On the public corpus this
|
|
16
|
+
found 34 skills where `scripts/skills-index.mjs` found 38, and both numbers
|
|
17
|
+
were reported with confidence.
|
|
18
|
+
|
|
19
|
+
Dot directories are now entered for skill discovery and nowhere else, with
|
|
20
|
+
`.git` still excluded. The depth bound is 6 and is measured rather than
|
|
21
|
+
picked: the deepest real skill in that corpus sits 4 directories below a
|
|
22
|
+
directory of repositories and 3 below a repository root, so six leaves two
|
|
23
|
+
levels of headroom while still keeping an accidental `check --skill /` from
|
|
24
|
+
walking a disk.
|
|
25
|
+
|
|
26
|
+
**And when the bound does bite, it says so.** `SkillSet.truncatedAt` lists
|
|
27
|
+
the directories the walk refused to enter, because a bound that truncates in
|
|
28
|
+
silence is the same failure as a check that examines nothing. On the public
|
|
29
|
+
corpus it fires 16 times, all of them XML schema trees inside the `xlsx` and
|
|
30
|
+
`pptx` skills, which is the bound doing its job audibly.
|
|
31
|
+
|
|
32
|
+
- **One walker instead of two**. `scripts/skills-index.mjs`
|
|
33
|
+
carried its own copy with different rules, which is why the two disagreed.
|
|
34
|
+
It now calls the CLI's discovery, so they cannot drift apart again, and it
|
|
35
|
+
passes the truncation warning through. The published July index regenerates
|
|
36
|
+
byte-identically under the shared walk, which is how this was checked.
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- **S107: a filename one capitalisation away from a skill**.
|
|
41
|
+
Set-level and warn-level, so it moves no grade, in the same way S103 treats
|
|
42
|
+
a collision that belongs to a pair rather than to either member.
|
|
43
|
+
|
|
44
|
+
The spec names `SKILL.md` exactly and discovery matches it exactly, so
|
|
45
|
+
`skill.md` is not a skill here on any platform. It is not nothing anywhere:
|
|
46
|
+
a case-insensitive filesystem, the default on macOS and Windows, may hand it
|
|
47
|
+
to a host that opens it by name, so the same repository can carry a working
|
|
48
|
+
skill on the author's laptop and no skill at all in Linux CI. Nothing in
|
|
49
|
+
this tool would previously have mentioned such a file, which is the worst
|
|
50
|
+
property a near miss can have. Five exist in the public corpus.
|
|
51
|
+
|
|
7
52
|
## [0.3.0] - 2026-09-03
|
|
8
53
|
|
|
9
54
|
### Added
|
|
@@ -34,9 +79,17 @@ All notable changes to efaimo are documented here. Format follows
|
|
|
34
79
|
both runs scored lower.
|
|
35
80
|
|
|
36
81
|
Measured on the public skills corpus, 2026-07-17 pinned against 2026-09-03
|
|
37
|
-
HEAD, same ruleset both sides
|
|
38
|
-
+61.4% with one skill at 96.5%
|
|
39
|
-
worsened.
|
|
82
|
+
HEAD, same ruleset both sides, over the **32 skills `check --skill` pairs**:
|
|
83
|
+
metadata -0.8%, body +7.5%, referenced files +61.4% with one skill at 96.5%
|
|
84
|
+
of that movement; grades 0 improved and 4 worsened.
|
|
85
|
+
|
|
86
|
+
The population matters and is worth stating, because
|
|
87
|
+
`research/skills-delta-2026-09-03/REPORT.md` reports body **+7.0%** over
|
|
88
|
+
**36** paired skills for the same corpus on the same dates. Both are true.
|
|
89
|
+
`scripts/skills-index.mjs` finds four skills that `check --skill` does not,
|
|
90
|
+
for the walker reason in Known below, so the two are measuring slightly
|
|
91
|
+
different populations. Every other figure is identical between them; the
|
|
92
|
+
body percentage is the one the four missing skills move.
|
|
40
93
|
|
|
41
94
|
- **`efaimo find` takes several servers**. Pass more than one
|
|
42
95
|
and their catalogs are merged into one and measured together, each tool
|
|
@@ -355,6 +408,7 @@ First release.
|
|
|
355
408
|
Documented rule set (`docs/RULES.md`), token methodology (`docs/METHODOLOGY.md`),
|
|
356
409
|
and integration guide (`docs/INTEGRATIONS.md`).
|
|
357
410
|
|
|
411
|
+
[0.4.0]: https://github.com/efaimo-ai/efaimo/releases/tag/v0.4.0
|
|
358
412
|
[0.3.0]: https://github.com/efaimo-ai/efaimo/releases/tag/v0.3.0
|
|
359
413
|
[0.2.0]: https://github.com/efaimo-ai/efaimo/releases/tag/v0.2.0
|
|
360
414
|
[0.1.2]: https://github.com/efaimo-ai/efaimo/releases/tag/v0.1.2
|
package/README.md
CHANGED
|
@@ -406,3 +406,7 @@ Built by [efaimo ai](https://efaimo.ai), open tooling for the space between host
|
|
|
406
406
|
and tools. `efaimo` is the flagship CLI; capabilities grow as subcommands under one
|
|
407
407
|
name. Apache-2.0. See [CONTRIBUTING.md](./CONTRIBUTING.md) and
|
|
408
408
|
[docs/RULES.md](./docs/RULES.md) to add a rule.
|
|
409
|
+
|
|
410
|
+
EFAIMO AI is a registered trademark of the publisher (KR 40-2585820, class 42).
|
|
411
|
+
The lowercase `efaimo ai` used throughout is the same mark; the package name and
|
|
412
|
+
the commands stay lowercase.
|
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ 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.
|
|
6
|
+
version: "0.4.0"
|
|
7
7
|
homepage: "https://efaimo.ai"
|
|
8
8
|
---
|
|
9
9
|
|
package/dist/check/check.js
CHANGED
|
@@ -85,5 +85,20 @@ export async function checkSkillSet(pathInput, label) {
|
|
|
85
85
|
return { name, dir: skill.dir, report: buildReport("skill", name, findings, []) };
|
|
86
86
|
});
|
|
87
87
|
const setFindings = set.skills.flatMap((skill) => runRules(setRules, { skill, set, weigh }));
|
|
88
|
+
// S107 is emitted here rather than by a SkillRule because its subject is a
|
|
89
|
+
// file that is NOT a skill: no rule that takes a parsed skill could ever see
|
|
90
|
+
// it. Set level and warn level on purpose, so it moves no grade, matching
|
|
91
|
+
// how S103 treats a collision that belongs to a pair rather than to either
|
|
92
|
+
// member.
|
|
93
|
+
for (const file of set.miscasedSkillFiles ?? []) {
|
|
94
|
+
setFindings.push({
|
|
95
|
+
ruleId: "S107",
|
|
96
|
+
severity: "warn",
|
|
97
|
+
title: "filename is one capitalisation away from a skill",
|
|
98
|
+
message: `"${path.relative(set.root, file)}" is not loaded here, because the spec names SKILL.md exactly. A case-insensitive filesystem (the macOS and Windows default) may still hand it to a host, so this can work on a laptop and be missing entirely in Linux CI.`,
|
|
99
|
+
target: path.relative(set.root, file),
|
|
100
|
+
fixHint: "rename it to SKILL.md, or delete it if it is not a skill",
|
|
101
|
+
});
|
|
102
|
+
}
|
|
88
103
|
return { label, root: set.root, perSkill, setFindings, weigh };
|
|
89
104
|
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -339,6 +339,24 @@ export interface SkillInfo {
|
|
|
339
339
|
export interface SkillSet {
|
|
340
340
|
root: string;
|
|
341
341
|
skills: SkillInfo[];
|
|
342
|
+
/**
|
|
343
|
+
* Directories the walk refused to enter because it hit its depth bound.
|
|
344
|
+
*
|
|
345
|
+
* Present only when the bound actually bit. A truncated search that reports
|
|
346
|
+
* a short list and no reason is the same failure as a check that examines
|
|
347
|
+
* nothing, so the caller is given the means to say what it did not look at.
|
|
348
|
+
*/
|
|
349
|
+
truncatedAt?: string[];
|
|
350
|
+
/**
|
|
351
|
+
* Files one capitalisation away from being a skill, such as `skill.md`.
|
|
352
|
+
*
|
|
353
|
+
* The spec names `SKILL.md` exactly and discovery matches it exactly, so
|
|
354
|
+
* these are not skills here on any platform. They may still be skills to a
|
|
355
|
+
* host reading a case-insensitive filesystem, which is the default on macOS
|
|
356
|
+
* and Windows, so the same repository can work on a laptop and have no
|
|
357
|
+
* skills at all in Linux CI. Reported by S107.
|
|
358
|
+
*/
|
|
359
|
+
miscasedSkillFiles?: string[];
|
|
342
360
|
}
|
|
343
361
|
export interface GradeInfo {
|
|
344
362
|
score: number;
|
package/dist/skills/parse.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { SkillInfo, SkillSet } from "../core/types.js";
|
|
2
2
|
export declare function parseSkillFile(file: string): SkillInfo;
|
|
3
3
|
/** Resolve a path (SKILL.md file, skill dir, or a directory of skills) into a SkillSet. */
|
|
4
|
+
/**
|
|
5
|
+
* How deep skill discovery walks. See the note inside `findSkills`: measured
|
|
6
|
+
* against the public corpus, where the deepest real skill is four directories
|
|
7
|
+
* below a directory of repositories.
|
|
8
|
+
*/
|
|
9
|
+
export declare const SKILL_WALK_MAX_DEPTH = 6;
|
|
4
10
|
export declare function findSkills(input: string): SkillSet;
|
package/dist/skills/parse.js
CHANGED
|
@@ -129,6 +129,12 @@ export function parseSkillFile(file) {
|
|
|
129
129
|
return base;
|
|
130
130
|
}
|
|
131
131
|
/** Resolve a path (SKILL.md file, skill dir, or a directory of skills) into a SkillSet. */
|
|
132
|
+
/**
|
|
133
|
+
* How deep skill discovery walks. See the note inside `findSkills`: measured
|
|
134
|
+
* against the public corpus, where the deepest real skill is four directories
|
|
135
|
+
* below a directory of repositories.
|
|
136
|
+
*/
|
|
137
|
+
export const SKILL_WALK_MAX_DEPTH = 6;
|
|
132
138
|
export function findSkills(input) {
|
|
133
139
|
const abs = path.resolve(input);
|
|
134
140
|
const stat = fs.statSync(abs);
|
|
@@ -139,11 +145,49 @@ export function findSkills(input) {
|
|
|
139
145
|
if (fs.existsSync(direct)) {
|
|
140
146
|
return { root: abs, skills: [parseSkillFile(direct)] };
|
|
141
147
|
}
|
|
148
|
+
// Discovery is deliberate rather than incidental, because two walkers with
|
|
149
|
+
// different rules is what produced the 2026-09-03 defect: this found 34
|
|
150
|
+
// skills in a corpus where `scripts/skills-index.mjs` found 38, and both
|
|
151
|
+
// reported their own number with confidence.
|
|
152
|
+
//
|
|
153
|
+
// maxDepth 6 is measured, not picked. In the public corpus the deepest real
|
|
154
|
+
// skill sits 4 directories below a directory-of-repositories
|
|
155
|
+
// (`<corpus>/<repo>/skills/custom_skills/<name>/SKILL.md`) and 3 below a
|
|
156
|
+
// repository root. Six leaves two levels of headroom and keeps an accidental
|
|
157
|
+
// `check --skill /` from walking a disk. When the bound does bite it is
|
|
158
|
+
// reported rather than swallowed, in `truncatedAt`.
|
|
159
|
+
//
|
|
160
|
+
// Dot directories are entered here and nowhere else in this codebase:
|
|
161
|
+
// `.claude/skills/<name>/` is where a project keeps its own skills, so
|
|
162
|
+
// skipping them made this blind to the most common real layout. `.git` is
|
|
163
|
+
// still excluded by DEFAULT_SKIP_DIRS.
|
|
142
164
|
const skills = [];
|
|
143
|
-
|
|
144
|
-
|
|
165
|
+
const truncatedAt = [];
|
|
166
|
+
const miscased = [];
|
|
167
|
+
for (const f of walkFiles(abs, {
|
|
168
|
+
maxDepth: SKILL_WALK_MAX_DEPTH,
|
|
169
|
+
includeDotDirs: true,
|
|
170
|
+
onDepthLimit: (d) => truncatedAt.push(d),
|
|
171
|
+
})) {
|
|
172
|
+
const base = path.basename(f);
|
|
173
|
+
if (base === "SKILL.md")
|
|
145
174
|
skills.push(parseSkillFile(f));
|
|
175
|
+
// A file that is only a capitalisation away from being a skill. The spec
|
|
176
|
+
// names SKILL.md exactly, and this comparison is exact, so `skill.md` is
|
|
177
|
+
// invisible here on every platform. It is not invisible everywhere: a
|
|
178
|
+
// case-insensitive filesystem, which is the default on macOS and Windows,
|
|
179
|
+
// may hand it to a host that opens it by name, so the same repository has
|
|
180
|
+
// a working skill on the author's laptop and no skill at all in Linux CI.
|
|
181
|
+
// Nothing else in this tool would ever mention it, which is the worst
|
|
182
|
+
// property a near miss can have.
|
|
183
|
+
else if (base.toLowerCase() === "skill.md")
|
|
184
|
+
miscased.push(f);
|
|
146
185
|
}
|
|
147
186
|
skills.sort((a, b) => a.file.localeCompare(b.file));
|
|
148
|
-
return {
|
|
187
|
+
return {
|
|
188
|
+
root: abs,
|
|
189
|
+
skills,
|
|
190
|
+
...(truncatedAt.length ? { truncatedAt } : {}),
|
|
191
|
+
...(miscased.length ? { miscasedSkillFiles: miscased.sort() } : {}),
|
|
192
|
+
};
|
|
149
193
|
}
|
package/dist/util/misc.d.ts
CHANGED
|
@@ -3,10 +3,28 @@ export declare function parseCommandString(input: string): {
|
|
|
3
3
|
command: string;
|
|
4
4
|
args: string[];
|
|
5
5
|
};
|
|
6
|
-
export
|
|
6
|
+
export interface WalkOptions {
|
|
7
7
|
maxDepth?: number;
|
|
8
8
|
skipDirs?: Set<string>;
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Descend into directories whose name starts with a dot.
|
|
11
|
+
*
|
|
12
|
+
* Off by default, because most of them are caches and metadata. On for skill
|
|
13
|
+
* discovery, because `.claude/skills/<name>/` is where a project keeps its
|
|
14
|
+
* own skills and a tool that cannot see the most common real layout is not
|
|
15
|
+
* auditing anything. `DEFAULT_SKIP_DIRS` still excludes `.git`.
|
|
16
|
+
*/
|
|
17
|
+
includeDotDirs?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Called once for each directory not entered because `maxDepth` was reached.
|
|
20
|
+
*
|
|
21
|
+
* A bound that truncates in silence is the same failure as a check that
|
|
22
|
+
* examines nothing: the caller sees a short list and no reason for it. This
|
|
23
|
+
* lets the caller say so.
|
|
24
|
+
*/
|
|
25
|
+
onDepthLimit?: (dir: string) => void;
|
|
26
|
+
}
|
|
27
|
+
export declare function walkFiles(root: string, opts?: WalkOptions): Generator<string>;
|
|
10
28
|
export declare function readTextSafe(file: string, maxBytes?: number): string | undefined;
|
|
11
29
|
export declare function isBinaryPath(file: string): boolean;
|
|
12
30
|
export declare function extractVersion(s: string): number[] | undefined;
|
package/dist/util/misc.js
CHANGED
|
@@ -59,9 +59,15 @@ export function* walkFiles(root, opts = {}) {
|
|
|
59
59
|
for (const e of entries) {
|
|
60
60
|
const full = path.join(dir, e.name);
|
|
61
61
|
if (e.isDirectory()) {
|
|
62
|
-
if (
|
|
63
|
-
|
|
62
|
+
if (skip.has(e.name))
|
|
63
|
+
continue;
|
|
64
|
+
if (!opts.includeDotDirs && e.name.startsWith("."))
|
|
65
|
+
continue;
|
|
66
|
+
if (depth >= maxDepth) {
|
|
67
|
+
opts.onDepthLimit?.(full);
|
|
68
|
+
continue;
|
|
64
69
|
}
|
|
70
|
+
stack.push({ dir: full, depth: depth + 1 });
|
|
65
71
|
}
|
|
66
72
|
else if (e.isFile()) {
|
|
67
73
|
yield full;
|
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.
|
|
2
|
+
export declare const VERSION = "0.4.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.
|
|
2
|
+
export const VERSION = "0.4.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-
|
|
68
|
+
## Skills (S101-S107)
|
|
69
69
|
|
|
70
70
|
| id | sev | what it catches | spec |
|
|
71
71
|
|---|---|---|---|
|
|
@@ -75,6 +75,7 @@ never reused, so a gap is permanent and is better stated than smoothed over.
|
|
|
75
75
|
| S104 | warn/info | metadata over ~100 tokens, body over ~5k tokens or 500 lines, or long body with no progressive disclosure | agentskills.io |
|
|
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
|
+
| 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 |
|
|
78
79
|
|
|
79
80
|
## Findability (E141-E145)
|
|
80
81
|
|