diff --git a/README.md b/README.md index f14ddd98fa..40917a9995 100644 --- a/README.md +++ b/README.md @@ -283,6 +283,11 @@ Host integrations are always part of uninstall. If a broken or unavailable host prevents safe cleanup, the command names the blocking plugin manager or configuration, gives exact manual cleanup instructions, and stops before deleting the binary. Complete that cleanup and rerun uninstall. +It also keeps the binary when an agent's config file can't be read (another +tool's too), or when a hook it did not install still runs Plannotator. The +error names the file, the tool, and what to do. A `plannotator-inbox` entry +you added yourself, or a Claude Code or Droid plugin installed for one project, +is named with how to remove it and does not keep the binary. These mechanics keep the ordinary confirmation default-negative, make the irreversible outcome require a stronger explicit word, and still give package managers and scripts a conventional non-interactive flag. diff --git a/apps/marketing/src/content/docs/getting-started/installation.md b/apps/marketing/src/content/docs/getting-started/installation.md index 7f05c62717..adec2b2f64 100644 --- a/apps/marketing/src/content/docs/getting-started/installation.md +++ b/apps/marketing/src/content/docs/getting-started/installation.md @@ -201,6 +201,11 @@ Host integrations are always part of uninstall. If a broken or unavailable host prevents safe cleanup, the command names the blocking plugin manager or configuration, gives exact manual cleanup instructions, and stops before deleting the binary. Complete that cleanup and rerun uninstall. +It also keeps the binary when an agent's config file can't be read (another +tool's too), or when a hook it did not install still runs Plannotator. The +error names the file, the tool, and what to do. A `plannotator-inbox` entry +you added yourself, or a Claude Code or Droid plugin installed for one project, +is named with how to remove it and does not keep the binary. The purge removes only known Plannotator entries from the configured data directory. Unknown top-level files are preserved rather than guessed at, and diff --git a/apps/marketing/src/content/docs/reference/inbox.md b/apps/marketing/src/content/docs/reference/inbox.md index 6f02864dd5..47af3989c0 100644 --- a/apps/marketing/src/content/docs/reference/inbox.md +++ b/apps/marketing/src/content/docs/reference/inbox.md @@ -88,7 +88,7 @@ Each entry runs ` inbox mcp`. The agent uses it from it - Editors (VS Code, Zed, JetBrains, Goose) are never changed. Settings has a one-click link or a snippet for each. - Qwen Code's `settings.json` may hold comments: Plannotator keeps every one, and adds only its own entry. It never uses `qwen mcp add`, which writes your other servers back with their `${VAR}` values filled in (a token kept in your environment would end up in the file). A file that is not valid JSON or is read-only is left as it was, and the terminal says why. A relative `QWEN_HOME` names a different folder in each project, so you connect Qwen Code by hand then. If the Inbox starts with another `QWEN_HOME` than the one Qwen Code was connected in, Plannotator leaves both folders as they are and tells you once. **Settings → Agents** shows Qwen Code off there, with the folder it is connected in; turn it on to move it. If the new folder's settings.json can't take it, nothing changes and the reason is shown. - `PLANNOTATOR_INBOX_CONNECT=0` skips this step. -- `plannotator uninstall` removes every entry it recorded adding (never one you added yourself). `--dry-run` lists them first. If its record (`~/.plannotator/inbox/agents.json`) is gone, it removes none: an entry that runs this Plannotator is named with how to remove it, since you may have added it by hand. +- `plannotator uninstall` removes every entry it recorded adding (never one you added yourself). `--dry-run` lists them first. If its record (`~/.plannotator/inbox/agents.json`) is gone, it removes none. Any `plannotator-inbox` entry that still runs Plannotator and that it did not add is named with how to remove it (for Claude Code's `~/.claude.json`, `claude mcp remove plannotator-inbox -s user`), and the `plannotator` command is still removed. If an agent's config file can't be read (not valid JSON or TOML, not UTF-8, or permission denied), uninstall leaves the file as it is and keeps the `plannotator` command. It names the file, the agent and the reason, and says what to do before you run it again. ### A note in each agent's instructions @@ -395,7 +395,7 @@ The Inbox stores the files agents attached as they were when sent, the messages plannotator uninstall --purge ``` -Both forms remove the `plannotator-inbox` entries `plannotator inbox` added to your agents (an entry you changed to run something else is left, and named). `--purge` also stops a running Inbox first, then deletes `~/.plannotator/inbox/` with the rest of the Plannotator data. If the Inbox does not stop, uninstall still removes the plugins, skills, hooks and config entries, but keeps your Plannotator data (`~/.plannotator`) and the `plannotator` binary. Quit the Inbox and run the command again to finish. Add `--dry-run` to see what would be removed. +Both forms remove the `plannotator-inbox` entries `plannotator inbox` added to your agents (an entry you changed to run something else is left, and named; so is one you added yourself). An agent config uninstall can't read keeps the `plannotator` binary until you fix it (see [Your agents are connected for you](#your-agents-are-connected-for-you)). `--purge` also stops a running Inbox first, then deletes `~/.plannotator/inbox/` with the rest of the Plannotator data. If the Inbox does not stop, uninstall still removes the plugins, skills, hooks and config entries, but keeps your Plannotator data (`~/.plannotator`) and the `plannotator` binary. Quit the Inbox and run the command again to finish. Add `--dry-run` to see what would be removed. ## Troubleshooting diff --git a/docs/agent-guides/inbox-foundation.md b/docs/agent-guides/inbox-foundation.md index 2acc3785ea..db814a0efa 100644 --- a/docs/agent-guides/inbox-foundation.md +++ b/docs/agent-guides/inbox-foundation.md @@ -63,7 +63,7 @@ One long-lived local server per machine. Agents leave messages, `:::question` bl - When: a person's `plannotator inbox` (with or without `--no-open`), never `--background`, the shim, a connection that starts a stopped Inbox, or the window's Restart (`spawnDetachedInbox` marks those `PLANNOTATOR_INBOX_DETACHED=1`, scrubbed at start; `PLANNOTATOR_INBOX_LOCK_HELD=1` alone counts as detached too, because an Inbox of 0.28.17 or older spawns its Restart into an updated binary, and every other detached start, with only that marker). Starting the Inbox: `inbox.connectAgents()` once it listens, off the request path, its lines on stderr when done. An Inbox already running (an agent started it): the command opens the window, then `POST /api/inbox/control/connect` (connection-route guards: loopback Host, no Origin, the registry token; an older Inbox answers 404 and nothing happens); the step then runs in that Inbox's process, with the environment (PATH, home) the agent started it with. `PLANNOTATOR_INBOX_CONNECT=0` in either environment skips it. One run at a time (`connectAgents` is single-flight); stdout is unchanged. - What: every agent found (the install script's rules: its command on PATH, or its folder; Cursor also by `cursor` / `cursor-agent`, Kiro by `kiro-cli`, Codex by `$CODEX_HOME` holding config, a relative one resolved against the working folder as Codex does) with no record in `inbox/agents.json` gets a `plannotator-inbox` MCP server running ` inbox mcp`, through its own `mcp add` where it has one (Codex `codex mcp add … --`, Gemini `gemini mcp add -s user` (`$GEMINI_CLI_HOME` honored), Windsurf/Devin `devin mcp add -s user … --`, Amp, Pi 0.99+ without the extension `pi mcp add`), each only after its `--version` line has that CLI's own shape (`VERSION_SHAPES`: a program that merely shares the name, cln's `pi`, amp.rs, is never handed it; a build older than its `mcp add` is left to the person: Pi before 0.99, which would read the words as a chat prompt, Codex CLI before 0.35.0 and Gemini CLI before 0.1.19), else one key in a JSON file (`mcpServers` in `~/.cursor/mcp.json`, `~/.kiro/settings/mcp.json`, the Claude desktop app's `claude_desktop_config.json`, and Qwen Code's `$QWEN_HOME/settings.json`; created when missing). JSON edits are lossless (`packages/server/json-edit.ts`: only that member's text is inserted or cut, in the file's own style and dominant line ending, BOM kept; a file that is not strict JSON or has a duplicate key on the path is refused with the reason; Qwen's settings.json is read as JSON with comments, as Qwen reads it, and every comment stays: a new member goes after a comment that ends the last member's line, a removal cuts the member and one comma only). Add then remove gives the file back byte for byte: the record keeps the add's exact text change (`undo: { before, after }` on the agent's record in `agents.json`), and removal puts `before` back while `after` is still in the file untouched, so an empty file stays empty, a final newline or its absence stays, and a container Plannotator made goes; a file Plannotator created (`created`) is deleted once only the empty containers are left; an entry the person edited is cut alone; written at a symlink's target, atomic, a no-op when nothing changes (agent-files.ts). Codex's config.toml is read with `Bun.TOML.parse` (every TOML form). Pi with Plannotator's extension (Pi's `settings.json` `packages`) and OpenCode with its plugin (the global `opencode.json(c)` `plugin`, or `plugins` strings or `{ package }`): `inboxTool. = true` in config.json, unless config.json already decides it (false is recorded `off`) or `PLANNOTATOR_INBOX_TOOL` does (nothing written). Claude Code: nothing (said "already on" when the plugin is enabled). Left to the person (`manual`): Codex or Gemini found without its CLI, Qwen with a relative `QWEN_HOME`, Pi before 0.99, a CLI whose version line is not its own, Cline (its `mcp add` is not verified), OpenCode without the plugin. Editors (VS Code, Zed, JetBrains, Goose) are never written. Qwen Code is never connected through `qwen mcp add` / `remove`: Qwen writes the whole `mcpServers` back as it resolved it, `$VAR` / `${VAR}` filled in from its environment, so a person's token would land in the file. Plannotator edits the file itself (`qwenWay`, `QWEN_SETTINGS_RULES`): a file that does not parse or is read-only is left as it is (said with the reason, tried again next run). An entry Plannotator added is checked and removed in the file its record names (`recordedFile`), not the one the environment names now. Places are compared as the file system names them (`samePlace`: the real path of the deepest existing folder, the rest as written), so a symlinked `~/.qwen`, `/tmp` and `/private/tmp`, a trailing slash, or another case on a volume that ignores case (the real path has the case on disk) are one place. Case is never folded by Plannotator: on a case-sensitive volume (Linux, or one so formatted on macOS) `qwen` and `Qwen` are two folders. When `QWEN_HOME` names another place than the add (set in one shell, not in another: an accident of how the Inbox was started), a run reads the recorded file only: an entry gone from it is removed outside, as anywhere; one still there is left with both files untouched, and said once per pair of places (`elsewhere` in the report, `elsewhere_said`, kept when a run from the matching shell comes between, cleared when Settings acts; a run whose `QWEN_HOME` matches drops the other shell's line from the band, and so does any Settings switch of that agent). Settings → Agents shows it off, state `elsewhere`: "Connected in . Turn on to move it here." Uninstall cleans the recorded file. Only the person moves it, by turning it on: the entry is added in the new home first (a relative `QWEN_HOME`, another program named `qwen`, a new settings.json that does not parse or is read-only: refused, nothing changed), then taken out of the recorded file; when that cannot be done (read-only, no longer parses), the new entry is taken back out, so only the recorded one is ever left (if even that fails, both stay, said, and the new one is kept on the record as a `stray` that uninstall removes too). Strays stay recorded through later Settings switches: turning it on where a stray is records that entry as added (the old one moved out), turning it off removes the strays too (one that cannot go stays recorded, said), and uninstall forgets an agent's record only when every entry of it went. A stray is checked as the agent's own entry is: one gone is nothing to do, one the person replaced with their own is theirs (named by uninstall, never touched, no longer recorded). A `qwen` on PATH is still asked its `--version` (any Qwen Code version will do; another program named `qwen` is left to the person), run in the home folder (`cwd`), so Qwen never migrates or resets the Inbox's working folder's `.qwen/settings.json`. The agent CLIs run with no stdin in their own process group; the time limit kills the group, and the answer comes at exit even if a grandchild keeps the output open. - Rules: an agent with a record is never added again by itself (turned off, removed outside Plannotator: `removed_outside_at`, checked read-only each run; foreign). An entry is ours when it runs exactly this CLI's command or the one its record says Plannotator wrote; for "already connected", also any `plannotator` binary or checkout running `inbox mcp` (`isPlannotatorInboxCommand`: the program's name is exactly `plannotator`, not `my-plannotator-proxy`). Anything else named `plannotator-inbox` is never replaced and is recorded `foreign`. A refused file or a failing or hung `mcp add` is tried again next run and said again only when its reason changes (`failures` in agents.json). Limits: 10 s per agent, 30 s in all (agents still going are `unfinished`). A test run (PLANNOTATOR_TEST_SANDBOX) whose home is not a scratch folder connects nothing (`agentHomeAllowed`). -- Said once: the first run "Connected Codex, Gemini CLI and Cursor to the Inbox (Claude Code was already on)." plus "Agents use it from their next session. PLANNOTATOR_INBOX_CONNECT=0 skips this step; plannotator uninstall removes what it added."; a later run "Connected Amp to the Inbox (new on this computer)."; failures and foreign entries name the file and reason (`formatConnectReport`); an agent only a person can connect (no CLI on PATH, Pi before 0.99, Cline) is named with its reason ("Pi needs connecting by hand: …"), once per reason (`manual_said` in `agents.json`, `manual_why` on the report); time limits are said as they are (`formatSeconds`: "1.5 s"). The run's report is kept in `agents.json` (`last_report`, `seen: false` when it has news) and served at `GET /api/inbox/agents` (local only; records with `~` paths). Uninstall removes only entries Plannotator recorded as `added` that still run the command it wrote; with no record at all (`agents.json` lost) nothing is removed, and an entry that runs exactly the binary being uninstalled is named in a warning with how to remove it, since the person may have added it by hand; one the person added (`found`) is left. +- Said once: the first run "Connected Codex, Gemini CLI and Cursor to the Inbox (Claude Code was already on)." plus "Agents use it from their next session. PLANNOTATOR_INBOX_CONNECT=0 skips this step; plannotator uninstall removes what it added."; a later run "Connected Amp to the Inbox (new on this computer)."; failures and foreign entries name the file and reason (`formatConnectReport`); an agent only a person can connect (no CLI on PATH, Pi before 0.99, Cline) is named with its reason ("Pi needs connecting by hand: …"), once per reason (`manual_said` in `agents.json`, `manual_why` on the report); time limits are said as they are (`formatSeconds`: "1.5 s"). The run's report is kept in `agents.json` (`last_report`, `seen: false` when it has news) and served at `GET /api/inbox/agents` (local only; records with `~` paths). Uninstall removes only entries Plannotator recorded as `added` that still run the command it wrote; with no record at all (`agents.json` lost) nothing is removed, and an entry that runs exactly the binary being uninstalled is named in a warning with how to remove it, since the person may have added it by hand; one the person added (`found`) is left. After that, every agent's file is read once more (`remainingInboxReferences`, `packages/server/uninstall-inbox-references.ts`, skipping the agents the plan already removed or named): any other `plannotator-inbox` that runs an install path, bare `plannotator`, or a source checkout's `inbox mcp` is named in a warning with how to remove it (`inboxEntryRemovalHint`; Claude Code's `~/.claude.json`: `claude mcp remove plannotator-inbox -s user`, never a hand edit of a file Claude Code rewrites while it runs). These are warnings, never a reason to keep the CLI. This check writes nothing, so it reads every agent's JSON leniently (comments and trailing commas, `RawReadOptions.lenient`), Cursor's, Kiro's and the Claude app's too. An agent's file that still cannot be read (not JSON / TOML, not UTF-8, permission denied) does keep it: uninstall cannot tell what it runs. The error names the file, the agent, the reason, that Plannotator did not change it, and the next step. Each file is said once per run. A file over 1 MiB (Claude Code's `~/.claude.json` holds its history) is first searched raw, a chunk at a time, for the text `plannotator-inbox` (`agentFileContains` in `agent-files.ts`): a file without it is never decoded or parsed, whatever it holds. One that has it is parsed if it is at most 64 MiB (`UNINSTALL_PARSE_MAX_BYTES`); a bigger one is not read, and a warning says so with how to remove the entry, never a reason to keep the CLI. Not covered: a key spelled with JSON `\u` escapes. - Tests: `packages/server/inbox-agents.test.ts` (fake agent CLIs on a scratch PATH and HOME, `tests/helpers/fake-agents.ts`; the real CLIs are never run), `packages/server/inbox-agents-qwen.test.ts` (Qwen: comments and a person's `${VAR}` kept byte for byte, `qwen --version` run in the home folder, `QWEN_HOME` from the environment or Qwen's `.env` files, a relative one, a foreign `qwen`, turned off, a malformed, trailing-comma or read-only settings.json, removal), `packages/server/inbox-connect-route.test.ts`, `apps/hook/server/inbox-connect-cli.test.ts` (the CLI as processes), the uninstall block in `packages/server/uninstall.test.ts`. The guard test also fails on a path from the real home to an agent's folder and on connecting agents with no agent environment (`agentHomeFindings`, `agentConnectFindings`). **The note in each agent's instructions** (`packages/server/inbox-agent-notes.ts`; decision 5). @@ -102,7 +102,7 @@ One long-lived local server per machine. Agents leave messages, `:::question` bl - **The release page's redirect** on github.com is read whatever the case of the owner and repository in its path; a mirror (`PLANNOTATOR_UPDATE_LATEST_URL`) is read exactly. - **The words.** Settings → General and About say "Up to date" only after a successful check in the last 24 h with checks on (`checkStatusWords`): otherwise "Update checks are off", "Can't check for updates since … · last tried …", "Last check failed