Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 111 additions & 45 deletions hindsight-integrations/devin-desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,74 +2,140 @@

Long-term memory for **Devin Desktop** (the editor formerly known as Windsurf / Codeium), powered by [Hindsight](https://github.com/vectorize-io/hindsight).

`hindsight-devin-desktop init` wires the Hindsight **MCP server** into Devin Desktop's
`~/.codeium/windsurf/mcp_config.json` and adds an always-on recall/retain rule to
`.devin/rules/hindsight.md`. Devin then has `recall` / `retain` / `reflect`
tools and — guided by the rule — recalls relevant memory at the start of a task
and retains durable facts as it works.

> **Note:** Cognition rebranded Windsurf to Devin Desktop (June 2026). The MCP
> config path still lives under `~/.codeium/windsurf/` — that's Devin Desktop's
> on-disk data directory and is unchanged by the rebrand. The workspace rule now
> lives under `.devin/rules/` (with `.windsurf/rules/` kept as a legacy fallback).

## How it works

Devin Desktop supports two things this integration uses:

- **MCP servers** in `~/.codeium/windsurf/mcp_config.json` under `mcpServers`,
including **remote servers** via `serverUrl` with headers — so the Hindsight
MCP endpoint connects directly:

```json
{
"mcpServers": {
"hindsight": {
"serverUrl": "https://api.hindsight.vectorize.io/mcp/my-project/",
"headers": { "Authorization": "Bearer hsk_..." }
}
}
}
```

- **Workspace rules** in `.devin/rules/`. A rule file with `trigger: always_on`
frontmatter is applied to every Devin request in the workspace — that's where
the recall/retain rule lives.
`hindsight-devin-desktop init` wires the Hindsight **MCP server** into Devin
Desktop and adds always-on recall/retain rules, so the agent has `recall` /
`retain` / `reflect` tools and — guided by the rules — recalls relevant memory at
the start of a task and retains durable facts as it works.

> **Note:** Cognition rebranded Windsurf to Devin Desktop (June 2026). This
> integration configures **both agents Devin Desktop ships** (see below), so
> memory works whichever one you use.

## Two agents, both covered

Devin Desktop runs two agents with **separate** configuration, so `init` wires
each:

| | **Cascade** (legacy Windsurf agent) | **Devin Local** (the successor agent) |
| --- | --- | --- |
| MCP server | `~/.codeium/windsurf/mcp_config.json` (`serverUrl`) | `~/.config/devin/config.json` (`url` + `transport` + `headers`) |
| Tool approval | automatic | pre-seeded allow-rule (`mcp__hindsight__*`) so tools don't prompt |
| Per-project rule | `.devin/rules/hindsight.md` | repo-root `AGENTS.md` |
| Global rule | `~/.codeium/windsurf/memories/global_rules.md` | `~/.config/devin/AGENTS.md` |
| Auto-recall | — (rule-driven) | **`SessionStart` hook** injects memory deterministically |
| Retain nudge | — | **`Stop` hook** forces a retain pass before the session ends |
| Visibility | **`post_mcp_tool_use` banner** (`hooks.json`) | native tool cards + rule narration |

Configuring one agent does **not** surface the server in the other, so the
integration writes both. All the file edits are surgical — dedicated files, or a
fenced managed block inside shared files (`AGENTS.md`, `global_rules.md`,
`hooks.json`). Paths above are macOS/Linux; on Windows the Devin Local config
lives under `%APPDATA%\devin\` (Cascade stays under `~/.codeium/windsurf\`).

### Hooks (Devin Local) — deterministic memory, always visible

The MCP tools + rules are *model-driven* — the agent recalls/retains because the
rule tells it to. For Devin Local, `init` also adds two hooks (opt out of both
with `--no-hooks`):

- **`SessionStart` auto-recall** — recalls project + global memory and injects it
into the agent's context at the start of every session, so relevant memory
loads **even if the model forgets to call `recall`**. It **always reports
status** (loaded N / empty / unavailable) so memory use is never silent, and
never blocks a session.
- **`Stop` retain-nudge** — before the agent stops, it forces one retain pass
(the model decides *what* is durable and calls `retain`). Loop-guarded. Costs
one extra turn per session; opt out with `--no-retain-hook`.

**Why not fully-automatic retain?** Devin's hooks can't hand a script the
conversation transcript, so a hook can't summarize-and-retain on its own — the
nudge is the closest deterministic option (guaranteed *trigger*, model authors
the content). Cascade can't do either hook (its hooks can't inject context), so
recall/retain there stay model-driven — but `init` adds a `post_mcp_tool_use`
banner (`show_output`) so Cascade **visibly shows** every `🧠 Hindsight: <tool>
used`.

## Two-tier memory: global + per-project

Memory is split across two Hindsight **banks** (isolated memory scopes), so work
on one repo never bleeds into another:

- **Global bank** (`devin-desktop`) — your cross-project memory: preferences,
coding style, who you are. Shared across every project.
- **Project bank** (`devin-desktop-<slug>`) — this repository's memory:
architecture, decisions, conventions. The `<slug>` is derived from the repo's
**git remote**, so it's stable across machines and identical for teammates.

The MCP server runs in **multi-bank mode** (a single endpoint ending in `/mcp/`),
and the always-on rule tells the agent which `bank_id` to use — recall both banks
at task start, retain project facts to the project bank and user facts to the
global bank. An `X-Bank-Id` header names the global bank as the default.

### Opting out of the shared layer (`--no-global-bank`)

If you'd rather your preferences *not* follow you across repos, pass
`--no-global-bank` (local-only mode): everything — project facts **and** your
preferences — stays in that repo's project bank, nothing is shared, and the
global rule files aren't written. Good for keeping work and personal machines
separate, or when a shared profile isn't wanted.

## Install

```bash
pip install hindsight-devin-desktop
cd your-project
hindsight-devin-desktop init --api-token YOUR_HINDSIGHT_API_KEY --bank-id my-project
hindsight-devin-desktop init --api-token YOUR_HINDSIGHT_API_KEY
```

`init` merges the `mcpServers` entry into `~/.codeium/windsurf/mcp_config.json`
(Devin Desktop's single global MCP config) and writes the rule into
`./.devin/rules/hindsight.md`. Reload Devin Desktop (or refresh MCP servers) and
the `hindsight` tools are available.
`init` (run inside a repo) derives the project bank from your git remote and
wires **both agents**: the MCP server entries, the per-project rules (**commit
`./.devin/rules/hindsight.md` and `./AGENTS.md`** so teammates share the project
bank), and the global rules. Then **activate the server** in whichever agent you
use (config isn't hot-reloaded):

- **Cascade** — open the MCP panel and press **Refresh**.
- **Devin Local** — open the **Devin MCP Marketplace**, find `hindsight` under
**Installed**, and click **Connect**.

The `hindsight` tools then load and are used automatically. (Not sure which agent
you're on? Check the **agent selector** in the bottom-right of Devin Desktop.)

### Verify it's working

Start a session and you'll *see* memory is on — nothing is silent:

- **Devin Local** — the reply opens with a status line like
`🧠 Hindsight preloaded 3 memories for this session` (or `no memory yet`, or
`⚠️ memory unavailable this session`). Every hook run is also logged to
`~/.hindsight/devin-hook.log`, so you can confirm the recall/retain hooks fired.
- **Cascade** — each `recall`/`retain` shows as a tool card, and expanding the
`post-tool hooks` line reveals the `🧠 Hindsight: <tool> used` banner.
- `hindsight-devin-desktop status` lists every component as installed for both
agents, and the banks it resolved.

Use a [Hindsight Cloud](https://hindsight.vectorize.io) key, or a self-hosted
server with `--api-url http://localhost:8888` (no token needed for an open local
server). If `mcp_config.json` isn't plain JSON, `init` prints the snippet to
paste instead of touching the file — or run `hindsight-devin-desktop init --print-only`
anytime.
server). Pass `--bank-id <id>` to set the project bank explicitly, or
`--global-bank <id>` to change the cross-project bank. Run
`hindsight-devin-desktop init --print-only` to see everything it would write
without touching a file.

## Commands

| Command | Description |
| --- | --- |
| `hindsight-devin-desktop init` | Add the MCP server + recall/retain rule |
| `hindsight-devin-desktop status` | Show whether the server + rule are configured |
| `hindsight-devin-desktop uninstall` | Remove the server + rule |
| `hindsight-devin-desktop init` | Wire both agents' MCP server + memory rules (derives the project bank) |
| `hindsight-devin-desktop status` | Show resolved banks + whether each agent is configured |
| `hindsight-devin-desktop uninstall` | Remove the MCP server + memory rules from both agents |

## Configuration

| Setting | Env var | Default |
| --- | --- | --- |
| API URL | `HINDSIGHT_API_URL` | `https://api.hindsight.vectorize.io` |
| API token | `HINDSIGHT_API_TOKEN` | _(none; required for Cloud)_ |
| Bank id | `HINDSIGHT_DEVIN_DESKTOP_BANK_ID` | `devin-desktop` |
| Global bank | `HINDSIGHT_DEVIN_DESKTOP_GLOBAL_BANK` | `devin-desktop` |
| Project bank | `HINDSIGHT_DEVIN_DESKTOP_BANK_ID` | _(derived from git remote)_ |

## Development

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
"""Hindsight memory integration for Devin Desktop (formerly Windsurf / Codeium).

Wires the Hindsight MCP server into Devin Desktop's ``~/.codeium/windsurf/mcp_config.json``
and writes an always-on recall/retain rule into ``.devin/rules/hindsight.md``,
so Devin has ``recall``/``retain``/``reflect`` tools and uses them automatically.
Wires the Hindsight MCP server (multi-bank mode) into **both** agents Devin
Desktop ships — Cascade and Devin Local — and writes always-on memory rules for
each: a per-project rule naming this repo's bank plus a global rule for
cross-project memory. Both agents then have ``recall``/``retain``/``reflect``
tools and use them automatically, scoped per project.

CLI::

hindsight-devin-desktop init --api-token hsk_... --bank-id my-project
cd your-project
hindsight-devin-desktop init --api-token hsk_...
"""

__version__ = "0.1.0"
__version__ = "0.2.0"
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"""Cascade visibility banner via a ``post_mcp_tool_use`` hook.

Cascade renders a hook's stdout in its UI when ``show_output: true``. We register
a ``post_mcp_tool_use`` command hook that prints ``🧠 Hindsight: <tool> used`` so
every recall/retain is visibly obvious. Cascade has no per-hook matcher, so the
hook fires for all MCP tools and filters to the hindsight server in-script (see
:func:`hindsight_devin_desktop.hook.cmd_banner`).

Hooks live in ``~/.codeium/windsurf/hooks.json`` (same path on every OS for the
Devin Desktop / ex-Windsurf user tier). The file is shared with the user's own
hooks, so we manage only our single entry — identified by our command marker —
and never clobber theirs. We only rewrite the file when it parses as strict JSON.

(Devin Local uses a different, richer hook system — see
:mod:`hindsight_devin_desktop.devin_local`.)
"""

from __future__ import annotations

import json
import shlex
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Optional

EVENT = "post_mcp_tool_use"
HOOK_MODULE = "hindsight_devin_desktop.hook"
_MARKER = f"{HOOK_MODULE} banner"


def default_hooks_path() -> Path:
"""Cascade's user hooks file (``~/.codeium/windsurf/hooks.json``)."""
return Path.home() / ".codeium" / "windsurf" / "hooks.json"


def banner_command() -> str:
return f"{shlex.quote(sys.executable)} -m {HOOK_MODULE} banner"


def _our_entry() -> dict[str, Any]:
cmd = banner_command()
# `command` = bash (macOS/Linux), `powershell` = Windows; same invocation works in both.
return {"command": cmd, "powershell": cmd, "show_output": True}


def _is_ours(entry: Any) -> bool:
return isinstance(entry, dict) and _MARKER in (entry.get("command", "") + entry.get("powershell", ""))


@dataclass
class BannerResult:
action: str # created / merged / unchanged / removed / manual
path: Path


def _load_strict(path: Path) -> Optional[dict[str, Any]]:
if not path.is_file():
return None
try:
data = json.loads(path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
return None
return data if isinstance(data, dict) else None


def apply_banner(path: Path) -> BannerResult:
"""Add our ``post_mcp_tool_use`` banner hook to ``hooks.json`` (idempotent)."""
if not path.is_file():
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps({"hooks": {EVENT: [_our_entry()]}}, indent=2) + "\n", encoding="utf-8")
return BannerResult("created", path)

data = _load_strict(path)
if data is None:
return BannerResult("manual", path)

hooks = data.get("hooks")
if not isinstance(hooks, dict):
hooks = {}
entries = hooks.get(EVENT)
if not isinstance(entries, list):
entries = []
if any(_is_ours(e) for e in entries):
return BannerResult("unchanged", path)
entries.append(_our_entry())
hooks[EVENT] = entries
data["hooks"] = hooks
path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
return BannerResult("merged", path)


def remove_banner(path: Path) -> BannerResult:
"""Remove our banner hook from ``hooks.json``, preserving the user's own."""
data = _load_strict(path)
if data is None:
return BannerResult("manual" if path.is_file() else "unchanged", path)

hooks = data.get("hooks")
entries = hooks.get(EVENT) if isinstance(hooks, dict) else None
if not isinstance(entries, list) or not any(_is_ours(e) for e in entries):
return BannerResult("unchanged", path)

kept = [e for e in entries if not _is_ours(e)]
if kept:
hooks[EVENT] = kept
else:
hooks.pop(EVENT, None)
if not hooks:
data.pop("hooks", None)
path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
return BannerResult("removed", path)


def is_installed(path: Path) -> bool:
"""Whether our banner hook is present in ``hooks.json``."""
data = _load_strict(path)
hooks = data.get("hooks") if data else None
entries = hooks.get(EVENT) if isinstance(hooks, dict) else None
return isinstance(entries, list) and any(_is_ours(e) for e in entries)
Loading