Skip to content
Closed
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
10 changes: 6 additions & 4 deletions docs/next/website/src/content/docs/integrations.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Integrations
description: Install Herdr integrations for Pi, OMP, Claude Code, Codex, GitHub Copilot CLI, Devin CLI, Droid, Kimi Code CLI, OpenCode, Kilo Code CLI, Hermes Agent, Qoder CLI, Cursor Agent CLI, MastraCode, and Grok CLI.
description: Install Herdr integrations for Pi, OMP, Claude Code, Codex, GitHub Copilot CLI, Devin CLI, Droid, Kimi Code CLI, OpenCode, Kilo Code CLI, Hermes Agent, Qoder CLI, Cursor Agent CLI, MastraCode, Grok CLI, and Antigravity CLI (agy).
---

Herdr detects supported agents automatically. Official integrations can add native session identity for restore, lifecycle state reports, or both.
Expand All @@ -27,6 +27,7 @@ herdr integration install qodercli
herdr integration install cursor
herdr integration install mastracode
herdr integration install grok
herdr integration install agy
```

## Uninstall integrations
Expand All @@ -47,6 +48,7 @@ herdr integration uninstall qodercli
herdr integration uninstall cursor
herdr integration uninstall mastracode
herdr integration uninstall grok
herdr integration uninstall agy
```

## How Herdr uses integrations
Expand All @@ -56,13 +58,13 @@ Herdr uses integrations in two different ways:
| Integration type | Agents | Effect |
| --- | --- | --- |
| Lifecycle authority | Pi, OMP, Kimi Code CLI, OpenCode, Kilo Code CLI, Hermes Agent, MastraCode | When installed and actively reporting for the pane, hook or plugin events author `idle`, `working`, and `blocked`. Herdr does not also use screen manifest fallback for that same lifecycle authority. |
| Session identity | Claude Code, Codex, GitHub Copilot CLI, Devin CLI, Droid, Qoder CLI, Cursor Agent CLI, Grok CLI | The integration reports native session references for restore. State still comes from Herdr's screen manifest detection. |
| Session identity | Claude Code, Codex, GitHub Copilot CLI, Devin CLI, Droid, Qoder CLI, Cursor Agent CLI, Grok CLI, Antigravity CLI (agy) | The integration reports native session references for restore. State still comes from Herdr's screen manifest detection. |

Custom socket integrations can also report state when they define state that is not visible in the native terminal UI.

Some integrations report native agent session references. Herdr uses official session references to resume Claude Code, Codex, Devin CLI, Droid, Kimi Code CLI, Qoder CLI, Cursor Agent CLI, Grok CLI, GitHub Copilot CLI, Pi, OMP, Hermes Agent, OpenCode, Kilo Code CLI, and MastraCode panes after a Herdr server restart unless `[session] resume_agents_on_restore = false` disables it.
Some integrations report native agent session references. Herdr uses official session references to resume Claude Code, Codex, Devin CLI, Droid, Kimi Code CLI, Qoder CLI, Cursor Agent CLI, Grok CLI, GitHub Copilot CLI, Pi, OMP, Hermes Agent, OpenCode, Kilo Code CLI, MastraCode, and Antigravity CLI (`agy`) panes after a Herdr server restart unless `[session] resume_agents_on_restore = false` disables it.

Native session restore requires current Herdr integrations: Pi integration version `2`, OMP version `3`, Claude Code version `6`, Codex version `5`, GitHub Copilot CLI version `2`, Devin CLI version `2`, Droid version `2`, Kimi Code CLI version `3`, Qoder CLI version `2`, Cursor Agent CLI version `1`, Grok CLI version `1`, OpenCode version `5`, Kilo Code CLI version `1`, Hermes Agent version `2`, or MastraCode version `1`. Check installed versions with `herdr integration status`.
Native session restore requires current Herdr integrations: Pi integration version `2`, OMP version `3`, Claude Code version `6`, Codex version `5`, GitHub Copilot CLI version `2`, Devin CLI version `2`, Droid version `2`, Kimi Code CLI version `3`, Qoder CLI version `2`, Cursor Agent CLI version `1`, Grok CLI version `1`, OpenCode version `5`, Kilo Code CLI version `1`, Hermes Agent version `2`, MastraCode version `1`, or Antigravity CLI version `1`. Check installed versions with `herdr integration status`.

## Pi

Expand Down
5 changes: 5 additions & 0 deletions src/agent_resume.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ pub fn is_reserved_native_state_source(source: &str, agent: &str) -> bool {
| ("herdr:qodercli", "qodercli")
| ("herdr:cursor", "cursor")
| ("herdr:grok", "grok")
| ("herdr:agy", "agy")
)
}

Expand Down Expand Up @@ -190,6 +191,9 @@ pub fn plan(source: &str, agent: &str, session_ref: &AgentSessionRef) -> Option<
("herdr:grok", "grok", AgentSessionRefKind::Id) => {
vec!["grok".into(), "--resume".into(), session_ref.value.clone()]
}
("herdr:agy", "agy", AgentSessionRefKind::Id) => {
vec!["agy".into(), "--resume".into(), session_ref.value.clone()]
}
_ => return None,
};

Expand Down Expand Up @@ -225,6 +229,7 @@ pub(crate) fn is_official_agent_source(source: &str, agent: &str) -> bool {
| ("herdr:kilo", "kilo")
| ("herdr:cursor", "cursor")
| ("herdr:grok", "grok")
| ("herdr:agy", "agy")
)
}

Expand Down
4 changes: 3 additions & 1 deletion src/api/schema/integrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@ pub enum IntegrationTarget {
Cursor,
Mastracode,
Grok,
Agy,
}

impl IntegrationTarget {
pub(crate) const ALL: [Self; 15] = [
pub(crate) const ALL: [Self; 16] = [
Self::Pi,
Self::Omp,
Self::Claude,
Expand All @@ -47,6 +48,7 @@ impl IntegrationTarget {
Self::Cursor,
Self::Mastracode,
Self::Grok,
Self::Agy,
];
}

Expand Down
9 changes: 6 additions & 3 deletions src/cli/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,13 @@ fn parse_integration_target(
) -> std::io::Result<Option<IntegrationTarget>> {
let Some(target) = args.first().map(|arg| arg.as_str()) else {
eprintln!(
"usage: herdr integration {action} <pi|omp|claude|codex|copilot|devin|droid|kimi|opencode|kilo|hermes|qodercli|cursor|mastracode|grok>"
"usage: herdr integration {action} <pi|omp|claude|codex|copilot|devin|droid|kimi|opencode|kilo|hermes|qodercli|cursor|mastracode|grok|agy>"
);
return Ok(None);
};
if args.len() != 1 {
eprintln!(
"usage: herdr integration {action} <pi|omp|claude|codex|copilot|devin|droid|kimi|opencode|kilo|hermes|qodercli|cursor|mastracode|grok>"
"usage: herdr integration {action} <pi|omp|claude|codex|copilot|devin|droid|kimi|opencode|kilo|hermes|qodercli|cursor|mastracode|grok|agy>"
);
return Ok(None);
}
Expand All @@ -130,10 +130,11 @@ fn parse_integration_target(
"cursor" => IntegrationTarget::Cursor,
"mastracode" => IntegrationTarget::Mastracode,
"grok" => IntegrationTarget::Grok,
"agy" | "antigravity" => IntegrationTarget::Agy,
_ => {
eprintln!("unknown integration target: {target}");
eprintln!(
"currently supported: pi, omp, claude, codex, copilot, devin, droid, kimi, opencode, kilo, hermes, qodercli, cursor, mastracode, grok"
"currently supported: pi, omp, claude, codex, copilot, devin, droid, kimi, opencode, kilo, hermes, qodercli, cursor, mastracode, grok, agy"
);
return Ok(None);
}
Expand All @@ -159,6 +160,7 @@ fn print_integration_help() {
eprintln!(" herdr integration install cursor");
eprintln!(" herdr integration install mastracode");
eprintln!(" herdr integration install grok");
eprintln!(" herdr integration install agy");
eprintln!(" herdr integration uninstall pi");
eprintln!(" herdr integration uninstall omp");
eprintln!(" herdr integration uninstall claude");
Expand All @@ -174,5 +176,6 @@ fn print_integration_help() {
eprintln!(" herdr integration uninstall cursor");
eprintln!(" herdr integration uninstall mastracode");
eprintln!(" herdr integration uninstall grok");
eprintln!(" herdr integration uninstall agy");
eprintln!(" herdr integration status [--outdated-only]");
}
52 changes: 46 additions & 6 deletions src/integration/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ use std::io;

use super::registry::{integration_target_label, integration_target_supported};
use super::targets::{
install_claude, install_codex, install_copilot, install_cursor, install_devin, install_droid,
install_grok, install_hermes, install_kilo, install_kimi, install_mastracode, install_omp,
install_opencode, install_pi, install_qodercli, uninstall_claude, uninstall_codex,
uninstall_copilot, uninstall_cursor, uninstall_devin, uninstall_droid, uninstall_grok,
uninstall_hermes, uninstall_kilo, uninstall_kimi, uninstall_mastracode, uninstall_omp,
uninstall_opencode, uninstall_pi, uninstall_qodercli,
install_agy, install_claude, install_codex, install_copilot, install_cursor, install_devin,
install_droid, install_grok, install_hermes, install_kilo, install_kimi, install_mastracode,
install_omp, install_opencode, install_pi, install_qodercli, uninstall_agy, uninstall_claude,
uninstall_codex, uninstall_copilot, uninstall_cursor, uninstall_devin, uninstall_droid,
uninstall_grok, uninstall_hermes, uninstall_kilo, uninstall_kimi, uninstall_mastracode,
uninstall_omp, uninstall_opencode, uninstall_pi, uninstall_qodercli,
};
use super::version::{agent_version_requirement, enforce_agent_version};
use super::{KIMI_MIN_VERSION, PI_EXTENSION_INSTALL_NAME};
Expand Down Expand Up @@ -217,6 +217,19 @@ fn install_target_inner(target: crate::api::schema::IntegrationTarget) -> io::Re
),
]
}
crate::api::schema::IntegrationTarget::Agy => {
let installed = install_agy()?;
vec![
format!(
"installed antigravity integration hook to {}",
installed.hook_path.display()
),
format!(
"ensured antigravity hooks at {}",
installed.hooks_path.display()
),
]
}
};

if let Some(warning) = version_warning {
Expand Down Expand Up @@ -598,6 +611,33 @@ pub(crate) fn uninstall_target(
}
messages
}
crate::api::schema::IntegrationTarget::Agy => {
let result = uninstall_agy()?;
let mut messages = Vec::new();
if result.removed_hook_file {
messages.push(format!(
"removed antigravity hook at {}",
result.hook_path.display()
));
} else {
messages.push(format!(
"no antigravity hook found at {}",
result.hook_path.display()
));
}
if result.updated_hooks {
messages.push(format!(
"removed herdr antigravity hook entries from {}",
result.hooks_path.display()
));
} else {
messages.push(format!(
"no herdr antigravity hook entries found in {}",
result.hooks_path.display()
));
}
messages
}
};

crate::logging::integration_action("uninstall", integration_target_label(target), "ok");
Expand Down
26 changes: 26 additions & 0 deletions src/integration/assets/agy/herdr-agent-state.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# installed by herdr
# managed by herdr; reinstalling or updating the integration overwrites this file.
# add custom hooks beside this file instead of editing it.
# HERDR_INTEGRATION_ID=agy
# HERDR_INTEGRATION_VERSION=1

param([string]$Action = "")

if ($Action -ne "session") { exit 0 }
if ($env:HERDR_ENV -ne "1") { exit 0 }
if ([string]::IsNullOrWhiteSpace($env:HERDR_PANE_ID)) { exit 0 }

$inputText = [Console]::In.ReadToEnd()
try {
$payload = if ([string]::IsNullOrWhiteSpace($inputText)) { $null } else { $inputText | ConvertFrom-Json }
} catch {
$payload = $null
}

if ($null -eq $payload -or [string]::IsNullOrWhiteSpace($payload.session_id)) { exit 0 }

$seq = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
try {
& herdr pane report-agent-session $env:HERDR_PANE_ID --source herdr:agy --agent agy --agent-session-id $payload.session_id --seq $seq 2>$null | Out-Null
} catch {
}
94 changes: 94 additions & 0 deletions src/integration/assets/agy/herdr-agent-state.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#!/bin/sh
# installed by herdr
# managed by herdr; reinstalling or updating the integration overwrites this file.
# add custom hooks beside this file instead of editing it.
# HERDR_INTEGRATION_ID=agy
# HERDR_INTEGRATION_VERSION=1

set -eu

action="${1:-}"
hook_input_file="$(mktemp "${TMPDIR:-/tmp}/herdr-agy-hook.XXXXXX")" || exit 0
trap 'rm -f "$hook_input_file"' EXIT HUP INT TERM
cat >"$hook_input_file" 2>/dev/null || true

case "$action" in
session) ;;
*) exit 0 ;;
esac

[ "${HERDR_ENV:-}" = "1" ] || exit 0
[ -n "${HERDR_SOCKET_PATH:-}" ] || exit 0
[ -n "${HERDR_PANE_ID:-}" ] || exit 0
command -v python3 >/dev/null 2>&1 || exit 0

HERDR_ACTION="$action" HERDR_HOOK_INPUT_FILE="$hook_input_file" python3 - <<'PY'
import json
import os
import random
import socket
import time

source = "herdr:agy"
pane_id = os.environ.get("HERDR_PANE_ID")
socket_path = os.environ.get("HERDR_SOCKET_PATH")
hook_input_file = os.environ.get("HERDR_HOOK_INPUT_FILE")

if not pane_id or not socket_path:
raise SystemExit(0)

hook_input = {}
if hook_input_file:
try:
with open(hook_input_file, encoding="utf-8") as handle:
content = handle.read()
if content.strip():
hook_input = json.loads(content)
except Exception:
hook_input = {}


def first_text(*keys):
for key in keys:
value = hook_input.get(key)
if isinstance(value, str) and value:
return value
return None


hook_event_name = first_text("hook_event_name", "hookEventName")
if hook_event_name not in (None, "session_start", "SessionStart", "sessionStart"):
raise SystemExit(0)

session_id = os.environ.get("AGY_SESSION_ID") or os.environ.get("ANTIGRAVITY_SESSION_ID") or first_text("session_id", "sessionId", "conversation_id", "conversationId")
agent_session_id = session_id if isinstance(session_id, str) and session_id else None
if not agent_session_id:
raise SystemExit(0)

request_id = f"{source}:{int(time.time() * 1000)}:{random.randrange(1_000_000):06d}"
report_seq = time.time_ns()
request = {
"id": request_id,
"method": "pane.report_agent_session",
"params": {
"pane_id": pane_id,
"source": source,
"agent": "agy",
"seq": report_seq,
"agent_session_id": agent_session_id,
},
}

try:
client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
client.settimeout(0.5)
client.connect(socket_path)
client.sendall((json.dumps(request) + "\n").encode())
try:
client.recv(4096)
except Exception:
pass
client.close()
except Exception:
pass
PY
12 changes: 12 additions & 0 deletions src/integration/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ pub(crate) const GROK_CONFIG_DIR_ENV_VAR: &str = "GROK_CONFIG_DIR";
/// The grok CLI's own config-home override (documented alongside
/// `$GROK_HOME/config.toml` and `$GROK_HOME/auth.json`).
pub(crate) const GROK_HOME_ENV_VAR: &str = "GROK_HOME";
pub(crate) const AGY_CONFIG_DIR_ENV_VAR: &str = "AGY_CONFIG_DIR";
pub(crate) const ANTIGRAVITY_CONFIG_DIR_ENV_VAR: &str = "ANTIGRAVITY_CONFIG_DIR";

pub(crate) fn apply_pane_base_env(cmd: &mut CommandBuilder) {
cmd.env(crate::api::SOCKET_PATH_ENV_VAR, crate::api::socket_path());
Expand Down Expand Up @@ -154,6 +156,16 @@ pub(crate) fn grok_dir() -> io::Result<PathBuf> {
config_dir_from_env_or_home(GROK_HOME_ENV_VAR, &[".grok"])
}

pub(crate) fn agy_dir() -> io::Result<PathBuf> {
if let Some(value) = std::env::var_os(AGY_CONFIG_DIR_ENV_VAR)
.or_else(|| std::env::var_os(ANTIGRAVITY_CONFIG_DIR_ENV_VAR))
.filter(|value| !value.is_empty())
{
return expand_tilde_path(PathBuf::from(value));
}
config_dir_from_env_or_home(AGY_CONFIG_DIR_ENV_VAR, &[".gemini", "antigravity-cli"])
}

pub(crate) fn home_dir() -> io::Result<PathBuf> {
if let Some(home) = std::env::var_os("HOME").filter(|value| !value.is_empty()) {
return Ok(PathBuf::from(home));
Expand Down
12 changes: 12 additions & 0 deletions src/integration/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,18 @@ const GROK_HOOK_INSTALL_NAME: &str = "herdr-agent-state.sh";
const GROK_HOOK_CONFIG_INSTALL_NAME: &str = "herdr.json";
const GROK_HOOK_ASSET: &str = include_str!("assets/grok/herdr-agent-state.sh");
const GROK_INTEGRATION_VERSION: u32 = 1;
const AGY_HOOK_INSTALL_NAME: &str = if cfg!(windows) {
"herdr-agent-state.ps1"
} else {
"herdr-agent-state.sh"
};
const AGY_HOOK_ASSET: &str = if cfg!(windows) {
include_str!("assets/agy/herdr-agent-state.ps1")
} else {
include_str!("assets/agy/herdr-agent-state.sh")
};
const AGY_INTEGRATION_VERSION: u32 = 1;
const AGY_HOOK_EVENTS: [(&str, &str); 1] = [("SessionStart", "session")];
const INTEGRATION_VERSION_MARKER: &str = "HERDR_INTEGRATION_VERSION=";

pub(crate) const INSTALL_WARNING_PREFIX: &str = "warning:";
Expand Down
Loading
Loading