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.
- package/CHANGELOG.md +47 -0
- package/LICENSE +202 -0
- package/NOTICE +6 -0
- package/README.md +281 -0
- package/SKILL.md +57 -0
- package/dist/check/check.d.ts +31 -0
- package/dist/check/check.js +77 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +372 -0
- package/dist/clients/introspect.d.ts +8 -0
- package/dist/clients/introspect.js +198 -0
- package/dist/clients/rawprobe.d.ts +69 -0
- package/dist/clients/rawprobe.js +597 -0
- package/dist/clients/stateless.d.ts +7 -0
- package/dist/clients/stateless.js +121 -0
- package/dist/core/engine.d.ts +6 -0
- package/dist/core/engine.js +17 -0
- package/dist/core/grade.d.ts +8 -0
- package/dist/core/grade.js +19 -0
- package/dist/core/types.d.ts +243 -0
- package/dist/core/types.js +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +26 -0
- package/dist/mcp/server.d.ts +3 -0
- package/dist/mcp/server.js +88 -0
- package/dist/reporters/badge.d.ts +12 -0
- package/dist/reporters/badge.js +37 -0
- package/dist/reporters/json.d.ts +1 -0
- package/dist/reporters/json.js +10 -0
- package/dist/reporters/markdown.d.ts +7 -0
- package/dist/reporters/markdown.js +121 -0
- package/dist/reporters/pretty.d.ts +15 -0
- package/dist/reporters/pretty.js +221 -0
- package/dist/rules/injection.d.ts +20 -0
- package/dist/rules/injection.js +106 -0
- package/dist/rules/mcp/index.d.ts +4 -0
- package/dist/rules/mcp/index.js +643 -0
- package/dist/rules/skill/index.d.ts +2 -0
- package/dist/rules/skill/index.js +279 -0
- package/dist/skills/parse.d.ts +4 -0
- package/dist/skills/parse.js +113 -0
- package/dist/targets/clientConfigs.d.ts +17 -0
- package/dist/targets/clientConfigs.js +110 -0
- package/dist/targets/repoScan.d.ts +2 -0
- package/dist/targets/repoScan.js +0 -0
- package/dist/targets/resolve.d.ts +28 -0
- package/dist/targets/resolve.js +48 -0
- package/dist/testing/anthropicRunner.d.ts +6 -0
- package/dist/testing/anthropicRunner.js +33 -0
- package/dist/testing/harness.d.ts +45 -0
- package/dist/testing/harness.js +96 -0
- package/dist/testing/openaiRunner.d.ts +12 -0
- package/dist/testing/openaiRunner.js +40 -0
- package/dist/util/childEnv.d.ts +1 -0
- package/dist/util/childEnv.js +43 -0
- package/dist/util/dotenv.d.ts +12 -0
- package/dist/util/dotenv.js +53 -0
- package/dist/util/misc.d.ts +15 -0
- package/dist/util/misc.js +121 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/dist/weigh/anthropicExact.d.ts +11 -0
- package/dist/weigh/anthropicExact.js +46 -0
- package/dist/weigh/diff.d.ts +14 -0
- package/dist/weigh/diff.js +19 -0
- package/dist/weigh/serializers.d.ts +21 -0
- package/dist/weigh/serializers.js +44 -0
- package/dist/weigh/tokens.d.ts +12 -0
- package/dist/weigh/tokens.js +23 -0
- package/dist/weigh/weigh.d.ts +6 -0
- package/dist/weigh/weigh.js +104 -0
- package/docs/INTEGRATIONS.md +119 -0
- package/docs/METHODOLOGY.md +83 -0
- package/docs/RULES.md +76 -0
- package/package.json +77 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { RC_VERSION, rcMeta, postMessage, StdioSession } from "./rawprobe.js";
|
|
2
|
+
/**
|
|
3
|
+
* 2026-07-28 introspection path: bare stateless requests, no initialize.
|
|
4
|
+
* The SDK client enforces the legacy handshake, so a stateless RC server makes
|
|
5
|
+
* client.connect() fail; this fallback lets weigh/check still work on exactly
|
|
6
|
+
* the servers the readiness rules are about. Returns undefined when the server
|
|
7
|
+
* does not answer the bare path either, so the caller can surface the original
|
|
8
|
+
* connect error.
|
|
9
|
+
*/
|
|
10
|
+
const MAX_PAGES = 20;
|
|
11
|
+
export async function introspectStateless(target, opts = {}) {
|
|
12
|
+
// The legacy path already burned the caller's timeout once; keep this
|
|
13
|
+
// attempt tight so a genuinely dead server fails in bounded time.
|
|
14
|
+
const budget = Math.min(Math.max(opts.timeoutMs ?? 15000, 1), 15000);
|
|
15
|
+
try {
|
|
16
|
+
return target.kind === "stdio" ? await statelessStdio(target, budget) : await statelessHttp(target);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async function statelessStdio(target, budget) {
|
|
23
|
+
const session = new StdioSession(target.command, target.args, target.env);
|
|
24
|
+
try {
|
|
25
|
+
const pages = [];
|
|
26
|
+
let rawFirst;
|
|
27
|
+
let cursor;
|
|
28
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
29
|
+
const params = cursor ? { ...rcMeta(), cursor } : rcMeta();
|
|
30
|
+
const reply = await session.request("tools/list", params, budget);
|
|
31
|
+
if (!reply.result) {
|
|
32
|
+
if (page === 0)
|
|
33
|
+
return undefined; // no bare answer: not a stateless server
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
if (page === 0)
|
|
37
|
+
rawFirst = reply.result;
|
|
38
|
+
pages.push(reply.result);
|
|
39
|
+
cursor = reply.result.nextCursor;
|
|
40
|
+
if (!cursor)
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
const discover = await session.request("server/discover", rcMeta(), Math.min(budget, 10000));
|
|
44
|
+
return buildIntrospection(target.label, "stdio", undefined, pages, rawFirst, discover.result);
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
session.kill();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function statelessHttp(target) {
|
|
51
|
+
const pages = [];
|
|
52
|
+
let rawFirst;
|
|
53
|
+
let cursor;
|
|
54
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
55
|
+
const params = cursor ? { ...rcMeta(), cursor } : rcMeta();
|
|
56
|
+
const reply = await postMessage(target.url, target.headers, { jsonrpc: "2.0", id: 100 + page, method: "tools/list", params }, undefined, RC_VERSION);
|
|
57
|
+
const result = reply.body?.result;
|
|
58
|
+
if (!result) {
|
|
59
|
+
if (page === 0)
|
|
60
|
+
return undefined;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
if (page === 0)
|
|
64
|
+
rawFirst = result;
|
|
65
|
+
pages.push(result);
|
|
66
|
+
cursor = result.nextCursor;
|
|
67
|
+
if (!cursor)
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
const discover = await postMessage(target.url, target.headers, { jsonrpc: "2.0", id: 99, method: "server/discover", params: rcMeta() }, undefined, RC_VERSION).catch(() => undefined);
|
|
71
|
+
return buildIntrospection(target.label, "http", "streamable", pages, rawFirst, discover?.body?.result);
|
|
72
|
+
}
|
|
73
|
+
function buildIntrospection(label, transport, httpTransport, pages, rawFirst, discoverResult) {
|
|
74
|
+
const tools = [];
|
|
75
|
+
for (const page of pages) {
|
|
76
|
+
const list = Array.isArray(page.tools) ? (page.tools) : [];
|
|
77
|
+
for (const raw of list) {
|
|
78
|
+
const t = raw;
|
|
79
|
+
if (typeof t.name !== "string")
|
|
80
|
+
continue;
|
|
81
|
+
tools.push({
|
|
82
|
+
name: t.name,
|
|
83
|
+
title: typeof t.title === "string" ? t.title : undefined,
|
|
84
|
+
description: typeof t.description === "string" ? t.description : undefined,
|
|
85
|
+
inputSchema: t.inputSchema,
|
|
86
|
+
outputSchema: t.outputSchema,
|
|
87
|
+
annotations: t.annotations && typeof t.annotations === "object" ? t.annotations : undefined,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const discover = (discoverResult ?? undefined);
|
|
92
|
+
const notes = [
|
|
93
|
+
"introspected via bare stateless requests (2026-07-28 path); the server does not accept the legacy initialize handshake",
|
|
94
|
+
];
|
|
95
|
+
if (!discover)
|
|
96
|
+
notes.push("server/discover did not answer, so server identity and capabilities are unknown");
|
|
97
|
+
const caps = discover?.capabilities;
|
|
98
|
+
if (caps && ("resources" in caps || "prompts" in caps)) {
|
|
99
|
+
notes.push("server declares resources/prompts; they are not enumerated on the stateless path yet");
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
targetLabel: label,
|
|
103
|
+
transport,
|
|
104
|
+
httpTransport,
|
|
105
|
+
serverInfo: discover?.serverInfo
|
|
106
|
+
? {
|
|
107
|
+
name: discover.serverInfo.name,
|
|
108
|
+
version: discover.serverInfo.version,
|
|
109
|
+
title: discover.serverInfo.title,
|
|
110
|
+
}
|
|
111
|
+
: undefined,
|
|
112
|
+
protocolVersion: discover?.supportedVersions?.includes(RC_VERSION) ? RC_VERSION : undefined,
|
|
113
|
+
instructions: discover?.instructions,
|
|
114
|
+
capabilities: caps,
|
|
115
|
+
tools,
|
|
116
|
+
resources: [],
|
|
117
|
+
prompts: [],
|
|
118
|
+
rawToolsListResult: rawFirst,
|
|
119
|
+
notes,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Finding, McpRuleContext } from "./types.js";
|
|
2
|
+
/** Run a list of rules, isolating each: a broken rule must never break the audit. */
|
|
3
|
+
export declare function runRules<Ctx>(rules: readonly {
|
|
4
|
+
check(ctx: Ctx): Finding[];
|
|
5
|
+
}[], ctx: Ctx): Finding[];
|
|
6
|
+
export declare function runMcpRules(ctx: McpRuleContext): Finding[];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { MCP_RULES } from "../rules/mcp/index.js";
|
|
2
|
+
/** Run a list of rules, isolating each: a broken rule must never break the audit. */
|
|
3
|
+
export function runRules(rules, ctx) {
|
|
4
|
+
const findings = [];
|
|
5
|
+
for (const rule of rules) {
|
|
6
|
+
try {
|
|
7
|
+
findings.push(...rule.check(ctx));
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
/* skip a throwing rule */
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return findings;
|
|
14
|
+
}
|
|
15
|
+
export function runMcpRules(ctx) {
|
|
16
|
+
return runRules(MCP_RULES, ctx);
|
|
17
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Finding, GradeInfo } from "./types.js";
|
|
2
|
+
export declare function gradeFindings(findings: Finding[]): GradeInfo;
|
|
3
|
+
export declare function countBySeverity(findings: Finding[]): {
|
|
4
|
+
error: number;
|
|
5
|
+
warn: number;
|
|
6
|
+
info: number;
|
|
7
|
+
};
|
|
8
|
+
export declare function sortFindings(findings: Finding[]): Finding[];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const PENALTY = { error: 15, warn: 5, info: 1 };
|
|
2
|
+
export function gradeFindings(findings) {
|
|
3
|
+
let score = 100;
|
|
4
|
+
for (const f of findings)
|
|
5
|
+
score -= PENALTY[f.severity] ?? 0;
|
|
6
|
+
score = Math.max(0, score);
|
|
7
|
+
const letter = score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : score >= 60 ? "D" : "F";
|
|
8
|
+
return { score, letter };
|
|
9
|
+
}
|
|
10
|
+
export function countBySeverity(findings) {
|
|
11
|
+
const counts = { error: 0, warn: 0, info: 0 };
|
|
12
|
+
for (const f of findings)
|
|
13
|
+
counts[f.severity]++;
|
|
14
|
+
return counts;
|
|
15
|
+
}
|
|
16
|
+
const SEV_ORDER = { error: 0, warn: 1, info: 2 };
|
|
17
|
+
export function sortFindings(findings) {
|
|
18
|
+
return [...findings].sort((a, b) => SEV_ORDER[a.severity] - SEV_ORDER[b.severity] || a.ruleId.localeCompare(b.ruleId));
|
|
19
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
export type Severity = "error" | "warn" | "info";
|
|
2
|
+
export interface Finding {
|
|
3
|
+
ruleId: string;
|
|
4
|
+
severity: Severity;
|
|
5
|
+
title: string;
|
|
6
|
+
message: string;
|
|
7
|
+
/** Tool name, skill name, or file path this finding is about. */
|
|
8
|
+
target?: string;
|
|
9
|
+
detail?: string;
|
|
10
|
+
fixHint?: string;
|
|
11
|
+
}
|
|
12
|
+
export type Surface = "mcp" | "skill";
|
|
13
|
+
export interface ToolDef {
|
|
14
|
+
name: string;
|
|
15
|
+
title?: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
inputSchema?: unknown;
|
|
18
|
+
outputSchema?: unknown;
|
|
19
|
+
annotations?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
export interface ResourceDef {
|
|
22
|
+
uri: string;
|
|
23
|
+
name?: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
mimeType?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface PromptDef {
|
|
28
|
+
name: string;
|
|
29
|
+
title?: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface ServerIntrospection {
|
|
33
|
+
targetLabel: string;
|
|
34
|
+
transport: "stdio" | "http";
|
|
35
|
+
/** For http targets: which transport actually worked. */
|
|
36
|
+
httpTransport?: "streamable" | "sse-legacy";
|
|
37
|
+
serverInfo?: {
|
|
38
|
+
name?: string;
|
|
39
|
+
version?: string;
|
|
40
|
+
title?: string;
|
|
41
|
+
};
|
|
42
|
+
protocolVersion?: string;
|
|
43
|
+
instructions?: string;
|
|
44
|
+
capabilities?: Record<string, unknown>;
|
|
45
|
+
tools: ToolDef[];
|
|
46
|
+
resources: ResourceDef[];
|
|
47
|
+
prompts: PromptDef[];
|
|
48
|
+
/** Raw JSON-RPC result object of tools/list (first page), for spec-field checks. */
|
|
49
|
+
rawToolsListResult?: unknown;
|
|
50
|
+
/** Diagnostics collected while connecting (stderr snippets, fallbacks used). */
|
|
51
|
+
notes: string[];
|
|
52
|
+
}
|
|
53
|
+
export interface ProbeOutcome {
|
|
54
|
+
ok: boolean;
|
|
55
|
+
/** How a non-ok outcome failed: a JSON-RPC error, a timeout, or the process exiting. */
|
|
56
|
+
kind?: "ok" | "error" | "timeout" | "exit";
|
|
57
|
+
errorCode?: number;
|
|
58
|
+
errorMessage?: string;
|
|
59
|
+
}
|
|
60
|
+
export interface ProbeResults {
|
|
61
|
+
/** RC-style request without the legacy initialize handshake. */
|
|
62
|
+
bareToolsList?: ProbeOutcome | {
|
|
63
|
+
skipped: string;
|
|
64
|
+
};
|
|
65
|
+
/** server/discover support (SEP-2575). */
|
|
66
|
+
serverDiscover?: {
|
|
67
|
+
supported: boolean;
|
|
68
|
+
errorMessage?: string;
|
|
69
|
+
} | {
|
|
70
|
+
skipped: string;
|
|
71
|
+
};
|
|
72
|
+
/** Whether tools/list result carried the RC-required resultType field. */
|
|
73
|
+
resultTypePresent?: boolean;
|
|
74
|
+
/** Whether tools/list result carried both RC-required ttlMs and cacheScope (SEP-2549). */
|
|
75
|
+
cacheFieldsPresent?: boolean;
|
|
76
|
+
/** Same tool order across two fresh connections. */
|
|
77
|
+
toolsOrderDeterministic?: boolean;
|
|
78
|
+
/** Non-JSON noise observed on stdout before/between JSON-RPC messages (stdio only). */
|
|
79
|
+
stdoutNoise?: string;
|
|
80
|
+
httpAuth?: {
|
|
81
|
+
required: boolean;
|
|
82
|
+
wwwAuthenticate?: string;
|
|
83
|
+
resourceMetadataUrl?: string;
|
|
84
|
+
authorizationServer?: string;
|
|
85
|
+
dcrRegistrationEndpoint?: boolean;
|
|
86
|
+
cimdSupported?: boolean | undefined;
|
|
87
|
+
};
|
|
88
|
+
serverCard?: {
|
|
89
|
+
found: boolean;
|
|
90
|
+
url?: string;
|
|
91
|
+
} | {
|
|
92
|
+
skipped: string;
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export interface RepoMatch {
|
|
96
|
+
category: "sampling" | "roots" | "logging" | "elicitation" | "sse-resume" | "session-state" | "subscribe" | "ping";
|
|
97
|
+
file: string;
|
|
98
|
+
line: number;
|
|
99
|
+
excerpt: string;
|
|
100
|
+
}
|
|
101
|
+
export interface RepoScan {
|
|
102
|
+
root: string;
|
|
103
|
+
sdk?: {
|
|
104
|
+
package: string;
|
|
105
|
+
range?: string;
|
|
106
|
+
language: "ts" | "python";
|
|
107
|
+
generation: "legacy" | "rc";
|
|
108
|
+
}[];
|
|
109
|
+
matches: RepoMatch[];
|
|
110
|
+
filesScanned: number;
|
|
111
|
+
}
|
|
112
|
+
export interface SerializedTokens {
|
|
113
|
+
rawJson: number;
|
|
114
|
+
claudeStyle: number;
|
|
115
|
+
openaiTools: number;
|
|
116
|
+
}
|
|
117
|
+
export interface ToolWeigh {
|
|
118
|
+
name: string;
|
|
119
|
+
tokens: SerializedTokens;
|
|
120
|
+
descriptionTokens: number;
|
|
121
|
+
schemaTokens: number;
|
|
122
|
+
}
|
|
123
|
+
export interface ServerWeighResult {
|
|
124
|
+
kind: "mcp";
|
|
125
|
+
label: string;
|
|
126
|
+
toolCount: number;
|
|
127
|
+
resourceCount: number;
|
|
128
|
+
promptCount: number;
|
|
129
|
+
perTool: ToolWeigh[];
|
|
130
|
+
totals: SerializedTokens;
|
|
131
|
+
/**
|
|
132
|
+
* Claude-style block framing: totals.claudeStyle minus the sum of per-tool
|
|
133
|
+
* lines, i.e. the <functions>/<function> wrapper the per-tool numbers
|
|
134
|
+
* exclude. perTool claudeStyle + this = totals.claudeStyle. Optional so
|
|
135
|
+
* pre-0.1.0 --diff baselines still parse.
|
|
136
|
+
*/
|
|
137
|
+
framingTokens?: number;
|
|
138
|
+
instructionsTokens: number;
|
|
139
|
+
/** Exact count via Anthropic count_tokens API, when a key was provided. */
|
|
140
|
+
anthropicExactTotal?: number;
|
|
141
|
+
notes: string[];
|
|
142
|
+
}
|
|
143
|
+
export interface SkillWeighEntry {
|
|
144
|
+
name: string;
|
|
145
|
+
dir: string;
|
|
146
|
+
metadataTokens: number;
|
|
147
|
+
bodyTokens: number;
|
|
148
|
+
bodyLines: number;
|
|
149
|
+
refFileCount: number;
|
|
150
|
+
refFileTokens: number;
|
|
151
|
+
}
|
|
152
|
+
export interface SkillSetWeighResult {
|
|
153
|
+
kind: "skill";
|
|
154
|
+
label: string;
|
|
155
|
+
perSkill: SkillWeighEntry[];
|
|
156
|
+
totals: {
|
|
157
|
+
metadata: number;
|
|
158
|
+
body: number;
|
|
159
|
+
refFiles: number;
|
|
160
|
+
};
|
|
161
|
+
notes: string[];
|
|
162
|
+
}
|
|
163
|
+
export type WeighResult = ServerWeighResult | SkillSetWeighResult;
|
|
164
|
+
export interface SkillInfo {
|
|
165
|
+
dir: string;
|
|
166
|
+
file: string;
|
|
167
|
+
name?: string;
|
|
168
|
+
description?: string;
|
|
169
|
+
frontmatter: Record<string, unknown>;
|
|
170
|
+
frontmatterRaw: string;
|
|
171
|
+
body: string;
|
|
172
|
+
bodyLines: number;
|
|
173
|
+
referencedPaths: {
|
|
174
|
+
raw: string;
|
|
175
|
+
resolved: string;
|
|
176
|
+
exists: boolean;
|
|
177
|
+
source: "link" | "code";
|
|
178
|
+
}[];
|
|
179
|
+
files: {
|
|
180
|
+
path: string;
|
|
181
|
+
bytes: number;
|
|
182
|
+
}[];
|
|
183
|
+
parseError?: string;
|
|
184
|
+
}
|
|
185
|
+
export interface SkillSet {
|
|
186
|
+
root: string;
|
|
187
|
+
skills: SkillInfo[];
|
|
188
|
+
}
|
|
189
|
+
export interface GradeInfo {
|
|
190
|
+
score: number;
|
|
191
|
+
letter: "A" | "B" | "C" | "D" | "F";
|
|
192
|
+
}
|
|
193
|
+
export interface CheckReport {
|
|
194
|
+
tool: "efaimo";
|
|
195
|
+
version: string;
|
|
196
|
+
surface: Surface;
|
|
197
|
+
target: string;
|
|
198
|
+
/** Graded findings: quality rules (and every skill rule). */
|
|
199
|
+
findings: Finding[];
|
|
200
|
+
counts: {
|
|
201
|
+
error: number;
|
|
202
|
+
warn: number;
|
|
203
|
+
info: number;
|
|
204
|
+
};
|
|
205
|
+
grade: GradeInfo;
|
|
206
|
+
/**
|
|
207
|
+
* MCP only: 2026-07-28 readiness findings (E101-E118), reported as an
|
|
208
|
+
* ungraded migration diff. The target spec is not final until 2026-07-28,
|
|
209
|
+
* so unreadiness is a to-do list, not a quality defect.
|
|
210
|
+
*/
|
|
211
|
+
readiness?: {
|
|
212
|
+
findings: Finding[];
|
|
213
|
+
counts: {
|
|
214
|
+
error: number;
|
|
215
|
+
warn: number;
|
|
216
|
+
info: number;
|
|
217
|
+
};
|
|
218
|
+
};
|
|
219
|
+
notes: string[];
|
|
220
|
+
}
|
|
221
|
+
export interface McpRuleContext {
|
|
222
|
+
intro: ServerIntrospection;
|
|
223
|
+
probes?: ProbeResults;
|
|
224
|
+
repo?: RepoScan;
|
|
225
|
+
weigh?: ServerWeighResult;
|
|
226
|
+
}
|
|
227
|
+
export interface SkillRuleContext {
|
|
228
|
+
skill: SkillInfo;
|
|
229
|
+
set: SkillSet;
|
|
230
|
+
weigh?: SkillSetWeighResult;
|
|
231
|
+
}
|
|
232
|
+
export interface McpRule {
|
|
233
|
+
id: string;
|
|
234
|
+
title: string;
|
|
235
|
+
surface: "mcp";
|
|
236
|
+
check(ctx: McpRuleContext): Finding[];
|
|
237
|
+
}
|
|
238
|
+
export interface SkillRule {
|
|
239
|
+
id: string;
|
|
240
|
+
title: string;
|
|
241
|
+
surface: "skill";
|
|
242
|
+
check(ctx: SkillRuleContext): Finding[];
|
|
243
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export * from "./core/types.js";
|
|
2
|
+
export { VERSION } from "./version.js";
|
|
3
|
+
export { resolveTarget, type ResolvedTarget, type ResolveOptions } from "./targets/resolve.js";
|
|
4
|
+
export { loadClientServers, SUPPORTED_CLIENTS } from "./targets/clientConfigs.js";
|
|
5
|
+
export { introspectServer } from "./clients/introspect.js";
|
|
6
|
+
export { weighServer, weighSkills } from "./weigh/weigh.js";
|
|
7
|
+
export { diffServerWeigh, type WeighDiff } from "./weigh/diff.js";
|
|
8
|
+
export { countTokens } from "./weigh/tokens.js";
|
|
9
|
+
export { findSkills, parseSkillFile } from "./skills/parse.js";
|
|
10
|
+
export { checkMcpTarget, checkMcpRepoOnly, checkSkillSet, type SkillReport, type CheckSkillResult } from "./check/check.js";
|
|
11
|
+
export { parseScenario, runScenario, armSystems, type Scenario, type TestReport, type Runner, } from "./testing/harness.js";
|
|
12
|
+
export { anthropicRunner } from "./testing/anthropicRunner.js";
|
|
13
|
+
export { openaiRunner, providerForModel } from "./testing/openaiRunner.js";
|
|
14
|
+
export { renderCheckPretty, renderSkillSetPretty, renderServerWeighPretty, renderSkillWeighPretty, renderDiffPretty, renderScenarioPlan, renderTestReportPretty, } from "./reporters/pretty.js";
|
|
15
|
+
export { renderCheckMarkdown, renderSkillSetMarkdown, renderWeighMarkdown, renderDiffMarkdown, } from "./reporters/markdown.js";
|
|
16
|
+
export { toJsonEnvelope } from "./reporters/json.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Public library API. Kept deliberately small: only task-level entry points,
|
|
2
|
+
// their types, and the renderers. Rule tables, the rule engine, injection
|
|
3
|
+
// patterns, and grading internals are intentionally NOT exported so they stay
|
|
4
|
+
// free to change without a breaking release. The CLI imports modules directly.
|
|
5
|
+
export * from "./core/types.js";
|
|
6
|
+
export { VERSION } from "./version.js";
|
|
7
|
+
// Targets
|
|
8
|
+
export { resolveTarget } from "./targets/resolve.js";
|
|
9
|
+
export { loadClientServers, SUPPORTED_CLIENTS } from "./targets/clientConfigs.js";
|
|
10
|
+
// Introspect + weigh
|
|
11
|
+
export { introspectServer } from "./clients/introspect.js";
|
|
12
|
+
export { weighServer, weighSkills } from "./weigh/weigh.js";
|
|
13
|
+
export { diffServerWeigh } from "./weigh/diff.js";
|
|
14
|
+
export { countTokens } from "./weigh/tokens.js";
|
|
15
|
+
// Skills
|
|
16
|
+
export { findSkills, parseSkillFile } from "./skills/parse.js";
|
|
17
|
+
// Check (audit)
|
|
18
|
+
export { checkMcpTarget, checkMcpRepoOnly, checkSkillSet } from "./check/check.js";
|
|
19
|
+
// Test (skill A/B outcome harness)
|
|
20
|
+
export { parseScenario, runScenario, armSystems, } from "./testing/harness.js";
|
|
21
|
+
export { anthropicRunner } from "./testing/anthropicRunner.js";
|
|
22
|
+
export { openaiRunner, providerForModel } from "./testing/openaiRunner.js";
|
|
23
|
+
// Reporters (report -> string; stable value-in/string-out contract)
|
|
24
|
+
export { renderCheckPretty, renderSkillSetPretty, renderServerWeighPretty, renderSkillWeighPretty, renderDiffPretty, renderScenarioPlan, renderTestReportPretty, } from "./reporters/pretty.js";
|
|
25
|
+
export { renderCheckMarkdown, renderSkillSetMarkdown, renderWeighMarkdown, renderDiffMarkdown, } from "./reporters/markdown.js";
|
|
26
|
+
export { toJsonEnvelope } from "./reporters/json.js";
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { checkSkillSet } from "../check/check.js";
|
|
5
|
+
import { weighSkills } from "../weigh/weigh.js";
|
|
6
|
+
import { findSkills } from "../skills/parse.js";
|
|
7
|
+
import { renderCheckMarkdown, renderSkillSetMarkdown, renderWeighMarkdown } from "../reporters/markdown.js";
|
|
8
|
+
import { VERSION } from "../version.js";
|
|
9
|
+
// Both tools only read local files, so the same hints apply: safe to call without
|
|
10
|
+
// a confirmation, safe to repeat, and not reaching out to any external service.
|
|
11
|
+
const READ_ONLY = { readOnlyHint: true, idempotentHint: true, openWorldHint: false };
|
|
12
|
+
function requireString(args, key) {
|
|
13
|
+
const v = args[key];
|
|
14
|
+
if (typeof v !== "string" || v.trim() === "") {
|
|
15
|
+
throw new Error(`"${key}" is required and must be a non-empty string`);
|
|
16
|
+
}
|
|
17
|
+
return v;
|
|
18
|
+
}
|
|
19
|
+
const pathSchema = (what) => ({
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: { path: { type: "string", description: what } },
|
|
22
|
+
required: ["path"],
|
|
23
|
+
additionalProperties: false,
|
|
24
|
+
});
|
|
25
|
+
const TOOLS = [
|
|
26
|
+
{
|
|
27
|
+
name: "efaimo_check_skill",
|
|
28
|
+
description: "Lint an Agent Skill, or a folder of skills, for spec compliance, trigger quality, " +
|
|
29
|
+
"context-window cost, reference integrity, and injection hygiene. Returns a grade from " +
|
|
30
|
+
"A to F and the findings. Read-only: reads files only.",
|
|
31
|
+
inputSchema: pathSchema("Path to a SKILL.md file or a directory that contains skills."),
|
|
32
|
+
annotations: READ_ONLY,
|
|
33
|
+
async run(args) {
|
|
34
|
+
const p = requireString(args, "path");
|
|
35
|
+
const res = await checkSkillSet(p, p);
|
|
36
|
+
if (res.perSkill.length === 1 && res.setFindings.length === 0) {
|
|
37
|
+
return renderCheckMarkdown(res.perSkill[0].report);
|
|
38
|
+
}
|
|
39
|
+
return renderSkillSetMarkdown(res);
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "efaimo_weigh_skill",
|
|
44
|
+
description: "Measure the context-window token cost of an Agent Skill: the metadata loaded into every " +
|
|
45
|
+
"session and the body loaded when the skill triggers. Returns token counts per skill. " +
|
|
46
|
+
"Read-only: reads files only.",
|
|
47
|
+
inputSchema: pathSchema("Path to a SKILL.md file or a directory that contains skills."),
|
|
48
|
+
annotations: READ_ONLY,
|
|
49
|
+
async run(args) {
|
|
50
|
+
const p = requireString(args, "path");
|
|
51
|
+
const set = findSkills(p);
|
|
52
|
+
if (!set.skills.length)
|
|
53
|
+
throw new Error(`no SKILL.md found under "${p}"`);
|
|
54
|
+
return renderWeighMarkdown(await weighSkills(set));
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
export function buildMcpServer() {
|
|
59
|
+
const server = new Server({ name: "efaimo", version: VERSION }, { capabilities: { tools: {} } });
|
|
60
|
+
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
61
|
+
tools: TOOLS.map((t) => ({
|
|
62
|
+
name: t.name,
|
|
63
|
+
description: t.description,
|
|
64
|
+
inputSchema: t.inputSchema,
|
|
65
|
+
annotations: t.annotations,
|
|
66
|
+
})),
|
|
67
|
+
}));
|
|
68
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
69
|
+
const tool = TOOLS.find((t) => t.name === req.params.name);
|
|
70
|
+
if (!tool) {
|
|
71
|
+
return { content: [{ type: "text", text: `unknown tool: ${req.params.name}` }], isError: true };
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const text = await tool.run(req.params.arguments ?? {});
|
|
75
|
+
return { content: [{ type: "text", text }] };
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
79
|
+
return { content: [{ type: "text", text: `error: ${message}` }], isError: true };
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
return server;
|
|
83
|
+
}
|
|
84
|
+
export async function runMcpServer() {
|
|
85
|
+
const server = buildMcpServer();
|
|
86
|
+
await server.connect(new StdioServerTransport());
|
|
87
|
+
console.error(`efaimo mcp v${VERSION}: read-only skill tools ready on stdio`);
|
|
88
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CheckReport, ServerWeighResult, SkillSetWeighResult } from "../core/types.js";
|
|
2
|
+
/** Flat badge SVG in the shields style, self-contained (no external fonts). */
|
|
3
|
+
export declare function makeBadgeSvg(label: string, message: string, color: string): string;
|
|
4
|
+
export interface BadgeSpec {
|
|
5
|
+
label: string;
|
|
6
|
+
message: string;
|
|
7
|
+
color: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function weighBadgeSpec(w: ServerWeighResult | SkillSetWeighResult): BadgeSpec;
|
|
10
|
+
export declare function gradeBadgeSpec(report: CheckReport): BadgeSpec;
|
|
11
|
+
/** shields.io endpoint JSON (https://shields.io/badges/endpoint-badge). */
|
|
12
|
+
export declare function toShieldsEndpoint(spec: BadgeSpec): string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { formatTokens } from "../util/misc.js";
|
|
2
|
+
/** Flat badge SVG in the shields style, self-contained (no external fonts). */
|
|
3
|
+
export function makeBadgeSvg(label, message, color) {
|
|
4
|
+
const charW = 6.5;
|
|
5
|
+
const pad = 10;
|
|
6
|
+
const lw = Math.round(label.length * charW + pad * 2);
|
|
7
|
+
const mw = Math.round(message.length * charW + pad * 2);
|
|
8
|
+
const w = lw + mw;
|
|
9
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="20" role="img" aria-label="${label}: ${message}">
|
|
10
|
+
<linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient>
|
|
11
|
+
<clipPath id="r"><rect width="${w}" height="20" rx="3" fill="#fff"/></clipPath>
|
|
12
|
+
<g clip-path="url(#r)">
|
|
13
|
+
<rect width="${lw}" height="20" fill="#555"/>
|
|
14
|
+
<rect x="${lw}" width="${mw}" height="20" fill="${color}"/>
|
|
15
|
+
<rect width="${w}" height="20" fill="url(#s)"/>
|
|
16
|
+
</g>
|
|
17
|
+
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
|
|
18
|
+
<text x="${lw / 2}" y="14">${label}</text>
|
|
19
|
+
<text x="${lw + mw / 2}" y="14">${message}</text>
|
|
20
|
+
</g>
|
|
21
|
+
</svg>
|
|
22
|
+
`;
|
|
23
|
+
}
|
|
24
|
+
export function weighBadgeSpec(w) {
|
|
25
|
+
const total = w.kind === "mcp" ? w.totals.claudeStyle : w.totals.metadata + w.totals.body;
|
|
26
|
+
const color = total < 2000 ? "#3fb950" : total < 8000 ? "#d29922" : total < 20000 ? "#db6d28" : "#f85149";
|
|
27
|
+
return { label: "context cost", message: `${formatTokens(total)} tok (o200k)`, color };
|
|
28
|
+
}
|
|
29
|
+
export function gradeBadgeSpec(report) {
|
|
30
|
+
const g = report.grade;
|
|
31
|
+
const color = g.letter === "A" ? "#3fb950" : g.letter === "B" ? "#7bc043" : g.letter === "C" ? "#d29922" : g.letter === "D" ? "#db6d28" : "#f85149";
|
|
32
|
+
return { label: "efaimo", message: `${g.letter} (${g.score})`, color };
|
|
33
|
+
}
|
|
34
|
+
/** shields.io endpoint JSON (https://shields.io/badges/endpoint-badge). */
|
|
35
|
+
export function toShieldsEndpoint(spec) {
|
|
36
|
+
return JSON.stringify({ schemaVersion: 1, label: spec.label, message: spec.message, color: spec.color.replace("#", "") }, null, 2);
|
|
37
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function toJsonEnvelope(kind: "check" | "weigh" | "test", data: unknown): string;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CheckReport, ServerWeighResult, SkillSetWeighResult } from "../core/types.js";
|
|
2
|
+
import type { CheckSkillResult } from "../check/check.js";
|
|
3
|
+
import type { WeighDiff } from "../weigh/diff.js";
|
|
4
|
+
export declare function renderCheckMarkdown(report: CheckReport): string;
|
|
5
|
+
export declare function renderSkillSetMarkdown(res: CheckSkillResult): string;
|
|
6
|
+
export declare function renderWeighMarkdown(w: ServerWeighResult | SkillSetWeighResult): string;
|
|
7
|
+
export declare function renderDiffMarkdown(d: WeighDiff): string;
|