Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 22 additions & 8 deletions eng/generate-website-data.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -185,25 +185,37 @@ function getCurrentCommitSha() {
/**
* Generate agents metadata
*/
function generateAgentsData(gitDates) {
export function generateAgentsData(gitDates, agentsDir = AGENTS_DIR) {
const agents = [];
const files = fs
.readdirSync(AGENTS_DIR)
.readdirSync(agentsDir)
.filter((f) => f.endsWith(".agent.md"));

// Track all unique values for filters
const allModels = new Set();
const allTools = new Set();

for (const file of files) {
const filePath = path.join(AGENTS_DIR, file);
const filePath = path.join(agentsDir, file);
const frontmatter = parseFrontmatter(filePath);
const relativePath = path
.relative(ROOT_FOLDER, filePath)
.replace(/\\/g, "/");

const model = frontmatter?.model || null;
const tools = frontmatter?.tools || [];
const rawTools = frontmatter?.tools;
const tools =
typeof rawTools === "string"
? rawTools.split(",").map((tool) => tool.trim()).filter(Boolean)
: rawTools ?? [];
if (
!Array.isArray(tools) ||
tools.some((tool) => typeof tool !== "string")
) {
throw new TypeError(
`Invalid tools in ${relativePath}: expected a string or an array of strings`
);
}
const handoffs = frontmatter?.handoffs || [];

// Track unique values
Expand Down Expand Up @@ -1809,7 +1821,9 @@ async function main() {
console.log(`\n✓ All data written to website/public/data/`);
}

main().catch((err) => {
console.error("Error generating website data:", err);
process.exit(1);
});
if (process.argv[1] && path.resolve(process.argv[1]) === __filename) {
main().catch((err) => {
console.error("Error generating website data:", err);
process.exit(1);
});
}
51 changes: 51 additions & 0 deletions eng/generate-website-data.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { test } from "node:test";
import { generateAgentsData } from "./generate-website-data.mjs";

function createAgent(t, toolsField) {
const agentsDir = fs.mkdtempSync(path.join(os.tmpdir(), "website-agents-"));
t.after(() => fs.rmSync(agentsDir, { recursive: true, force: true }));
fs.writeFileSync(
path.join(agentsDir, "sample.agent.md"),
`---\nname: Sample\ndescription: Sample agent\n${toolsField}\n---\n`
);
return agentsDir;
}

for (const [name, field, expected] of [
["scalar MCP wildcard", "tools: new-relic-mcp-server/*", ["new-relic-mcp-server/*"]],
["comma-separated tools", "tools: ' read, new-relic-mcp-server/*, edit, '", ["read", "new-relic-mcp-server/*", "edit"]],
["YAML list", "tools:\n - read\n - new-relic-mcp-server/*", ["read", "new-relic-mcp-server/*"]],
["omitted tools", "", []],
["empty list", "tools: []", []],
["empty string", "tools: ''", []],
["null tools", "tools:", []],
]) {
test(`generates agent tools and filter facets for ${name}`, (t) => {
const agentsDir = createAgent(t, field);
const data = generateAgentsData(new Map(), agentsDir);

assert.equal(data.items.length, 1);
assert.deepEqual(data.items[0].tools, expected);
assert.deepEqual(data.filters.tools, [...expected].sort());
});
}

for (const field of [
"tools: false",
"tools: 42",
"tools: {read: true}",
"tools: [read, 42]",
]) {
test(`reports invalid ${field} with the source filename`, (t) => {
const agentsDir = createAgent(t, field);

assert.throws(() => generateAgentsData(new Map(), agentsDir), {
name: "TypeError",
message: /Invalid tools in .*sample\.agent\.md: expected a string or an array of strings/,
});
});
}
9 changes: 9 additions & 0 deletions website/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ npm run website:dev # generate data + start the dev server
npm run website:build # full production build
```

Agent metadata generation accepts `tools` as either a YAML string array or a
comma-separated string, including MCP wildcards such as `new-relic-mcp-server/*`.
Both forms produce arrays in the generated metadata and tool filter facets.
Run the focused metadata regression tests from the repository root:

```bash
node --test eng/generate-website-data.test.mjs
```

## Accessibility

The website has an automated axe-core + Playwright audit. Run it locally with `npm run website:a11y` from the repository root, or run `npm run a11y` from `website/` after building `dist` first.
Expand Down
Loading