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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,7 @@ pup skills install codex
pup skills install opencode
pup skills install pi
pup skills install devin
pup skills install hermes

# Install for every supported platform at once
pup skills install all
Expand All @@ -662,7 +663,7 @@ pup skills list --type=agent
pup skills install claude --name dd-monitors
```

For Claude Code, skills install to `~/.claude/skills/` (or `.claude/skills/` with `--project`) and agents install to `~/.claude/agents/` (native subagent format). If the `CLAUDE_CONFIG_DIR` environment variable is set, user-scope installs go to `$CLAUDE_CONFIG_DIR/skills/` and `$CLAUDE_CONFIG_DIR/agents/` instead of `~/.claude/`. For Cursor, Codex, opencode, and Devin, everything installs as `SKILL.md` under that tool's skills directory (e.g. `~/.cursor/skills/`, `~/.codex/skills/`, `~/.config/opencode/skills/`, and Devin's `~/.agents/skills/` — or `.agents/skills/` with `--project`).
For Claude Code, skills install to `~/.claude/skills/` (or `.claude/skills/` with `--project`) and agents install to `~/.claude/agents/` (native subagent format). If the `CLAUDE_CONFIG_DIR` environment variable is set, user-scope installs go to `$CLAUDE_CONFIG_DIR/skills/` and `$CLAUDE_CONFIG_DIR/agents/` instead of `~/.claude/`. For Cursor, Codex, opencode, Devin, and Hermes, everything installs as `SKILL.md` under that tool's skills directory (e.g. `~/.cursor/skills/`, `~/.codex/skills/`, `~/.config/opencode/skills/`, Devin's `~/.agents/skills/` — or `.agents/skills/` with `--project` — and Hermes Agent's `~/.hermes/skills/` — or `.hermes/skills/` with `--project`; Hermes only loads project-local skills from repos trusted via `hermes skills trust`).

Pup ships plugin manifest files for several AI coding assistants:

Expand Down
2 changes: 1 addition & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ pup/v0.1.0 (rust; os darwin; arch arm64; ai-agent claude-code) # With agent
**AI Agent Detection** (`src/useragent.rs`):

Table-driven registry detecting AI coding agents via environment variables. First match wins:
- Claude Code (`CLAUDECODE`, `CLAUDE_CODE`), Cursor (`CURSOR_AGENT`; or presence of `CURSOR_TRACE_ID`), Codex (`CODEX`, `OPENAI_CODEX`; or presence of `CODEX_SESSION_ID`/`CODEX_THREAD_ID`/`CODEX_VERSION`/`CODEX_SANDBOX`/`CODEX_CI`, which Codex injects into shell commands), OpenCode (`OPENCODE`), Aider (`AIDER`), Cline (`CLINE`), Windsurf (`WINDSURF_AGENT`), GitHub Copilot (`GITHUB_COPILOT`, `COPILOT_CLI`; or presence of `COPILOT_AGENT_SESSION_ID`), Amazon Q (`AMAZON_Q`, `AWS_Q_DEVELOPER`), Gemini Code Assist (`GEMINI_CODE_ASSIST`), Gemini CLI (`GEMINI_CLI`), Sourcegraph Cody (`SRC_CODY`), pi.dev (`PI_CODING_AGENT`), Devin (presence of `DEVIN_SESSION_ID`), Generic Agent (`AGENT`)
- Claude Code (`CLAUDECODE`, `CLAUDE_CODE`), Cursor (`CURSOR_AGENT`; or presence of `CURSOR_TRACE_ID`), Codex (`CODEX`, `OPENAI_CODEX`; or presence of `CODEX_SESSION_ID`/`CODEX_THREAD_ID`/`CODEX_VERSION`/`CODEX_SANDBOX`/`CODEX_CI`, which Codex injects into shell commands), OpenCode (`OPENCODE`), Aider (`AIDER`), Cline (`CLINE`), Windsurf (`WINDSURF_AGENT`), GitHub Copilot (`GITHUB_COPILOT`, `COPILOT_CLI`; or presence of `COPILOT_AGENT_SESSION_ID`), Amazon Q (`AMAZON_Q`, `AWS_Q_DEVELOPER`), Gemini Code Assist (`GEMINI_CODE_ASSIST`), Gemini CLI (`GEMINI_CLI`), Sourcegraph Cody (`SRC_CODY`), pi.dev (`PI_CODING_AGENT`), Devin (presence of `DEVIN_SESSION_ID`), Hermes Agent (`HERMES_AGENT`, which Hermes exports into shell commands), Generic Agent (`AGENT`)

Detection adds the `ai-agent <name>` token to the User-Agent header and auto-approves CLI prompts (`cfg.auto_approve`, also set in the extension pre-parse path so extension children get `PUP_AUTO_APPROVE`). It does not change output, help, or colors: agents get the same JSON envelope and text help as humans. The legacy `--no-agent` flag opts out of agent auto-approval; `--agent` is a hidden no-op.

Expand Down
2 changes: 1 addition & 1 deletion docs/COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ pup <domain> <subgroup> <action> [options] # Nested commands
| code-coverage | branch-summary, commit-summary | src/commands/code_coverage.rs | ✅ |
| hamr | connections (get, create) | src/commands/hamr.rs | ✅ |
| fleet | agents (list, get, versions, tracers), deployments (list, get, configure, upgrade, cancel), schedules (list, get, create, update, delete, trigger), tracers (list), clusters (list), instrumented-pods (list) | src/commands/fleet.rs | ✅ |
| skills | list, install, path (positional `<platform>`: claude/cursor/codex/opencode/windsurf/gemini/pi/devin/all; `--name`, `--type`, `--project` for project-local scope; bundled skills include supplementary references) | src/commands/skills.rs | ✅ |
| skills | list, install, path (positional `<platform>`: claude/cursor/codex/opencode/windsurf/gemini/pi/devin/hermes/all; `--name`, `--type`, `--project` for project-local scope; bundled skills include supplementary references) | src/commands/skills.rs | ✅ |
| runbooks | list, describe, run, import, validate | src/commands/runbooks.rs | ✅ |
| setup | (all arguments passed to AI Setup, run from npm with npx; needs Node.js 22+) | src/commands/setup.rs | ✅ |
| workflows | get, create, update, diff, delete, run, instances (list, get, cancel), connections (get, create, update, delete) | src/commands/workflows.rs | ✅ |
Expand Down
16 changes: 12 additions & 4 deletions src/commands/skills.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ fn resolve_or_bail(input: Option<&str>) -> Result<Vec<String>> {
if platforms.iter().any(|p| p.is_empty()) {
bail!(
"could not auto-detect AI assistant. Specify a platform: claude, \
cursor, codex, opencode, windsurf, gemini, pi, devin, or `all`."
cursor, codex, opencode, windsurf, gemini, pi, devin, hermes, or `all`."
);
}
for p in &platforms {
Expand All @@ -24,12 +24,12 @@ fn resolve_or_bail(input: Option<&str>) -> Result<Vec<String>> {
bail!(
"auto-detected '{p}' is not a supported platform. Specify \
one explicitly: claude, cursor, codex, opencode, windsurf, \
gemini, pi, devin, or `all`."
gemini, pi, devin, hermes, or `all`."
);
}
bail!(
"unknown platform: '{p}'. Supported: claude, cursor, codex, \
opencode, windsurf, gemini, pi, devin, or `all`."
opencode, windsurf, gemini, pi, devin, hermes, or `all`."
);
}
}
Expand Down Expand Up @@ -295,7 +295,15 @@ mod tests {
fn resolve_or_bail_expands_all() {
let p = resolve_or_bail(Some("all")).unwrap();
// All known platforms — must include each canonical name.
for expected in ["claude-code", "cursor", "codex", "opencode", "pi"] {
for expected in [
"claude-code",
"cursor",
"codex",
"opencode",
"pi",
"devin",
"hermes",
] {
assert!(p.iter().any(|x| x == expected), "missing {expected}");
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2755,7 +2755,7 @@ enum Commands {
/// extension Multi-file bundle for a coding-agent platform (e.g. pi)
///
/// PLATFORMS:
/// claude (or claude-code), cursor, codex, opencode, windsurf, gemini, pi, devin
/// claude (or claude-code), cursor, codex, opencode, windsurf, gemini, pi, devin, hermes
/// Pass `all` to install for every supported platform.
/// If omitted, pup auto-detects the platform from the environment.
///
Expand Down
50 changes: 50 additions & 0 deletions src/skills.rs
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,17 @@ pub static PLATFORMS: &[PlatformSpec] = &[
user_extensions: "",
uses_agent_md: false,
},
PlatformSpec {
name: "hermes",
aliases: &[],
project_skills: ".hermes/skills",
user_skills: ".hermes/skills",
project_agents: "",
user_agents: "",
project_extensions: "",
user_extensions: "",
uses_agent_md: false,
},
];

/// CLI-typed selector for `pup skills install <platform>` and `pup skills
Expand All @@ -703,6 +714,7 @@ pub enum SkillsPlatform {
#[clap(alias = "pi-dev")]
Pi,
Devin,
Hermes,
All,
}

Expand All @@ -721,6 +733,7 @@ impl SkillsPlatform {
SkillsPlatform::GeminiCode => "gemini-code",
SkillsPlatform::Pi => "pi",
SkillsPlatform::Devin => "devin",
SkillsPlatform::Hermes => "hermes",
SkillsPlatform::All => "all",
}
}
Expand Down Expand Up @@ -1357,6 +1370,24 @@ mod tests {
);
}

#[test]
fn test_skills_dir_hermes_project_scope() {
let root = PathBuf::from("/tmp/test-project");
assert_eq!(
skills_dir_with_home("hermes", None, &root, false, None),
Some(root.join(".hermes/skills"))
);
}

#[test]
fn test_skills_dir_hermes_user_scope() {
let home = PathBuf::from("/tmp/fake-home");
assert_eq!(
skills_dir_with_home("hermes", Some(&home), &PathBuf::from("/unused"), true, None),
Some(home.join(".hermes/skills"))
);
}

#[test]
fn test_skills_dir_unknown_returns_none() {
let root = PathBuf::from("/tmp/test-project");
Expand Down Expand Up @@ -1550,6 +1581,16 @@ mod tests {
assert_eq!(fmt, InstallFormat::SkillMd);
}

#[test]
fn test_install_path_agent_hermes_as_skill() {
let root = PathBuf::from("/tmp/test-project");
let e = entry("logs", "agent", "");
let (path, fmt) = install_path(&e, "hermes", &root, None, false).unwrap();
assert_eq!(path, root.join(".hermes/skills/logs/SKILL.md"));
assert_ne!(path, root.join(".hermes/skills/logs.md"));
assert_eq!(fmt, InstallFormat::SkillMd);
}

#[test]
fn test_install_path_dir_override() {
let root = PathBuf::from("/tmp/test-project");
Expand Down Expand Up @@ -1636,6 +1677,15 @@ mod tests {
assert!(!spec.is_extension_only());
}

#[test]
fn test_lookup_platform_hermes() {
let spec = lookup_platform("hermes").expect("hermes must be a known platform");
assert_eq!(spec.name, "hermes");
assert!(spec.aliases.is_empty());
assert!(!spec.uses_agent_md);
assert!(!spec.is_extension_only());
}

#[test]
fn test_lookup_platform_aliases() {
assert_eq!(
Expand Down
28 changes: 28 additions & 0 deletions src/useragent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ static AGENT_DETECTORS: &[AgentDetector] = &[
truthy_vars: &[],
presence_vars: &["DEVIN_SESSION_ID"],
},
AgentDetector {
name: "hermes",
// Hermes Agent exports HERMES_AGENT=true (alongside AI_AGENT=hermes-agent)
// into every shell command it runs (tools/environments/base_session_env.py).
truthy_vars: &["HERMES_AGENT"],
presence_vars: &[],
},
AgentDetector {
name: "generic-agent",
truthy_vars: &["AGENT"],
Expand Down Expand Up @@ -442,6 +449,27 @@ mod tests {
std::env::remove_var("DEVIN_SESSION_ID");
}

#[test]
fn test_detect_agent_info_hermes() {
let _guard = ENV_LOCK.blocking_lock();
clear_all_agent_vars();
std::env::set_var("HERMES_AGENT", "true");
let info = detect_agent_info();
assert!(info.detected);
assert_eq!(info.name, "hermes");
std::env::remove_var("HERMES_AGENT");
}

#[test]
fn test_detect_agent_info_hermes_false_not_detected() {
let _guard = ENV_LOCK.blocking_lock();
clear_all_agent_vars();
std::env::set_var("HERMES_AGENT", "false");
let info = detect_agent_info();
assert!(!info.detected);
std::env::remove_var("HERMES_AGENT");
}

#[test]
fn test_detect_agent_info_codex_via_session_id() {
let _guard = ENV_LOCK.blocking_lock();
Expand Down
Loading