Skip to content

Commit 46256b3

Browse files
committed
feat(website): Add last updated dates for agents, prompts, instructions, and skills
- Add git-dates.mjs utility to extract file modification dates from git history - Include lastUpdated field in JSON data for all resource types - Display relative time (e.g., '3 days ago') with full date on hover - Add 'Recently Updated' sort option to agents, prompts, instructions, and skills pages - Update deploy-website.yml to use fetch-depth: 0 for full git history CI overhead: ~20-30s additional for full git checkout
1 parent b492cfe commit 46256b3

13 files changed

Lines changed: 358 additions & 14 deletions

File tree

.github/workflows/deploy-website.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ jobs:
4040
steps:
4141
- name: Checkout
4242
uses: actions/checkout@v4
43+
with:
44+
fetch-depth: 0 # Full history needed for git-based last updated dates
4345

4446
- name: Setup Node.js
4547
uses: actions/setup-node@v4

eng/generate-website-data.mjs

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
parseSkillMetadata,
2525
parseYamlFile,
2626
} from "./yaml-parser.mjs";
27+
import { getGitFileDates } from "./utils/git-dates.mjs";
2728

2829
const __filename = fileURLToPath(import.meta.url);
2930
const __dirname = dirname(__filename);
@@ -65,7 +66,7 @@ function extractTitle(filePath, frontmatter) {
6566
/**
6667
* Generate agents metadata
6768
*/
68-
function generateAgentsData() {
69+
function generateAgentsData(gitDates) {
6970
const agents = [];
7071
const files = fs
7172
.readdirSync(AGENTS_DIR)
@@ -106,6 +107,7 @@ function generateAgentsData() {
106107
: [],
107108
path: relativePath,
108109
filename: file,
110+
lastUpdated: gitDates.get(relativePath) || null,
109111
});
110112
}
111113

@@ -124,7 +126,7 @@ function generateAgentsData() {
124126
/**
125127
* Generate prompts metadata
126128
*/
127-
function generatePromptsData() {
129+
function generatePromptsData(gitDates) {
128130
const prompts = [];
129131
const files = fs
130132
.readdirSync(PROMPTS_DIR)
@@ -152,6 +154,7 @@ function generatePromptsData() {
152154
tools: tools,
153155
path: relativePath,
154156
filename: file,
157+
lastUpdated: gitDates.get(relativePath) || null,
155158
});
156159
}
157160

@@ -207,7 +210,7 @@ function extractExtensionFromPattern(pattern) {
207210
/**
208211
* Generate instructions metadata
209212
*/
210-
function generateInstructionsData() {
213+
function generateInstructionsData(gitDates) {
211214
const instructions = [];
212215
const files = fs
213216
.readdirSync(INSTRUCTIONS_DIR)
@@ -254,6 +257,7 @@ function generateInstructionsData() {
254257
extensions: [...new Set(extensions)],
255258
path: relativePath,
256259
filename: file,
260+
lastUpdated: gitDates.get(relativePath) || null,
257261
});
258262
}
259263

@@ -317,7 +321,7 @@ function categorizeSkill(name, description) {
317321
/**
318322
* Generate skills metadata
319323
*/
320-
function generateSkillsData() {
324+
function generateSkillsData(gitDates) {
321325
const skills = [];
322326

323327
if (!fs.existsSync(SKILLS_DIR)) {
@@ -344,6 +348,9 @@ function generateSkillsData() {
344348
// Get all files in the skill folder recursively
345349
const files = getSkillFiles(skillPath, relativePath);
346350

351+
// Get last updated from SKILL.md file
352+
const skillFilePath = `${relativePath}/SKILL.md`;
353+
347354
skills.push({
348355
id: folder,
349356
name: metadata.name,
@@ -357,8 +364,9 @@ function generateSkillsData() {
357364
assetCount: metadata.assets.length,
358365
category: category,
359366
path: relativePath,
360-
skillFile: `${relativePath}/SKILL.md`,
367+
skillFile: skillFilePath,
361368
files: files,
369+
lastUpdated: gitDates.get(skillFilePath) || null,
362370
});
363371
}
364372
}
@@ -407,7 +415,7 @@ function getSkillFiles(skillPath, relativePath) {
407415
/**
408416
* Generate collections metadata
409417
*/
410-
function generateCollectionsData() {
418+
function generateCollectionsData(gitDates) {
411419
const collections = [];
412420

413421
if (!fs.existsSync(COLLECTIONS_DIR)) {
@@ -448,6 +456,7 @@ function generateCollectionsData() {
448456
})),
449457
path: relativePath,
450458
filename: file,
459+
lastUpdated: gitDates.get(relativePath) || null,
451460
});
452461
}
453462
}
@@ -543,6 +552,7 @@ function generateSearchIndex(
543552
title: agent.title,
544553
description: agent.description,
545554
path: agent.path,
555+
lastUpdated: agent.lastUpdated,
546556
searchText: `${agent.title} ${agent.description} ${agent.tools.join(
547557
" "
548558
)}`.toLowerCase(),
@@ -556,6 +566,7 @@ function generateSearchIndex(
556566
title: prompt.title,
557567
description: prompt.description,
558568
path: prompt.path,
569+
lastUpdated: prompt.lastUpdated,
559570
searchText: `${prompt.title} ${prompt.description}`.toLowerCase(),
560571
});
561572
}
@@ -567,6 +578,7 @@ function generateSearchIndex(
567578
title: instruction.title,
568579
description: instruction.description,
569580
path: instruction.path,
581+
lastUpdated: instruction.lastUpdated,
570582
searchText: `${instruction.title} ${instruction.description} ${
571583
instruction.applyTo || ""
572584
}`.toLowerCase(),
@@ -580,6 +592,7 @@ function generateSearchIndex(
580592
title: skill.title,
581593
description: skill.description,
582594
path: skill.skillFile,
595+
lastUpdated: skill.lastUpdated,
583596
searchText: `${skill.title} ${skill.description}`.toLowerCase(),
584597
});
585598
}
@@ -592,6 +605,7 @@ function generateSearchIndex(
592605
description: collection.description,
593606
path: collection.path,
594607
tags: collection.tags,
608+
lastUpdated: collection.lastUpdated,
595609
searchText: `${collection.name} ${
596610
collection.description
597611
} ${collection.tags.join(" ")}`.toLowerCase(),
@@ -704,32 +718,40 @@ async function main() {
704718

705719
ensureDataDir();
706720

721+
// Load git dates for all resource files (single efficient git command)
722+
console.log("Loading git history for last updated dates...");
723+
const gitDates = getGitFileDates(
724+
["agents/", "prompts/", "instructions/", "skills/", "collections/"],
725+
ROOT_FOLDER
726+
);
727+
console.log(`✓ Loaded dates for ${gitDates.size} files\n`);
728+
707729
// Generate all data
708-
const agentsData = generateAgentsData();
730+
const agentsData = generateAgentsData(gitDates);
709731
const agents = agentsData.items;
710732
console.log(
711733
`✓ Generated ${agents.length} agents (${agentsData.filters.models.length} models, ${agentsData.filters.tools.length} tools)`
712734
);
713735

714-
const promptsData = generatePromptsData();
736+
const promptsData = generatePromptsData(gitDates);
715737
const prompts = promptsData.items;
716738
console.log(
717739
`✓ Generated ${prompts.length} prompts (${promptsData.filters.tools.length} tools)`
718740
);
719741

720-
const instructionsData = generateInstructionsData();
742+
const instructionsData = generateInstructionsData(gitDates);
721743
const instructions = instructionsData.items;
722744
console.log(
723745
`✓ Generated ${instructions.length} instructions (${instructionsData.filters.extensions.length} extensions)`
724746
);
725747

726-
const skillsData = generateSkillsData();
748+
const skillsData = generateSkillsData(gitDates);
727749
const skills = skillsData.items;
728750
console.log(
729751
`✓ Generated ${skills.length} skills (${skillsData.filters.categories.length} categories)`
730752
);
731753

732-
const collectionsData = generateCollectionsData();
754+
const collectionsData = generateCollectionsData(gitDates);
733755
const collections = collectionsData.items;
734756
console.log(
735757
`✓ Generated ${collections.length} collections (${collectionsData.filters.tags.length} tags)`

eng/utils/git-dates.mjs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Utility to extract last modification dates from git history.
5+
* Uses a single git log command for efficiency.
6+
*/
7+
8+
import { execSync } from "child_process";
9+
import path from "path";
10+
11+
/**
12+
* Get the last modification date for all tracked files in specified directories.
13+
* Returns a Map of file path -> ISO date string.
14+
*
15+
* @param {string[]} directories - Array of directory paths to scan
16+
* @param {string} rootDir - Root directory for relative paths
17+
* @returns {Map<string, string>} Map of relative file path to ISO date string
18+
*/
19+
export function getGitFileDates(directories, rootDir) {
20+
const fileDates = new Map();
21+
22+
try {
23+
// Get git log with file names for all specified directories
24+
// Format: ISO date, then file names that were modified in that commit
25+
const gitArgs = [
26+
"--no-pager",
27+
"log",
28+
"--format=%aI", // Author date in ISO 8601 format
29+
"--name-only",
30+
"--diff-filter=ACMR", // Added, Copied, Modified, Renamed
31+
"--",
32+
...directories,
33+
];
34+
35+
const output = execSync(`git ${gitArgs.join(" ")}`, {
36+
encoding: "utf8",
37+
cwd: rootDir,
38+
stdio: ["pipe", "pipe", "pipe"],
39+
});
40+
41+
// Parse the output: alternating date lines and file name lines
42+
// Format is:
43+
// 2026-01-15T10:30:00+00:00
44+
//
45+
// file1.md
46+
// file2.md
47+
//
48+
// 2026-01-14T09:00:00+00:00
49+
// ...
50+
51+
let currentDate = null;
52+
const lines = output.split("\n");
53+
54+
for (const line of lines) {
55+
const trimmed = line.trim();
56+
57+
if (!trimmed) {
58+
continue;
59+
}
60+
61+
// Check if this is a date line (ISO 8601 format)
62+
if (/^\d{4}-\d{2}-\d{2}T/.test(trimmed)) {
63+
currentDate = trimmed;
64+
} else if (currentDate && trimmed) {
65+
// This is a file path - only set if we haven't seen this file yet
66+
// (first occurrence is the most recent modification)
67+
if (!fileDates.has(trimmed)) {
68+
fileDates.set(trimmed, currentDate);
69+
}
70+
}
71+
}
72+
} catch (error) {
73+
// Git command failed - might not be a git repo or no history
74+
console.warn("Warning: Could not get git dates:", error.message);
75+
}
76+
77+
return fileDates;
78+
}
79+
80+
/**
81+
* Get the last modification date for a single file.
82+
*
83+
* @param {string} filePath - Path to the file (relative to git root)
84+
* @param {string} rootDir - Root directory
85+
* @returns {string|null} ISO date string or null if not found
86+
*/
87+
export function getGitFileDate(filePath, rootDir) {
88+
try {
89+
const output = execSync(
90+
`git --no-pager log -1 --format="%aI" -- "${filePath}"`,
91+
{
92+
encoding: "utf8",
93+
cwd: rootDir,
94+
stdio: ["pipe", "pipe", "pipe"],
95+
}
96+
);
97+
98+
const date = output.trim();
99+
return date || null;
100+
} catch (error) {
101+
return null;
102+
}
103+
}

website/public/styles/global.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1415,6 +1415,14 @@ a:hover {
14151415
flex-shrink: 0;
14161416
}
14171417

1418+
/* Last Updated */
1419+
.last-updated {
1420+
font-size: 12px;
1421+
color: var(--color-text-muted);
1422+
cursor: default;
1423+
margin-left: auto;
1424+
}
1425+
14181426
/* Collection Items */
14191427
.collection-items {
14201428
margin-top: 12px;

website/src/pages/agents.astro

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ import Modal from '../components/Modal.astro';
3535
Has Handoffs
3636
</label>
3737
</div>
38+
<div class="filter-group">
39+
<label for="sort-select">Sort:</label>
40+
<select id="sort-select" aria-label="Sort by">
41+
<option value="title">Name (A-Z)</option>
42+
<option value="lastUpdated">Recently Updated</option>
43+
</select>
44+
</div>
3845
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
3946
</div>
4047

website/src/pages/instructions.astro

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ import Modal from '../components/Modal.astro';
2424
<label for="filter-extension">File Extension:</label>
2525
<select id="filter-extension" multiple aria-label="Filter by file extension"></select>
2626
</div>
27+
<div class="filter-group">
28+
<label for="sort-select">Sort:</label>
29+
<select id="sort-select" aria-label="Sort by">
30+
<option value="title">Name (A-Z)</option>
31+
<option value="lastUpdated">Recently Updated</option>
32+
</select>
33+
</div>
2734
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
2835
</div>
2936

website/src/pages/prompts.astro

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ import Modal from '../components/Modal.astro';
2424
<label for="filter-tool">Tool:</label>
2525
<select id="filter-tool" multiple aria-label="Filter by tool"></select>
2626
</div>
27+
<div class="filter-group">
28+
<label for="sort-select">Sort:</label>
29+
<select id="sort-select" aria-label="Sort by">
30+
<option value="title">Name (A-Z)</option>
31+
<option value="lastUpdated">Recently Updated</option>
32+
</select>
33+
</div>
2734
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
2835
</div>
2936

website/src/pages/skills.astro

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ import Modal from '../components/Modal.astro';
3030
Has Bundled Assets
3131
</label>
3232
</div>
33+
<div class="filter-group">
34+
<label for="sort-select">Sort:</label>
35+
<select id="sort-select" aria-label="Sort by">
36+
<option value="title">Name (A-Z)</option>
37+
<option value="lastUpdated">Recently Updated</option>
38+
</select>
39+
</div>
3340
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
3441
</div>
3542

0 commit comments

Comments
 (0)