Skip to content

refactor: make OpenCode adapter a thin wrapper routing through peon.sh#430

Merged
garysheng merged 2 commits intoPeonPing:mainfrom
ImBIOS:fix/opencode-real-adapter
Apr 2, 2026
Merged

refactor: make OpenCode adapter a thin wrapper routing through peon.sh#430
garysheng merged 2 commits intoPeonPing:mainfrom
ImBIOS:fix/opencode-real-adapter

Conversation

@ImBIOS
Copy link
Copy Markdown
Contributor

@ImBIOS ImBIOS commented Apr 1, 2026

Summary

Rewrite the OpenCode TypeScript plugin from a 950-line partial re-implementation of peon.sh into a 160-line thin adapter that routes all events through peon.sh — the same pattern used by every other IDE adapter (Cursor, Codex, Windsurf, Kiro, etc.).

Problem

The OpenCode plugin (adapters/opencode/peon-ping.ts) duplicated massive amounts of peon.sh logic in TypeScript:

  • Sound playback (per-platform: macOS afplay, Linux PulseAudio, WSL PowerShell)
  • Desktop notifications (osascript, terminal-notifier, notify-send)
  • CESP manifest parsing and pack management
  • Spam detection, debouncing, session cooldown
  • Config and state management (separate from peon.sh's config)
  • SSH/devcontainer relay support

Any feature added to peon.sh had to be manually ported to TypeScript. This caused:

  • Trainer not working (Trainer reminders not working in OpenCode (missing from TypeScript plugin) #428) — trainer logic in peon.sh was never ported
  • Missing features — TTS, session-specific overrides, mobile notifications, etc.
  • Config drift — separate config at ~/.config/opencode/peon-ping/config.json vs ~/.claude/hooks/peon-ping/config.json
  • Bug fix duplication — fixes applied to peon.sh didn't automatically reach OpenCode users

Solution

The new plugin is a thin adapter that:

  1. Catches OpenCode events (session.created, session.idle, session.error, permission.asked, session.status)
  2. Maps them to peon.sh hook event names (SessionStart, Stop, PostToolUseFailure, PermissionRequest, UserPromptSubmit)
  3. Builds the standard JSON payload (hook_event_name, cwd, session_id, notification_type, permission_mode, source)
  4. Spawns bash peon.sh with the JSON on stdin

This matches exactly how Cursor, Codex, Windsurf, and Kiro adapters work — they all translate IDE-specific events to the peon.sh JSON contract.

Event Mapping

OpenCode Event hook_event_name
session.created (no parent) SessionStart
session.status (busy/running) UserPromptSubmit
session.idle Stop
session.error PostToolUseFailure
permission.asked PermissionRequest

OpenCode-specific features preserved

  • Tab title updates (process.stdout.write OSC escape sequences)
  • Subagent filtering (sessions with parentID are skipped)

Changes

adapters/opencode/peon-ping.ts (950 → 160 lines)

  • Removed: all sound playback, notification logic, CESP parsing, pack management, spam detection, debouncing, config/state, relay support
  • Added: firePeon() — builds JSON payload and spawns peon.sh
  • Kept: tab titles, subagent filtering

adapters/opencode.sh (190 → 80 lines)

  • Added: preflight check for peon.sh at ~/.claude/hooks/peon-ping/peon.sh or ~/.openclaw/hooks/peon-ping/peon.sh
  • Removed: separate config creation, pack download, registry fetch (all handled by peon.sh's own install)
  • Adapter is now an add-on to peon-ping, not a standalone installation

What OpenCode users gain automatically

Testing

  • TypeScript transpiles successfully (bun build)
  • Shell syntax validated (bash -n)
  • Changes are additive only — no existing tests modified

Closes

Closes #427
Closes #428

The OpenCode TypeScript plugin was a 950-line partial re-implementation of
peon.sh: own sound playback, notifications, config, state management, pack
loading, spam detection, debouncing, etc. This meant any feature added to
peon.sh (trainer, TTS, new categories) had to be duplicated in TypeScript.

Replace it with a 160-line thin adapter that:
- Catches OpenCode events
- Maps them to peon.sh hook_event_name (SessionStart, Stop, UserPromptSubmit, etc.)
- Spawns peon.sh with the JSON payload on stdin

This gives OpenCode users access to ALL peon-ping features automatically:
trainer reminders, TTS, spam detection, SSH/devcontainer relay, pack rotation,
desktop notifications, and any future features added to peon.sh.

The installer now checks for peon.sh first and refuses to install without it,
since the adapter is an add-on to peon-ping, not a standalone installation.

Closes PeonPing#427
Closes PeonPing#428
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 1, 2026

@ImBIOS is attempting to deploy a commit to the Gary Sheng's projects Team on Vercel.

A member of the Team first needs to authorize it.

@garysheng
Copy link
Copy Markdown
Collaborator

This refactor is exactly right — the OpenCode adapter was way too much of a standalone re-implementation. The 950→160 line reduction speaks for itself, and routing through peon.sh means OpenCode users automatically get trainer, TTS, all future features.

Tests are failing because opencode.bats tests the old behavior (standalone install with its own config + pack download). The new adapter now requires peon.sh to exist first, so all those tests fail with "peon.sh not found" before they can do anything.

The test suite needs to be updated to:

  1. Add a mock peon.sh in the test's MOCK_BIN (or mock one of the candidate paths like ~/.claude/hooks/peon-ping/peon.sh) so the preflight check passes
  2. Remove/update tests that check for config creation and pack download (those are now owned by peon.sh's own install, not opencode.sh)
  3. Add tests that verify the adapter downloads and places peon-ping.ts correctly (the one remaining job of opencode.sh)

The core logic change is correct — just needs the test file to catch up with the new contract.

Copy link
Copy Markdown

@enolive enolive left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added remarks for differences from my PR result.

other than that, at least two things are missing here:

  • change the kilo adapter installer as well
  • change the PS1 variants of the installers
  • the vitest tests are not really covering anything inside this plugin. I fail to see why they couldn't tbh 😉

"WezTerm",
"ghostty",
"Hyper",
const PEON_SH_PATHS = [
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my version of resolving the path done by Caaaarl was actually a little bit more sophisticated:

function resolvePeonSh(): string | null {
  const candidates = [
    process.env.PEON_DIR,
    process.env.CLAUDE_PEON_DIR,
    process.env.CLAUDE_CONFIG_DIR
      ? path.join(process.env.CLAUDE_CONFIG_DIR, "hooks", "peon-ping")
      : null,
    path.join(os.homedir(), ".claude", "hooks", "peon-ping"),
  ]
  for (const dir of candidates) {
    if (!dir) continue
    const sh = path.join(dir, "peon.sh")
    if (fs.existsSync(sh)) return sh
  }
  return null
}

// Tab Title
// ---------------------------------------------------------------------------

function setTabTitle(title: string): void {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

peon.sh is also doing a thing with changing the tab title. In my PR attempt no tab title changes were done by the opencode adapter and it changed netherless. So is this really important?

notifyTitle: `${projectName} \u2014 Error occurred`,
})
setTabTitle(`\u25cf ${projectName}: error`)
firePeon("PostToolUseFailure")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

most adapters (like for instance codex.sh) are also setting the tool_name and error properties in the payload if this kind of thing happens.

I fail to see it here

notifyTitle: `${projectName} \u2014 Task complete`,
})
setTabTitle(`\u25cf ${projectName}: done`)
firePeon("Stop")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that the call to start peon.sh is ignoring the session id from the opencoe event using instead its own. is this the correct behavior?

@garysheng
Copy link
Copy Markdown
Collaborator

Pushed a fix for the failing bats tests: updated tests/opencode.bats to match the new thin-adapter contract.

Changes:

  • Added mock peon.sh in setup() so the preflight check passes
  • Removed stale tests for config.json and pack download (those are now peon.sh's job)
  • Renamed fresh install test to only verify peon-ping.ts is downloaded
  • Updated uninstall test (only plugin file removed now)
  • Updated XDG_CONFIG_HOME test (only plugin path checked)
  • Added install fails when peon.sh is not found preflight test

All 9 opencode.bats tests pass locally. CI should be green now.

@garysheng garysheng merged commit 4d112fa into PeonPing:main Apr 2, 2026
3 of 4 checks passed
@ImBIOS ImBIOS deleted the fix/opencode-real-adapter branch April 3, 2026 08:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Trainer reminders not working in OpenCode (missing from TypeScript plugin) turn opencode from a re-implementation to a real adapter?

3 participants