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
37 changes: 37 additions & 0 deletions crates/hk-core/src/adapter/claude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,11 @@ impl AgentAdapter for ClaudeAdapter {
return vec![];
};

// Map marketplace name → upstream "owner/repo" from the agent's own
// catalog, so each plugin is attributed to its real source instead of
// the `.git` its cache dir happens to sit under.
let marketplace_repo = read_marketplace_repos(&self.base_dir().join("plugins"));

// Also read enabledPlugins from settings.json to know which are enabled
let enabled_set: std::collections::HashSet<String> = self
.read_settings()
Expand Down Expand Up @@ -321,11 +326,16 @@ impl AgentAdapter for ClaudeAdapter {
.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
.map(|dt| dt.with_timezone(&chrono::Utc));

let source_url = marketplace_repo
.get(&source)
.map(|repo| format!("https://github.com/{repo}"));

entries.push(PluginEntry {
name,
source: source.clone(),
enabled: enabled_set.contains(key),
path: install_path,
source_url,
uri: None,
installed_at,
updated_at,
Expand All @@ -335,6 +345,33 @@ impl AgentAdapter for ClaudeAdapter {
}
}

/// Parse `<plugins_dir>/known_marketplaces.json` into a `marketplace name →
/// "owner/repo"` map. Empty when the catalog is missing or unreadable.
fn read_marketplace_repos(plugins_dir: &Path) -> std::collections::HashMap<String, String> {
let path = plugins_dir.join("known_marketplaces.json");
let Ok(content) = std::fs::read_to_string(&path) else {
return std::collections::HashMap::new();
};
let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) else {
return std::collections::HashMap::new();
};
let Some(obj) = json.as_object() else {
return std::collections::HashMap::new();
};
obj.iter()
.filter_map(|(name, entry)| {
let src = entry.get("source")?;
// Only github marketplaces map cleanly to a github.com/<repo> URL;
// skip others (gitlab, local, …) so we don't fabricate a wrong URL.
if src.get("source").and_then(|v| v.as_str()) != Some("github") {
return None;
}
let repo = src.get("repo")?.as_str()?.to_string();
Some((name.clone(), repo))
})
.collect()
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
1 change: 1 addition & 0 deletions crates/hk-core/src/adapter/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,7 @@ impl AgentAdapter for CodexAdapter {
enabled: !disabled_plugins
.contains(&format!("{}@{}", name, &marketplace_name)),
path: Some(version_dir.path().to_path_buf()), // version level — matches manifest location
source_url: None,
uri: None,
installed_at: None,
updated_at: None,
Expand Down
2 changes: 2 additions & 0 deletions crates/hk-core/src/adapter/copilot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ impl AgentAdapter for CopilotAdapter {
source: mp_name.clone(),
enabled: true,
path: Some(plugin.path()),
source_url: None,
uri: None,
installed_at: None,
updated_at: None,
Expand Down Expand Up @@ -295,6 +296,7 @@ impl AgentAdapter for CopilotAdapter {
source: marketplace.to_string(),
enabled,
path: Some(plugin_path),
source_url: None,
uri: Some(plugin_uri.to_string()),
installed_at: None,
updated_at: None,
Expand Down
2 changes: 2 additions & 0 deletions crates/hk-core/src/adapter/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ impl AgentAdapter for CursorAdapter {
source: "local".into(),
enabled: true,
path: Some(dir.path()),
source_url: None,
uri: None,
installed_at: None,
updated_at: None,
Expand Down Expand Up @@ -273,6 +274,7 @@ impl AgentAdapter for CursorAdapter {
source: mp_name.clone(),
enabled: true,
path: Some(plugin.path()),
source_url: None,
uri: None,
installed_at: None,
updated_at: None,
Expand Down
1 change: 1 addition & 0 deletions crates/hk-core/src/adapter/gemini.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ impl AgentAdapter for GeminiAdapter {
source: "gemini".into(),
enabled,
path: Some(dir.path()),
source_url: None,
uri: None,
installed_at: None,
updated_at: None,
Expand Down
1 change: 1 addition & 0 deletions crates/hk-core/src/adapter/hermes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ impl HermesAdapter {
name,
source: "hermes".into(),
path: dir,
source_url: None,
uri: None,
installed_at: None,
updated_at: None,
Expand Down
5 changes: 5 additions & 0 deletions crates/hk-core/src/adapter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ pub struct PluginEntry {
pub source: String,
pub enabled: bool,
pub path: Option<std::path::PathBuf>,
/// Authoritative upstream URL resolved from the agent's own plugin manifest
/// (e.g. Claude's marketplace → repo mapping). When set, it overrides the
/// `.git`-walk source detection, which mis-attributes plugins cached inside
/// a dotfiles repo. `None` for agents without such a manifest.
pub source_url: Option<String>,
/// Agent-specific URI for the plugin (e.g. VS Code pluginUri "file:///...").
/// Used by toggle to identify the plugin in the agent's state store.
pub uri: Option<String>,
Expand Down
1 change: 1 addition & 0 deletions crates/hk-core/src/adapter/opencode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ impl AgentAdapter for OpencodeAdapter {
source: "local".into(),
enabled,
path: Some(path),
source_url: None,
uri: None,
installed_at: None,
updated_at: None,
Expand Down
2 changes: 2 additions & 0 deletions crates/hk-core/src/auditor/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -998,6 +998,7 @@ mod tests {
url: None,
version: None,
commit_hash: None,
from_manifest: false,
},
file_path: "SKILL.md".into(),
mcp_command: None,
Expand All @@ -1024,6 +1025,7 @@ mod tests {
url: None,
version: None,
commit_hash: None,
from_manifest: false,
},
file_path: "config.json".into(),
mcp_command: Some(command.into()),
Expand Down
3 changes: 2 additions & 1 deletion crates/hk-core/src/kits/tests/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1120,7 +1120,7 @@ fn make_skill_ext(
kind: ExtensionKind::Skill,
name: name.into(),
description: String::new(),
source: Source { origin, url: url.map(String::from), version: None, commit_hash: None },
source: Source { origin, url: url.map(String::from), version: None, commit_hash: None, from_manifest: false },
agents: vec![agent.into()],
tags: vec![],
pack: None,
Expand Down Expand Up @@ -1301,6 +1301,7 @@ fn insert_kit_asset(
url: None,
version: None,
commit_hash: None,
from_manifest: false,
},
agents: vec!["claude".into()],
tags: vec![],
Expand Down
1 change: 1 addition & 0 deletions crates/hk-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub mod models;
pub mod sanitize;
pub mod scanner;
pub mod service;
pub mod skills_cli;
pub mod store;

pub use error::HkError;
5 changes: 5 additions & 0 deletions crates/hk-core/src/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1138,6 +1138,7 @@ mod tests {
url: None,
version: None,
commit_hash: None,
from_manifest: false,
},
agents: vec!["claude".into()],
tags: vec![],
Expand Down Expand Up @@ -1184,6 +1185,7 @@ mod tests {
url: None,
version: None,
commit_hash: None,
from_manifest: false,
},
agents: vec!["claude".into()],
tags: vec![],
Expand Down Expand Up @@ -1245,6 +1247,7 @@ mod tests {
url: None,
version: None,
commit_hash: None,
from_manifest: false,
},
agents: vec!["claude".into()],
tags: vec![],
Expand Down Expand Up @@ -1896,6 +1899,7 @@ mod tests {
url: None,
version: None,
commit_hash: None,
from_manifest: false,
},
agents: vec!["claude".into()],
tags: vec![],
Expand Down Expand Up @@ -2394,6 +2398,7 @@ mod tests {
url: None,
version: None,
commit_hash: None,
from_manifest: false,
},
agents: vec!["claude".into()],
tags: vec![],
Expand Down
10 changes: 10 additions & 0 deletions crates/hk-core/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,16 @@ pub struct Source {
pub url: Option<String>,
pub version: Option<String>,
pub commit_hash: Option<String>,
/// True when `url` was read from an authoritative install manifest — the
/// `skills` CLI `.skill-lock.json` for skills, or a plugin marketplace map —
/// rather than inferred from the nearest enclosing `.git`. Only a
/// manifest-derived source is trusted to correct a stored install record, so
/// an HK-git-installed extension that merely sits under a dotfiles repo is
/// never re-attributed to it. For skills it also marks the row as externally
/// managed, which routes its update to the `skills` CLI (see
/// `service::is_externally_managed`).
#[serde(default)]
pub from_manifest: bool,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
Expand Down
Loading
Loading