feat: add omp integration
This commit is contained in:
parent
5954ece9f7
commit
ed11779fca
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
### Added
|
||||
- Added `terminal.new_cwd` to choose whether new panes, tabs, and workspaces follow the source pane/workspace, start in `$HOME`, use Herdr's process directory, or use a fixed path.
|
||||
- Added `herdr integration install omp` for OMP's `.omp` extension directory. The extension reports OMP pane state through Herdr's socket API without relying on native `omp` process detection.
|
||||
|
||||
### Fixed
|
||||
- Remote clients using local keybindings no longer show stale server keybinding warnings from the remote host.
|
||||
|
|
|
|||
|
|
@ -191,10 +191,11 @@ for agents outside the built-in list, herdr still works as a terminal multiplexe
|
|||
|
||||
### direct integrations
|
||||
|
||||
the built-in pi, claude code, codex, opencode, and hermes integrations forward semantic state to herdr over the socket api. install with:
|
||||
the built-in pi, omp, claude code, codex, opencode, and hermes integrations forward semantic state to herdr over the socket api. install with:
|
||||
|
||||
```bash
|
||||
herdr integration install pi
|
||||
herdr integration install omp
|
||||
herdr integration install claude
|
||||
herdr integration install codex
|
||||
herdr integration install opencode
|
||||
|
|
@ -277,7 +278,7 @@ full logging and environment variable details: [configuration docs](https://herd
|
|||
## docs
|
||||
|
||||
- [configuration](https://herdr.dev/docs/configuration/) — keybindings, themes, notifications, environment variables
|
||||
- [integrations](https://herdr.dev/docs/integrations/) — pi, claude code, codex, opencode, hermes integrations
|
||||
- [integrations](https://herdr.dev/docs/integrations/) — pi, omp, claude code, codex, opencode, hermes integrations
|
||||
- [`SKILL.md`](./SKILL.md) — reusable agent skill
|
||||
- [socket api](https://herdr.dev/docs/socket-api/) — socket protocol and cli reference
|
||||
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ Install integrations for agents that expose hooks or plugins:
|
|||
|
||||
```bash
|
||||
herdr integration install pi
|
||||
herdr integration install omp
|
||||
herdr integration install claude
|
||||
herdr integration install codex
|
||||
herdr integration install opencode
|
||||
|
|
|
|||
|
|
@ -165,11 +165,13 @@ Use `wait output` for normal commands and servers. Use `wait agent-status` for c
|
|||
|
||||
```bash
|
||||
herdr integration install pi
|
||||
herdr integration install omp
|
||||
herdr integration install claude
|
||||
herdr integration install codex
|
||||
herdr integration install opencode
|
||||
herdr integration install hermes
|
||||
herdr integration uninstall pi
|
||||
herdr integration uninstall omp
|
||||
herdr integration uninstall claude
|
||||
herdr integration uninstall codex
|
||||
herdr integration uninstall opencode
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: Integrations
|
||||
description: Install Herdr integrations for Pi, Claude Code, Codex, OpenCode, and Hermes Agent.
|
||||
description: Install Herdr integrations for Pi, OMP, Claude Code, Codex, OpenCode, and Hermes Agent.
|
||||
---
|
||||
|
||||
Herdr detects supported agents automatically. Integrations make that detection more precise by reporting semantic state directly to Herdr.
|
||||
|
|
@ -13,6 +13,7 @@ Open settings inside Herdr and use the integrations tab to install recommended i
|
|||
|
||||
```bash
|
||||
herdr integration install pi
|
||||
herdr integration install omp
|
||||
herdr integration install claude
|
||||
herdr integration install codex
|
||||
herdr integration install opencode
|
||||
|
|
@ -23,6 +24,7 @@ herdr integration install hermes
|
|||
|
||||
```bash
|
||||
herdr integration uninstall pi
|
||||
herdr integration uninstall omp
|
||||
herdr integration uninstall claude
|
||||
herdr integration uninstall codex
|
||||
herdr integration uninstall opencode
|
||||
|
|
@ -57,6 +59,24 @@ Herdr writes the bundled extension to:
|
|||
|
||||
If `PI_CODING_AGENT_DIR` is set, Herdr writes to `$PI_CODING_AGENT_DIR/extensions/herdr-agent-state.ts` instead. The extensions directory must already exist. Uninstall removes only that extension file.
|
||||
|
||||
## OMP
|
||||
|
||||
Install the OMP integration:
|
||||
|
||||
```bash
|
||||
herdr integration install omp
|
||||
```
|
||||
|
||||
Herdr writes the bundled extension to:
|
||||
|
||||
```text
|
||||
~/.omp/agent/extensions/herdr-omp-agent-state.ts
|
||||
```
|
||||
|
||||
If `PI_CODING_AGENT_DIR` is set, Herdr writes to `$PI_CODING_AGENT_DIR/extensions/herdr-omp-agent-state.ts` instead. The extensions directory must already exist. Uninstall removes only that extension file.
|
||||
|
||||
The OMP integration reports `omp` as the agent label through Herdr's socket API. It does not require native process detection for the `omp` executable.
|
||||
|
||||
## Claude Code
|
||||
|
||||
Install the Claude Code hook:
|
||||
|
|
|
|||
|
|
@ -394,6 +394,7 @@ pub struct IntegrationUninstallParams {
|
|||
#[serde(rename_all = "snake_case")]
|
||||
pub enum IntegrationTarget {
|
||||
Pi,
|
||||
Omp,
|
||||
Claude,
|
||||
Codex,
|
||||
Opencode,
|
||||
|
|
|
|||
|
|
@ -102,23 +102,24 @@ fn parse_integration_target(
|
|||
action: &str,
|
||||
) -> std::io::Result<Option<IntegrationTarget>> {
|
||||
let Some(target) = args.first().map(|arg| arg.as_str()) else {
|
||||
eprintln!("usage: herdr integration {action} <pi|claude|codex|opencode|hermes>");
|
||||
eprintln!("usage: herdr integration {action} <pi|omp|claude|codex|opencode|hermes>");
|
||||
return Ok(None);
|
||||
};
|
||||
if args.len() != 1 {
|
||||
eprintln!("usage: herdr integration {action} <pi|claude|codex|opencode|hermes>");
|
||||
eprintln!("usage: herdr integration {action} <pi|omp|claude|codex|opencode|hermes>");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let parsed = match target {
|
||||
"pi" => IntegrationTarget::Pi,
|
||||
"omp" => IntegrationTarget::Omp,
|
||||
"claude" => IntegrationTarget::Claude,
|
||||
"codex" => IntegrationTarget::Codex,
|
||||
"opencode" => IntegrationTarget::Opencode,
|
||||
"hermes" => IntegrationTarget::Hermes,
|
||||
_ => {
|
||||
eprintln!("unknown integration target: {target}");
|
||||
eprintln!("currently supported: pi, claude, codex, opencode, hermes");
|
||||
eprintln!("currently supported: pi, omp, claude, codex, opencode, hermes");
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
|
|
@ -129,11 +130,13 @@ fn parse_integration_target(
|
|||
fn print_integration_help() {
|
||||
eprintln!("herdr integration commands:");
|
||||
eprintln!(" herdr integration install pi");
|
||||
eprintln!(" herdr integration install omp");
|
||||
eprintln!(" herdr integration install claude");
|
||||
eprintln!(" herdr integration install codex");
|
||||
eprintln!(" herdr integration install opencode");
|
||||
eprintln!(" herdr integration install hermes");
|
||||
eprintln!(" herdr integration uninstall pi");
|
||||
eprintln!(" herdr integration uninstall omp");
|
||||
eprintln!(" herdr integration uninstall claude");
|
||||
eprintln!(" herdr integration uninstall codex");
|
||||
eprintln!(" herdr integration uninstall opencode");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,289 @@
|
|||
// installed by herdr
|
||||
// safe to edit. this integration only activates inside herdr-managed panes.
|
||||
// HERDR_INTEGRATION_ID=omp
|
||||
// HERDR_INTEGRATION_VERSION=1
|
||||
// @ts-nocheck
|
||||
|
||||
import { createConnection } from "node:net";
|
||||
|
||||
const HERDR_ENV = process.env.HERDR_ENV;
|
||||
const socketPath = process.env.HERDR_SOCKET_PATH;
|
||||
const paneId = process.env.HERDR_PANE_ID;
|
||||
const source = "herdr:omp";
|
||||
|
||||
function enabled() {
|
||||
return HERDR_ENV === "1" && !!socketPath && !!paneId;
|
||||
}
|
||||
|
||||
function sendRequest(request: unknown): Promise<void> {
|
||||
if (!enabled()) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
let done = false;
|
||||
const finish = () => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
socket.destroy();
|
||||
resolve();
|
||||
};
|
||||
|
||||
const socket = createConnection(socketPath!);
|
||||
socket.on("error", finish);
|
||||
socket.on("connect", () => socket.write(`${JSON.stringify(request)}\n`));
|
||||
socket.on("data", finish);
|
||||
socket.on("end", finish);
|
||||
const timeout = setTimeout(finish, 500);
|
||||
timeout.unref?.();
|
||||
});
|
||||
}
|
||||
|
||||
type AgentState = "working" | "blocked" | "idle";
|
||||
|
||||
type QueuedState = {
|
||||
state: AgentState;
|
||||
message?: string;
|
||||
seq: number;
|
||||
};
|
||||
|
||||
const idleDebounceMs = parseDurationEnv("HERDR_OMP_IDLE_DEBOUNCE_MS", 250);
|
||||
const retryGraceMs = parseDurationEnv("HERDR_OMP_RETRY_GRACE_MS", 2500);
|
||||
const retryableErrorPattern =
|
||||
/overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|http2 request did not get a response|timed? out|timeout|terminated|retry delay/i;
|
||||
let reportSeq = Date.now() * 1000;
|
||||
|
||||
function nextReportSeq(): number {
|
||||
reportSeq += 1;
|
||||
return reportSeq;
|
||||
}
|
||||
|
||||
function parseDurationEnv(name: string, fallback: number): number {
|
||||
const raw = process.env[name];
|
||||
if (!raw) {
|
||||
return fallback;
|
||||
}
|
||||
const parsed = Number.parseInt(raw, 10);
|
||||
if (!Number.isFinite(parsed) || parsed < 0) {
|
||||
return fallback;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function sendState(state: AgentState, message?: string, seq = nextReportSeq()): Promise<void> {
|
||||
return sendRequest({
|
||||
id: `${source}:${Date.now()}:${Math.random().toString(36).slice(2)}`,
|
||||
method: "pane.report_agent",
|
||||
params: {
|
||||
pane_id: paneId,
|
||||
source,
|
||||
agent: "omp",
|
||||
state,
|
||||
message,
|
||||
seq,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let sendInFlight = false;
|
||||
let queuedState: QueuedState | undefined;
|
||||
|
||||
function queueState(state: AgentState, message?: string): void {
|
||||
queuedState = { state, message, seq: nextReportSeq() };
|
||||
if (!sendInFlight) {
|
||||
void drainStateQueue();
|
||||
}
|
||||
}
|
||||
|
||||
async function drainStateQueue(): Promise<void> {
|
||||
if (sendInFlight) {
|
||||
return;
|
||||
}
|
||||
|
||||
sendInFlight = true;
|
||||
try {
|
||||
while (queuedState) {
|
||||
const next = queuedState;
|
||||
queuedState = undefined;
|
||||
await sendState(next.state, next.message, next.seq);
|
||||
}
|
||||
} finally {
|
||||
sendInFlight = false;
|
||||
if (queuedState) {
|
||||
void drainStateQueue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function lastAssistantMessage(messages: unknown[]): any | undefined {
|
||||
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
||||
const message = messages[i] as any;
|
||||
if (message?.role === "assistant") {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function retryableErrorMessage(event: any): string | undefined {
|
||||
const messages = Array.isArray(event?.messages) ? event.messages : [];
|
||||
const assistant = lastAssistantMessage(messages);
|
||||
if (assistant?.stopReason !== "error") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const errorMessage = String(assistant.errorMessage ?? "");
|
||||
if (!retryableErrorPattern.test(errorMessage)) {
|
||||
return undefined;
|
||||
}
|
||||
return errorMessage || "retryable provider error";
|
||||
}
|
||||
|
||||
function releaseAgent(): Promise<void> {
|
||||
return sendRequest({
|
||||
id: `${source}:release:${Date.now()}:${Math.random().toString(36).slice(2)}`,
|
||||
method: "pane.release_agent",
|
||||
params: {
|
||||
pane_id: paneId,
|
||||
source,
|
||||
agent: "omp",
|
||||
seq: nextReportSeq(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default function (pi) {
|
||||
if (!enabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let agentActive = false;
|
||||
let retryHoldActive = false;
|
||||
let failureBlocked = false;
|
||||
let failureMessage: string | undefined;
|
||||
let blockedCount = 0;
|
||||
let blockedMessage: string | undefined;
|
||||
let lastState: AgentState | undefined;
|
||||
let lastMessage: string | undefined;
|
||||
let idleTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
let retryTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
function clearTimer(timer: ReturnType<typeof setTimeout> | undefined) {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
function clearPendingTimers() {
|
||||
clearTimer(idleTimer);
|
||||
clearTimer(retryTimer);
|
||||
idleTimer = undefined;
|
||||
retryTimer = undefined;
|
||||
}
|
||||
|
||||
function clearFailureState() {
|
||||
retryHoldActive = false;
|
||||
failureBlocked = false;
|
||||
failureMessage = undefined;
|
||||
}
|
||||
|
||||
function desiredState() {
|
||||
if (blockedCount > 0) {
|
||||
return { state: "blocked" as const, message: blockedMessage };
|
||||
}
|
||||
if (failureBlocked) {
|
||||
return { state: "blocked" as const, message: failureMessage };
|
||||
}
|
||||
if (agentActive || retryHoldActive) {
|
||||
return { state: "working" as const, message: undefined };
|
||||
}
|
||||
return { state: "idle" as const, message: undefined };
|
||||
}
|
||||
|
||||
function publishState() {
|
||||
const next = desiredState();
|
||||
if (next.state === lastState && next.message === lastMessage) {
|
||||
return;
|
||||
}
|
||||
lastState = next.state;
|
||||
lastMessage = next.message;
|
||||
queueState(next.state, next.message);
|
||||
}
|
||||
|
||||
function scheduleIdle() {
|
||||
clearPendingTimers();
|
||||
clearFailureState();
|
||||
idleTimer = setTimeout(() => {
|
||||
idleTimer = undefined;
|
||||
publishState();
|
||||
}, idleDebounceMs);
|
||||
idleTimer.unref?.();
|
||||
}
|
||||
|
||||
function holdForRetry(message: string) {
|
||||
clearPendingTimers();
|
||||
retryHoldActive = true;
|
||||
failureBlocked = false;
|
||||
failureMessage = message;
|
||||
publishState();
|
||||
|
||||
retryTimer = setTimeout(() => {
|
||||
retryTimer = undefined;
|
||||
retryHoldActive = false;
|
||||
failureBlocked = true;
|
||||
publishState();
|
||||
}, retryGraceMs);
|
||||
retryTimer.unref?.();
|
||||
}
|
||||
|
||||
pi.events.on("herdr:blocked", (data) => {
|
||||
if (!data?.active) {
|
||||
blockedCount = Math.max(0, blockedCount - 1);
|
||||
if (blockedCount === 0) {
|
||||
blockedMessage = undefined;
|
||||
}
|
||||
publishState();
|
||||
return;
|
||||
}
|
||||
|
||||
clearPendingTimers();
|
||||
blockedCount += 1;
|
||||
blockedMessage = data.label;
|
||||
publishState();
|
||||
});
|
||||
|
||||
pi.on("session_start", () => {
|
||||
publishState();
|
||||
});
|
||||
|
||||
pi.on("agent_start", () => {
|
||||
clearPendingTimers();
|
||||
clearFailureState();
|
||||
agentActive = true;
|
||||
publishState();
|
||||
});
|
||||
|
||||
pi.on("agent_end", (event) => {
|
||||
if (!agentActive) {
|
||||
// OMP can emit duplicate/late end events while auto-retry is already
|
||||
// holding the pane in Working. Do not let an unqualified duplicate end
|
||||
// cancel the retry hold and publish a false Idle.
|
||||
return;
|
||||
}
|
||||
|
||||
agentActive = false;
|
||||
|
||||
const retryableMessage = retryableErrorMessage(event);
|
||||
if (retryableMessage) {
|
||||
holdForRetry(retryableMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
scheduleIdle();
|
||||
});
|
||||
|
||||
pi.on("session_shutdown", async () => {
|
||||
clearPendingTimers();
|
||||
await releaseAgent();
|
||||
});
|
||||
}
|
||||
|
|
@ -13,6 +13,9 @@ pub(crate) const HERDR_PANE_ID_ENV_VAR: &str = "HERDR_PANE_ID";
|
|||
const PI_EXTENSION_INSTALL_NAME: &str = "herdr-agent-state.ts";
|
||||
const PI_EXTENSION_ASSET: &str = include_str!("assets/pi/herdr-agent-state.ts");
|
||||
const PI_INTEGRATION_VERSION: u32 = 1;
|
||||
const OMP_EXTENSION_INSTALL_NAME: &str = "herdr-omp-agent-state.ts";
|
||||
const OMP_EXTENSION_ASSET: &str = include_str!("assets/omp/herdr-agent-state.ts");
|
||||
const OMP_INTEGRATION_VERSION: u32 = 1;
|
||||
const PI_CODING_AGENT_DIR_ENV_VAR: &str = "PI_CODING_AGENT_DIR";
|
||||
const CLAUDE_HOOK_INSTALL_NAME: &str = "herdr-agent-state.sh";
|
||||
const CLAUDE_HOOK_ASSET: &str = include_str!("assets/claude/herdr-agent-state.sh");
|
||||
|
|
@ -51,6 +54,12 @@ pub(crate) struct OpenCodeInstallPaths {
|
|||
pub plugin_path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct OmpInstallPaths {
|
||||
pub extension_path: PathBuf,
|
||||
pub removed_legacy_pi_extension: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct HermesInstallPaths {
|
||||
pub plugin_dir: PathBuf,
|
||||
|
|
@ -105,6 +114,12 @@ pub(crate) struct PiUninstallResult {
|
|||
pub removed_extension: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct OmpUninstallResult {
|
||||
pub extension_path: PathBuf,
|
||||
pub removed_extension: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ClaudeUninstallResult {
|
||||
pub hook_path: PathBuf,
|
||||
|
|
@ -149,6 +164,24 @@ pub(crate) fn install_target(
|
|||
let path = install_pi()?;
|
||||
vec![format!("installed pi integration to {}", path.display())]
|
||||
}
|
||||
crate::api::schema::IntegrationTarget::Omp => {
|
||||
let installed = install_omp()?;
|
||||
let mut messages = Vec::new();
|
||||
if installed.removed_legacy_pi_extension {
|
||||
messages.push(format!(
|
||||
"removed legacy pi integration from omp extension directory at {}",
|
||||
installed
|
||||
.extension_path
|
||||
.with_file_name(PI_EXTENSION_INSTALL_NAME)
|
||||
.display()
|
||||
));
|
||||
}
|
||||
messages.push(format!(
|
||||
"installed omp integration to {}",
|
||||
installed.extension_path.display()
|
||||
));
|
||||
messages
|
||||
}
|
||||
crate::api::schema::IntegrationTarget::Claude => {
|
||||
let installed = install_claude()?;
|
||||
vec![
|
||||
|
|
@ -220,6 +253,20 @@ pub(crate) fn uninstall_target(
|
|||
)]
|
||||
}
|
||||
}
|
||||
crate::api::schema::IntegrationTarget::Omp => {
|
||||
let result = uninstall_omp()?;
|
||||
if result.removed_extension {
|
||||
vec![format!(
|
||||
"removed omp integration extension at {}",
|
||||
result.extension_path.display()
|
||||
)]
|
||||
} else {
|
||||
vec![format!(
|
||||
"no omp integration extension found at {}",
|
||||
result.extension_path.display()
|
||||
)]
|
||||
}
|
||||
}
|
||||
crate::api::schema::IntegrationTarget::Claude => {
|
||||
let result = uninstall_claude()?;
|
||||
let mut messages = Vec::new();
|
||||
|
|
@ -330,6 +377,7 @@ pub(crate) fn integration_target_label(
|
|||
) -> &'static str {
|
||||
match target {
|
||||
crate::api::schema::IntegrationTarget::Pi => "pi",
|
||||
crate::api::schema::IntegrationTarget::Omp => "omp",
|
||||
crate::api::schema::IntegrationTarget::Claude => "claude",
|
||||
crate::api::schema::IntegrationTarget::Codex => "codex",
|
||||
crate::api::schema::IntegrationTarget::Opencode => "opencode",
|
||||
|
|
@ -340,6 +388,7 @@ pub(crate) fn integration_target_label(
|
|||
fn integration_target_command(target: crate::api::schema::IntegrationTarget) -> &'static str {
|
||||
match target {
|
||||
crate::api::schema::IntegrationTarget::Pi => "pi",
|
||||
crate::api::schema::IntegrationTarget::Omp => "omp",
|
||||
crate::api::schema::IntegrationTarget::Claude => "claude",
|
||||
crate::api::schema::IntegrationTarget::Codex => "codex",
|
||||
crate::api::schema::IntegrationTarget::Opencode => "opencode",
|
||||
|
|
@ -417,13 +466,18 @@ fn integration_specs() -> [(
|
|||
crate::api::schema::IntegrationTarget,
|
||||
io::Result<PathBuf>,
|
||||
u32,
|
||||
); 5] {
|
||||
); 6] {
|
||||
[
|
||||
(
|
||||
crate::api::schema::IntegrationTarget::Pi,
|
||||
pi_extension_dir().map(|dir| dir.join(PI_EXTENSION_INSTALL_NAME)),
|
||||
PI_INTEGRATION_VERSION,
|
||||
),
|
||||
(
|
||||
crate::api::schema::IntegrationTarget::Omp,
|
||||
omp_extension_dir().map(|dir| dir.join(OMP_EXTENSION_INSTALL_NAME)),
|
||||
OMP_INTEGRATION_VERSION,
|
||||
),
|
||||
(
|
||||
crate::api::schema::IntegrationTarget::Claude,
|
||||
claude_dir().map(|dir| dir.join("hooks").join(CLAUDE_HOOK_INSTALL_NAME)),
|
||||
|
|
@ -546,6 +600,39 @@ pub(crate) fn install_pi() -> io::Result<PathBuf> {
|
|||
Ok(path)
|
||||
}
|
||||
|
||||
pub(crate) fn install_omp() -> io::Result<OmpInstallPaths> {
|
||||
let dir = omp_extension_dir()?;
|
||||
if !dir.is_dir() {
|
||||
return Err(io::Error::other(format!(
|
||||
"omp extension directory not found at {}. install omp and create the extensions directory first",
|
||||
dir.display()
|
||||
)));
|
||||
}
|
||||
|
||||
let removed_legacy_pi_extension = remove_legacy_pi_extension_from_omp_dir(&dir)?;
|
||||
let extension_path = dir.join(OMP_EXTENSION_INSTALL_NAME);
|
||||
fs::write(&extension_path, OMP_EXTENSION_ASSET)?;
|
||||
Ok(OmpInstallPaths {
|
||||
extension_path,
|
||||
removed_legacy_pi_extension,
|
||||
})
|
||||
}
|
||||
|
||||
fn remove_legacy_pi_extension_from_omp_dir(dir: &Path) -> io::Result<bool> {
|
||||
let legacy_path = dir.join(PI_EXTENSION_INSTALL_NAME);
|
||||
if !legacy_path.is_file() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(&legacy_path)?;
|
||||
if content.contains("HERDR_INTEGRATION_ID=pi") {
|
||||
fs::remove_file(legacy_path)?;
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
pub(crate) fn install_claude() -> io::Result<ClaudeInstallPaths> {
|
||||
let dir = claude_dir()?;
|
||||
if !dir.is_dir() {
|
||||
|
|
@ -790,6 +877,16 @@ pub(crate) fn uninstall_pi() -> io::Result<PiUninstallResult> {
|
|||
})
|
||||
}
|
||||
|
||||
pub(crate) fn uninstall_omp() -> io::Result<OmpUninstallResult> {
|
||||
let extension_path = omp_extension_dir()?.join(OMP_EXTENSION_INSTALL_NAME);
|
||||
let removed_extension = remove_file_if_exists(&extension_path)?;
|
||||
|
||||
Ok(OmpUninstallResult {
|
||||
extension_path,
|
||||
removed_extension,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn uninstall_claude() -> io::Result<ClaudeUninstallResult> {
|
||||
let hook_path = claude_dir()?.join("hooks").join(CLAUDE_HOOK_INSTALL_NAME);
|
||||
let settings_path = claude_dir()?.join("settings.json");
|
||||
|
|
@ -1355,6 +1452,13 @@ fn pi_extension_dir() -> io::Result<PathBuf> {
|
|||
)
|
||||
}
|
||||
|
||||
fn omp_extension_dir() -> io::Result<PathBuf> {
|
||||
Ok(
|
||||
config_dir_from_env_or_home(PI_CODING_AGENT_DIR_ENV_VAR, &[".omp", "agent"])?
|
||||
.join("extensions"),
|
||||
)
|
||||
}
|
||||
|
||||
fn claude_dir() -> io::Result<PathBuf> {
|
||||
config_dir_from_env_or_home(CLAUDE_CONFIG_DIR_ENV_VAR, &[".claude"])
|
||||
}
|
||||
|
|
@ -1553,6 +1657,144 @@ mod tests {
|
|||
let _ = fs::remove_dir_all(base);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn install_omp_writes_embedded_asset_to_omp_extensions_dir() {
|
||||
let _lock = integration_env_lock();
|
||||
let base = unique_base();
|
||||
let home = base.join("home");
|
||||
let ext_dir = home.join(".omp/agent/extensions");
|
||||
fs::create_dir_all(&ext_dir).unwrap();
|
||||
std::env::set_var("HOME", &home);
|
||||
|
||||
let installed = install_omp().unwrap();
|
||||
let content = fs::read_to_string(&installed.extension_path).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
installed.extension_path,
|
||||
ext_dir.join(OMP_EXTENSION_INSTALL_NAME)
|
||||
);
|
||||
assert!(!installed.removed_legacy_pi_extension);
|
||||
assert_eq!(content, OMP_EXTENSION_ASSET);
|
||||
|
||||
std::env::remove_var("HOME");
|
||||
let _ = fs::remove_dir_all(base);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn install_omp_removes_legacy_pi_integration_from_omp_extensions_dir() {
|
||||
let _lock = integration_env_lock();
|
||||
let base = unique_base();
|
||||
let home = base.join("home");
|
||||
let ext_dir = home.join(".omp/agent/extensions");
|
||||
fs::create_dir_all(&ext_dir).unwrap();
|
||||
let legacy_path = ext_dir.join(PI_EXTENSION_INSTALL_NAME);
|
||||
fs::write(&legacy_path, PI_EXTENSION_ASSET).unwrap();
|
||||
std::env::set_var("HOME", &home);
|
||||
|
||||
let installed = install_omp().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
installed.extension_path,
|
||||
ext_dir.join(OMP_EXTENSION_INSTALL_NAME)
|
||||
);
|
||||
assert!(installed.removed_legacy_pi_extension);
|
||||
assert!(!legacy_path.exists());
|
||||
|
||||
std::env::remove_var("HOME");
|
||||
let _ = fs::remove_dir_all(base);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn install_omp_preserves_non_herdr_file_with_pi_install_name() {
|
||||
let _lock = integration_env_lock();
|
||||
let base = unique_base();
|
||||
let home = base.join("home");
|
||||
let ext_dir = home.join(".omp/agent/extensions");
|
||||
fs::create_dir_all(&ext_dir).unwrap();
|
||||
let user_path = ext_dir.join(PI_EXTENSION_INSTALL_NAME);
|
||||
fs::write(&user_path, "// user extension\n").unwrap();
|
||||
std::env::set_var("HOME", &home);
|
||||
|
||||
let installed = install_omp().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
installed.extension_path,
|
||||
ext_dir.join(OMP_EXTENSION_INSTALL_NAME)
|
||||
);
|
||||
assert!(!installed.removed_legacy_pi_extension);
|
||||
assert_eq!(
|
||||
fs::read_to_string(user_path).unwrap(),
|
||||
"// user extension\n"
|
||||
);
|
||||
|
||||
std::env::remove_var("HOME");
|
||||
let _ = fs::remove_dir_all(base);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn install_omp_uses_pi_coding_agent_dir_env() {
|
||||
let _lock = integration_env_lock();
|
||||
let base = unique_base();
|
||||
let agent_dir = base.join("custom-omp-agent");
|
||||
let ext_dir = agent_dir.join("extensions");
|
||||
fs::create_dir_all(&ext_dir).unwrap();
|
||||
std::env::set_var(PI_CODING_AGENT_DIR_ENV_VAR, &agent_dir);
|
||||
|
||||
let installed = install_omp().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
installed.extension_path,
|
||||
ext_dir.join(OMP_EXTENSION_INSTALL_NAME)
|
||||
);
|
||||
assert!(!installed.removed_legacy_pi_extension);
|
||||
|
||||
clear_integration_path_env();
|
||||
let _ = fs::remove_dir_all(base);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uninstall_omp_removes_embedded_extension_when_present() {
|
||||
let _lock = integration_env_lock();
|
||||
let base = unique_base();
|
||||
let home = base.join("home");
|
||||
let ext_dir = home.join(".omp/agent/extensions");
|
||||
fs::create_dir_all(&ext_dir).unwrap();
|
||||
fs::write(
|
||||
ext_dir.join(OMP_EXTENSION_INSTALL_NAME),
|
||||
OMP_EXTENSION_ASSET,
|
||||
)
|
||||
.unwrap();
|
||||
std::env::set_var("HOME", &home);
|
||||
|
||||
let result = uninstall_omp().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
result.extension_path,
|
||||
ext_dir.join(OMP_EXTENSION_INSTALL_NAME)
|
||||
);
|
||||
assert!(result.removed_extension);
|
||||
assert!(!result.extension_path.exists());
|
||||
|
||||
std::env::remove_var("HOME");
|
||||
let _ = fs::remove_dir_all(base);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn install_omp_errors_when_extension_dir_missing() {
|
||||
let _lock = integration_env_lock();
|
||||
let base = unique_base();
|
||||
let home = base.join("home");
|
||||
fs::create_dir_all(&home).unwrap();
|
||||
std::env::set_var("HOME", &home);
|
||||
|
||||
let err = install_omp().unwrap_err().to_string();
|
||||
|
||||
assert!(err.contains("omp extension directory not found"));
|
||||
|
||||
std::env::remove_var("HOME");
|
||||
let _ = fs::remove_dir_all(base);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uninstall_pi_removes_embedded_extension_when_present() {
|
||||
let _lock = integration_env_lock();
|
||||
|
|
|
|||
Loading…
Reference in New Issue