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
40 changes: 40 additions & 0 deletions crates/agent-core/src/anthropic-skills-index.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "anthropic",
"repository": "https://github.com/anthropics/skills",
"revision": "3b3fad96af16a10759d930941b4520ba0c40edae",
"skills": [
{ "id": "academy-guide", "path": "skills/academy-guide/SKILL.md" },
{ "id": "algorithmic-art", "path": "skills/algorithmic-art/SKILL.md" },
{ "id": "brand-guidelines", "path": "skills/brand-guidelines/SKILL.md" },
{ "id": "canvas-design", "path": "skills/canvas-design/SKILL.md" },
{ "id": "claude-api", "path": "skills/claude-api/SKILL.md" },
{ "id": "discernment-nudge", "path": "skills/discernment-nudge/SKILL.md" },
{ "id": "doc-coauthoring", "path": "skills/doc-coauthoring/SKILL.md" },
{ "id": "frontend-design", "path": "skills/frontend-design/SKILL.md" },
{ "id": "internal-comms", "path": "skills/internal-comms/SKILL.md" },
{ "id": "mcp-builder", "path": "skills/mcp-builder/SKILL.md" },
{ "id": "skill-creator", "path": "skills/skill-creator/SKILL.md" },
{ "id": "slack-gif-creator", "path": "skills/slack-gif-creator/SKILL.md" },
{ "id": "theme-factory", "path": "skills/theme-factory/SKILL.md" },
{ "id": "web-artifacts-builder", "path": "skills/web-artifacts-builder/SKILL.md" },
{ "id": "webapp-testing", "path": "skills/webapp-testing/SKILL.md" }
],
"excluded": [
{
"id": "docx",
"reason": "source-available document skill; not redistributed by JuCode"
},
{
"id": "pdf",
"reason": "source-available document skill; not redistributed by JuCode"
},
{
"id": "pptx",
"reason": "source-available document skill; not redistributed by JuCode"
},
{
"id": "xlsx",
"reason": "source-available document skill; not redistributed by JuCode"
}
]
}
36 changes: 36 additions & 0 deletions crates/agent-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,9 @@ pub struct Config {
/// (`enable_browser_open` in config.json, default true). It is only ever
/// exposed when running under JuCode Desktop (JUCODE_DESKTOP set).
pub enable_browser_open: bool,
/// Optional additional GitHub skill repository. "anthropic" selects the
/// pinned built-in index for https://github.com/anthropics/skills.
pub extra_skills_source: Option<String>,
pub extensions: Vec<ExtensionConfig>,
pub mcp_servers: Vec<McpServerConfig>,
path: PathBuf,
Expand Down Expand Up @@ -333,6 +336,7 @@ impl Config {
approval_mode: ApprovalMode::default(),
edit_tools: default_edit_tools(),
enable_browser_open: true,
extra_skills_source: None,
extensions: Vec::new(),
mcp_servers: Vec::new(),
path,
Expand Down Expand Up @@ -415,6 +419,7 @@ impl Config {
approval_mode: read_approval_mode(&value)?,
edit_tools: read_edit_tools(&value)?,
enable_browser_open: read_bool(&value, "enable_browser_open", true),
extra_skills_source: read_optional_string(&value, "extra_skills_source"),
extensions: read_extensions(&value),
mcp_servers: read_mcp_servers(&value),
path,
Expand Down Expand Up @@ -449,6 +454,7 @@ impl Config {
"approval_mode": self.approval_mode.as_str(),
"edit_tools": self.edit_tools,
"enable_browser_open": self.enable_browser_open,
"extra_skills_source": self.extra_skills_source,
"extensions": self.extensions.iter().map(extension_config_value).collect::<Vec<_>>(),
"mcp_servers": self.mcp_servers.iter().map(mcp_server_config_value).collect::<Vec<_>>()
});
Expand Down Expand Up @@ -605,6 +611,15 @@ fn read_string(value: &Value, key: &str, default: &str) -> String {
.to_string()
}

fn read_optional_string(value: &Value, key: &str) -> Option<String> {
value
.get(key)
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string)
}

fn read_usize(value: &Value, key: &str, default: usize) -> usize {
value
.get(key)
Expand Down Expand Up @@ -1309,6 +1324,7 @@ mod tests {
approval_mode: ApprovalMode::default(),
edit_tools: default_edit_tools(),
enable_browser_open: true,
extra_skills_source: None,
extensions: Vec::new(),
mcp_servers: Vec::new(),
path: PathBuf::from("config.json"),
Expand Down Expand Up @@ -1536,6 +1552,26 @@ mod tests {
assert!(error.to_string().contains("array"));
}

#[test]
fn extra_skills_source_is_optional_and_trimmed() {
assert_eq!(
read_optional_string(&json!({}), "extra_skills_source"),
None
);
assert_eq!(
read_optional_string(
&json!({ "extra_skills_source": " anthropic " }),
"extra_skills_source"
)
.as_deref(),
Some("anthropic")
);
assert_eq!(
read_optional_string(&json!({ "extra_skills_source": "" }), "extra_skills_source"),
None
);
}

#[test]
fn canonical_edit_tool_name_covers_aliases_and_rejects_others() {
assert_eq!(canonical_edit_tool_name("edit"), Some("str_replace"));
Expand Down
122 changes: 100 additions & 22 deletions crates/agent-core/src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -712,11 +712,9 @@ impl AgentCore {
Err(error) => format!("Project skills: failed to read ({error})"),
}
};
match self.fetch_marketplace() {
let marketplace = match self.fetch_marketplace() {
Ok(marketplace) if marketplace.skills.is_empty() => {
vec![AgentEvent::Info(format!(
"{installed}\n\n{project}\n\nSkills marketplace is empty"
))]
"Source: JuCode marketplace\nNo skills available".to_string()
}
Ok(marketplace) => {
let defaults = marketplace
Expand All @@ -733,15 +731,49 @@ impl AgentCore {
};
lines.push(format!("{}{} — {}", skill.id, marker, skill.description));
}
vec![AgentEvent::Info(format!(
"{installed}\n\n{project}\n\nAvailable marketplace skills:\n{}\n\nInstall with /skills install <id>; update with /skills update <id>; sync defaults with /skills sync.",
lines.join("\n")
))]
format!("Source: JuCode marketplace\n{}", lines.join("\n"))
}
Err(error) => vec![AgentEvent::Info(format!(
"{installed}\n\n{project}\n\nMarketplace unavailable: {error}"
))],
}
Err(error) => format!("Source: JuCode marketplace (unavailable: {error})"),
};
let extra = match self.fetch_extra_skill_source() {
Ok(Some(source)) => {
let mut lines = source
.skills
.iter()
.map(|skill| skill.id.clone())
.collect::<Vec<_>>();
if !source.excluded.is_empty() {
lines.push(format!(
"Not offered: {}",
source
.excluded
.iter()
.map(|skill| format!("{} ({})", skill.id, skill.reason))
.collect::<Vec<_>>()
.join(", ")
));
}
Some(format!(
"Source: {} ({})\n{}",
source.name,
source.repository,
if lines.is_empty() {
"No skills available".to_string()
} else {
lines.join("\n")
}
))
}
Ok(None) => None,
Err(error) => Some(format!("Extra skills source unavailable: {error}")),
};
let mut sections = vec![installed, project, marketplace];
sections.extend(extra);
sections.push(
"Install with /skills install <id>; update with /skills update <id>; sync JuCode defaults with /skills sync."
.to_string(),
);
vec![AgentEvent::Info(sections.join("\n\n"))]
}

fn install_marketplace_skill_events(&mut self, id: &str, verb: &str) -> Vec<AgentEvent> {
Expand All @@ -750,26 +782,63 @@ impl AgentCore {
"installed skill not found: {id}"
))];
}
match self.fetch_marketplace() {
Ok(marketplace) => {
let Some(skill) = marketplace.skills.iter().find(|skill| skill.id == id) else {
return vec![AgentEvent::Error(format!(
"marketplace skill not found: {id}"
))];
};
match skills::install_marketplace_skill(self.config.profile_dir(), skill) {
let marketplace = self.fetch_marketplace();
if let Ok(marketplace) = &marketplace {
if let Some(skill) = marketplace.skills.iter().find(|skill| skill.id == id) {
return match skills::install_marketplace_skill(self.config.profile_dir(), skill) {
Ok(()) => vec![
AgentEvent::Status(format!("{verb} skill {}", skill.id)),
AgentEvent::Status(format!(
"{verb} skill {} from JuCode marketplace",
skill.id
)),
self.command_list_event(),
],
Err(error) => vec![AgentEvent::Error(format!(
"failed to install skill {}: {error}",
skill.id
))],
};
}
}
match self.fetch_extra_skill_source() {
Ok(Some(source)) => {
if let Some(skill) = source.skills.iter().find(|skill| skill.id == id) {
return match skills::install_extra_skill(
self.config.profile_dir(),
&source,
skill,
) {
Ok(()) => vec![
AgentEvent::Status(format!(
"{verb} skill {} from {}",
skill.id, source.name
)),
self.command_list_event(),
],
Err(error) => vec![AgentEvent::Error(format!(
"failed to install skill {} from {}: {error}",
skill.id, source.name
))],
};
}
if let Some(excluded) = source.excluded.iter().find(|skill| skill.id == id) {
return vec![AgentEvent::Error(format!(
"skill {} is not offered by {}: {}",
excluded.id, source.name, excluded.reason
))];
}
}
Ok(None) => {}
Err(error) => {
return vec![AgentEvent::Error(format!(
"failed to load extra skills source: {error}"
))];
}
}
match marketplace {
Ok(_) => vec![AgentEvent::Error(format!("skill not found in configured sources: {id}"))],
Err(error) => vec![AgentEvent::Error(format!(
"failed to fetch skills marketplace: {error}"
"skill not found in configured extra source and JuCode marketplace is unavailable: {error}"
))],
}
}
Expand Down Expand Up @@ -836,6 +905,15 @@ impl AgentCore {
skills::fetch_marketplace(&self.config.jucode_api_url, self.auth.jucode_access_token())
}

fn fetch_extra_skill_source(&self) -> Result<Option<skills::ExtraSkillSource>, String> {
skills::fetch_extra_skill_source(
self.config
.extra_skills_source
.as_deref()
.unwrap_or_default(),
)
}

/// Returns the bearer token for the active provider: the JuCode OAuth
/// access token for the jucode provider, otherwise the raw provider key.
fn provider_api_key(&self) -> Option<&str> {
Expand Down
Loading
Loading