@netanelyasi/agent-ready 0.2.1 → 0.2.3
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/README.md +1 -2
- package/dist/generators/generate.js +39 -4
- package/dist/scanner/scanProject.js +41 -29
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
<p align="center">
|
|
16
16
|
<img alt="Status" src="https://img.shields.io/badge/status-experimental-f59e0b?style=flat-square" />
|
|
17
|
-
<img alt="Version" src="https://img.shields.io/badge/version-0.2.
|
|
17
|
+
<img alt="Version" src="https://img.shields.io/badge/version-0.2.3-111827?style=flat-square" />
|
|
18
18
|
<img alt="License" src="https://img.shields.io/badge/license-MIT-0f766e?style=flat-square" />
|
|
19
19
|
<img alt="Runtime" src="https://img.shields.io/badge/runtime-Node.js-3c873a?style=flat-square" />
|
|
20
20
|
<img alt="Built by BrainboxAI" src="https://img.shields.io/badge/by-BrainboxAI-111827?style=flat-square" />
|
|
@@ -429,7 +429,6 @@ Planned improvements:
|
|
|
429
429
|
- richer monorepo workspace detection
|
|
430
430
|
- generated `CONTRIBUTING.md` and `SECURITY.md` templates
|
|
431
431
|
- optional AI-assisted repository summary mode
|
|
432
|
-
- npm package release
|
|
433
432
|
- plugin/export presets for Claude Code, Cursor, Codex, and other agents
|
|
434
433
|
- CI mode for failing builds when agent readiness drops below a threshold
|
|
435
434
|
|
|
@@ -4,7 +4,7 @@ export function generateFiles(scan, score, force) {
|
|
|
4
4
|
add("CLAUDE.md", generateClaudeMd(scan));
|
|
5
5
|
add("CODEMAP.md", generateCodemap(scan));
|
|
6
6
|
add(".aiignore", generateAiIgnore(scan));
|
|
7
|
-
add(".claude/settings.json", generateClaudeSettings(
|
|
7
|
+
add(".claude/settings.json", generateClaudeSettings());
|
|
8
8
|
add(".claude/hooks/prevent-destructive.mjs", generatePreventDestructiveHook());
|
|
9
9
|
add(".claude/hooks/protect-generated.mjs", generateProtectGeneratedHook());
|
|
10
10
|
add(".claude/hooks/suggest-validation.mjs", generateSuggestValidationHook(scan));
|
|
@@ -132,6 +132,20 @@ function commandLines(scan) {
|
|
|
132
132
|
lines.push("- No standard validation commands were detected. Add project-specific commands here.");
|
|
133
133
|
return lines;
|
|
134
134
|
}
|
|
135
|
+
// Flat one-command-per-rule list for the validation skill, e.g. "test: `npm run test`".
|
|
136
|
+
// Unlike commandLines (which nests for CLAUDE.md), this stays flat so it renders
|
|
137
|
+
// correctly as a SKILL.md bullet list.
|
|
138
|
+
function validationSkillRules(scan) {
|
|
139
|
+
const order = ["dev", "build", "test", "lint", "typecheck", "format"];
|
|
140
|
+
const rules = [];
|
|
141
|
+
for (const name of order) {
|
|
142
|
+
for (const command of (scan.commands[name] ?? []).slice(0, 8))
|
|
143
|
+
rules.push(`${name}: \`${command}\``);
|
|
144
|
+
}
|
|
145
|
+
if (!rules.length)
|
|
146
|
+
rules.push("No standard validation commands were detected. Add project-specific commands here.");
|
|
147
|
+
return rules;
|
|
148
|
+
}
|
|
135
149
|
function generateCodemap(scan) {
|
|
136
150
|
return [
|
|
137
151
|
`# ${scan.name} — CODEMAP`,
|
|
@@ -228,7 +242,7 @@ function generateAiIgnore(scan) {
|
|
|
228
242
|
add(`${noisy}/`);
|
|
229
243
|
return `${base.join("\n")}\n`;
|
|
230
244
|
}
|
|
231
|
-
function generateClaudeSettings(
|
|
245
|
+
function generateClaudeSettings() {
|
|
232
246
|
const deniedPathPatterns = [
|
|
233
247
|
"node_modules/**",
|
|
234
248
|
".next/**",
|
|
@@ -467,7 +481,7 @@ function hookRecommendations(scan) {
|
|
|
467
481
|
function generateSkills(scan) {
|
|
468
482
|
const skills = [
|
|
469
483
|
{ slug: "codebase-navigation", content: skill("codebase-navigation", "Use when starting work in this repository or when a task spans unfamiliar directories.", ["Read CODEMAP.md first.", "Use narrow searches from the relevant directory before global search.", "Prefer symbol/reference search when LSP is available.", "Do not inspect ignored/generated directories unless explicitly needed."]) },
|
|
470
|
-
{ slug: "validation", content: skill("validation", "Use after code edits or before declaring a task complete.", [...
|
|
484
|
+
{ slug: "validation", content: skill("validation", "Use after code edits or before declaring a task complete.", [...validationSkillRules(scan), "Run the narrowest relevant command first.", "If validation cannot be run, report the exact reason."]) },
|
|
471
485
|
];
|
|
472
486
|
if (scan.frameworks.includes("Next.js"))
|
|
473
487
|
skills.push({ slug: "nextjs-hydration", content: skill("nextjs-hydration", "Use when editing Next.js/React components, routes, or client/server boundaries.", ["Do not read localStorage/sessionStorage/window/document during server render or initial state.", "Use useEffect or guarded client-only code for browser APIs.", "Avoid Math.random() or new Date() in render paths that must hydrate identically.", "Keep server/client boundaries explicit."]) });
|
|
@@ -480,7 +494,28 @@ function generateSkills(scan) {
|
|
|
480
494
|
return skills;
|
|
481
495
|
}
|
|
482
496
|
function skill(name, description, rules) {
|
|
483
|
-
return [
|
|
497
|
+
return [
|
|
498
|
+
"---",
|
|
499
|
+
`name: ${name}`,
|
|
500
|
+
`description: ${yamlScalar(description)}`,
|
|
501
|
+
"---",
|
|
502
|
+
"",
|
|
503
|
+
`# ${name}`,
|
|
504
|
+
"",
|
|
505
|
+
description,
|
|
506
|
+
"",
|
|
507
|
+
"## Rules",
|
|
508
|
+
...rules.map((rule) => `- ${rule}`),
|
|
509
|
+
"",
|
|
510
|
+
].join("\n");
|
|
511
|
+
}
|
|
512
|
+
// Claude Code parses SKILL.md frontmatter as YAML. Quote scalars that contain
|
|
513
|
+
// characters YAML would otherwise treat as structure (`:`, leading `#`, etc.).
|
|
514
|
+
function yamlScalar(value) {
|
|
515
|
+
if (/^[^\s].*[:#]|^[#&*!|>%@`"']|:\s|\s#/.test(value)) {
|
|
516
|
+
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
517
|
+
}
|
|
518
|
+
return value;
|
|
484
519
|
}
|
|
485
520
|
function list(items) {
|
|
486
521
|
return items.length ? items.join(", ") : "none detected";
|
|
@@ -36,7 +36,7 @@ export async function scanProject(rootInput) {
|
|
|
36
36
|
claudeSettings: await harnessFileState(root, path.join(".claude", "settings.json")),
|
|
37
37
|
skillsDir: await pathExists(path.join(root, ".agent-ready", "skills")) || await pathExists(path.join(root, ".claude", "skills")),
|
|
38
38
|
};
|
|
39
|
-
const codeGraph = await analyzeCodeGraph(root, files, packages);
|
|
39
|
+
const codeGraph = await analyzeCodeGraph(root, files, packages, frameworks);
|
|
40
40
|
return {
|
|
41
41
|
root,
|
|
42
42
|
name: rootPackage?.name ?? path.basename(root),
|
|
@@ -107,6 +107,8 @@ function detectFrameworks(files, deps) {
|
|
|
107
107
|
found.add(deps.nuxt ? "Nuxt" : "Vue");
|
|
108
108
|
if (deps.svelte || deps["@sveltejs/kit"])
|
|
109
109
|
found.add(deps["@sveltejs/kit"] ? "SvelteKit" : "Svelte");
|
|
110
|
+
if (deps["@remix-run/react"] || deps["@remix-run/node"] || deps["@remix-run/server-runtime"] || files.includes("remix.config.js") || files.includes("remix.config.ts"))
|
|
111
|
+
found.add("Remix");
|
|
110
112
|
if (deps.vite || files.some((file) => file.startsWith("vite.config.")))
|
|
111
113
|
found.add("Vite");
|
|
112
114
|
if (deps.express)
|
|
@@ -267,7 +269,7 @@ async function harnessFileState(root, relativePath) {
|
|
|
267
269
|
const generatedByAgentReady = Boolean(text && /generated by `?agent-ready`?|agent-ready:|Generated by agent-ready/i.test(text));
|
|
268
270
|
return { exists: true, generatedByAgentReady, countsAsMaintainerAuthored: !generatedByAgentReady };
|
|
269
271
|
}
|
|
270
|
-
async function analyzeCodeGraph(root, files, packages) {
|
|
272
|
+
async function analyzeCodeGraph(root, files, packages, frameworks) {
|
|
271
273
|
const sourceFiles = files
|
|
272
274
|
.filter((file) => /\.(tsx?|jsx?|mjs|cjs|py|go|rs|svelte)$/.test(file))
|
|
273
275
|
.filter((file) => !file.endsWith(".d.ts"))
|
|
@@ -275,7 +277,7 @@ async function analyzeCodeGraph(root, files, packages) {
|
|
|
275
277
|
.slice(0, 2000);
|
|
276
278
|
const sourceSet = new Set(sourceFiles.map((file) => rel(root, file)));
|
|
277
279
|
const goModulePath = await readGoModulePath(root);
|
|
278
|
-
const entryPoints = detectEntryPoints(root, packages, sourceSet);
|
|
280
|
+
const entryPoints = detectEntryPoints(root, packages, sourceSet, frameworks);
|
|
279
281
|
const importEdges = [];
|
|
280
282
|
const externalImportMap = new Map();
|
|
281
283
|
for (const file of sourceFiles) {
|
|
@@ -323,7 +325,7 @@ async function analyzeCodeGraph(root, files, packages) {
|
|
|
323
325
|
unresolvedRelativeImports: importEdges.filter((edge) => !edge.resolved).slice(0, 30),
|
|
324
326
|
};
|
|
325
327
|
}
|
|
326
|
-
function detectEntryPoints(root, packages, sourceSet) {
|
|
328
|
+
function detectEntryPoints(root, packages, sourceSet, frameworks) {
|
|
327
329
|
const entries = new Map();
|
|
328
330
|
for (const pkg of packages) {
|
|
329
331
|
const dir = path.posix.dirname(pkg.path) === "." ? "." : path.posix.dirname(pkg.path);
|
|
@@ -360,7 +362,7 @@ function detectEntryPoints(root, packages, sourceSet) {
|
|
|
360
362
|
if (!source.startsWith(packageRoot))
|
|
361
363
|
continue;
|
|
362
364
|
const local = source.slice(packageRoot.length);
|
|
363
|
-
const frameworkEntry = frameworkEntryPoint(local);
|
|
365
|
+
const frameworkEntry = frameworkEntryPoint(local, frameworks);
|
|
364
366
|
if (frameworkEntry)
|
|
365
367
|
entries.set(source, { path: source, ...frameworkEntry });
|
|
366
368
|
if (/^cmd\/[^/]+\/main\.go$/.test(local))
|
|
@@ -376,32 +378,42 @@ function detectEntryPoints(root, packages, sourceSet) {
|
|
|
376
378
|
}
|
|
377
379
|
return [...entries.values()].sort((a, b) => a.path.localeCompare(b.path)).slice(0, 40);
|
|
378
380
|
}
|
|
379
|
-
function frameworkEntryPoint(localPath) {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
381
|
+
function frameworkEntryPoint(localPath, frameworks) {
|
|
382
|
+
// Framework conventions like `pages/` and `app/` are generic folder names that
|
|
383
|
+
// also appear in Vite/React-Router apps. Only treat them as framework entry
|
|
384
|
+
// points when the framework is actually detected, or every such project is
|
|
385
|
+
// mislabeled (e.g. a Vite `src/pages/*.tsx` reported as a Next.js route).
|
|
386
|
+
if (frameworks.includes("Next.js")) {
|
|
387
|
+
if (/^(src\/)?app\/(page|layout|route)\.(tsx?|jsx?)$/.test(localPath)) {
|
|
388
|
+
const file = localPath.includes("/layout.") ? "layout" : localPath.includes("/route.") ? "route handler" : "page";
|
|
389
|
+
return { kind: `Next.js ${file}`, reason: "App Router root entry" };
|
|
390
|
+
}
|
|
391
|
+
if (/^(src\/)?app\/.+\/(page|layout|route|loading|error|not-found)\.(tsx?|jsx?)$/.test(localPath)) {
|
|
392
|
+
return { kind: "Next.js route", reason: "App Router route segment entry" };
|
|
393
|
+
}
|
|
394
|
+
if (/^(src\/)?pages\/index\.(tsx?|jsx?)$/.test(localPath))
|
|
395
|
+
return { kind: "Next.js route", reason: "Pages Router index" };
|
|
396
|
+
if (/^(src\/)?pages\/(api\/.+|.+)\.(tsx?|jsx?)$/.test(localPath))
|
|
397
|
+
return { kind: "Next.js route", reason: "Pages Router route/API entry" };
|
|
398
|
+
if (/^(src\/)?middleware\.(tsx?|jsx?)$/.test(localPath))
|
|
399
|
+
return { kind: "Next.js middleware", reason: "Next.js request middleware entry" };
|
|
400
|
+
if (/^next\.config\.(tsx?|jsx?|mjs|cjs)$/.test(localPath))
|
|
401
|
+
return { kind: "Next.js config", reason: "Next.js configuration entry" };
|
|
402
|
+
}
|
|
403
|
+
if (frameworks.includes("Remix")) {
|
|
404
|
+
if (/^app\/(root|entry\.(client|server))\.(tsx?|jsx?)$/.test(localPath))
|
|
405
|
+
return { kind: "Remix entry", reason: "Remix root/client/server entry" };
|
|
406
|
+
if (/^app\/routes\/.+\.(tsx?|jsx?)$/.test(localPath))
|
|
407
|
+
return { kind: "Remix route", reason: "Remix route module" };
|
|
383
408
|
}
|
|
384
|
-
if (
|
|
385
|
-
|
|
409
|
+
if (frameworks.includes("SvelteKit")) {
|
|
410
|
+
if (/^src\/routes\/(\+page|\+layout|\+server)\.(svelte|tsx?|jsx?)$/.test(localPath))
|
|
411
|
+
return { kind: "SvelteKit route", reason: "SvelteKit root route entry" };
|
|
412
|
+
if (/^src\/routes\/.+\/(\+page|\+layout|\+server)\.(svelte|tsx?|jsx?)$/.test(localPath))
|
|
413
|
+
return { kind: "SvelteKit route", reason: "SvelteKit route entry" };
|
|
414
|
+
if (/^src\/hooks(\.server)?\.(tsx?|jsx?)$/.test(localPath))
|
|
415
|
+
return { kind: "SvelteKit hook", reason: "SvelteKit lifecycle hook entry" };
|
|
386
416
|
}
|
|
387
|
-
if (/^(src\/)?pages\/index\.(tsx?|jsx?)$/.test(localPath))
|
|
388
|
-
return { kind: "Next.js route", reason: "Pages Router index" };
|
|
389
|
-
if (/^(src\/)?pages\/(api\/.+|.+)\.(tsx?|jsx?)$/.test(localPath))
|
|
390
|
-
return { kind: "Next.js route", reason: "Pages Router route/API entry" };
|
|
391
|
-
if (/^(src\/)?middleware\.(tsx?|jsx?)$/.test(localPath))
|
|
392
|
-
return { kind: "Next.js middleware", reason: "Next.js request middleware entry" };
|
|
393
|
-
if (/^next\.config\.(tsx?|jsx?|mjs|cjs)$/.test(localPath))
|
|
394
|
-
return { kind: "Next.js config", reason: "Next.js configuration entry" };
|
|
395
|
-
if (/^app\/(root|entry\.(client|server))\.(tsx?|jsx?)$/.test(localPath))
|
|
396
|
-
return { kind: "Remix entry", reason: "Remix root/client/server entry" };
|
|
397
|
-
if (/^app\/routes\/.+\.(tsx?|jsx?)$/.test(localPath))
|
|
398
|
-
return { kind: "Remix route", reason: "Remix route module" };
|
|
399
|
-
if (/^src\/routes\/(\+page|\+layout|\+server)\.(svelte|tsx?|jsx?)$/.test(localPath))
|
|
400
|
-
return { kind: "SvelteKit route", reason: "SvelteKit root route entry" };
|
|
401
|
-
if (/^src\/routes\/.+\/(\+page|\+layout|\+server)\.(svelte|tsx?|jsx?)$/.test(localPath))
|
|
402
|
-
return { kind: "SvelteKit route", reason: "SvelteKit route entry" };
|
|
403
|
-
if (/^src\/hooks(\.server)?\.(tsx?|jsx?)$/.test(localPath))
|
|
404
|
-
return { kind: "SvelteKit hook", reason: "SvelteKit lifecycle hook entry" };
|
|
405
417
|
return undefined;
|
|
406
418
|
}
|
|
407
419
|
function extractImportSpecifiers(text, from) {
|
package/package.json
CHANGED