Skip to content

Commit f210688

Browse files
authored
Merge pull request #17 from RealZST/fix/extension-management
fix: plugin toggle, delete cleanup, and Gemini native extension support
2 parents 2e42207 + 38ff097 commit f210688

23 files changed

Lines changed: 2707 additions & 201 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/target
22
/dist
33
/node_modules
4+
/social
45
*.swp
56
*.swo
67
.DS_Store

crates/hk-core/src/adapter/claude.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,7 @@ impl AgentAdapter for ClaudeAdapter {
325325
source: source.clone(),
326326
enabled: enabled_set.contains(key),
327327
path: install_path,
328+
uri: None,
328329
installed_at,
329330
updated_at,
330331
});

crates/hk-core/src/adapter/codex.rs

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,23 @@ impl AgentAdapter for CodexAdapter {
203203
}
204204

205205
fn read_plugins(&self) -> Vec<PluginEntry> {
206+
// Read disabled plugins from config.toml [plugins."name@source"] enabled = false
207+
let disabled_plugins: std::collections::HashSet<String> = std::fs::read_to_string(self.mcp_config_path()).ok()
208+
.and_then(|content| content.parse::<toml::Table>().ok())
209+
.and_then(|doc| doc.get("plugins").and_then(|v| v.as_table()).cloned())
210+
.map(|plugins| {
211+
plugins.into_iter()
212+
.filter(|(_, v)| {
213+
v.as_table()
214+
.and_then(|t| t.get("enabled"))
215+
.and_then(|e| e.as_bool())
216+
== Some(false)
217+
})
218+
.map(|(k, _)| k)
219+
.collect()
220+
})
221+
.unwrap_or_default();
222+
206223
// Codex plugins are cached at ~/.codex/plugins/cache/{marketplace}/{plugin}/{version}/
207224
// Each has .codex-plugin/plugin.json manifest
208225
let cache_dir = self.base_dir().join("plugins").join("cache");
@@ -261,10 +278,11 @@ impl AgentAdapter for CodexAdapter {
261278
plugin_name.clone()
262279
};
263280
entries.push(PluginEntry {
264-
name,
281+
name: name.clone(),
265282
source: marketplace_name.clone(),
266-
enabled: true,
283+
enabled: !disabled_plugins.contains(&format!("{}@{}", name, &marketplace_name)),
267284
path: Some(version_dir.path().to_path_buf()), // version level — matches manifest location
285+
uri: None,
268286
installed_at: None,
269287
updated_at: None,
270288
});
@@ -408,6 +426,33 @@ mod tests {
408426
assert!(plugins.is_empty());
409427
}
410428

429+
#[test]
430+
fn read_plugins_respects_config_toml_enabled() {
431+
let tmp = tempfile::tempdir().unwrap();
432+
let adapter = CodexAdapter::with_home(tmp.path().to_path_buf());
433+
434+
// Set up a plugin in cache
435+
let plugin_dir = tmp.path().join(".codex/plugins/cache/test-marketplace/my-plugin/1.0.0/.codex-plugin");
436+
fs::create_dir_all(&plugin_dir).unwrap();
437+
fs::write(plugin_dir.join("plugin.json"), r#"{"name":"my-plugin"}"#).unwrap();
438+
439+
// No config.toml → plugin should be enabled (default)
440+
let entries = adapter.read_plugins();
441+
assert_eq!(entries.len(), 1);
442+
assert!(entries[0].enabled);
443+
444+
// config.toml with plugin disabled
445+
let config_path = tmp.path().join(".codex/config.toml");
446+
fs::write(&config_path, r#"
447+
[plugins."my-plugin@test-marketplace"]
448+
enabled = false
449+
"#).unwrap();
450+
451+
let entries = adapter.read_plugins();
452+
assert_eq!(entries.len(), 1);
453+
assert!(!entries[0].enabled, "Plugin should be disabled per config.toml");
454+
}
455+
411456
#[test]
412457
fn read_hooks_object_format() {
413458
let tmp = tempfile::tempdir().unwrap();

crates/hk-core/src/adapter/copilot.rs

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,40 @@
1616
// Events: SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, PreCompact, SubagentStart, SubagentStop, Stop
1717

1818
use super::{AgentAdapter, HookEntry, HookFormat, McpFormat, McpServerEntry, PluginEntry};
19-
use std::path::PathBuf;
19+
use std::path::{Path, PathBuf};
20+
21+
/// Read VS Code agent plugin enablement from state.vscdb.
22+
/// Returns the set of plugin URIs that are disabled (enabled = false).
23+
/// Gracefully returns empty set on any error (DB not found, locked, schema change).
24+
fn read_vscode_disabled_plugins(vscode_user_dir: &Path) -> std::collections::HashSet<String> {
25+
let db_path = vscode_user_dir
26+
.join("globalStorage")
27+
.join("state.vscdb");
28+
let mut disabled = std::collections::HashSet::new();
29+
let Ok(conn) = rusqlite::Connection::open_with_flags(
30+
&db_path,
31+
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX,
32+
) else {
33+
return disabled;
34+
};
35+
let Ok(value): Result<String, _> = conn.query_row(
36+
"SELECT value FROM ItemTable WHERE key = 'agentPlugins.enablement'",
37+
[],
38+
|row| row.get(0),
39+
) else {
40+
return disabled;
41+
};
42+
// Format: [["file:///path/to/plugin", true/false], ...]
43+
let Ok(entries) = serde_json::from_str::<Vec<(String, bool)>>(&value) else {
44+
return disabled;
45+
};
46+
for (uri, enabled) in entries {
47+
if !enabled {
48+
disabled.insert(uri);
49+
}
50+
}
51+
disabled
52+
}
2053

2154
pub struct CopilotAdapter {
2255
home: PathBuf,
@@ -67,6 +100,9 @@ impl AgentAdapter for CopilotAdapter {
67100
fn detect(&self) -> bool {
68101
self.base_dir().exists()
69102
}
103+
fn vscode_user_dir(&self) -> Option<PathBuf> {
104+
Some(self.vscode_user_dir())
105+
}
70106
fn skill_dirs(&self) -> Vec<PathBuf> {
71107
vec![
72108
self.base_dir().join("skills"),
@@ -146,6 +182,7 @@ impl AgentAdapter for CopilotAdapter {
146182
let mut entries = Vec::new();
147183

148184
// 1. Copilot CLI plugins: ~/.copilot/installed-plugins/{marketplace}/{plugin}/
185+
// CLI has no native enable/disable — always enabled if present.
149186
let cli_base = self.base_dir().join("installed-plugins");
150187
if let Ok(marketplaces) = std::fs::read_dir(&cli_base) {
151188
for marketplace in marketplaces.flatten() {
@@ -169,6 +206,7 @@ impl AgentAdapter for CopilotAdapter {
169206
source: mp_name.clone(),
170207
enabled: true,
171208
path: Some(plugin.path()),
209+
uri: None,
172210
installed_at: None,
173211
updated_at: None,
174212
});
@@ -178,8 +216,14 @@ impl AgentAdapter for CopilotAdapter {
178216

179217
// 2. VS Code agent plugins: ~/.vscode/agent-plugins/installed.json
180218
// Each entry has pluginUri pointing to plugins/{name}/ with .github/plugin/plugin.json
219+
// Enable/disable state stored in VS Code's state.vscdb under "agentPlugins.enablement".
181220
let vscode_base = self.home.join(".vscode").join("agent-plugins");
182221
let installed_json = vscode_base.join("installed.json");
222+
223+
// Read VS Code agent plugin enablement state from state.vscdb
224+
// Format: [["file:///path/to/plugin", true/false], ...]
225+
let disabled_uris = read_vscode_disabled_plugins(&self.vscode_user_dir());
226+
183227
if let Ok(content) = std::fs::read_to_string(&installed_json)
184228
&& let Ok(registry) = serde_json::from_str::<serde_json::Value>(&content)
185229
&& let Some(installed) = registry.get("installed").and_then(|v| v.as_array())
@@ -206,11 +250,13 @@ impl AgentAdapter for CopilotAdapter {
206250
.map(|n| n.to_string_lossy().to_string())
207251
.unwrap_or_else(|| marketplace.to_string())
208252
});
253+
let enabled = !disabled_uris.contains(plugin_uri);
209254
entries.push(PluginEntry {
210255
name,
211256
source: marketplace.to_string(),
212-
enabled: true,
257+
enabled,
213258
path: Some(plugin_path),
259+
uri: Some(plugin_uri.to_string()),
214260
installed_at: None,
215261
updated_at: None,
216262
});

crates/hk-core/src/adapter/cursor.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ impl AgentAdapter for CursorAdapter {
206206
source: "local".into(),
207207
enabled: true,
208208
path: Some(dir.path()),
209+
uri: None,
209210
installed_at: None,
210211
updated_at: None,
211212
});
@@ -242,6 +243,7 @@ impl AgentAdapter for CursorAdapter {
242243
source: mp_name.clone(),
243244
enabled: true,
244245
path: Some(plugin.path()),
246+
uri: None,
245247
installed_at: None,
246248
updated_at: None,
247249
});

0 commit comments

Comments
 (0)