Canonical source for system topology, runtime boundaries, and component responsibilities. Other docs should link here rather than restating architecture details.
buddy is an npm-distributed developer tool that renders a floating, always-on-top, transparent pet character directly on the Windows desktop. The npm package is named @ag9898/buddy, and it installs the buddy command. It is built on a four-component architecture: a CLI entry point (buddy) that handles install-time setup and runtime commands, an Electron main process managing the window and HTTP sidecar, a Svelte renderer driving pet animation, and a Rust CLI binary (petdex-bridge) that runs in WSL and bridges shell hook events from WSL agents into the Windows-side Electron process. The system is entirely local — there are no cloud services, no accounts, and no network traffic leaving the machine.
buddy is installed via npm install -g @ag9898/buddy from either a Windows terminal or a WSL terminal. The npm package installs the buddy CLI and an npm-managed Electron runtime dependency so buddy start can launch the built app without a source checkout or devDependencies. When installed in WSL the CLI uses the WSL interop layer to launch the Windows Electron process; when installed on Windows it launches directly.
All components run on a single developer workstation. There is no server, no cloud service, and no external network dependency.
- buddy CLI (
src/cli/): npmbinentry point. Handlesstart,stop,status,hooks(install/status/uninstall),state <name>,doctor,hatch <prompt>,pets(list/current/use, withshowas an alias ofcurrent), andsize <scale-or-width>subcommands, and renders the bare-buddyoperational overview. The CLI UX command and output contract is complete. It detects whether it is running in WSL or on Windows and adjusts behavior accordingly — in WSL it launches the Windows app via WSL interop (buddy.exe). For asset generation,buddy hatchdelegates visual generation to a Codex run that can use$imagegen, then packages the deterministic hatch-pet outputs for buddy. - Electron main process (Windows,
src/main/): Manages the transparent frameless BrowserWindow, runs the local HTTP sidecar on127.0.0.1:7777, persists state to disk, resolves the selected pet assets, and owns the system tray. - Svelte renderer (Windows, Electron webview,
src/renderer/): Renders the selected pet character from the main-provided manifest and spritesheet URL, drives the sprite animation state machine, and handles pointer interactivity and dragging. - petdex-bridge (WSL, Rust CLI binary,
petdex-bridge/): A tiny cross-compiled Linux binary distributed for WSL hook integration. Invoked by WSL shell hooks; reads the shared update token and POSTs agent lifecycle events to the Electron HTTP sidecar via WSL localhost passthrough.
Detailed command behavior, terminal output rules, hatch progress expectations, pet
selection UX, and CLI lifecycle semantics live in CLI.md.
Owns:
- The npm
binentry point — thebuddycommand developers run in their terminal. - Environment detection: reads
/proc/versionto determine if running in WSL or natively on Windows. - On Windows:
buddy startwaits for the detached Electron app child to emitspawn, reports a typedapp.startresult, then returns control to the terminal;buddy stoprequests a token-authenticated graceful shutdown. - In WSL:
buddy startinvokes the Windows-sidebuddy.exevia WSL interop and reports success only after the interop child has spawned and exited zero; unavailable interop, spawn errors, and non-zero exits become typed actionable failures. buddy hooks install: writes user-level hook entries for Claude Code CLI and Codex CLI. Claude Code receives entries in~/.claude/settings.json; Codex CLI receives entries in~/.codex/hooks.json. Windows hooks callbuddy state <name>, and WSL hooks callpetdex-bridge state <name>.buddy hooks status: reports per-assistant and per-event coverage using the same detectionbuddy doctorandbuddy statususe. It is strictly read-only and never opens a configuration file for writing.buddy hooks uninstall: removes only buddy-owned entries (a mapped event carryingbuddy state <name>orpetdex-bridge state <name>), preserving unrelated commands, entries, events, and top-level keys. It writes nothing when there is nothing to remove, so repeated runs are no-ops and untouched files stay byte-for-byte identical. A file that does not parse as JSON is reported and left unchanged. This is the only command that removes hook configuration; explicit invocation is the authorization.buddy state <name>: sends an HTTP POST to the running sidecar (works from both Windows and WSL via localhost passthrough).buddy size <scale-or-width>: sends an HTTP POST toPOST /resizeon the sidecar. Accepts a scale factor (e.g.,1.5,2x) or explicit WxH dimensions (e.g.,400x300). Valid range: 80–1200 pixels per dimension. Uses the same token-authenticated flow asbuddy state. Main process applies the new bounds viasetBoundsand persists them viasaveBounds.buddy statusand barebuddy: send a token-authenticatedGET /statusto the sidecar and combine that bounded snapshot with locally detected hook coverage and the package version. The CLI never fails the invocation for a stopped app — an unreachable sidecar, a missing token, or an unparseable body degrades to a "not running" report carrying only the stable failure code. Barebuddyadds the TTY-only banner and suggested next commands; both emit the identicalapp.statusJSON payload.buddy doctor: checks that the Electron process is running, the sidecar responds, the update token exists, and hooks are installed.buddy hatch <prompt>: prepares a hatch-pet run, verifies Codex CLI is installed and ready withcodex doctor, then invokescodex execas the image-generation worker so$imagegenis provided by Codex rather than by a buddy-owned image API adapter. By default, it packages the result in<buddy data dir>\\pets\\<derived-id>so generated pets remain personal;--outputselects an explicit destination and--package-preset <id>is an explicit source-checkout maintainer workflow for bundled assets. Hatching never replaces the application’s shared Windows icon.- Pet-management commands enumerate and select valid buddy-managed pets, packaged pets, and Codex-compatible pet asset folders. Selection is persisted in buddy-owned state only.
- CLI output is concise by default, styled when supported, plain in non-TTY contexts, and avoids dumping raw child-process output unless verbose/debug behavior is explicitly requested.
- Shared path helpers:
src/shared/buddy-paths.tsdefines the buddy-owned data root and derived state, runtime token, and buddy-managed pet paths. Windows defaults to%USERPROFILE%\.petdex-win; WSL defaults to$HOME/.petdex-win, which should be a symlink/copy bridge to the Windows-owned directory or overridden withBUDDY_DATA_DIR. - Pet discovery module:
src/cli/pets.ts—discoverPets()enumerates valid pets from<buddy data dir>\pets(buddy-managed), the packagepetsdirectory (packaged built-ins), and%USERPROFILE%\.codex\pets(Codex-compatible, read-only).validatePetFolder()checkspet.jsonstructure and spritesheet existence before admitting a candidate.BUDDY_SPRITES_DIRoverrides only the buddy-managed pets directory. - Files:
src/cli/index.ts,src/cli/program.ts,src/cli/commands/,src/cli/pets.ts,src/cli/sidecar-client.ts,src/cli/version.ts. - Build output:
npm run build:apprunselectron-vite buildfor Electron bundles and a Vite CLI build that emitsout/cli/index.js, the packagebin.buddytarget. - Runtime launch helpers:
src/cli/runtime.tsresolves the installed package root, the Electron app path, and the npm-installed Electron executable.buddy startspawns that executable detached with the package root as the app argument, andbuddy doctorreports whether the runtime dependency is available. If graceful stop cannot connect,buddy stopreads Electron's buddy-owned runtime process record and uses Windows process metadata to verify its exact executable plus app path before issuing a PID-only fallback; authorization failures never fall back to process termination.
Does NOT:
- Never manages window state directly — all window operations go through the Electron main process.
- Never starts a long-running server process itself — it starts the Electron app which owns the sidecar.
- Never owns image-provider credentials for hatch generation. If a user starts hatching from Claude Code or any other shell, buddy still delegates the visual generation phase to Codex CLI so
$imagegenrouting remains centralized. - Never reads or writes Codex internal state while discovering Codex-compatible pet folders.
The public npm package name is @ag9898/buddy, but the installed binary remains buddy.
The package is curated through the package.json files allowlist plus an .npmignore
guardrail. npm pack runs npm run build:app through prepack, then ships only built
runtime output (out/), packaged pets, the Windows icon/build metadata, README/LICENSE,
and selected public docs. Agent skill folders, source-only planning files, workboard
metadata, tests, local build caches, and Rust source are excluded from the npm tarball.
Electron is listed as an optional production dependency for the npm package so global
installs receive the Electron executable even though source checkouts still use it as a
development build dependency.
Owns:
- BrowserWindow lifecycle: creates a transparent, frameless, always-on-top, non-focusable, skip-taskbar window (
frame:false, transparent:true, hasShadow:false, skipTaskbar:true, alwaysOnTop:true, focusable:false, thickFrame:false, roundedCorners:false, backgroundColor:"#00000000"). - After window creation: calls
setVisibleOnAllWorkspaces(true),setAlwaysOnTop(true, "floating"), andshowInactive(). - Click-through toggle via
setIgnoreMouseEvents(true, { forward: true })— disabled when renderer signals pointer is over an interactive region. - Local HTTP sidecar on
127.0.0.1:7777(configurable viaBUDDY_PORT): receives hook events and CLI commands, validates theX-Petdex-Update-Tokenheader, and forwards events to main/renderer via Electron IPC. Current endpoints areGET /health(unauthenticated and limited to{ "status": "ok" }),GET /status(token-authenticated bounded snapshot of app running/visibility, resolved pet id/name/source, and current window width/height),POST /state(token-authenticated, forwardsCH_STATE_CHANGEto renderer),POST /resize(token-authenticated, forwardsCH_CLI_RESIZEto main),POST /pets/use(token-authenticated, validates and persists a selected id, sendsCH_ACTIVE_PET_CHANGEwith the bounded active-pet payload, then returns that payload), andPOST /shutdown(token-authenticated and accepted once, then calls Electron's state-persisting graceful quit after the HTTP response finishes). Control responses expose only buddy-owned runtime state and never arbitrary filesystem contents or assistant configuration. - State persistence: reads and writes
<buddy data dir>\state.json(window open/hidden, bounds, pet id, current animation state). Restored bounds are clamped into the nearest display work area before the window is created so stale or off-screen coordinates cannot hide the pet. - Window resize: receives resize interactions from the renderer, updates BrowserWindow bounds, preserves click-through behavior, and persists final bounds on resize end.
- Pet selection: owns active pet state and asset loading.
pet-assets.tsresolves the persistedstate.pet.idagainst validated buddy-managed, packaged, and Codex-compatible pet folders, falls back to the packageddefaultpet with a diagnostic when the selected id is missing or invalid, and exposes only the active manifest plus a renderer-safe spritesheetfile://URL over preload IPC. Its live-selection service never treats a request id as a path: it requires an exact validated candidate match, persists throughstate-store.ts, and publishes the same bounded payload to the renderer afterPOST /pets/usesucceeds. - System tray (Show / Hide / Quit) to keep the process alive when the window is hidden.
- Hook installation:
hooks-install.tsexportsinstallHooks(options),getHooksStatus(options),uninstallHooks(options), and theisBuddyHookCommand(command, event)ownership predicate the CLI lifecycle shares. For Claude Code CLI it writes hook entries to~/.claude/settings.json(hooks section); for Codex CLI it writes hook entries to~/.codex/hooks.json. All operations are idempotent and preserve unrelated entries; removal additionally recognises both runtime command forms, so ownership does not depend on which shell the uninstall runs in, and it never rewrites a file it could not parse. The installer is runtime-aware: Windows hooks invokebuddy state <name>, while WSL hooks invokepetdex-bridge state <name>. The module contains no top-level Electron import and is safe to call from the CLI layer (FEAT-09) without an Electron environment. TheinstallHooksWithDialog()helper is intended for tray use only and dynamically requires Electron'sdialogAPI at call time. - Operational status:
GET /statusresolves the selected pet from canonical persisted state and reports only running/visibility, pet id/name/source, and the BrowserWindow width/height. It does not read hook or assistant configuration, token files, or asset paths. An authenticated one-time shutdown request persists the final window state before Electron exits. A validated live pet selection replaces renderer assets without recreating, moving, showing, or focusing the window. Every state-changing request is token-authenticated, Electron main remains the owner of the operation, and window display continues to useshowInactive(). - Files:
main.ts,avatar-window.ts,state-store.ts,sidecar.ts,tray.ts,hooks-install.ts.
Does NOT:
- Never bind the HTTP sidecar to
0.0.0.0— loopback only. - Never read or write Codex's internal state files.
- Never steal window focus — always use
showInactive().
Shared IPC channels (src/shared/ipc-channels.ts) and Preload / contextBridge (src/preload/preload.ts)
Owns:
src/shared/ipc-channels.tsowns all IPC channel name string constants (CH_STATE_SET,CH_STATE_CHANGE,CH_ACTIVE_PET_CHANGE,CH_PTR_INTERACTIVE,CH_DRAG_START,CH_DRAG_MOVE,CH_DRAG_END,CH_RENDERER_READY,CH_RESIZE_START,CH_RESIZE_MOVE,CH_RESIZE_END). Main, preload, and tests import constants from this side-effect-free shared module; channel strings are never hardcoded elsewhere.src/preload/preload.tsowns thepetApiobject exposed to the renderer viacontextBridge.exposeInMainWorld('petApi', ...). This is the sole communication surface between the Svelte renderer and the Electron main process.
petApi methods:
setState(state)— sendsCH_STATE_SETto main to request a state transition.onStateChange(cb)— registers a listener forCH_STATE_CHANGEpushed by main and returns an unsubscribe function.onActivePetChange(cb)— registers a listener for validatedCH_ACTIVE_PET_CHANGEpayloads and returns an unsubscribe function.getActivePet()— invokesCH_ACTIVE_PET_GETto receive the resolved active pet manifest, source label, spritesheet URL, and startup animation state. The renderer does not receive pet directory listings or arbitrary filesystem access.setPointerInteractive(interactive)— sendsCH_PTR_INTERACTIVEto toggle click-through.dragStart(offsetX, offsetY)— sendsCH_DRAG_STARTwith pointer offset within window.dragMove()— sendsCH_DRAG_MOVE; main repositions the window to track the cursor.dragEnd()— sendsCH_DRAG_END; main clears drag state.rendererReady()— sendsCH_RENDERER_READY; main shows the window viashowInactive().resizeStart(initialWidth, initialHeight)— sendsCH_RESIZE_START; main records the window origin and starting dimensions.resizeMove(screenX, screenY)— sendsCH_RESIZE_MOVE; main computes new width/height from cursor position relative to the window origin and callssetBounds.resizeEnd()— sendsCH_RESIZE_END; main saves the final bounds to the state store.
Does NOT:
- Never expose Node.js APIs directly —
contextIsolationis alwaystrue,nodeIntegrationis alwaysfalse.
Owns:
- Renders the pet character as a CSS
background-positionanimation over the spritesheet URL provided by Electron main. - Drives the sprite animation state machine from the active pet manifest provided by Electron main.
- Handles drag events and sends
drag-start/drag-move/drag-endIPC messages to the main process. - Detects pointer entry/exit on interactive regions (
[data-avatar-mascot],.resize-handle) and signals the main process to toggle click-through. - Responds to
pet:state-changeIPC events to switch the active animation state. - Replaces its active manifest, spritesheet URL, and animation state after a validated live pet-selection event. The initial request cannot overwrite a newer event, and a missing persisted state falls back to
idle(or the first valid state). - Provides a visual resize handle for the pet window and sends resize lifecycle events through preload IPC.
- Files:
index.html,main.ts,App.svelte,PetSprite.svelte,styles.css.
Does NOT:
- Never perform any file I/O or HTTP calls — all external communication goes through IPC to the main process.
- Never load sprite assets from paths not provided by the main process.
Owns:
- A single-purpose CLI binary cross-compiled for
x86_64-unknown-linux-gnu(runs in WSL). - Reads the shared update token from
BUDDY_TOKENwhen set, otherwise from<buddy data dir>/runtime/update-token($HOME/.petdex-win/runtime/update-tokenby default in WSL). - Accepts a state name as a CLI argument (
petdex-bridge state running) and POSTs{"state":"<name>","source":"claude-code"}tohttp://127.0.0.1:${BUDDY_PORT}/statewith theX-Petdex-Update-Tokenheader. - Relies on WSL localhost passthrough to reach the Windows-side HTTP sidecar automatically.
- Emits actionable non-zero errors when the token is missing,
BUDDY_PORTis invalid, the sidecar rejects the token, WSL localhost passthrough is not routing, or the Electron app is not running.
Does NOT:
- Never reads or writes any Codex internal state files.
- Never opens a GUI or interacts with the Windows desktop directly — all output flows through the HTTP POST.
- An agent CLI event fires in WSL (e.g., Claude Code
PreToolUsehook). - The user-level hook entry calls
petdex-bridge state running. - petdex-bridge reads the token from
BUDDY_TOKENor<buddy data dir>/runtime/update-token. - petdex-bridge POSTs
{"state":"running","source":"claude-code"}tohttp://127.0.0.1:7777/statewith theX-Petdex-Update-Tokenheader. WSL localhost passthrough routes this to the Windows host automatically. - The Electron HTTP sidecar validates the token and receives the payload.
- The sidecar sends
pet:state-change { state: "running" }to the renderer via Electron IPC. - The Svelte renderer switches the animation state machine to the "running" frame sequence.
- The CSS
background-positionanimation plays the running sprite frames at 120 ms/frame.
Hook event → pet state mapping:
| Hook event | Pet state |
|---|---|
UserPromptSubmit |
jumping |
PreToolUse |
running |
PostToolUse |
idle |
PermissionRequest |
waiting |
Stop |
waving |
- A CLI tool (Codex CLI or Claude Code) fires a
PreToolUsehook in a Windows terminal. - The hook calls
buddy state running(the buddy CLI, which POSTs to the local sidecar). - buddy POSTs
{"state":"running","source":"codex-cli"}tohttp://127.0.0.1:7777/state. - From step 5 onward, the path is identical to flow (a) above.
buddy has no user-facing authentication. Access to the HTTP sidecar is secured by a shared-secret token:
- Token location:
<buddy data dir>\runtime\update-token. Windows creates it under%USERPROFILE%\.petdex-win; WSL reads$HOME/.petdex-win/runtime/update-tokenby default, with the expected setup being a symlink/copy bridge to the Windows-owned directory orBUDDY_DATA_DIRpointing at the Windows-mounted.petdex-winroot.petdex-bridgealso accepts aBUDDY_TOKENoverride for temporary debugging. - Enforcement: Every state-changing sidecar request, including
/state, resize, graceful shutdown, and live pet selection, must carry theX-Petdex-Update-Tokenheader. Operational status beyond the minimal/healthresponse is also token-authenticated. The Electron sidecar rejects requests with a missing or incorrect token with HTTP 401. - Scope: Loopback-only binding (
127.0.0.1) means the token is a defense-in-depth measure against other local processes — there is no remote attack surface. - Rotation: Delete and regenerate the token file; restart the Electron app to pick up the new value.
- Shutdown replay protection:
/shutdownaccepts a valid token exactly once for each Electron process. The process record used only when the sidecar cannot be reached contains a pid, executable path, and app path; the CLI verifies all three against Windows process metadata before a PID-only fallback can run.
| Dependency | Purpose | Required / Optional |
|---|---|---|
| Electron | BrowserWindow, IPC, system tray, app packaging shell | Required |
| electron-builder | npm package publishing and production build tooling | Required (production build) |
| Svelte + Vite | Renderer framework and dev/build tooling | Required |
| commander (or yargs) | CLI entry point (buddy command) argument parsing |
Required |
| Codex CLI | Executes hatch-pet visual generation jobs with $imagegen for buddy hatch; command can be overridden with BUDDY_CODEX_COMMAND |
Required for asset generation only |
Rust toolchain (x86_64-unknown-linux-gnu cross-compile target) |
Build petdex-bridge for WSL | Required (for WSL hook support) |
There are no buddy-owned cloud services, managed databases, auth providers, or image API integrations. Codex CLI may use its own configured model/image-generation backend when buddy hatch delegates $imagegen work to it, but buddy does not read or store those credentials.
| Environment | Electron app | petdex-bridge | State file |
|---|---|---|---|
| Local dev | npm run dev — Electron + Vite dev server on localhost |
cargo build --release --target x86_64-unknown-linux-gnu, binary copied to WSL $PATH |
%USERPROFILE%\.petdex-win\state.json (created on first run) |
| Production | npm install -g @ag9898/buddy — electron-builder packages the app; the npm package ships the Electron runtime and exposes the buddy CLI via the bin field |
WSL bridge install path ships the pre-built x86_64-unknown-linux-gnu binary |
Same path — persisted across updates |
| WSL-only install | npm install -g @ag9898/buddy in WSL — CLI detects WSL, installs shell hooks, and invokes the Windows-side buddy.exe via WSL interop to launch the GUI |
Same as above | Same path |
See ENV_VARS.md for the canonical variable and secret matrix per environment.
- Windows-only. The Electron app and renderer target
win32exclusively. No macOS, no Linux native GUI. - HTTP sidecar must bind
127.0.0.1only. Host binding is not a user-facing configuration surface; never bind to0.0.0.0or any non-loopback address. - Never read or write any AI assistant CLI's internal config or state files. buddy must never touch
.codex/,.claude/, or equivalent internal directories of any CLI tool it integrates with. - Window must be non-focusable by default. Always use
showInactive()to display the window; never callfocus()orshow()in a way that steals focus from the user's active application. - Bounds must be saved on close, drag-end, and display-change events. State must not be lost on crash — write
state.jsondefensively at each of these points, not only on graceful exit. - DPI awareness is required. The window bounds calculation must account for Windows display scaling. Test at 100%, 125%, and mixed-DPI multi-monitor configurations.
- WSL agents cannot launch Windows GUI processes directly. The supported path is WSL interop: invoking
buddy.exefrom a WSL shell hands execution off to the Windows host. Do not attemptelectron .or direct GUI invocations from within a WSL shell. - WSL interop is optional infrastructure, not a hard requirement. The CLI must detect when
/proc/versiondoes not containMicrosoftor whencmd.exeis not reachable, and print a clear fallback message rather than crashing. - petdex-bridge must be cross-compiled for
x86_64-unknown-linux-gnu. Do not use the host Rust target for this binary — it must run inside WSL, not on the Windows host. - electron-builder is the packaging tool. Configuration lives in
electron-builder.yml. Do not swap to another packager without updatingelectron-builder.yml,package.jsonscripts, and this doc.