@capitalthought/agentsfirst-mcp 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/AGENTS.md +71 -0
- package/LICENSE +21 -0
- package/README.md +137 -0
- package/dist/prep.d.ts +12 -0
- package/dist/prep.js +90 -0
- package/dist/principles.d.ts +28 -0
- package/dist/principles.js +133 -0
- package/dist/probe.d.ts +192 -0
- package/dist/probe.js +528 -0
- package/dist/score.d.ts +40 -0
- package/dist/score.js +614 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +153 -0
- package/package.json +49 -0
package/dist/probe.js
ADDED
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
// Agents First signal gatherer — ported from ~/.claude/skills/agentsfirst/probe.mjs.
|
|
2
|
+
//
|
|
3
|
+
// Two modes:
|
|
4
|
+
// - probeCodebase(path) → filesystem probes for the 8 principles
|
|
5
|
+
// - probeWebsite(url) → HTTP probes for agent-discoverable surfaces
|
|
6
|
+
//
|
|
7
|
+
// Pure signal-gathering. Scoring lives in score.ts.
|
|
8
|
+
import { execSync } from 'node:child_process';
|
|
9
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
10
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
// ─── Codebase mode ────────────────────────────────────────────────────────────
|
|
13
|
+
export async function probeCodebase(rootArg) {
|
|
14
|
+
const root = path.resolve(rootArg);
|
|
15
|
+
if (!existsSync(root)) {
|
|
16
|
+
throw new Error(`Path not found: ${root}`);
|
|
17
|
+
}
|
|
18
|
+
const out = {
|
|
19
|
+
mode: 'codebase',
|
|
20
|
+
target: root,
|
|
21
|
+
project_name: path.basename(root),
|
|
22
|
+
timestamp: new Date().toISOString(),
|
|
23
|
+
signals: {
|
|
24
|
+
agents_md: await probeAgentsMd(root),
|
|
25
|
+
mcp_server: await probeMcpServer(root),
|
|
26
|
+
cli: await probeCli(root),
|
|
27
|
+
typed_sdk: await probeTypedSdk(root),
|
|
28
|
+
prep_gate: await probePrepGate(root),
|
|
29
|
+
typed_state: await probeTypedState(root),
|
|
30
|
+
visible_outputs: await probeVisibleOutputs(root),
|
|
31
|
+
multi_model: await probeMultiModel(root),
|
|
32
|
+
perspective_dispatch: await probePerspectiveDispatch(root),
|
|
33
|
+
autonomous_recovery: await probeRecovery(root),
|
|
34
|
+
tool_count: await countTools(root),
|
|
35
|
+
discoverability: await probeDiscoverability(root),
|
|
36
|
+
staleness: probeStaleness(root),
|
|
37
|
+
},
|
|
38
|
+
derived: {
|
|
39
|
+
has_any_agent_interface: false,
|
|
40
|
+
god_server_risk: 'ok',
|
|
41
|
+
agents_without_rules_risk: false,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
out.derived.has_any_agent_interface =
|
|
45
|
+
out.signals.mcp_server.detected ||
|
|
46
|
+
out.signals.cli.detected ||
|
|
47
|
+
out.signals.typed_sdk.detected;
|
|
48
|
+
const total = out.signals.tool_count.estimated_total;
|
|
49
|
+
out.derived.god_server_risk = total > 100 ? 'fail' : total > 30 ? 'warn' : 'ok';
|
|
50
|
+
out.derived.agents_without_rules_risk =
|
|
51
|
+
(out.signals.mcp_server.detected || out.signals.cli.detected) &&
|
|
52
|
+
!out.signals.agents_md.exists;
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
async function probeAgentsMd(root) {
|
|
56
|
+
const candidates = ['AGENTS.md', '.agents.md', 'docs/AGENTS.md'];
|
|
57
|
+
for (const rel of candidates) {
|
|
58
|
+
const full = path.join(root, rel);
|
|
59
|
+
if (existsSync(full)) {
|
|
60
|
+
const content = readFileSync(full, 'utf8');
|
|
61
|
+
const lower = content.toLowerCase();
|
|
62
|
+
return {
|
|
63
|
+
exists: true,
|
|
64
|
+
path: rel,
|
|
65
|
+
size_bytes: content.length,
|
|
66
|
+
line_count: content.split('\n').length,
|
|
67
|
+
sections: {
|
|
68
|
+
permissions: /## *permissions?\b/i.test(content),
|
|
69
|
+
sequence: /## *sequence|## *required prep|## *tool[- ]?call (order|sequence)/i.test(content),
|
|
70
|
+
identifiers: /## *identif|## *naming|never invent ids/i.test(lower),
|
|
71
|
+
errors: /## *errors?\b/i.test(content),
|
|
72
|
+
visible_outputs: /## *visible outputs?|human[- ]?readable/i.test(lower),
|
|
73
|
+
anti_patterns: /## *anti[- ]?patterns?|lazy wrapper|god server/i.test(lower),
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { exists: false };
|
|
79
|
+
}
|
|
80
|
+
async function probeMcpServer(root) {
|
|
81
|
+
const out = { detected: false, indicators: {} };
|
|
82
|
+
const pkgJson = await readJsonIfExists(path.join(root, 'package.json'));
|
|
83
|
+
if (pkgJson) {
|
|
84
|
+
const rawDeps = (pkgJson['dependencies'] ?? {});
|
|
85
|
+
const rawDevDeps = (pkgJson['devDependencies'] ?? {});
|
|
86
|
+
const deps = { ...rawDeps, ...rawDevDeps };
|
|
87
|
+
const mcpDep = deps['@modelcontextprotocol/sdk'];
|
|
88
|
+
if (mcpDep !== undefined) {
|
|
89
|
+
out.detected = true;
|
|
90
|
+
out.indicators.package_json_dep = String(mcpDep);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const grepResults = grepMany(root, [
|
|
94
|
+
'@modelcontextprotocol/sdk',
|
|
95
|
+
'McpServer',
|
|
96
|
+
'registerTool',
|
|
97
|
+
'setRequestHandler',
|
|
98
|
+
]);
|
|
99
|
+
out.indicators.matching_files = Object.fromEntries(Object.entries(grepResults).map(([term, files]) => [term, files.length]));
|
|
100
|
+
if ((grepResults['McpServer']?.length ?? 0) > 0 ||
|
|
101
|
+
(grepResults['registerTool']?.length ?? 0) > 0 ||
|
|
102
|
+
(grepResults['@modelcontextprotocol/sdk']?.length ?? 0) > 0) {
|
|
103
|
+
out.detected = true;
|
|
104
|
+
}
|
|
105
|
+
const verbFirst = grepRegex(root, /registerTool\(['"]([a-z_]+)['"]/g);
|
|
106
|
+
if (verbFirst.length) {
|
|
107
|
+
const verbCount = verbFirst.filter((name) => /^(create|list|get|update|delete|set|send|fetch|run|search|dispatch|publish|subscribe|register|score|probe|add|remove|verify|check|prep)/.test(name)).length;
|
|
108
|
+
out.indicators.tool_names_sampled = verbFirst.slice(0, 10);
|
|
109
|
+
out.indicators.verb_first_ratio = verbFirst.length ? verbCount / verbFirst.length : 0;
|
|
110
|
+
}
|
|
111
|
+
out.indicators.uses_zod_for_params = grepCount(root, /from ['"]zod['"]/) > 0;
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
async function probeCli(root) {
|
|
115
|
+
const out = { detected: false, indicators: {} };
|
|
116
|
+
const pkgJson = await readJsonIfExists(path.join(root, 'package.json'));
|
|
117
|
+
const bin = pkgJson?.['bin'];
|
|
118
|
+
if (bin) {
|
|
119
|
+
out.detected = true;
|
|
120
|
+
out.indicators.package_json_bin =
|
|
121
|
+
typeof bin === 'string'
|
|
122
|
+
? [bin]
|
|
123
|
+
: Object.keys(bin);
|
|
124
|
+
}
|
|
125
|
+
const pyproject = await readTextIfExists(path.join(root, 'pyproject.toml'));
|
|
126
|
+
if (pyproject && /\[project\.scripts\]/.test(pyproject)) {
|
|
127
|
+
out.detected = true;
|
|
128
|
+
out.indicators.pyproject_scripts = true;
|
|
129
|
+
}
|
|
130
|
+
const cargo = await readTextIfExists(path.join(root, 'Cargo.toml'));
|
|
131
|
+
if (cargo && /\[\[bin\]\]/.test(cargo)) {
|
|
132
|
+
out.detected = true;
|
|
133
|
+
out.indicators.cargo_bins = true;
|
|
134
|
+
}
|
|
135
|
+
const binDir = path.join(root, 'bin');
|
|
136
|
+
if (existsSync(binDir)) {
|
|
137
|
+
try {
|
|
138
|
+
const entries = await readdir(binDir);
|
|
139
|
+
if (entries.length > 0) {
|
|
140
|
+
out.detected = true;
|
|
141
|
+
out.indicators.bin_dir_entries = entries.slice(0, 10);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
/* ignore */
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (grepCount(root, /process\.argv|argparse|click\.command|clap::|getopt/) > 0) {
|
|
149
|
+
out.indicators.argv_handling = true;
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
async function probeTypedSdk(root) {
|
|
154
|
+
const out = { detected: false, indicators: {} };
|
|
155
|
+
const pkgJson = await readJsonIfExists(path.join(root, 'package.json'));
|
|
156
|
+
if (pkgJson) {
|
|
157
|
+
const types = pkgJson['types'];
|
|
158
|
+
const typings = pkgJson['typings'];
|
|
159
|
+
if (typeof types === 'string' || typeof typings === 'string') {
|
|
160
|
+
out.detected = true;
|
|
161
|
+
out.indicators.declared_types_entry =
|
|
162
|
+
typeof types === 'string' ? types : typings;
|
|
163
|
+
}
|
|
164
|
+
if (pkgJson['exports']) {
|
|
165
|
+
out.indicators.has_exports_field = true;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const distDts = grepCount(root, /\.d\.ts$/);
|
|
169
|
+
if (distDts > 0) {
|
|
170
|
+
out.indicators.dts_files = distDts;
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
async function probePrepGate(root) {
|
|
175
|
+
const out = { detected: false, indicators: {} };
|
|
176
|
+
const prepFiles = findFiles(root, /^(prep|preflight|preflightcheck|agentprep)\.(ts|js|mjs|sh|py)$/i);
|
|
177
|
+
if (prepFiles.length) {
|
|
178
|
+
out.detected = true;
|
|
179
|
+
out.indicators.prep_files = prepFiles.slice(0, 5);
|
|
180
|
+
}
|
|
181
|
+
const projectName = path.basename(root).toLowerCase();
|
|
182
|
+
const prepToolPattern = new RegExp(`['"\`](${projectName}_prep|${projectName.replace(/-/g, '_')}_prep|[a-z_]+_prep)['"\`]`);
|
|
183
|
+
const prepToolHits = grepCount(root, prepToolPattern);
|
|
184
|
+
if (prepToolHits > 0) {
|
|
185
|
+
out.detected = true;
|
|
186
|
+
out.indicators.prep_tool_registrations = prepToolHits;
|
|
187
|
+
}
|
|
188
|
+
if (grepCount(root, /export (async )?function (runPrep|preflightCheck|agentPrep)/) > 0) {
|
|
189
|
+
out.detected = true;
|
|
190
|
+
out.indicators.prep_function_exported = true;
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
async function probeTypedState(root) {
|
|
195
|
+
const indicators = {
|
|
196
|
+
zod_imports: grepCount(root, /from ['"]zod['"]/),
|
|
197
|
+
json_schema_files: findFiles(root, /\.schema\.(json|ts|js)$/).length,
|
|
198
|
+
migration_files: findFiles(root, /(\d{3,}|\d{14})[-_].+\.(sql|ts|js)$/).length,
|
|
199
|
+
prisma: existsSync(path.join(root, 'prisma/schema.prisma')),
|
|
200
|
+
drizzle: grepCount(root, /from ['"]drizzle-orm/),
|
|
201
|
+
typescript_strict: await checkTsStrict(root),
|
|
202
|
+
};
|
|
203
|
+
return {
|
|
204
|
+
detected: indicators.zod_imports > 0 ||
|
|
205
|
+
indicators.json_schema_files > 0 ||
|
|
206
|
+
indicators.prisma ||
|
|
207
|
+
indicators.drizzle > 0,
|
|
208
|
+
has_migrations: indicators.migration_files > 0 || indicators.prisma,
|
|
209
|
+
indicators,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
async function probeVisibleOutputs(root) {
|
|
213
|
+
const indicators = {
|
|
214
|
+
slack_imports: grepCount(root, /['"]@slack\/|slackapi/),
|
|
215
|
+
postmark: grepCount(root, /['"]postmark['"]|POSTMARK_/),
|
|
216
|
+
resend: grepCount(root, /['"]resend['"]/),
|
|
217
|
+
email_general: grepCount(root, /sendmail|nodemailer|@sendgrid/),
|
|
218
|
+
asana: grepCount(root, /asana/i),
|
|
219
|
+
linear: grepCount(root, /['"]@linear\/sdk['"]|linearapp/),
|
|
220
|
+
audit_log_files: findFiles(root, /audit[-_]?log\./).length,
|
|
221
|
+
todo_ship_markers: grepCount(root, /TODO:.*ship to (slack|email|task|asana|linear)/i),
|
|
222
|
+
};
|
|
223
|
+
return {
|
|
224
|
+
detected: indicators.slack_imports > 0 ||
|
|
225
|
+
indicators.postmark > 0 ||
|
|
226
|
+
indicators.resend > 0 ||
|
|
227
|
+
indicators.email_general > 0 ||
|
|
228
|
+
indicators.linear > 0 ||
|
|
229
|
+
indicators.audit_log_files > 0,
|
|
230
|
+
indicators,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
async function probeMultiModel(root) {
|
|
234
|
+
const counts = {
|
|
235
|
+
anthropic: grepCount(root, /['"]@anthropic-ai\/sdk['"]/),
|
|
236
|
+
openai: grepCount(root, /['"]openai['"]/),
|
|
237
|
+
google_genai: grepCount(root, /['"]@google\/genai|@google\/generative-ai/),
|
|
238
|
+
xai: grepCount(root, /['"]grok|api\.x\.ai/),
|
|
239
|
+
};
|
|
240
|
+
const distinctSdks = ['anthropic', 'openai', 'google_genai', 'xai'].filter((k) => counts[k] > 0);
|
|
241
|
+
const consensusPattern = grepCount(root, /consensus|multipov|multi[-_]?model/i);
|
|
242
|
+
return {
|
|
243
|
+
detected: distinctSdks.length >= 2 && consensusPattern > 0,
|
|
244
|
+
indicators: {
|
|
245
|
+
...counts,
|
|
246
|
+
distinct_llm_sdks: [...distinctSdks],
|
|
247
|
+
consensus_pattern: consensusPattern,
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
async function probePerspectiveDispatch(root) {
|
|
252
|
+
const indicators = {
|
|
253
|
+
reviewer_pattern: grepCount(root, /reviewer|persona|perspective[-_]?dispatch/i),
|
|
254
|
+
};
|
|
255
|
+
const agentsDir = path.join(root, 'agents');
|
|
256
|
+
if (existsSync(agentsDir)) {
|
|
257
|
+
try {
|
|
258
|
+
const entries = await readdir(agentsDir);
|
|
259
|
+
const personaFiles = entries.filter((e) => /\.(md|yml|json)$/.test(e));
|
|
260
|
+
indicators.persona_files = personaFiles.length;
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
/* ignore */
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
detected: (indicators.persona_files ?? 0) > 1 || indicators.reviewer_pattern > 5,
|
|
268
|
+
indicators,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
async function probeRecovery(root) {
|
|
272
|
+
const indicators = {
|
|
273
|
+
retry_helper: grepCount(root, /\bwithRetry\b|retryWithBackoff|exponentialBackoff/),
|
|
274
|
+
recovery_files: findFiles(root, /^(recovery|retry|resilience)\.(ts|js|mjs|py)$/i).length,
|
|
275
|
+
escalation_pattern: grepCount(root, /escalate\(|manual_action|self[-_]?heal/i),
|
|
276
|
+
};
|
|
277
|
+
return {
|
|
278
|
+
detected: indicators.retry_helper > 0 || indicators.recovery_files > 0,
|
|
279
|
+
has_escalation: indicators.escalation_pattern > 0,
|
|
280
|
+
indicators,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
async function countTools(root) {
|
|
284
|
+
const mcp = grepCount(root, /\bregisterTool\(/);
|
|
285
|
+
const pkgJson = await readJsonIfExists(path.join(root, 'package.json'));
|
|
286
|
+
const bin = pkgJson?.['bin'];
|
|
287
|
+
const bins = bin
|
|
288
|
+
? typeof bin === 'string'
|
|
289
|
+
? 1
|
|
290
|
+
: Object.keys(bin).length
|
|
291
|
+
: 0;
|
|
292
|
+
return {
|
|
293
|
+
mcp_register_tool_calls: mcp,
|
|
294
|
+
cli_bin_entries: bins,
|
|
295
|
+
estimated_total: Math.max(mcp, bins),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
async function probeDiscoverability(root) {
|
|
299
|
+
const indicators = {};
|
|
300
|
+
const pkgJson = await readJsonIfExists(path.join(root, 'package.json'));
|
|
301
|
+
if (pkgJson) {
|
|
302
|
+
const name = pkgJson['name'];
|
|
303
|
+
indicators.package_name = typeof name === 'string' ? name : null;
|
|
304
|
+
indicators.has_repository_field = !!pkgJson['repository'];
|
|
305
|
+
indicators.has_homepage = !!pkgJson['homepage'];
|
|
306
|
+
indicators.publishable = pkgJson['private'] !== true && typeof name === 'string';
|
|
307
|
+
}
|
|
308
|
+
const readme = await readTextIfExists(path.join(root, 'README.md'));
|
|
309
|
+
if (readme) {
|
|
310
|
+
indicators.readme_mentions_mcp = /\bmcp\b|model context protocol/i.test(readme);
|
|
311
|
+
indicators.readme_mentions_install =
|
|
312
|
+
/(npm install|npx|pip install|cargo install|brew install)/i.test(readme);
|
|
313
|
+
indicators.readme_mentions_agents_first = /agents?[ -]?first|agentsfirst\.dev/i.test(readme);
|
|
314
|
+
}
|
|
315
|
+
return { indicators };
|
|
316
|
+
}
|
|
317
|
+
function probeStaleness(root) {
|
|
318
|
+
const tracked = ['AGENTS.md', 'src/server.ts', 'src/prep.ts', 'bin/index.js'];
|
|
319
|
+
const ages = {};
|
|
320
|
+
for (const rel of tracked) {
|
|
321
|
+
const full = path.join(root, rel);
|
|
322
|
+
if (existsSync(full)) {
|
|
323
|
+
const s = statSync(full);
|
|
324
|
+
ages[rel] = {
|
|
325
|
+
mtime: s.mtime.toISOString(),
|
|
326
|
+
days_ago: Math.floor((Date.now() - s.mtime.getTime()) / 86_400_000),
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return { agent_file_ages: ages };
|
|
331
|
+
}
|
|
332
|
+
// ─── Website mode ─────────────────────────────────────────────────────────────
|
|
333
|
+
export async function probeWebsite(url) {
|
|
334
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
335
|
+
throw new Error(`URL must start with http:// or https:// — got: ${url}`);
|
|
336
|
+
}
|
|
337
|
+
const origin = new URL(url).origin;
|
|
338
|
+
const probes = [
|
|
339
|
+
['robots_txt', `${origin}/robots.txt`],
|
|
340
|
+
['llms_txt', `${origin}/llms.txt`],
|
|
341
|
+
['llms_full_txt', `${origin}/llms-full.txt`],
|
|
342
|
+
['agents_md', `${origin}/AGENTS.md`],
|
|
343
|
+
['well_known_agent_rules', `${origin}/.well-known/agent-rules`],
|
|
344
|
+
['well_known_ai_plugin', `${origin}/.well-known/ai-plugin.json`],
|
|
345
|
+
['well_known_oauth', `${origin}/.well-known/oauth-authorization-server`],
|
|
346
|
+
['well_known_mcp', `${origin}/.well-known/mcp-server-card`],
|
|
347
|
+
['sitemap', `${origin}/sitemap.xml`],
|
|
348
|
+
['openapi_root', `${origin}/openapi.json`],
|
|
349
|
+
['openapi_v1', `${origin}/v1/openapi.json`],
|
|
350
|
+
['openapi_api', `${origin}/api/openapi.json`],
|
|
351
|
+
];
|
|
352
|
+
const surfaces = {};
|
|
353
|
+
await Promise.all(probes.map(async ([key, probeUrl]) => {
|
|
354
|
+
surfaces[key] = await fetchSurface(probeUrl);
|
|
355
|
+
}));
|
|
356
|
+
const out = {
|
|
357
|
+
mode: 'website',
|
|
358
|
+
target: url,
|
|
359
|
+
timestamp: new Date().toISOString(),
|
|
360
|
+
signals: {
|
|
361
|
+
surfaces,
|
|
362
|
+
markdown_negotiation: await probeMarkdownNegotiation(url),
|
|
363
|
+
homepage: await fetchSurface(url),
|
|
364
|
+
},
|
|
365
|
+
};
|
|
366
|
+
const robotsBody = surfaces['robots_txt']?.body ?? '';
|
|
367
|
+
if (robotsBody) {
|
|
368
|
+
const aiAgents = [
|
|
369
|
+
'GPTBot',
|
|
370
|
+
'anthropic-ai',
|
|
371
|
+
'ClaudeBot',
|
|
372
|
+
'Claude-Web',
|
|
373
|
+
'Google-Extended',
|
|
374
|
+
'PerplexityBot',
|
|
375
|
+
'Bytespider',
|
|
376
|
+
'CCBot',
|
|
377
|
+
'Omgilibot',
|
|
378
|
+
'cohere-ai',
|
|
379
|
+
];
|
|
380
|
+
const declared = aiAgents.filter((ua) => new RegExp(`User-agent:\\s*${ua}`, 'i').test(robotsBody));
|
|
381
|
+
out.signals.robots_analysis = {
|
|
382
|
+
ai_agents_addressed: declared,
|
|
383
|
+
address_count: declared.length,
|
|
384
|
+
blanket_disallow: /User-agent:\s*\*[\s\S]*?Disallow:\s*\//i.test(robotsBody),
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
const homepageBody = out.signals.homepage?.body;
|
|
388
|
+
if (homepageBody) {
|
|
389
|
+
out.signals.homepage_analysis = {
|
|
390
|
+
mentions_mcp: /\b(mcp|model context protocol)\b/i.test(homepageBody),
|
|
391
|
+
mentions_npx: /\bnpx\b/.test(homepageBody),
|
|
392
|
+
mentions_cli: /\b(cli|command[- ]?line)\b/i.test(homepageBody),
|
|
393
|
+
mentions_sdk: /\bsdk\b/i.test(homepageBody),
|
|
394
|
+
mentions_api: /\bapi\b/i.test(homepageBody),
|
|
395
|
+
mentions_oauth: /oauth/i.test(homepageBody),
|
|
396
|
+
mentions_agents_first: /agents?[ -]?first|agentsfirst\.dev/i.test(homepageBody),
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
return out;
|
|
400
|
+
}
|
|
401
|
+
async function fetchSurface(url) {
|
|
402
|
+
try {
|
|
403
|
+
const ctrl = new AbortController();
|
|
404
|
+
const timeout = setTimeout(() => ctrl.abort(), 8000);
|
|
405
|
+
const res = await fetch(url, {
|
|
406
|
+
method: 'GET',
|
|
407
|
+
headers: { 'User-Agent': 'agentsfirst-mcp/0.1 (https://agentsfirst.dev)' },
|
|
408
|
+
redirect: 'follow',
|
|
409
|
+
signal: ctrl.signal,
|
|
410
|
+
});
|
|
411
|
+
clearTimeout(timeout);
|
|
412
|
+
const text = await res.text().catch(() => '');
|
|
413
|
+
return {
|
|
414
|
+
url,
|
|
415
|
+
status: res.status,
|
|
416
|
+
ok: res.ok,
|
|
417
|
+
content_type: res.headers.get('content-type') ?? null,
|
|
418
|
+
length: text.length,
|
|
419
|
+
body: text.length < 50_000 ? text : text.slice(0, 50_000),
|
|
420
|
+
truncated: text.length >= 50_000,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
catch (e) {
|
|
424
|
+
return { url, error: e.message };
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
async function probeMarkdownNegotiation(url) {
|
|
428
|
+
try {
|
|
429
|
+
const res = await fetch(url, {
|
|
430
|
+
method: 'GET',
|
|
431
|
+
headers: {
|
|
432
|
+
Accept: 'text/markdown,text/plain;q=0.5',
|
|
433
|
+
'User-Agent': 'agentsfirst-mcp/0.1',
|
|
434
|
+
},
|
|
435
|
+
redirect: 'follow',
|
|
436
|
+
});
|
|
437
|
+
const ct = res.headers.get('content-type') ?? '';
|
|
438
|
+
return {
|
|
439
|
+
requested: 'text/markdown',
|
|
440
|
+
content_type_returned: ct,
|
|
441
|
+
served_markdown: /text\/markdown/i.test(ct),
|
|
442
|
+
status: res.status,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
catch (e) {
|
|
446
|
+
return { error: e.message };
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
// ─── helpers ──────────────────────────────────────────────────────────────────
|
|
450
|
+
async function readJsonIfExists(p) {
|
|
451
|
+
if (!existsSync(p))
|
|
452
|
+
return null;
|
|
453
|
+
try {
|
|
454
|
+
return JSON.parse(readFileSync(p, 'utf8'));
|
|
455
|
+
}
|
|
456
|
+
catch {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
async function readTextIfExists(p) {
|
|
461
|
+
if (!existsSync(p))
|
|
462
|
+
return null;
|
|
463
|
+
try {
|
|
464
|
+
return await readFile(p, 'utf8');
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const GREP_FLAGS = '-rE --no-messages --binary-files=without-match --exclude-dir=node_modules --exclude-dir=.git --exclude-dir=dist --exclude-dir=build --exclude-dir=_site --exclude-dir=.next --exclude-dir=coverage --exclude-dir=vendor';
|
|
471
|
+
function grepCount(root, pattern) {
|
|
472
|
+
try {
|
|
473
|
+
const rgArg = pattern instanceof RegExp ? pattern.source : pattern;
|
|
474
|
+
const out = execSync(`grep ${GREP_FLAGS} -c -e ${shellEscape(rgArg)} ${shellEscape(root)} 2>/dev/null | awk -F: '{sum+=$NF} END {print sum+0}'`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
475
|
+
return parseInt(out.trim(), 10) || 0;
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
return 0;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
function grepMany(root, terms) {
|
|
482
|
+
const result = {};
|
|
483
|
+
for (const t of terms) {
|
|
484
|
+
try {
|
|
485
|
+
const out = execSync(`grep ${GREP_FLAGS} -l -F ${shellEscape(t)} ${shellEscape(root)} 2>/dev/null`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
486
|
+
result[t] = out.split('\n').filter(Boolean);
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
result[t] = [];
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return result;
|
|
493
|
+
}
|
|
494
|
+
function grepRegex(root, pattern) {
|
|
495
|
+
try {
|
|
496
|
+
const out = execSync(`grep ${GREP_FLAGS} -hoE -e ${shellEscape(pattern.source)} ${shellEscape(root)} 2>/dev/null`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
497
|
+
const matches = [];
|
|
498
|
+
for (const line of out.split('\n')) {
|
|
499
|
+
if (!line)
|
|
500
|
+
continue;
|
|
501
|
+
const re = new RegExp(pattern.source);
|
|
502
|
+
const m = re.exec(line);
|
|
503
|
+
if (m && m[1])
|
|
504
|
+
matches.push(m[1]);
|
|
505
|
+
}
|
|
506
|
+
return matches;
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
return [];
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
function findFiles(root, regex) {
|
|
513
|
+
try {
|
|
514
|
+
const out = execSync(`find ${shellEscape(root)} -type d \\( -name node_modules -o -name .git -o -name dist -o -name build -o -name _site -o -name .next -o -name coverage -o -name vendor \\) -prune -o -type f -print 2>/dev/null`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 32 * 1024 * 1024 });
|
|
515
|
+
return out.split('\n').filter((p) => p && regex.test(path.basename(p)));
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
return [];
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
async function checkTsStrict(root) {
|
|
522
|
+
const tsconfig = await readJsonIfExists(path.join(root, 'tsconfig.json'));
|
|
523
|
+
const co = (tsconfig?.compilerOptions ?? {});
|
|
524
|
+
return co.strict === true;
|
|
525
|
+
}
|
|
526
|
+
function shellEscape(s) {
|
|
527
|
+
return `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
528
|
+
}
|
package/dist/score.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { CodebaseSignals, WebsiteSignals } from './probe.js';
|
|
2
|
+
import { type AntiPatternSlug, type PrincipleSlug } from './principles.js';
|
|
3
|
+
export type Status = 'pass' | 'partial' | 'fail';
|
|
4
|
+
export interface PrincipleScore {
|
|
5
|
+
pts: number;
|
|
6
|
+
max: number;
|
|
7
|
+
status: Status;
|
|
8
|
+
notes: string[];
|
|
9
|
+
}
|
|
10
|
+
export interface AntiPatternFlag {
|
|
11
|
+
slug: AntiPatternSlug;
|
|
12
|
+
name: string;
|
|
13
|
+
evidence: string;
|
|
14
|
+
}
|
|
15
|
+
export interface TopMove {
|
|
16
|
+
rank: number;
|
|
17
|
+
principle: PrincipleSlug | 'discoverability';
|
|
18
|
+
gap_pts: number;
|
|
19
|
+
action: string;
|
|
20
|
+
}
|
|
21
|
+
export interface CodebaseScore {
|
|
22
|
+
score: number;
|
|
23
|
+
level: 0 | 1 | 2 | 3 | 4;
|
|
24
|
+
level_name: string;
|
|
25
|
+
principles: Record<string, PrincipleScore>;
|
|
26
|
+
anti_patterns_flagged: AntiPatternFlag[];
|
|
27
|
+
top_moves: TopMove[];
|
|
28
|
+
probe_signals: CodebaseSignals;
|
|
29
|
+
}
|
|
30
|
+
export interface WebsiteScore {
|
|
31
|
+
score: number;
|
|
32
|
+
level: 0 | 1 | 2 | 3 | 4;
|
|
33
|
+
level_name: string;
|
|
34
|
+
dimensions: Record<string, PrincipleScore>;
|
|
35
|
+
anti_patterns_flagged: AntiPatternFlag[];
|
|
36
|
+
top_moves: TopMove[];
|
|
37
|
+
probe_signals: WebsiteSignals;
|
|
38
|
+
}
|
|
39
|
+
export declare function scoreCodebase(signals: CodebaseSignals): CodebaseScore;
|
|
40
|
+
export declare function scoreWebsite(signals: WebsiteSignals): WebsiteScore;
|