diff --git a/packages/homeassistant/README.md b/packages/homeassistant/README.md index 268ca65..7963e83 100644 --- a/packages/homeassistant/README.md +++ b/packages/homeassistant/README.md @@ -210,6 +210,24 @@ To enable this, add the following to your config: | `permissionResponseEntity` | `input_text.opencode_permission_response` | Entity the plugin polls for the user's response | | `permissionTimeout` | `120` | Seconds to wait for a response before giving up | +> [!IMPORTANT] +> `${ENV_VAR}` expansion reads the **opencode process environment**, not your interactive shell. Exporting the variable in `.zshrc` is not enough when opencode is launched from a GUI app, a launch agent, or any non-login context, and opencode does not read `.env` files. If the variable is missing, remote replies are silently skipped: the notification still arrives, but tapping it does nothing. +> +> A reliable option is a mise `[env]` entry, which does reach opencode: +> +> ```toml +> # ~/.config/mise/config.local.toml (machine-local, untracked) +> [env] +> HA_LONG_LIVED_TOKEN = "eyJhbGciOi..." +> ``` +> +> To check what opencode actually sees, run it with `--log-level DEBUG` and answer a prompt. The plugin names the exact missing variable: +> +> ```text +> not polling because haToken references HA_LONG_LIVED_TOKEN, +> which is not set in the opencode process environment +> ``` + **How it works:** 1. Plugin sends the `waiting` webhook (with `waiting.id` set to the permission ID or question request ID) diff --git a/packages/homeassistant/src/index.ts b/packages/homeassistant/src/index.ts index 2cb345a..cf6115f 100644 --- a/packages/homeassistant/src/index.ts +++ b/packages/homeassistant/src/index.ts @@ -211,6 +211,29 @@ export const HomeAssistantPlugin: Plugin = async ({ client, directory }) => { ); staleSessionSweep.unref(); + function unsetTokenVars(): string[] { + if (!config.haToken) return []; + return [...config.haToken.matchAll(/\$\{([^}]+)}/g)] + .map((match) => match[1] as string) + .filter((name) => !process.env[name]); + } + + function describeUnsetTokenVars(names: string[]): string { + return `haToken references ${names.join(', ')}, which ${ + names.length > 1 ? 'are' : 'is' + } not set in the opencode process environment`; + } + + function describeMissingHaConfig(): string | undefined { + if (!config.haApiUrl) return 'haApiUrl is not set'; + if (!config.haToken) return 'haToken is not set'; + if (resolveEnvVars(config.haToken)) return undefined; + + const unset = unsetTokenVars(); + if (unset.length > 0) return describeUnsetTokenVars(unset); + return 'haToken resolved to an empty value'; + } + function resolveHaConfig(): { apiUrl: string; token: string; entity: string } | undefined { if (!config.haApiUrl || !config.haToken) return undefined; const token = resolveEnvVars(config.haToken); @@ -226,12 +249,20 @@ export const HomeAssistantPlugin: Plugin = async ({ client, directory }) => { const ha = resolveHaConfig(); if (!ha) { await report( - `${requestId}: Home Assistant config unavailable (haApiUrl, haToken or ${'${ENV_VAR}'} expansion missing), not polling`, + `${requestId}: not polling because ${describeMissingHaConfig() ?? 'the Home Assistant config is unavailable'}`, undefined, ); return { kind: 'no-config' }; } + const partiallyUnset = unsetTokenVars(); + if (partiallyUnset.length > 0) { + await report( + `${requestId}: ${describeUnsetTokenVars(partiallyUnset)}, so the token is incomplete`, + undefined, + ); + } + const timeoutMs = (config.permissionTimeout ?? DEFAULT_PERMISSION_TIMEOUT) * 1000; const deadline = Date.now() + timeoutMs; activePermissionPolls.add(requestId);