diff --git a/src/agents/skills-status.ts b/src/agents/skills-status.ts index be949b757..ee369595e 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,9 @@ 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 +242,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 2f3ae5b24..27a1eee25 100644 --- a/src/agents/skills/frontmatter.ts +++ b/src/agents/skills/frontmatter.ts @@ -104,6 +104,7 @@ export function resolveOpenClawMetadata( 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 b518d4bb6..c3464e7f8 100644 --- a/src/agents/skills/types.ts +++ b/src/agents/skills/types.ts @@ -22,6 +22,8 @@ export type OpenClawSkillMetadata = { 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/src/cli/skills-cli.ts b/src/cli/skills-cli.ts index da10f6029..936199d2b 100644 --- a/src/cli/skills-cli.ts +++ b/src/cli/skills-cli.ts @@ -84,6 +84,7 @@ export function formatSkillsList(report: SkillStatusReport, opts: SkillsListOpti source: s.source, primaryEnv: s.primaryEnv, homepage: s.homepage, + repository: s.repository, missing: s.missing, })), }; @@ -181,6 +182,9 @@ export function formatSkillInfo( if (skill.homepage) { lines.push(`${theme.muted(" Homepage:")} ${skill.homepage}`); } + if (skill.repository) { + lines.push(`${theme.muted(" Repository:")} ${skill.repository}`); + } if (skill.primaryEnv) { lines.push(`${theme.muted(" Primary env:")} ${skill.primaryEnv}`); } 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`