pub const HOOK_NAMES: &[&str] = &[
"on-init",
"on-prompt",
"on-response",
"on-turn-start",
"on-turn-end",
"on-message-update",
"on-tool-start",
"on-tool-end",
"on-error",
"on-complete",
"prepare-next-run",
"before-agent-start",
"message-end",
"transform-context",
"on-before-compact",
"on-compact",
];
pub fn filter_existing_dirs(candidates: &[std::path::PathBuf]) -> Vec<std::path::PathBuf> {
candidates.iter().filter(|p| p.is_dir()).cloned().collect()
}
#[derive(Debug, Clone)]
pub struct LoadedPlugin {
#[cfg_attr(not(feature = "plugin"), allow(dead_code))]
pub stem: String,
#[cfg_attr(not(feature = "plugin"), allow(dead_code))]
pub files: Vec<std::path::PathBuf>,
#[cfg_attr(not(feature = "plugin"), allow(dead_code))]
pub hooks_registered: Vec<String>,
}
pub fn load_plugin(
mgr: &mut super::PluginManager,
path: &std::path::Path,
) -> Result<LoadedPlugin, String> {
let (stem, files) = if path.is_dir() {
let dir_name = path
.file_name()
.and_then(|s| s.to_str())
.ok_or_else(|| format!("plugin dir has no name: {}", path.display()))?
.to_string();
let mut janet_files: Vec<std::path::PathBuf> = std::fs::read_dir(path)
.map_err(|e| format!("cannot read plugin dir {}: {}", path.display(), e))?
.filter_map(|e| e.ok().map(|x| x.path()))
.filter(|p| p.is_file() && p.extension().is_some_and(|ext| ext == "janet"))
.collect();
janet_files.sort();
if janet_files.is_empty() {
return Err(format!(
"plugin dir {} contains no .janet files",
path.display()
));
}
(dir_name, janet_files)
} else {
let stem = path
.file_stem()
.and_then(|s| s.to_str())
.ok_or_else(|| format!("plugin file has no stem: {}", path.display()))?
.to_string();
(stem, vec![path.to_path_buf()])
};
for file in &files {
mgr.load_file(file)
.map_err(|e| format!("failed to load {}: {}", file.display(), e))?;
}
let mut hooks_registered = Vec::new();
for hook in HOOK_NAMES {
let prefixed = format!("{}-{}", stem, hook);
let escaped_hook = super::escape_janet_string(hook);
let escaped_prefixed = super::escape_janet_string(&prefixed);
let alias_code = format!(
r#"(let [env (curenv)
bare-sym (symbol "{bare}")
prefixed-sym (symbol "{prefixed}")
bare-entry (get env bare-sym)]
(when (and bare-entry (not (get env prefixed-sym)))
(put env prefixed-sym bare-entry)))"#,
bare = escaped_hook,
prefixed = escaped_prefixed,
);
let _ = mgr.eval(&alias_code);
if mgr.has_symbol(&prefixed) {
mgr.register(hook, &prefixed);
hooks_registered.push(hook.to_string());
}
}
let loaded = LoadedPlugin {
stem,
files,
hooks_registered,
};
mgr.push_loaded_plugin(loaded.clone());
Ok(loaded)
}