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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ cn usage status

## Features

- **Multi-tool support** - Claude Code, OpenAI Codex, Google Gemini CLI
- **Multi-tool support** - Claude Code, OpenAI Codex, Google Gemini CLI, Oh My Pi (omp)
- **Works everywhere** - Terminal, VSCode, Cursor, or any editor
- **Cross-platform** - macOS, Linux, Windows
- **Native notifications** - Uses system notification APIs
Expand Down Expand Up @@ -145,6 +145,7 @@ npm packages are published with GitHub Actions Trusted Publisher and npm provena
| `cn on claude` | Enable for Claude Code only |
| `cn on codex` | Enable for Codex only |
| `cn on gemini` | Enable for Gemini CLI only |
| `cn on omp` | Enable for Oh My Pi (omp) only |
| `cn off` | Disable notifications |
| `cn off all` | Explicit alias for disabling all tools |
| `cn test` | Send test notification |
Expand Down Expand Up @@ -173,10 +174,13 @@ Code-Notify uses the hook systems built into AI coding tools:
- **Claude Code**: `~/.claude/settings.json`
- **Codex**: `~/.codex/config.toml`
- **Gemini CLI**: `~/.gemini/settings.json`
- **Oh My Pi (omp)**: `~/.omp/agent/extensions/code-notify.js`

For Codex, Code-Notify configures `notify = ["/absolute/path/to/notifier.sh", "codex"]` and reads the JSON payload Codex appends on completion.
Codex currently exposes completion events through `notify`; approval and `request_permissions` prompts do not currently arrive through this hook.

For omp, Code-Notify writes a small managed extension module to `~/.omp/agent/extensions/code-notify.js`, because omp loads extension modules instead of reading hook commands from a config file. The extension forwards omp's `agent_end` event to the same `notifier.sh`, so sound, voice, Slack/Discord channels, click-through, the global mute switch, and rate limiting all work unchanged. Like Codex, omp currently exposes completion events; approval and idle prompts are not yet wired.

When enabled, it adds hooks that call the notification script when tasks complete:

```json
Expand Down
21 changes: 18 additions & 3 deletions lib/code-notify/commands/global.sh
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ enable_notifications_global() {

if [[ -z "$installed_tools" ]]; then
warning "No supported AI tools detected"
info "Supported tools: Claude Code, Codex, Gemini CLI"
info "Supported tools: Claude Code, Codex, Gemini CLI, Oh My Pi (omp)"
return 1
fi

Expand Down Expand Up @@ -440,6 +440,7 @@ enable_single_tool() {
"claude") config_file="$GLOBAL_SETTINGS_FILE" ;;
"codex") config_file="$CODEX_CONFIG_FILE" ;;
"gemini") config_file="$GEMINI_SETTINGS_FILE" ;;
"omp") config_file="$OMP_EXTENSION_FILE" ;;
esac

success "$tool: ENABLED"
Expand Down Expand Up @@ -470,7 +471,7 @@ disable_notifications_global() {
# No tool specified - disable all enabled tools
local disabled_count=0

for t in claude codex gemini; do
for t in claude codex gemini omp; do
if is_tool_enabled "$t"; then
if disable_single_tool "$t" "quiet"; then
((disabled_count++))
Expand Down Expand Up @@ -580,6 +581,19 @@ show_status() {
echo " ${DIM}- Gemini CLI: not installed${RESET}"
fi

# Oh My Pi (omp)
if is_tool_installed "omp"; then
if is_tool_enabled "omp"; then
echo " ${CHECK_MARK} omp: ${GREEN}ENABLED${RESET}"
echo " Config: $OMP_EXTENSION_FILE"
echo " Events: completion via agent_end extension"
else
echo " ${MUTE} omp: ${DIM}DISABLED${RESET}"
fi
else
echo " ${DIM}- omp: not installed${RESET}"
fi

# Voice status
echo ""
if is_voice_enabled "global"; then
Expand Down Expand Up @@ -937,12 +951,13 @@ show_voice_status() {
fi

# Per-tool voice
for tool in claude codex gemini; do
for tool in claude codex gemini omp; do
local tool_display
case "$tool" in
"claude") tool_display="Claude" ;;
"codex") tool_display="Codex" ;;
"gemini") tool_display="Gemini" ;;
"omp") tool_display="omp" ;;
esac

if is_voice_enabled "tool" "$tool"; then
Expand Down
104 changes: 104 additions & 0 deletions lib/code-notify/core/config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ CODEX_CONFIG_FILE="$CODEX_HOME/config.toml"
GEMINI_HOME="${GEMINI_HOME:-$HOME/.gemini}"
GEMINI_SETTINGS_FILE="$GEMINI_HOME/settings.json"

# Oh My Pi (omp) paths
OMP_HOME="${OMP_HOME:-$HOME/.omp}"
OMP_EXTENSIONS_DIR="${OMP_EXTENSIONS_DIR:-$OMP_HOME/agent/extensions}"
OMP_EXTENSION_FILE="$OMP_EXTENSIONS_DIR/code-notify.js"
OMP_EXTENSION_MARKER="code-notify: managed omp extension"

# Ensure config directory exists
ensure_config_dir() {
mkdir -p "$CONFIG_DIR" "$BACKUP_DIR"
Expand Down Expand Up @@ -1551,6 +1557,95 @@ PYTHON
fi
}

# ============================================
# Oh My Pi (omp) integration
# ============================================
# omp loads JS/TS extension modules from ~/.omp/agent/extensions instead of
# reading hook commands from a config file. So enabling = writing a managed
# extension that forwards omp's agent_end event to notifier.sh; disabling =
# removing that file. All downstream delivery (sound, voice, channels,
# click-through, kill switch, rate limiting) is reused through notifier.sh.

# Generate the omp extension module that forwards completion events to the notifier
generate_omp_extension() {
local notify_script="$1"
# Escape backslashes and double quotes for safe embedding in the JS string literal
notify_script="${notify_script//\\/\\\\}"
notify_script="${notify_script//\"/\\\"}"
cat << EOF
// ${OMP_EXTENSION_MARKER}
// Created by \`cn on omp\`; removed by \`cn off omp\`. Do not edit -- regenerated on enable.
// omp (Oh My Pi) loads extension modules instead of config-file hooks, so this
// file forwards the "agent finished" event to code-notify's notifier.
const NOTIFIER = "${notify_script}";

export default function codeNotify(pi) {
pi.on("agent_end", async (event, ctx) => {
// Only ping the top-level interactive session; skip subagents/headless runs
// unless OMP_NOTIFY_ALL=1 is set.
if (!ctx.hasUI && process.env.OMP_NOTIFY_ALL !== "1") return;
// Map the final stop reason to the notifier hook type so a failed run produces an
// error notification instead of a false success. Scan back to the last assistant
// turn: the array can end in a tool-result or custom (bash/compaction) message
// that would otherwise mask the outcome.
const messages = Array.isArray(event?.messages) ? event.messages : [];
let reason;
for (let i = messages.length - 1; i >= 0; i--) {
if (messages[i] && messages[i].role === "assistant") { reason = messages[i].stopReason; break; }
}
const hook = (reason === "error" || reason === "aborted" || reason === "length")
? "error" : "stop";
try {
// Pass the project name via cwd so the notifier derives it from basename(\$PWD),
// keeping the notification scoped globally (not project-scoped) so that
// \`cn off\` / the global kill switch correctly suppresses it.
await pi.exec(NOTIFIER, [hook, "omp"], { timeout: 5000, cwd: ctx.cwd || undefined });
} catch (err) {
// Never let notification failures disrupt the session.
pi.logger?.debug?.(\`code-notify: notifier exec failed: \${err}\`);
}
});
}
EOF
}

# Check if omp notifications are enabled
is_omp_enabled() {
[[ -f "$OMP_EXTENSION_FILE" ]] && grep -q "$OMP_EXTENSION_MARKER" "$OMP_EXTENSION_FILE" 2>/dev/null
}

# Enable omp notifications by writing a managed extension module
enable_omp_hooks() {
local notify_script
notify_script="$(get_notify_script)"

# Refuse to overwrite a file we didn't create (consistent with disable_omp_hooks which
# preserves non-managed files). Check before creating any dirs so a refused enable
# leaves the filesystem untouched.
if [[ -f "$OMP_EXTENSION_FILE" ]] && ! grep -q "$OMP_EXTENSION_MARKER" "$OMP_EXTENSION_FILE" 2>/dev/null; then
error "Refusing to overwrite existing non-code-notify extension: $OMP_EXTENSION_FILE"
info "Remove or rename that file manually, then run \`cn on omp\` again."
return 1
fi

mkdir -p "$OMP_EXTENSIONS_DIR"
atomic_write "$OMP_EXTENSION_FILE" "$(generate_omp_extension "$notify_script")"
}

# Disable omp notifications by removing our managed extension module
disable_omp_hooks() {
if [[ ! -f "$OMP_EXTENSION_FILE" ]]; then
return 0
fi

# Only remove our own managed file. Never touch ~/.omp/agent or its subdirs --
# that is omp's data directory (agent.db, sessions, config.yml, ...), not ours.
if grep -q "$OMP_EXTENSION_MARKER" "$OMP_EXTENSION_FILE" 2>/dev/null; then
rm -f "$OMP_EXTENSION_FILE"
fi
return 0
}

# ============================================
# Multi-tool helpers
# ============================================
Expand All @@ -1569,6 +1664,9 @@ enable_tool() {
"gemini")
enable_gemini_hooks
;;
"omp")
enable_omp_hooks
;;
*)
return 1
;;
Expand All @@ -1589,6 +1687,9 @@ disable_tool() {
"gemini")
disable_gemini_hooks
;;
"omp")
disable_omp_hooks
;;
*)
return 1
;;
Expand All @@ -1609,6 +1710,9 @@ is_tool_enabled() {
"gemini")
is_gemini_enabled
;;
"omp")
is_omp_enabled
;;
*)
return 1
;;
Expand Down
1 change: 1 addition & 0 deletions lib/code-notify/core/notifier.sh
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ get_tool_display_name() {
"claude") echo "Claude" ;;
"codex") echo "Codex" ;;
"gemini") echo "Gemini" ;;
"omp") echo "omp" ;;
*) echo "AI" ;;
esac
}
Expand Down
22 changes: 22 additions & 0 deletions lib/code-notify/utils/detect.sh
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,21 @@ detect_gemini_cli() {
return 1
}

# Detect Oh My Pi (omp) installation.
# Binary-only, like detect_codex / detect_gemini_cli: a directory is not a reliable
# "installed" signal here. ~/.omp/agent is omp's data dir (agent.db, sessions, ...) --
# it persists after the binary is uninstalled, and code-notify itself creates the
# extensions/ subdir on enable, so keying on it gives false positives. The legacy `pi`
# alias is intentionally NOT accepted: `pi` is a common generic binary name and would
# misfire. OMP_HOME is still honoured for the echoed config location.
detect_omp() {
if command -v omp &> /dev/null; then
echo "${OMP_HOME:-$HOME/.omp}"
return 0
fi
return 1
}

# Get list of all installed AI coding tools
get_installed_tools() {
local tools=()
Expand All @@ -137,6 +152,10 @@ get_installed_tools() {
tools+=("gemini")
fi

if detect_omp &> /dev/null; then
tools+=("omp")
fi

# Return space-separated list
echo "${tools[*]}"
}
Expand All @@ -155,6 +174,9 @@ is_tool_installed() {
"gemini")
detect_gemini_cli &> /dev/null
;;
"omp")
detect_omp &> /dev/null
;;
*)
return 1
;;
Expand Down
6 changes: 4 additions & 2 deletions lib/code-notify/utils/help.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ show_help() {
${BOLD}Code-Notify${RESET} - Desktop notifications for AI coding tools

${BOLD}SUPPORTED TOOLS:${RESET}
Claude Code, OpenAI Codex, Google Gemini CLI
Claude Code, OpenAI Codex, Google Gemini CLI, Oh My Pi (omp)

${BOLD}USAGE:${RESET}
$cmd_name <command> [tool]

${BOLD}COMMANDS:${RESET}
${GREEN}on${RESET} Enable notifications (all detected tools)
${GREEN}on${RESET} all Enable notifications (explicit alias for all detected tools)
${GREEN}on${RESET} <tool> Enable for specific tool (claude/codex/gemini)
${GREEN}on${RESET} <tool> Enable for specific tool (claude/codex/gemini/omp)
${GREEN}off${RESET} Disable notifications (all tools)
${GREEN}off${RESET} all Disable notifications (explicit alias for all tools)
${GREEN}off${RESET} <tool> Disable for specific tool
Expand Down Expand Up @@ -51,6 +51,7 @@ ${BOLD}TOOL NAMES:${RESET}
${CYAN}claude${RESET} Claude Code
${CYAN}codex${RESET} OpenAI Codex CLI
${CYAN}gemini${RESET} Google Gemini CLI
${CYAN}omp${RESET} Oh My Pi (omp)

${BOLD}PROJECT COMMANDS:${RESET}
${GREEN}project on${RESET} Enable for current project
Expand All @@ -67,6 +68,7 @@ ${BOLD}ALERT TYPES:${RESET}
Claude events: ${CYAN}SubagentStart${RESET}, ${CYAN}SubagentStop${RESET}, ${CYAN}TeammateIdle${RESET}, ${CYAN}TaskCreated${RESET}, ${CYAN}TaskCompleted${RESET}
Note: alert-type matching applies to Claude Code and Gemini CLI hooks.
Codex currently exposes completion events through its notify payload.
omp exposes completion events through a generated extension module.

${BOLD}VOICE COMMANDS:${RESET}
${GREEN}voice on${RESET} Enable voice for all tools
Expand Down
1 change: 1 addition & 0 deletions lib/code-notify/utils/voice.sh
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ disable_voice() {
rm -f "$VOICE_DIR/voice-claude"
rm -f "$VOICE_DIR/voice-codex"
rm -f "$VOICE_DIR/voice-gemini"
rm -f "$VOICE_DIR/voice-omp"
;;
"global"|*)
rm -f "$GLOBAL_VOICE_FILE"
Expand Down
13 changes: 10 additions & 3 deletions scripts/install-windows.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -1908,24 +1908,31 @@ function Invoke-CodeNotify {
)

$toolCommands = @("claude", "codex", "gemini")
$unsupportedTools = @("omp")

switch ($Command.ToLower()) {
"on" {
if ($SubCommand -and ($toolCommands -contains $SubCommand.ToLower())) {
if ($SubCommand -and ($unsupportedTools -contains $SubCommand.ToLower())) {
Write-Warning "omp is not yet supported on Windows. Windows support is planned."
} elseif ($SubCommand -and ($toolCommands -contains $SubCommand.ToLower())) {
Enable-Notifications -Tool $SubCommand
} else {
Enable-Notifications
}
}
"off" {
if ($SubCommand -and ($toolCommands -contains $SubCommand.ToLower())) {
if ($SubCommand -and ($unsupportedTools -contains $SubCommand.ToLower())) {
Write-Warning "omp is not yet supported on Windows. Windows support is planned."
} elseif ($SubCommand -and ($toolCommands -contains $SubCommand.ToLower())) {
Disable-Notifications -Tool $SubCommand
} else {
Disable-Notifications
}
}
"status" {
if ($SubCommand -and ($toolCommands -contains $SubCommand.ToLower())) {
if ($SubCommand -and ($unsupportedTools -contains $SubCommand.ToLower())) {
Write-Warning "omp is not yet supported on Windows. Windows support is planned."
} elseif ($SubCommand -and ($toolCommands -contains $SubCommand.ToLower())) {
Show-Status -Tool $SubCommand
} else {
Show-Status
Expand Down
8 changes: 8 additions & 0 deletions scripts/run_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,14 @@ else
test_fail "ask_user alert preservation failed"
fi

# Test 24: omp (Oh My Pi) extension install/detect/notify
test_start "omp extension support"
if bash tests/test-omp-extension.sh >/dev/null 2>&1; then
test_pass
else
test_fail "omp extension support failed"
fi

# Summary
echo ""
echo "Test Summary:"
Expand Down
Loading
Loading