# OpenCode integration This document describes how FamiliarOS Desktop integrates with OpenCode, what files are configured, how OpenCode reaches the desktop app, and which OpenCode events trigger familiar reactions or speech. ## Integration surfaces OpenCode support has three related surfaces: 1. **MCP tools** — OpenCode gets the `familiaros_status`, `familiaros_react`, and `familiaros_say` tools through `@familiaros/cli mcp`. 2. **OpenCode instructions** — OpenCode loads a managed `familiaros.md` instruction file telling agents when to use the tools. 3. **OpenCode plugin hooks** — `@familiaros/opencode` turns OpenCode activity into automatic familiar reactions and short safe speech. The desktop setup UI for OpenCode is implemented in `apps/desktop/src/agent-setup.ts`. It previews, installs, doctors, and removes global OpenCode setup through `@familiaros/opencode` helpers. This document covers **Desktop global setup** first. The CLI also supports **project-local OpenCode setup**; see [Project-local OpenCode setup](#project-local-opencode-setup). ## Global config location FamiliarOS uses OpenCode's global config directory: ```text OPENCODE_CONFIG_DIR, if set Windows: %APPDATA%/opencode macOS/Linux: $XDG_CONFIG_HOME/opencode, or ~/.config/opencode fallback ``` Inside that directory FamiliarOS considers these config files, in order: ```text config.json opencode.json opencode.jsonc ``` If no file exists, the default creation target is: ```text opencode.jsonc ``` FamiliarOS also writes a global instruction file: ```text /familiaros.md ``` ## Files and config managed by FamiliarOS OpenCode global setup writes or updates three config fields. ### MCP entry Published setup creates an MCP entry like: ```jsonc { "mcp": { "familiaros": { "type": "local", "command": ["npx", "-y", "@familiaros/cli@", "mcp"], "enabled": true } } } ``` If a familiar is selected, the command includes it: ```jsonc "command": ["npx", "-y", "@familiaros/cli@", "mcp", "--familiar", ""] ``` In local development or packaged desktop mode, FamiliarOS can use: ```jsonc "command": ["node", "", "mcp", ...] ``` Packaged and local `node ` commands require `node` to be available on OpenCode's `PATH`. Desktop setup reports an error instead of installing a bundled command if `node --version` cannot run from the agent environment. ### Instructions FamiliarOS adds the managed instruction file to OpenCode's `instructions` array: ```jsonc { "instructions": ["/familiaros.md"] } ``` That file contains: ```md ## FamiliarOS FamiliarOS MCP tools may be available. Use FamiliarOS as a short visible status channel for meaningful coding progress: - Use `familiaros_say` when starting, completing, blocking, or needing review on non-trivial work. - Keep messages brief, user-facing, and non-sensitive. - Do not include code, logs, secrets, URLs, or file paths. - Use `familiaros_react` for small visual or emotional feedback. - Use `familiaros_status` only when checking availability or the targeted familiar. - Do not spam every internal step. ``` ### Plugin FamiliarOS adds the OpenCode plugin: ```jsonc { "plugin": ["@familiaros/opencode@"] } ``` If a familiar is selected, the plugin entry includes options: ```jsonc { "plugin": [["@familiaros/opencode@", { "familiar": "" }]] } ``` The plugin id is: ```text familiaros-opencode ``` ## Setup behavior FamiliarOS global setup is conservative: - It parses OpenCode config as JSONC, not strict JSON. - It preserves unrelated user config. - It avoids unsafe symlinks and oversized files. - It writes temp files and backups before replacing existing files. - It refuses to overwrite custom FamiliarOS-like config entries that it cannot prove are managed by FamiliarOS. - It cleans up stale managed entries from any non-selected global config file when one selected config file becomes the owner. FamiliarOS chooses one global config file as the owner for the managed MCP, instructions, and plugin entries. It first prefers the effective owner of relevant array fields such as `plugin` or `instructions`, then reuses an existing managed FamiliarOS owner, then uses the highest-precedence existing global config file, and finally creates `opencode.jsonc` if none exists. Setup refuses ambiguous cases, such as plugin and instruction arrays living in different files, managed FamiliarOS entries appearing in multiple global files, or higher-precedence arrays shadowing lower-precedence user entries. Removal deletes only FamiliarOS-managed MCP, instructions, and plugin entries. It leaves unrelated OpenCode config intact. ## Project-local OpenCode setup The `@familiaros/cli` package can configure OpenCode inside a project: ```sh familiaros configure --agent opencode --familiar ``` Project-local setup writes inside the current project directory: ```text .opencode/opencode.jsonc .opencode/familiaros.md ``` If another supported OpenCode project config already exists, FamiliarOS updates that file instead of creating `.opencode/opencode.jsonc`. Project-local setup adds the same three kinds of entries as global setup: - `mcp.familiaros` using `@familiaros/cli mcp --familiar `. - `instructions` pointing to `.opencode/familiaros.md`. - `plugin` using `@familiaros/opencode` with `{ "familiar": "" }`. Project-local config and instructions can be committed to the repository and include the selected familiar id. That is useful for shared project identity, but users should review those files before committing. ## Runtime path When OpenCode calls an FamiliarOS MCP tool or when the OpenCode plugin reacts to an event, the runtime path is: ```text OpenCode -> @familiaros/cli mcp or @familiaros/opencode plugin -> @familiaros/client -> FamiliarOS desktop local IPC discovery file -> FamiliarOS desktop IPC socket/pipe -> default familiar controller or explicit agent familiar controller ``` The desktop app writes a discovery file such as: ```text macOS: ~/Library/Application Support/FamiliarOS/runtime/ipc.json Windows: %APPDATA%/FamiliarOS/runtime/ipc.json Linux: $XDG_RUNTIME_DIR/familiaros/ipc.json, or ~/.config/FamiliarOS/runtime/ipc.json fallback ``` That file contains the IPC endpoint and a per-run token. Clients must send that token with every request. ## MCP tools OpenCode sees these tools when the MCP entry is enabled: | Tool | Purpose | Desktop IPC method | | --- | --- | --- | | `familiaros_status` | Check whether FamiliarOS is reachable and which familiar is targeted. | `status` | | `familiaros_react` | Set a short reaction on the target familiar. | `familiar.react` | | `familiaros_say` | Show a short safe speech bubble, optionally with a reaction. | `familiar.say` | Depending on OpenCode's MCP naming, tool names may appear with a server prefix, for example `familiaros_familiaros_say`. The OpenCode plugin explicitly ignores FamiliarOS tool calls so the familiar does not react to its own status messages. `familiaros_say` is validated before it reaches the desktop app. Messages must be short, single-line, and must not look like code, logs, secrets, URLs, or file paths. ## Familiar targeting If the MCP command or plugin was configured without a familiar, events target the desktop default familiar. If configured with a familiar, FamiliarOS asks the desktop app for a lease: ```text lease.acquire({ requestedPetId: "" }) ``` Routing rules: - Missing `familiar` option -> default familiar. - Requested familiar is the default familiar or built-in familiar -> default familiar. - Invalid, missing, or broken familiar -> default familiar with a fallback reason. - Valid installed non-default familiar -> explicit agent familiar window. Explicit leases expire after a short TTL unless refreshed. The plugin caches and reuses a lease while it is still valid. When the last explicit lease for a familiar expires or is released, FamiliarOS clears and closes that agent familiar window. ## OpenCode plugin reaction and speech mapping The OpenCode plugin is decorative and best-effort. It schedules FamiliarOS work in the background and returns immediately so it does not slow down OpenCode hooks. If FamiliarOS is closed or IPC is unavailable, the plugin swallows the error. | OpenCode event/hook | Trigger condition | Reaction | Speech bubble | | --- | --- | --- | --- | | `chat.message` | OpenCode receives a chat message. | `thinking`, throttled. | None. | | `tool.execute.before` | Tool name contains `edit`, `write`, `patch`, or `apply_patch`. | `editing` | None. | | `tool.execute.before` | Tool name contains `bash`, `shell`, or `terminal`, and command args look test-like. | `testing` | None. | | `tool.execute.before` | Tool name contains `bash`, `shell`, or `terminal`, and command args do not look test-like. | None. | None. | | `tool.execute.before` | Any other non-FamiliarOS tool. | None. | None. | | `event` | Bus event type is `permission.asked` for a non-FamiliarOS tool. | `waiting` | `Approval needed`, throttled with a short cooldown. | | `event` | Bus event type is `session.error`. | `error` | One error-pool message, throttled. | | `event` | Bus event type is `session.status` and status type is `idle`. | `success`, throttled. | None. | | `tool.execute.after` | Any tool completed. | None | None. | | FamiliarOS MCP tool | Tool name is `familiaros_status`, `familiaros_say`, `familiaros_react`, or server-prefixed equivalent. | None | None. | | Unknown event | Event is not recognized. | None | None. | Test-like command detection is intentionally coarse and private. It may inspect only a bounded command string to classify the reaction. The command text is never sent to speech. OpenCode hooks do **not** use the left/right walking animations or the `running` reaction. The `running-right` and `running-left` sprite rows are reserved for familiar drag/move motion, and generic shell activity is silent by default. Hook reactions use quieter phase changes instead: editing maps to the generic active-work row, thinking maps to `review`, waiting/testing maps to `waiting`, success maps to `jumping`, and errors map to `failed`. ## Plugin speech pools When the plugin is allowed to speak, it chooses one static local message from the matching pool: | Category | Possible messages | | --- | --- | | `thinking` | `Thinking it through`, `Let me check`, `On it`, `Working it out` | | `success` | `Done`, `That worked`, `All set`, `Nice, finished` | | `error` | `Something failed`, `Needs another look`, `Hit a snag`, `Not quite there` | | `permission` | `Approval needed` | Speech is throttled by category so hooks do not spam bubbles. Current speech cooldowns are: | Category | Cooldown | | --- | --- | | `permission` | 3 seconds | | `thinking`, `success`, `error` | 20 seconds each | Normal thinking and success hooks no longer speak, so their speech pools are retained for shared/static message support but are not used by OpenCode's default quiet plugin policy. Reaction-only hooks are also deduped, with the same reaction suppressed for about 10 seconds. OpenCode stores throttle state separately from Claude in an OpenCode-specific file, for example: ```text Windows: %LOCALAPPDATA%/FamiliarOS/opencode-hook-throttle.json macOS/Linux: ${XDG_STATE_HOME:-~/.local/state}/familiaros/opencode-hook-throttle.json fallback: os.tmpdir()/familiaros-/opencode-hook-throttle.json ``` Throttle state stores only speech-category and reaction timestamp keys, never prompts, commands, tool input, output, code, logs, or transcripts. ## Safety rules - Plugin hooks return immediately and never await FamiliarOS IPC directly. - Plugin errors are swallowed unless debug logging is enabled. - Debug logs sanitize paths and secret-looking values. - Speech is static and local; it does not include model-generated text. - Tool args and command text are used only for coarse reaction classification. - The plugin ignores FamiliarOS MCP tools to avoid feedback loops. - Managed setup refuses unsafe symlinks, non-regular files, oversized config files, and conflicting custom FamiliarOS-like config.