From 4f0f02f9f01853528c82478517d404a3db28ba9d Mon Sep 17 00:00:00 2001 From: Aditya Bhuran Date: Wed, 28 Jan 2026 14:43:36 -0500 Subject: [PATCH] feat(skills): add repository field for source transparency --- src/agents/skills-status.ts | 8 ++++ src/agents/skills/frontmatter.test.ts | 53 ++++++++++++++++++++++++++- src/agents/skills/frontmatter.ts | 1 + src/agents/skills/types.ts | 2 + ui/src/ui/types.ts | 2 + ui/src/ui/views/skills.ts | 19 ++++++++++ 6 files changed, 84 insertions(+), 1 deletion(-) diff --git a/src/agents/skills-status.ts b/src/agents/skills-status.ts index 1444f9d2d..e2c6067a3 100644 --- a/src/agents/skills-status.ts +++ b/src/agents/skills-status.ts @@ -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, diff --git a/src/agents/skills/frontmatter.test.ts b/src/agents/skills/frontmatter.test.ts index 82a9cdd75..7aaa0d58a 100644 --- a/src/agents/skills/frontmatter.test.ts +++ b/src/agents/skills/frontmatter.test.ts @@ -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(); + }); +}); diff --git a/src/agents/skills/frontmatter.ts b/src/agents/skills/frontmatter.ts index 5a2c42382..4470a706e 100644 --- a/src/agents/skills/frontmatter.ts +++ b/src/agents/skills/frontmatter.ts @@ -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, diff --git a/src/agents/skills/types.ts b/src/agents/skills/types.ts index 1cb3a2684..d22aec1a7 100644 --- a/src/agents/skills/types.ts +++ b/src/agents/skills/types.ts @@ -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[]; diff --git a/ui/src/ui/types.ts b/ui/src/ui/types.ts index 1a5ec0731..b70213917 100644 --- a/ui/src/ui/types.ts +++ b/ui/src/ui/types.ts @@ -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; diff --git a/ui/src/ui/views/skills.ts b/ui/src/ui/views/skills.ts index cfc024cb1..f0e600dbf 100644 --- a/ui/src/ui/views/skills.ts +++ b/ui/src/ui/views/skills.ts @@ -101,6 +101,25 @@ function renderSkill(skill: SkillStatusEntry, props: SkillsProps) { ${skill.disabled ? html`disabled` : nothing} + ${skill.repository + ? html` +
+ + + + + Source + +
+ ` + : nothing} ${missing.length > 0 ? html`