efaimo 0.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 (75) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +6 -0
  4. package/README.md +281 -0
  5. package/SKILL.md +57 -0
  6. package/dist/check/check.d.ts +31 -0
  7. package/dist/check/check.js +77 -0
  8. package/dist/cli.d.ts +2 -0
  9. package/dist/cli.js +372 -0
  10. package/dist/clients/introspect.d.ts +8 -0
  11. package/dist/clients/introspect.js +198 -0
  12. package/dist/clients/rawprobe.d.ts +69 -0
  13. package/dist/clients/rawprobe.js +597 -0
  14. package/dist/clients/stateless.d.ts +7 -0
  15. package/dist/clients/stateless.js +121 -0
  16. package/dist/core/engine.d.ts +6 -0
  17. package/dist/core/engine.js +17 -0
  18. package/dist/core/grade.d.ts +8 -0
  19. package/dist/core/grade.js +19 -0
  20. package/dist/core/types.d.ts +243 -0
  21. package/dist/core/types.js +1 -0
  22. package/dist/index.d.ts +16 -0
  23. package/dist/index.js +26 -0
  24. package/dist/mcp/server.d.ts +3 -0
  25. package/dist/mcp/server.js +88 -0
  26. package/dist/reporters/badge.d.ts +12 -0
  27. package/dist/reporters/badge.js +37 -0
  28. package/dist/reporters/json.d.ts +1 -0
  29. package/dist/reporters/json.js +10 -0
  30. package/dist/reporters/markdown.d.ts +7 -0
  31. package/dist/reporters/markdown.js +121 -0
  32. package/dist/reporters/pretty.d.ts +15 -0
  33. package/dist/reporters/pretty.js +221 -0
  34. package/dist/rules/injection.d.ts +20 -0
  35. package/dist/rules/injection.js +106 -0
  36. package/dist/rules/mcp/index.d.ts +4 -0
  37. package/dist/rules/mcp/index.js +643 -0
  38. package/dist/rules/skill/index.d.ts +2 -0
  39. package/dist/rules/skill/index.js +279 -0
  40. package/dist/skills/parse.d.ts +4 -0
  41. package/dist/skills/parse.js +113 -0
  42. package/dist/targets/clientConfigs.d.ts +17 -0
  43. package/dist/targets/clientConfigs.js +110 -0
  44. package/dist/targets/repoScan.d.ts +2 -0
  45. package/dist/targets/repoScan.js +0 -0
  46. package/dist/targets/resolve.d.ts +28 -0
  47. package/dist/targets/resolve.js +48 -0
  48. package/dist/testing/anthropicRunner.d.ts +6 -0
  49. package/dist/testing/anthropicRunner.js +33 -0
  50. package/dist/testing/harness.d.ts +45 -0
  51. package/dist/testing/harness.js +96 -0
  52. package/dist/testing/openaiRunner.d.ts +12 -0
  53. package/dist/testing/openaiRunner.js +40 -0
  54. package/dist/util/childEnv.d.ts +1 -0
  55. package/dist/util/childEnv.js +43 -0
  56. package/dist/util/dotenv.d.ts +12 -0
  57. package/dist/util/dotenv.js +53 -0
  58. package/dist/util/misc.d.ts +15 -0
  59. package/dist/util/misc.js +121 -0
  60. package/dist/version.d.ts +2 -0
  61. package/dist/version.js +2 -0
  62. package/dist/weigh/anthropicExact.d.ts +11 -0
  63. package/dist/weigh/anthropicExact.js +46 -0
  64. package/dist/weigh/diff.d.ts +14 -0
  65. package/dist/weigh/diff.js +19 -0
  66. package/dist/weigh/serializers.d.ts +21 -0
  67. package/dist/weigh/serializers.js +44 -0
  68. package/dist/weigh/tokens.d.ts +12 -0
  69. package/dist/weigh/tokens.js +23 -0
  70. package/dist/weigh/weigh.d.ts +6 -0
  71. package/dist/weigh/weigh.js +104 -0
  72. package/docs/INTEGRATIONS.md +119 -0
  73. package/docs/METHODOLOGY.md +83 -0
  74. package/docs/RULES.md +76 -0
  75. package/package.json +77 -0
@@ -0,0 +1,121 @@
1
+ import { VERSION } from "../version.js";
2
+ const SEV_LABEL = { error: "🔴 error", warn: "🟡 warn", info: "🔵 info" };
3
+ function esc(s) {
4
+ return s.replace(/\|/g, "\\|").replace(/\n/g, " ");
5
+ }
6
+ function findingsTable(lines, findings) {
7
+ lines.push("| severity | rule | finding |");
8
+ lines.push("|---|---|---|");
9
+ for (const f of findings) {
10
+ lines.push(`| ${SEV_LABEL[f.severity]} | ${f.ruleId} | ${esc(f.message)} |`);
11
+ }
12
+ }
13
+ export function renderCheckMarkdown(report) {
14
+ const lines = [];
15
+ lines.push(`## efaimo check (${report.surface}): grade ${report.grade.letter} (${report.grade.score})`);
16
+ lines.push("");
17
+ lines.push(`target: \`${report.target}\``);
18
+ lines.push("");
19
+ if (!report.findings.length) {
20
+ lines.push(report.readiness ? "no quality findings. clean." : "no findings. clean.");
21
+ }
22
+ else {
23
+ findingsTable(lines, report.findings);
24
+ }
25
+ if (report.readiness) {
26
+ lines.push("");
27
+ lines.push(`### 2026-07-28 readiness (a migration diff, not graded)`);
28
+ lines.push("");
29
+ if (report.readiness.findings.length) {
30
+ findingsTable(lines, report.readiness.findings);
31
+ }
32
+ else {
33
+ lines.push("clean, nothing to migrate.");
34
+ }
35
+ }
36
+ lines.push("");
37
+ for (const note of report.notes)
38
+ lines.push(`> ${esc(note)}`);
39
+ lines.push("");
40
+ lines.push(`<sub>generated by [efaimo](https://github.com/efaimo-ai/efaimo) v${report.version} | [rule docs](https://github.com/efaimo-ai/efaimo/blob/main/docs/RULES.md)</sub>`);
41
+ return lines.join("\n");
42
+ }
43
+ export function renderSkillSetMarkdown(res) {
44
+ const lines = [];
45
+ lines.push(`## efaimo check: ${res.perSkill.length} Agent Skills`);
46
+ lines.push("");
47
+ lines.push("| skill | grade | errors | warnings | info |");
48
+ lines.push("|---|---|---|---|---|");
49
+ for (const s of [...res.perSkill].sort((a, b) => a.report.grade.score - b.report.grade.score)) {
50
+ const c = s.report.counts;
51
+ lines.push(`| \`${esc(s.name)}\` | ${s.report.grade.letter} (${s.report.grade.score}) | ${c.error} | ${c.warn} | ${c.info} |`);
52
+ }
53
+ if (res.setFindings.length) {
54
+ lines.push("");
55
+ lines.push("**Across the set**");
56
+ for (const f of res.setFindings)
57
+ lines.push(`- ${SEV_LABEL[f.severity]} ${f.ruleId}: ${esc(f.message)}`);
58
+ }
59
+ lines.push("");
60
+ lines.push(`<sub>generated by [efaimo](https://github.com/efaimo-ai/efaimo) v${VERSION}</sub>`);
61
+ return lines.join("\n");
62
+ }
63
+ export function renderWeighMarkdown(w) {
64
+ const lines = [];
65
+ if (w.kind === "mcp") {
66
+ lines.push(`## efaimo weigh: \`${w.label}\``);
67
+ lines.push("");
68
+ lines.push(`tools ${w.toolCount} | resources ${w.resourceCount} | prompts ${w.promptCount}`);
69
+ lines.push("");
70
+ lines.push("| serialization | tokens (o200k, est.) |");
71
+ lines.push("|---|---|");
72
+ lines.push(`| raw JSON | ${w.totals.rawJson.toLocaleString("en-US")} |`);
73
+ lines.push(`| Claude-style | ${w.totals.claudeStyle.toLocaleString("en-US")} |`);
74
+ lines.push(`| OpenAI tools | ${w.totals.openaiTools.toLocaleString("en-US")} |`);
75
+ if (w.anthropicExactTotal !== undefined) {
76
+ lines.push(`| anthropic exact (API) | ${w.anthropicExactTotal.toLocaleString("en-US")} |`);
77
+ }
78
+ lines.push("");
79
+ if (w.perTool.length) {
80
+ lines.push("| heaviest tools | tokens | desc | schema |");
81
+ lines.push("|---|---|---|---|");
82
+ for (const t of w.perTool.slice(0, 10)) {
83
+ lines.push(`| \`${esc(t.name)}\` | ${t.tokens.claudeStyle} | ${t.descriptionTokens} | ${t.schemaTokens} |`);
84
+ }
85
+ if (w.framingTokens !== undefined && w.framingTokens > 0) {
86
+ lines.push(`| block framing (\`<functions>\` wrapper) | ${w.framingTokens} | - | - |`);
87
+ }
88
+ }
89
+ }
90
+ else {
91
+ lines.push(`## efaimo weigh (skills): \`${w.label}\``);
92
+ lines.push("");
93
+ lines.push("| skill | metadata (always) | body (on trigger) | lines | referenced files |");
94
+ lines.push("|---|---|---|---|---|");
95
+ for (const s of w.perSkill) {
96
+ lines.push(`| \`${esc(s.name)}\` | ${s.metadataTokens} | ${s.bodyTokens} | ${s.bodyLines} | ${s.refFileCount ? `${s.refFileCount} (${s.refFileTokens} tok)` : "-"} |`);
97
+ }
98
+ lines.push("");
99
+ lines.push(`totals: metadata **${w.totals.metadata}** | body **${w.totals.body}** | referenced **${w.totals.refFiles}**`);
100
+ }
101
+ lines.push("");
102
+ for (const note of w.notes)
103
+ lines.push(`> ${esc(note)}`);
104
+ return lines.join("\n");
105
+ }
106
+ export function renderDiffMarkdown(d) {
107
+ const sign = d.delta >= 0 ? "+" : "";
108
+ const lines = [];
109
+ lines.push(`## efaimo weigh diff`);
110
+ lines.push("");
111
+ lines.push(`Claude-style total: ${d.before.toLocaleString("en-US")} -> ${d.after.toLocaleString("en-US")} (**${sign}${d.delta.toLocaleString("en-US")}**, ${sign}${d.pct.toFixed(1)}%)`);
112
+ if (d.toolChanges.length) {
113
+ lines.push("");
114
+ lines.push("| tool | before | after |");
115
+ lines.push("|---|---|---|");
116
+ for (const t of d.toolChanges.slice(0, 15)) {
117
+ lines.push(`| \`${esc(t.name)}\` | ${t.before ?? "added"} | ${t.after ?? "removed"} |`);
118
+ }
119
+ }
120
+ return lines.join("\n");
121
+ }
@@ -0,0 +1,15 @@
1
+ import type { CheckReport, ServerWeighResult, SkillSetWeighResult } from "../core/types.js";
2
+ import type { CheckSkillResult } from "../check/check.js";
3
+ import type { Scenario, TestReport } from "../testing/harness.js";
4
+ import type { WeighDiff } from "../weigh/diff.js";
5
+ export declare function setColor(on: boolean): void;
6
+ export declare function renderCheckPretty(report: CheckReport): string;
7
+ export declare function renderScenarioPlan(s: Scenario): string;
8
+ export declare function renderTestReportPretty(r: TestReport): string;
9
+ export declare function renderSkillSetPretty(res: CheckSkillResult): string;
10
+ export declare function renderServerWeighPretty(w: ServerWeighResult): string;
11
+ export declare function renderSkillWeighPretty(w: SkillSetWeighResult): string;
12
+ export declare function renderDiffPretty(d: WeighDiff, opts: {
13
+ maxTokens?: number;
14
+ allowIncreasePct?: number;
15
+ }): string;
@@ -0,0 +1,221 @@
1
+ import pc from "picocolors";
2
+ import { sortFindings } from "../core/grade.js";
3
+ import { VERSION } from "../version.js";
4
+ const DOCS_RULES = "https://github.com/efaimo-ai/efaimo/blob/main/docs/RULES.md";
5
+ let colorOn = true;
6
+ export function setColor(on) {
7
+ colorOn = on;
8
+ }
9
+ function paint(fn, s) {
10
+ return colorOn ? fn(s) : s;
11
+ }
12
+ function sevGlyph(f) {
13
+ if (f.severity === "error")
14
+ return paint(pc.red, "x");
15
+ if (f.severity === "warn")
16
+ return paint(pc.yellow, "!");
17
+ return paint(pc.cyan, "i");
18
+ }
19
+ function gradeColor(letter, text) {
20
+ if (letter === "A")
21
+ return paint(pc.green, text);
22
+ if (letter === "B")
23
+ return paint(pc.green, text);
24
+ if (letter === "C")
25
+ return paint(pc.yellow, text);
26
+ if (letter === "D")
27
+ return paint(pc.yellow, text);
28
+ return paint(pc.red, text);
29
+ }
30
+ function n(x) {
31
+ return x.toLocaleString("en-US");
32
+ }
33
+ function renderFinding(lines, f) {
34
+ lines.push(` ${sevGlyph(f)} ${paint(pc.bold, f.ruleId)} ${f.message}`);
35
+ if (f.detail) {
36
+ for (const d of f.detail.split("\n"))
37
+ lines.push(paint(pc.dim, ` ${d}`));
38
+ }
39
+ if (f.fixHint)
40
+ lines.push(paint(pc.dim, ` fix: ${f.fixHint}`));
41
+ }
42
+ export function renderCheckPretty(report) {
43
+ const lines = [];
44
+ lines.push(paint(pc.dim, `efaimo v${VERSION}`));
45
+ lines.push(`check ${report.surface} ${paint(pc.bold, report.target)}`);
46
+ const g = report.grade;
47
+ const qualityLabel = report.readiness ? "quality: " : "";
48
+ lines.push(`grade ${gradeColor(g.letter, `${g.letter} (${g.score})`)} ${qualityLabel}` +
49
+ `${paint(pc.red, String(report.counts.error))} error${report.counts.error === 1 ? "" : "s"} ` +
50
+ `${paint(pc.yellow, String(report.counts.warn))} warning${report.counts.warn === 1 ? "" : "s"} ` +
51
+ `${paint(pc.cyan, String(report.counts.info))} info`);
52
+ lines.push("");
53
+ if (!report.findings.length) {
54
+ lines.push(paint(pc.green, report.readiness ? " no quality findings. clean." : " no findings. clean."));
55
+ }
56
+ for (const f of report.findings)
57
+ renderFinding(lines, f);
58
+ if (report.readiness) {
59
+ const r = report.readiness;
60
+ lines.push("");
61
+ if (r.findings.length) {
62
+ lines.push(paint(pc.bold, `2026-07-28 readiness`) +
63
+ ` ${r.findings.length} item${r.findings.length === 1 ? "" : "s"} to migrate ` +
64
+ paint(pc.dim, "(a migration diff, not graded: the spec finalizes 2026-07-28)"));
65
+ for (const f of r.findings)
66
+ renderFinding(lines, f);
67
+ }
68
+ else {
69
+ lines.push(paint(pc.bold, `2026-07-28 readiness`) + paint(pc.green, " clean, nothing to migrate"));
70
+ }
71
+ }
72
+ lines.push("");
73
+ for (const note of report.notes)
74
+ lines.push(paint(pc.dim, `note: ${note}`));
75
+ lines.push(paint(pc.dim, `rules: ${DOCS_RULES}`));
76
+ return lines.join("\n");
77
+ }
78
+ export function renderScenarioPlan(s) {
79
+ const calls = s.trials * 2 * 2;
80
+ const firstLine = s.task.split("\n")[0].slice(0, 60);
81
+ return [
82
+ paint(pc.dim, `efaimo v${VERSION}`),
83
+ `test (dry run) ${paint(pc.bold, s.name)}`,
84
+ "",
85
+ ` skill ${s.skillName}`,
86
+ ` model ${s.model}`,
87
+ ` plan ${s.trials} trials x 2 arms x (task + judge) = ${paint(pc.bold, String(calls))} API calls`,
88
+ ` task ${paint(pc.dim, firstLine + (s.task.length > 60 ? "..." : ""))}`,
89
+ "",
90
+ paint(pc.yellow, "dry run: no API calls made. add --live to run it (Claude models need ANTHROPIC_API_KEY, GPT models need OPENAI_API_KEY)."),
91
+ ].join("\n");
92
+ }
93
+ export function renderTestReportPretty(r) {
94
+ const verdictColor = r.verdict === "helps" ? pc.green : r.verdict === "hurts" ? pc.red : pc.yellow;
95
+ const sign = r.deltaPoints >= 0 ? "+" : "";
96
+ const lines = [
97
+ paint(pc.dim, `efaimo v${VERSION}`),
98
+ `test ${paint(pc.bold, r.scenario)}`,
99
+ `skill ${r.skill} model ${r.model}`,
100
+ "",
101
+ ` with skill ${r.withSkill.passes}/${r.withSkill.trials} pass (${r.withSkill.passRate.toFixed(0)}%)`,
102
+ ` without skill ${r.withoutSkill.passes}/${r.withoutSkill.trials} pass (${r.withoutSkill.passRate.toFixed(0)}%)`,
103
+ ` delta ${paint(pc.bold, `${sign}${r.deltaPoints} points`)} ${paint(verdictColor, r.verdict)}`,
104
+ "",
105
+ ];
106
+ for (const n of r.notes)
107
+ lines.push(paint(pc.dim, `note: ${n}`));
108
+ return lines.join("\n");
109
+ }
110
+ export function renderSkillSetPretty(res) {
111
+ const lines = [];
112
+ lines.push(paint(pc.dim, `efaimo v${VERSION}`));
113
+ lines.push(`check skills ${paint(pc.bold, res.label)} ${res.perSkill.length} skill${res.perSkill.length === 1 ? "" : "s"}`);
114
+ const dist = { A: 0, B: 0, C: 0, D: 0, F: 0 };
115
+ for (const s of res.perSkill)
116
+ dist[s.report.grade.letter]++;
117
+ lines.push("");
118
+ lines.push("grades " +
119
+ ["A", "B", "C", "D", "F"]
120
+ .map((g) => (dist[g] ? `${gradeColor(g, g)}x${dist[g]}` : ""))
121
+ .filter(Boolean)
122
+ .join(" "));
123
+ lines.push("");
124
+ const nameW = Math.min(34, Math.max(8, ...res.perSkill.map((s) => s.name.length)));
125
+ for (const s of [...res.perSkill].sort((a, b) => a.report.grade.score - b.report.grade.score)) {
126
+ const g = s.report.grade;
127
+ const c = s.report.counts;
128
+ lines.push(` ${gradeColor(g.letter, `${g.letter} (${String(g.score).padStart(3)})`)} ${s.name.padEnd(nameW)} ` +
129
+ `${paint(pc.red, String(c.error))}e ${paint(pc.yellow, String(c.warn))}w ${paint(pc.cyan, String(c.info))}i`);
130
+ }
131
+ if (res.setFindings.length) {
132
+ lines.push("");
133
+ lines.push(paint(pc.bold, "across the set"));
134
+ for (const f of sortFindings(res.setFindings)) {
135
+ lines.push(` ${sevGlyph(f)} ${paint(pc.bold, f.ruleId)} ${f.message}`);
136
+ }
137
+ }
138
+ const flagged = res.perSkill.filter((s) => s.report.findings.length);
139
+ if (flagged.length) {
140
+ lines.push("");
141
+ for (const s of flagged) {
142
+ lines.push(paint(pc.bold, `${s.name} ${gradeColor(s.report.grade.letter, s.report.grade.letter)}`));
143
+ for (const f of s.report.findings) {
144
+ lines.push(` ${sevGlyph(f)} ${paint(pc.bold, f.ruleId)} ${f.message}`);
145
+ if (f.fixHint)
146
+ lines.push(paint(pc.dim, ` fix: ${f.fixHint}`));
147
+ }
148
+ }
149
+ }
150
+ lines.push("");
151
+ lines.push(paint(pc.dim, `${res.perSkill.length} skills under ${res.root}`));
152
+ lines.push(paint(pc.dim, `rules: ${DOCS_RULES}`));
153
+ return lines.join("\n");
154
+ }
155
+ export function renderServerWeighPretty(w) {
156
+ const lines = [];
157
+ lines.push(paint(pc.dim, `efaimo v${VERSION}`));
158
+ lines.push(`weigh mcp ${paint(pc.bold, w.label)}`);
159
+ lines.push(`tools ${w.toolCount} resources ${w.resourceCount} prompts ${w.promptCount}`);
160
+ lines.push("");
161
+ lines.push("context cost of tool definitions (o200k tokens, estimated)");
162
+ const pct = ((w.totals.claudeStyle / 200000) * 100).toFixed(1);
163
+ lines.push(` raw JSON ${n(w.totals.rawJson).padStart(8)}`);
164
+ lines.push(` Claude-style ${n(w.totals.claudeStyle).padStart(8)} (~${pct}% of a 200k window)`);
165
+ lines.push(` OpenAI tools ${n(w.totals.openaiTools).padStart(8)}`);
166
+ if (w.instructionsTokens > 0)
167
+ lines.push(` server instructions ${n(w.instructionsTokens).padStart(4)}`);
168
+ if (w.anthropicExactTotal !== undefined) {
169
+ lines.push(` ${paint(pc.green, "anthropic exact")} ${n(w.anthropicExactTotal).padStart(8)} (count_tokens API)`);
170
+ }
171
+ if (w.perTool.length) {
172
+ lines.push("");
173
+ lines.push("heaviest tools (Claude-style)");
174
+ for (const [i, t] of w.perTool.slice(0, 8).entries()) {
175
+ lines.push(` ${String(i + 1).padStart(2)}. ${t.name.padEnd(28)} ${n(t.tokens.claudeStyle).padStart(7)} desc ${n(t.descriptionTokens)} | schema ${n(t.schemaTokens)}`);
176
+ }
177
+ if (w.perTool.length > 8)
178
+ lines.push(paint(pc.dim, ` (+${w.perTool.length - 8} more)`));
179
+ if (w.framingTokens !== undefined && w.framingTokens > 0) {
180
+ lines.push(paint(pc.dim, ` block framing ${" ".repeat(15)}${n(w.framingTokens).padStart(7)} (<functions> wrapper; per-tool lines + this = total)`));
181
+ }
182
+ }
183
+ lines.push("");
184
+ for (const note of w.notes)
185
+ lines.push(paint(pc.dim, `note: ${note}`));
186
+ return lines.join("\n");
187
+ }
188
+ export function renderSkillWeighPretty(w) {
189
+ const lines = [];
190
+ lines.push(paint(pc.dim, `efaimo v${VERSION}`));
191
+ lines.push(`weigh skills ${paint(pc.bold, w.label)} ${w.perSkill.length} skill${w.perSkill.length === 1 ? "" : "s"}`);
192
+ lines.push("");
193
+ lines.push(` ${"skill".padEnd(28)} ${"metadata".padStart(8)} ${"body".padStart(9)} ${"lines".padStart(6)} refs`);
194
+ for (const s of w.perSkill) {
195
+ lines.push(` ${s.name.padEnd(28)} ${n(s.metadataTokens).padStart(8)} ${n(s.bodyTokens).padStart(9)} ${String(s.bodyLines).padStart(6)} ${s.refFileCount ? `${s.refFileCount} files ${n(s.refFileTokens)}` : "-"}`);
196
+ }
197
+ lines.push("");
198
+ lines.push(`totals: metadata ${paint(pc.bold, n(w.totals.metadata))} (always loaded) | body ${n(w.totals.body)} (on trigger) | referenced ${n(w.totals.refFiles)} (on demand)`);
199
+ lines.push("");
200
+ for (const note of w.notes)
201
+ lines.push(paint(pc.dim, `note: ${note}`));
202
+ return lines.join("\n");
203
+ }
204
+ export function renderDiffPretty(d, opts) {
205
+ const lines = [];
206
+ const sign = d.delta >= 0 ? "+" : "";
207
+ const deltaText = `${sign}${n(d.delta)} tokens (${sign}${d.pct.toFixed(1)}%)`;
208
+ lines.push(`context budget diff (Claude-style): ${n(d.before)} -> ${n(d.after)} ${d.delta > 0 ? paint(pc.yellow, deltaText) : paint(pc.green, deltaText)}`);
209
+ for (const t of d.toolChanges.slice(0, 10)) {
210
+ const b = t.before === undefined ? "added" : n(t.before);
211
+ const a = t.after === undefined ? "removed" : n(t.after);
212
+ lines.push(` ${t.name.padEnd(28)} ${b} -> ${a}`);
213
+ }
214
+ if (opts.maxTokens !== undefined && d.after > opts.maxTokens) {
215
+ lines.push(paint(pc.red, `budget exceeded: ${n(d.after)} > --max-tokens ${n(opts.maxTokens)}`));
216
+ }
217
+ if (opts.allowIncreasePct !== undefined && d.pct > opts.allowIncreasePct) {
218
+ lines.push(paint(pc.red, `increase ${d.pct.toFixed(1)}% > --allow-increase ${opts.allowIncreasePct}%`));
219
+ }
220
+ return lines.join("\n");
221
+ }
@@ -0,0 +1,20 @@
1
+ import type { Finding, Severity } from "../core/types.js";
2
+ interface InjectionPattern {
3
+ key: string;
4
+ re: RegExp;
5
+ severity: Severity;
6
+ label: string;
7
+ }
8
+ /**
9
+ * Surface-level heuristics for instruction-injection patterns in text an agent
10
+ * will read (tool descriptions, skill bodies). This is intentionally a linter,
11
+ * not a security scanner: deep analysis belongs to dedicated tools
12
+ * (Snyk agent-scan, Cisco MCP Scanner). Patterns favor precision over recall.
13
+ */
14
+ export declare const INJECTION_PATTERNS: InjectionPattern[];
15
+ export declare function scanTextForInjection(text: string, opts: {
16
+ ruleId: string;
17
+ where: string;
18
+ cap?: number;
19
+ }): Finding[];
20
+ export {};
@@ -0,0 +1,106 @@
1
+ import { truncate } from "../util/misc.js";
2
+ /**
3
+ * Surface-level heuristics for instruction-injection patterns in text an agent
4
+ * will read (tool descriptions, skill bodies). This is intentionally a linter,
5
+ * not a security scanner: deep analysis belongs to dedicated tools
6
+ * (Snyk agent-scan, Cisco MCP Scanner). Patterns favor precision over recall.
7
+ */
8
+ export const INJECTION_PATTERNS = [
9
+ {
10
+ key: "override-instructions",
11
+ re: /ignore\s+(?:all\s+|any\s+)?(?:previous|prior|earlier|above)\s+(?:instructions|messages|rules|prompts)/i,
12
+ severity: "error",
13
+ label: "attempts to override prior instructions",
14
+ },
15
+ {
16
+ key: "hide-from-user",
17
+ re: /do\s+not\s+(?:tell|reveal|mention|inform|show|disclose)\b[\s\S]{0,40}\b(?:user|human|operator)/i,
18
+ severity: "error",
19
+ label: "instructs the agent to hide behavior from the user",
20
+ },
21
+ {
22
+ key: "act-without-consent",
23
+ re: /without\s+(?:telling|asking|notifying|informing)\s+(?:the\s+)?(?:user|human)/i,
24
+ severity: "error",
25
+ label: "instructs the agent to act without user consent",
26
+ },
27
+ {
28
+ key: "credential-exfil",
29
+ // "post" and "token" dropped: they match API documentation ("Token counting
30
+ // POST /v1/...") far more than exfiltration.
31
+ re: /\b(?:api[_-]?key|secret|password|credential)s?\b[\s\S]{0,40}\b(?:send|forward|exfiltrat|upload|leak|transmit|include\s+it)\b/i,
32
+ severity: "error",
33
+ label: "references sending credentials or secrets somewhere",
34
+ },
35
+ {
36
+ key: "sensitive-file-read",
37
+ re: /\b(?:read|open|cat|print|upload)\b[\s\S]{0,40}(?:\.env\b|\.ssh\b|id_rsa|credentials|\.aws\b|\.npmrc\b)/i,
38
+ severity: "error",
39
+ label: "references reading sensitive local files",
40
+ },
41
+ {
42
+ key: "disable-safety",
43
+ re: /\b(?:disable|bypass|ignore|remove)\s+(?:safety|guardrails?|filters?|restrictions?|policy|policies)\b/i,
44
+ severity: "error",
45
+ label: "asks to bypass safety or policy",
46
+ },
47
+ {
48
+ key: "cross-tool-steering",
49
+ re: /before\s+(?:using|calling|running)\b[^.?!]{0,40}?\btools?\b[\s,]{1,4}(?:always\s+|first\s+|please\s+)?(?:call|run|use|fetch|invoke)\b/i,
50
+ severity: "warn",
51
+ label: "steers the agent to call another tool first (cross-tool steering)",
52
+ },
53
+ {
54
+ key: "templated-exfil-url",
55
+ // Bounded runs (no adjacent unbounded same-class quantifiers) to avoid ReDoS.
56
+ re: /https?:\/\/[^\s"'<>]{1,150}[?&][^\s"'<>]{0,150}(?:\$\{[^}]{1,60}\}|\{\{[^}]{1,60}\}\}|\{[^}]{1,60}\})/,
57
+ severity: "warn",
58
+ label: "URL with a templated query parameter (possible exfiltration channel)",
59
+ },
60
+ {
61
+ key: "zero-width-chars",
62
+ // eslint-disable-next-line no-control-regex
63
+ re: /[​‌‍⁠]/,
64
+ severity: "warn",
65
+ label: "zero-width or invisible characters present",
66
+ },
67
+ {
68
+ key: "hidden-html-comment",
69
+ re: /<!--[^>]{0,240}\b(?:must|always|never|call|run|fetch|send|ignore)\b[^>]{0,240}-->/i,
70
+ severity: "warn",
71
+ label: "HTML comment containing imperative instructions (hidden from rendered view)",
72
+ },
73
+ {
74
+ key: "persona-override",
75
+ re: /\byou\s+are\s+now\b|\bpretend\s+to\s+be\b|\bnew\s+system\s+prompt\b/i,
76
+ severity: "warn",
77
+ label: "persona or system-prompt override language",
78
+ },
79
+ ];
80
+ export function scanTextForInjection(text, opts) {
81
+ const findings = [];
82
+ const cap = opts.cap ?? 10;
83
+ // Bound the scanned text: these regexes are cheap but not worth running on
84
+ // megabytes, and anything this long is its own finding (body-size rules).
85
+ const scanned = text.length > 64 * 1024 ? text.slice(0, 64 * 1024) : text;
86
+ for (const p of INJECTION_PATTERNS) {
87
+ if (findings.length >= cap)
88
+ break;
89
+ const m = p.re.exec(scanned);
90
+ if (m) {
91
+ findings.push({
92
+ ruleId: opts.ruleId,
93
+ // Deliberately info: these are shallow heuristics an attacker evades
94
+ // trivially. Surfacing them must never move a grade or fail CI, or a
95
+ // clean report would read as a security pass (it is not one).
96
+ severity: "info",
97
+ title: "possible injection pattern (heuristic)",
98
+ message: `${opts.where}: ${p.label}`,
99
+ target: opts.where,
100
+ detail: `matched: "${truncate(m[0].replace(/\s+/g, " "), 100)}". This is a shallow lint hint, not a security verdict.`,
101
+ fixHint: "Remove instruction-like language from agent-readable text. efaimo is not a security scanner; for real supply-chain safety use a dedicated scanner (for example Snyk agent-scan).",
102
+ });
103
+ }
104
+ }
105
+ return findings;
106
+ }
@@ -0,0 +1,4 @@
1
+ import type { McpRule } from "../../core/types.js";
2
+ export declare const MCP_RULES: McpRule[];
3
+ /** E101-E118 are 2026-07-28 readiness rules: reported as a migration diff, not graded. */
4
+ export declare function isReadinessRuleId(id: string): boolean;