feat(skills): add repository field for source transparency

This commit is contained in:
Aditya Bhuran 2026-01-28 14:43:36 -05:00
parent 109ac1c549
commit 4f0f02f9f0
6 changed files with 84 additions and 1 deletions

View File

@ -40,6 +40,8 @@ export type SkillStatusEntry = {
primaryEnv?: string;
emoji?: string;
homepage?: string;
/** URL to the source repository (e.g., GitHub) for transparency and auditability. */
repository?: string;
always: boolean;
disabled: boolean;
blockedByAllowlist: boolean;
@ -164,6 +166,11 @@ function buildSkillStatus(
entry.frontmatter.website ??
entry.frontmatter.url;
const homepage = homepageRaw?.trim() ? homepageRaw.trim() : undefined;
const repositoryRaw =
entry.metadata?.repository ??
entry.frontmatter.repository ??
entry.frontmatter.repo;
const repository = repositoryRaw?.trim() ? repositoryRaw.trim() : undefined;
const requiredBins = entry.metadata?.requires?.bins ?? [];
const requiredAnyBins = entry.metadata?.requires?.anyBins ?? [];
@ -237,6 +244,7 @@ function buildSkillStatus(
primaryEnv: entry.metadata?.primaryEnv,
emoji,
homepage,
repository,
always,
disabled,
blockedByAllowlist,

View File

@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { resolveSkillInvocationPolicy } from "./frontmatter.js";
import { resolveMoltbotMetadata, resolveSkillInvocationPolicy } from "./frontmatter.js";
describe("resolveSkillInvocationPolicy", () => {
it("defaults to enabled behaviors", () => {
@ -18,3 +18,54 @@ describe("resolveSkillInvocationPolicy", () => {
expect(policy.disableModelInvocation).toBe(true);
});
});
describe("resolveMoltbotMetadata", () => {
it("extracts repository from metadata", () => {
const frontmatter = {
metadata: JSON.stringify({
moltbot: {
repository: "https://github.com/example/skill-repo",
},
}),
};
const result = resolveMoltbotMetadata(frontmatter);
expect(result).toBeDefined();
expect(result?.repository).toBe("https://github.com/example/skill-repo");
});
it("extracts homepage and repository together", () => {
const frontmatter = {
metadata: JSON.stringify({
moltbot: {
homepage: "https://example.com",
repository: "https://github.com/example/skill-repo",
},
}),
};
const result = resolveMoltbotMetadata(frontmatter);
expect(result?.homepage).toBe("https://example.com");
expect(result?.repository).toBe("https://github.com/example/skill-repo");
});
it("returns undefined repository when not present", () => {
const frontmatter = {
metadata: JSON.stringify({
moltbot: {
emoji: "🔧",
},
}),
};
const result = resolveMoltbotMetadata(frontmatter);
expect(result).toBeDefined();
expect(result?.repository).toBeUndefined();
});
it("returns undefined for missing metadata", () => {
const frontmatter = { name: "test-skill" };
const result = resolveMoltbotMetadata(frontmatter);
expect(result).toBeUndefined();
});
});

View File

@ -98,6 +98,7 @@ export function resolveMoltbotMetadata(
always: typeof metadataObj.always === "boolean" ? metadataObj.always : undefined,
emoji: typeof metadataObj.emoji === "string" ? metadataObj.emoji : undefined,
homepage: typeof metadataObj.homepage === "string" ? metadataObj.homepage : undefined,
repository: typeof metadataObj.repository === "string" ? metadataObj.repository : undefined,
skillKey: typeof metadataObj.skillKey === "string" ? metadataObj.skillKey : undefined,
primaryEnv: typeof metadataObj.primaryEnv === "string" ? metadataObj.primaryEnv : undefined,
os: osRaw.length > 0 ? osRaw : undefined,

View File

@ -22,6 +22,8 @@ export type MoltbotSkillMetadata = {
primaryEnv?: string;
emoji?: string;
homepage?: string;
/** URL to the source repository (e.g., GitHub) for transparency and auditability. */
repository?: string;
os?: string[];
requires?: {
bins?: string[];

View File

@ -484,6 +484,8 @@ export type SkillStatusEntry = {
primaryEnv?: string;
emoji?: string;
homepage?: string;
/** URL to the source repository (e.g., GitHub) for transparency and auditability. */
repository?: string;
always: boolean;
disabled: boolean;
blockedByAllowlist: boolean;

View File

@ -101,6 +101,25 @@ function renderSkill(skill: SkillStatusEntry, props: SkillsProps) {
</span>
${skill.disabled ? html`<span class="chip chip-warn">disabled</span>` : nothing}
</div>
${skill.repository
? html`
<div style="margin-top: 6px;">
<a
href="${skill.repository}"
target="_blank"
rel="noopener noreferrer"
class="repo-link"
style="display: inline-flex; align-items: center; gap: 4px; font-size: 12px; color: var(--link-color, #0969da); text-decoration: none;"
title="View source repository"
>
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
</svg>
Source
</a>
</div>
`
: nothing}
${missing.length > 0
? html`
<div class="muted" style="margin-top: 6px;">