From aac336901d179738418698196daed47080de5d1c Mon Sep 17 00:00:00 2001 From: DK09876 Date: Mon, 22 Jun 2026 10:32:02 -0700 Subject: [PATCH 1/3] feat(windsurf): add Windsurf (Codeium) integration via MCP Config-only CLI that wires the Hindsight MCP server into Windsurf's ~/.codeium/windsurf/mcp_config.json (mcpServers, remote serverUrl + auth header) and writes an always-on recall/retain rule to .windsurf/rules/hindsight.md (trigger: always_on). Cascade then has recall/retain/reflect and uses them automatically. - hindsight_windsurf: config, mcp_config (strict-JSON parse-or-print), rules (dedicated sentinel-marked file), cli (init/status/uninstall) - 25 unit tests + gated live-MCP-endpoint E2E - CI job, release + changelog registries, docs page, icon, README row Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/test.yml | 43 ++++ .../hindsight_dev/generate_changelog.py | 1 + hindsight-docs/docs-integrations/windsurf.md | 42 ++++ hindsight-docs/src/data/integrations.json | 10 + hindsight-docs/static/img/icons/windsurf.svg | 5 + hindsight-integrations/README.md | 1 + hindsight-integrations/windsurf/README.md | 79 ++++++++ .../windsurf/hindsight_windsurf/__init__.py | 12 ++ .../windsurf/hindsight_windsurf/cli.py | 172 ++++++++++++++++ .../windsurf/hindsight_windsurf/config.py | 77 ++++++++ .../windsurf/hindsight_windsurf/mcp_config.py | 130 ++++++++++++ .../windsurf/hindsight_windsurf/py.typed | 0 .../windsurf/hindsight_windsurf/rules.py | 59 ++++++ .../windsurf/pyproject.toml | 47 +++++ .../windsurf/tests/__init__.py | 0 .../windsurf/tests/test_cli.py | 71 +++++++ .../windsurf/tests/test_config.py | 34 ++++ .../windsurf/tests/test_e2e.py | 60 ++++++ .../windsurf/tests/test_mcp_config.py | 95 +++++++++ .../windsurf/tests/test_rules.py | 49 +++++ hindsight-integrations/windsurf/uv.lock | 185 ++++++++++++++++++ scripts/release-integration.sh | 2 +- 22 files changed, 1173 insertions(+), 1 deletion(-) create mode 100644 hindsight-docs/docs-integrations/windsurf.md create mode 100644 hindsight-docs/static/img/icons/windsurf.svg create mode 100644 hindsight-integrations/windsurf/README.md create mode 100644 hindsight-integrations/windsurf/hindsight_windsurf/__init__.py create mode 100644 hindsight-integrations/windsurf/hindsight_windsurf/cli.py create mode 100644 hindsight-integrations/windsurf/hindsight_windsurf/config.py create mode 100644 hindsight-integrations/windsurf/hindsight_windsurf/mcp_config.py create mode 100644 hindsight-integrations/windsurf/hindsight_windsurf/py.typed create mode 100644 hindsight-integrations/windsurf/hindsight_windsurf/rules.py create mode 100644 hindsight-integrations/windsurf/pyproject.toml create mode 100644 hindsight-integrations/windsurf/tests/__init__.py create mode 100644 hindsight-integrations/windsurf/tests/test_cli.py create mode 100644 hindsight-integrations/windsurf/tests/test_config.py create mode 100644 hindsight-integrations/windsurf/tests/test_e2e.py create mode 100644 hindsight-integrations/windsurf/tests/test_mcp_config.py create mode 100644 hindsight-integrations/windsurf/tests/test_rules.py create mode 100644 hindsight-integrations/windsurf/uv.lock diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b6492bdeb..017c91caf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,6 +59,7 @@ jobs: integrations-lockfiles: ${{ steps.filter.outputs.integrations-lockfiles }} integrations-openai-agents: ${{ steps.filter.outputs.integrations-openai-agents }} integrations-openhands: ${{ steps.filter.outputs.integrations-openhands }} + integrations-windsurf: ${{ steps.filter.outputs.integrations-windsurf }} integrations-pipecat: ${{ steps.filter.outputs.integrations-pipecat }} integrations-agentcore: ${{ steps.filter.outputs.integrations-agentcore }} integrations-smolagents: ${{ steps.filter.outputs.integrations-smolagents }} @@ -189,6 +190,8 @@ jobs: - 'hindsight-integrations/openai-agents/**' integrations-openhands: - 'hindsight-integrations/openhands/**' + integrations-windsurf: + - 'hindsight-integrations/windsurf/**' integrations-pipecat: - 'hindsight-integrations/pipecat/**' integrations-agentcore: @@ -3787,6 +3790,45 @@ jobs: # (requires_real_llm) needs a live Hindsight server and runs separately. run: uv run pytest tests -v -m "not requires_real_llm" + test-windsurf-integration: + needs: [detect-changes] + if: >- + (github.event_name == 'workflow_dispatch' || + needs.detect-changes.outputs.integrations-windsurf == 'true' || + needs.detect-changes.outputs.ci == 'true') + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ github.event.pull_request.head.sha || '' }} + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + prune-cache: false + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version-file: ".python-version" + + - name: Build windsurf integration + working-directory: ./hindsight-integrations/windsurf + run: uv build + + - name: Install dependencies + working-directory: ./hindsight-integrations/windsurf + run: uv sync --frozen + + - name: Run tests + working-directory: ./hindsight-integrations/windsurf + # PR CI runs only the deterministic bucket; the real-LLM E2E bucket + # (requires_real_llm) needs a live Hindsight server and runs separately. + run: uv run pytest tests -v -m "not requires_real_llm" + test-claude-agent-sdk-integration: needs: [detect-changes] if: >- @@ -4837,6 +4879,7 @@ jobs: - test-llamaindex-integration - test-openai-agents-integration - test-openhands-integration + - test-windsurf-integration - test-agentcore-integration - test-haystack-integration - test-pip-slim diff --git a/hindsight-dev/hindsight_dev/generate_changelog.py b/hindsight-dev/hindsight_dev/generate_changelog.py index 152846fb2..3ed6bcdce 100644 --- a/hindsight-dev/hindsight_dev/generate_changelog.py +++ b/hindsight-dev/hindsight_dev/generate_changelog.py @@ -80,6 +80,7 @@ class IntegrationMeta: "continue": IntegrationMeta("hindsight-continue", "Continue"), "zed": IntegrationMeta("hindsight-zed", "Zed"), "openhands": IntegrationMeta("hindsight-openhands", "OpenHands"), + "windsurf": IntegrationMeta("hindsight-windsurf", "Windsurf"), } VALID_INTEGRATIONS = list(INTEGRATIONS.keys()) diff --git a/hindsight-docs/docs-integrations/windsurf.md b/hindsight-docs/docs-integrations/windsurf.md new file mode 100644 index 000000000..7a276910d --- /dev/null +++ b/hindsight-docs/docs-integrations/windsurf.md @@ -0,0 +1,42 @@ +--- +sidebar_position: 7 +title: "Windsurf Persistent Memory with Hindsight | Integration" +description: "Add long-term memory to Windsurf (Codeium) with Hindsight via MCP. One command wires the Hindsight MCP server into mcp_config.json plus an always-on recall/retain rule, so memory works automatically in Cascade." +--- + +# Windsurf + +Long-term memory for [Windsurf](https://windsurf.com) (Codeium), powered by [Hindsight](https://vectorize.io/hindsight). One command connects Cascade to the Hindsight MCP server and adds a rule telling the agent to use it — so it recalls relevant memory at the start of a task and retains durable facts as it goes. Recall happens at query time against your actual message, and from your seat it's automatic. + +## How It Works + +Windsurf supports two things this integration uses: + +- **MCP servers:** Windsurf runs MCP servers configured under `mcpServers` in `~/.codeium/windsurf/mcp_config.json` and surfaces their tools in Cascade. Remote servers connect via a `serverUrl` field with optional headers, so the Hindsight MCP endpoint connects directly — no bridge needed — giving the agent `recall` / `retain` / `reflect` tools. +- **Workspace rules** in `.windsurf/rules/`. A rule file with `trigger: always_on` frontmatter is included in every Cascade request in the workspace. The integration writes a small rule there telling the agent to recall first and retain what it learns. + +## Setup + +```bash +pip install hindsight-windsurf +cd your-project +hindsight-windsurf init --api-token YOUR_HINDSIGHT_API_KEY --bank-id my-memory +``` + +`init` adds the `hindsight` MCP server to `~/.codeium/windsurf/mcp_config.json` (Windsurf's single global MCP config) and writes the recall/retain rule to `./.windsurf/rules/hindsight.md`. Reload Windsurf (or refresh MCP servers in Cascade), and the `hindsight` server's tools become available. + +Use a [Hindsight Cloud](https://hindsight.vectorize.io) key, or point at a self-hosted server with `--api-url http://localhost:8888` (no token needed for an open local server). If your `mcp_config.json` isn't plain JSON, `init` prints the entry to paste rather than rewriting the file — or run `hindsight-windsurf init --print-only` anytime. + +## Commands + +| Command | Description | +| --- | --- | +| `hindsight-windsurf init` | Add the MCP server + recall/retain rule | +| `hindsight-windsurf status` | Show whether the server + rule are configured | +| `hindsight-windsurf uninstall` | Remove the server + rule | + +## Note + +Recall and retain run through MCP tools the agent calls, guided by the always-on rule. This makes recall query-time precise (no lag), with the tradeoff that it relies on the agent following the "recall first" instruction rather than the editor enforcing it. + +See the [package README](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/windsurf) for full configuration options. diff --git a/hindsight-docs/src/data/integrations.json b/hindsight-docs/src/data/integrations.json index ae614839f..e05238b21 100644 --- a/hindsight-docs/src/data/integrations.json +++ b/hindsight-docs/src/data/integrations.json @@ -549,6 +549,16 @@ "category": "tool", "link": "/sdks/integrations/openhands", "icon": "/img/icons/openhands.png" + }, + { + "id": "windsurf", + "name": "Windsurf", + "description": "Long-term memory for Windsurf (Codeium) via its native MCP support. One command wires the Hindsight MCP server into mcp_config.json plus an always-on recall/retain rule, so memory works automatically in Cascade.", + "type": "official", + "by": "hindsight", + "category": "tool", + "link": "/sdks/integrations/windsurf", + "icon": "/img/icons/windsurf.svg" } ] } diff --git a/hindsight-docs/static/img/icons/windsurf.svg b/hindsight-docs/static/img/icons/windsurf.svg new file mode 100644 index 000000000..f675a68d0 --- /dev/null +++ b/hindsight-docs/static/img/icons/windsurf.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/hindsight-integrations/README.md b/hindsight-integrations/README.md index ef939e2ec..13fb79b5d 100644 --- a/hindsight-integrations/README.md +++ b/hindsight-integrations/README.md @@ -13,6 +13,7 @@ Each integration lives in its own subdirectory with its own README, configuratio | [**Codex CLI**](./codex) | Python hook scripts for OpenAI's Codex CLI. Auto-recall on `UserPromptSubmit`, auto-retain on `Stop`. | `curl -fsSL https://hindsight.vectorize.io/get-codex \| bash` | | [**Cursor CLI**](./cursor-cli) | Python hook scripts for Cursor CLI. Auto-recall on `beforeSubmitPrompt`, auto-retain on `stop`, final flush on `sessionEnd`. | `./scripts/install.sh` | | [**Continue.dev**](./continue) | HTTP context provider for precise `@hindsight` recall in chat, plus optional MCP-server + rules for automatic recall/retain in agent mode. | `pip install hindsight-continue` | +| [**Windsurf**](./windsurf) | Native MCP server config + always-on recall/retain rule for Windsurf (Codeium) Cascade. | `pip install hindsight-windsurf` | | [**Roo Code**](./roo-code) | Persistent memory for Roo Code VS Code extension. | See README | | [**Hermes (OpenAI Agents SDK)**](./hermes) | Memory layer for OpenAI Agents SDK. | See README | | [**Grok Build**](./grok-build) | Hooks for Grok Build (xAI). | See README | diff --git a/hindsight-integrations/windsurf/README.md b/hindsight-integrations/windsurf/README.md new file mode 100644 index 000000000..461a6e1b8 --- /dev/null +++ b/hindsight-integrations/windsurf/README.md @@ -0,0 +1,79 @@ +# hindsight-windsurf + +Long-term memory for **Windsurf** (Codeium), powered by [Hindsight](https://github.com/vectorize-io/hindsight). + +`hindsight-windsurf init` wires the Hindsight **MCP server** into Windsurf's +`~/.codeium/windsurf/mcp_config.json` and adds an always-on recall/retain rule to +`.windsurf/rules/hindsight.md`. Cascade 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. + +## How it works + +Windsurf 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 `.windsurf/rules/`. A rule file with `trigger: always_on` + frontmatter is applied to every Cascade request in the workspace — that's where + the recall/retain rule lives. + +## Install + +```bash +pip install hindsight-windsurf +cd your-project +hindsight-windsurf init --api-token YOUR_HINDSIGHT_API_KEY --bank-id my-project +``` + +`init` merges the `mcpServers` entry into `~/.codeium/windsurf/mcp_config.json` +(Windsurf's single global MCP config) and writes the rule into +`./.windsurf/rules/hindsight.md`. Reload Windsurf (or refresh MCP servers in +Cascade) and the `hindsight` tools are available. + +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-windsurf init --print-only` +anytime. + +## Commands + +| Command | Description | +| --- | --- | +| `hindsight-windsurf init` | Add the MCP server + recall/retain rule | +| `hindsight-windsurf status` | Show whether the server + rule are configured | +| `hindsight-windsurf uninstall` | Remove the server + rule | + +## 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_WINDSURF_BANK_ID` | `windsurf` | + +## Development + +```bash +uv sync +uv run pytest tests -v -m 'not requires_real_llm' # deterministic suite +uv run pytest tests -v -m requires_real_llm # gated MCP-endpoint check +``` + +## License + +MIT diff --git a/hindsight-integrations/windsurf/hindsight_windsurf/__init__.py b/hindsight-integrations/windsurf/hindsight_windsurf/__init__.py new file mode 100644 index 000000000..240f9af91 --- /dev/null +++ b/hindsight-integrations/windsurf/hindsight_windsurf/__init__.py @@ -0,0 +1,12 @@ +"""Hindsight memory integration for Windsurf (Codeium). + +Wires the Hindsight MCP server into Windsurf's ``~/.codeium/windsurf/mcp_config.json`` +and writes an always-on recall/retain rule into ``.windsurf/rules/hindsight.md``, +so Cascade has ``recall``/``retain``/``reflect`` tools and uses them automatically. + +CLI:: + + hindsight-windsurf init --api-token hsk_... --bank-id my-project +""" + +__version__ = "0.1.0" diff --git a/hindsight-integrations/windsurf/hindsight_windsurf/cli.py b/hindsight-integrations/windsurf/hindsight_windsurf/cli.py new file mode 100644 index 000000000..11863a4ee --- /dev/null +++ b/hindsight-integrations/windsurf/hindsight_windsurf/cli.py @@ -0,0 +1,172 @@ +"""CLI for the Hindsight Windsurf integration. + +``hindsight-windsurf init`` wires the Hindsight MCP server into Windsurf's +``~/.codeium/windsurf/mcp_config.json`` and writes an always-on recall/retain +rule into ``.windsurf/rules/hindsight.md``. Cascade then exposes +``recall``/``retain``/``reflect`` and (via the rule) uses them automatically. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Optional + +from . import __version__ +from .config import USER_CONFIG_FILE, WindsurfConfig, load_config +from .mcp_config import ( + McpResult, + apply_to_mcp, + build_http_server, + default_mcp_path, + remove_from_mcp, + render_snippet, +) +from .mcp_config import is_installed as server_installed +from .rules import RULE_TEXT, clear_rule, default_rules_path, write_rule +from .rules import is_installed as rule_installed + + +@dataclass +class InstallOutcome: + mcp: McpResult + rules_path: Path + + +def build_install(config: WindsurfConfig, mcp_path: Path, rules_path: Path) -> InstallOutcome: + """Apply the MCP server entry and the recall/retain rule (the testable core).""" + server = build_http_server(config.hindsight_api_url, config.hindsight_api_token, config.bank_id) + mcp = apply_to_mcp(mcp_path, server) + write_rule(rules_path) + return InstallOutcome(mcp=mcp, rules_path=rules_path) + + +def _resolve_config(args: argparse.Namespace) -> WindsurfConfig: + cfg = load_config(config_file=_user_config_path(args)) + if args.api_url: + cfg.hindsight_api_url = args.api_url + if args.api_token: + cfg.hindsight_api_token = args.api_token + if args.bank_id: + cfg.bank_id = args.bank_id + return cfg + + +def _user_config_path(args: argparse.Namespace) -> Path: + return Path(args.user_config_path) if args.user_config_path else USER_CONFIG_FILE + + +def _mcp_path(args: argparse.Namespace) -> Path: + return Path(args.mcp_path) if args.mcp_path else default_mcp_path() + + +def _rules_path(args: argparse.Namespace) -> Path: + return Path(args.rules_path) if args.rules_path else default_rules_path() + + +def _scaffold_user_config(cfg: WindsurfConfig, path: Path) -> None: + if path.is_file(): + return + data = {"hindsightApiUrl": cfg.hindsight_api_url, "bankId": cfg.bank_id} + if cfg.hindsight_api_token: + data["hindsightApiToken"] = cfg.hindsight_api_token + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8") + + +def cmd_init(args: argparse.Namespace) -> None: + cfg = _resolve_config(args) + mcp_path = _mcp_path(args) + rules_path = _rules_path(args) + server = build_http_server(cfg.hindsight_api_url, cfg.hindsight_api_token, cfg.bank_id) + + if args.print_only: + print("Add this to your ~/.codeium/windsurf/mcp_config.json:\n") + print(render_snippet(server)) + print("\nAnd save this rule as .windsurf/rules/hindsight.md:\n") + print(RULE_TEXT) + return + + print("Setting up Hindsight for Windsurf ...") + _scaffold_user_config(cfg, _user_config_path(args)) + outcome = build_install(cfg, mcp_path, rules_path) + + if outcome.mcp.action == "manual": + print(f" Your {outcome.mcp.path} isn't plain JSON, so I won't rewrite it.") + print(" Add this `mcpServers` entry yourself:\n") + print(render_snippet(server)) + else: + verb = {"created": "Created", "merged": "Updated", "unchanged": "Already configured in"}[outcome.mcp.action] + print(f" {verb} {outcome.mcp.path} (MCP server: hindsight -> bank '{cfg.bank_id}')") + print(f" Wrote always-on recall/retain rule to {outcome.rules_path}") + print("\nDone. Reload Windsurf (or refresh MCP servers in Cascade) and the") + print("hindsight MCP tools (recall/retain/reflect) are available + used automatically.") + + +def cmd_status(args: argparse.Namespace) -> None: + mcp_path = _mcp_path(args) + rules_path = _rules_path(args) + print(f"MCP server in {mcp_path}: {'installed' if server_installed(mcp_path) else 'not installed'}") + print(f"Recall/retain rule in {rules_path}: {'installed' if rule_installed(rules_path) else 'not installed'}") + + +def cmd_uninstall(args: argparse.Namespace) -> None: + mcp_path = _mcp_path(args) + rules_path = _rules_path(args) + result = remove_from_mcp(mcp_path) + if result.action == "manual": + print(f" {mcp_path} isn't plain JSON — remove the `hindsight` server entry yourself.") + elif result.action == "removed": + print(f" Removed the hindsight MCP server from {mcp_path}") + else: + print(f" No hindsight MCP server found in {mcp_path}") + clear_rule(rules_path) + print(f" Removed the recall/retain rule at {rules_path}") + + +def _add_overrides(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + "--mcp-path", default=None, help="mcp_config.json path (default: ~/.codeium/windsurf/mcp_config.json)" + ) + parser.add_argument( + "--rules-path", default=None, help="rule file path (default: ./.windsurf/rules/hindsight.md)" + ) + parser.add_argument("--user-config-path", default=None, help=argparse.SUPPRESS) + + +def main(argv: Optional[list[str]] = None) -> int: + parser = argparse.ArgumentParser( + prog="hindsight-windsurf", description="Hindsight memory for Windsurf (Codeium, via MCP)" + ) + parser.add_argument("--version", action="version", version=f"hindsight-windsurf {__version__}") + sub = parser.add_subparsers(dest="command") + + init_p = sub.add_parser("init", help="Configure Windsurf's MCP server + recall/retain rule") + init_p.add_argument("--api-url", default=None, help="Hindsight API URL (default: cloud)") + init_p.add_argument("--api-token", default=None, help="Hindsight API token (for Cloud)") + init_p.add_argument("--bank-id", default=None, help="Memory bank for the MCP server (default: windsurf)") + init_p.add_argument("--print-only", action="store_true", help="Print the config to add manually; write nothing") + _add_overrides(init_p) + init_p.set_defaults(func=cmd_init) + + status_p = sub.add_parser("status", help="Show whether the MCP server + rule are configured") + _add_overrides(status_p) + status_p.set_defaults(func=cmd_status) + + uninst_p = sub.add_parser("uninstall", help="Remove the MCP server + rule") + _add_overrides(uninst_p) + uninst_p.set_defaults(func=cmd_uninstall) + + args = parser.parse_args(argv) + if not hasattr(args, "func"): + parser.print_help() + return 1 + args.func(args) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/hindsight-integrations/windsurf/hindsight_windsurf/config.py b/hindsight-integrations/windsurf/hindsight_windsurf/config.py new file mode 100644 index 000000000..7644a41bd --- /dev/null +++ b/hindsight-integrations/windsurf/hindsight_windsurf/config.py @@ -0,0 +1,77 @@ +"""Configuration for the Hindsight Windsurf integration. + +Settings layer (later wins): built-in defaults -> ``~/.hindsight/windsurf.json`` +-> environment variables. Resolved into a typed :class:`WindsurfConfig`. + +The integration is configuration-only: it wires the Hindsight MCP server into +Windsurf's ``~/.codeium/windsurf/mcp_config.json`` and writes an always-on +recall/retain rule into ``.windsurf/rules/hindsight.md`` (which Cascade applies +to every request in the workspace). Memory operations run through the MCP server +at runtime. +""" + +from __future__ import annotations + +import json +import os +from dataclasses import dataclass +from pathlib import Path +from typing import Optional + +DEFAULT_HINDSIGHT_API_URL = "https://api.hindsight.vectorize.io" +DEFAULT_BANK_ID = "windsurf" + +USER_CONFIG_FILE = Path.home() / ".hindsight" / "windsurf.json" + + +@dataclass +class WindsurfConfig: + """Resolved configuration for the Windsurf MCP setup.""" + + hindsight_api_url: str = DEFAULT_HINDSIGHT_API_URL + hindsight_api_token: Optional[str] = None + # The memory bank the MCP server is scoped to (the last path segment of the + # MCP endpoint URL). + bank_id: str = DEFAULT_BANK_ID + + +_FILE_KEYS = { + "hindsightApiUrl": "hindsight_api_url", + "hindsightApiToken": "hindsight_api_token", + "bankId": "bank_id", +} + +_ENV_KEYS = { + "HINDSIGHT_API_URL": "hindsight_api_url", + "HINDSIGHT_API_TOKEN": "hindsight_api_token", + "HINDSIGHT_WINDSURF_BANK_ID": "bank_id", +} + + +def load_config(config_file: Optional[Path] = None, env: Optional[dict] = None) -> WindsurfConfig: + """Load and resolve configuration from file then environment.""" + cfg = WindsurfConfig() + env = os.environ if env is None else env + + path = config_file if config_file is not None else USER_CONFIG_FILE + if path.is_file(): + try: + data = json.loads(path.read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError): + data = {} + for key, attr in _FILE_KEYS.items(): + value = data.get(key) + if value: + setattr(cfg, attr, str(value)) + + for key, attr in _ENV_KEYS.items(): + value = env.get(key) + if value: + setattr(cfg, attr, str(value)) + + if not cfg.hindsight_api_url: + cfg.hindsight_api_url = DEFAULT_HINDSIGHT_API_URL + if not cfg.bank_id: + cfg.bank_id = DEFAULT_BANK_ID + + return cfg diff --git a/hindsight-integrations/windsurf/hindsight_windsurf/mcp_config.py b/hindsight-integrations/windsurf/hindsight_windsurf/mcp_config.py new file mode 100644 index 000000000..62c51a650 --- /dev/null +++ b/hindsight-integrations/windsurf/hindsight_windsurf/mcp_config.py @@ -0,0 +1,130 @@ +"""Wire Hindsight into Windsurf's MCP config (``~/.codeium/windsurf/mcp_config.json``). + +Windsurf's Cascade reads MCP servers from ``~/.codeium/windsurf/mcp_config.json`` +under the ``mcpServers`` key. For a remote server it uses a ``serverUrl`` field +(plus optional ``headers``), so the Hindsight MCP endpoint connects with no +bridge:: + + { + "mcpServers": { + "hindsight": { + "serverUrl": "https://api.hindsight.vectorize.io/mcp//", + "headers": { "Authorization": "Bearer hsk_..." } + } + } + } + +Unlike VS Code, Windsurf has no project-local MCP file — ``mcp_config.json`` is +a single global file. We only edit it in place when it parses as strict JSON; +otherwise we return the exact snippet to paste, never risking the user's file. +""" + +from __future__ import annotations + +import json +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Optional + +SERVER_NAME = "hindsight" + + +def default_mcp_path() -> Path: + """The global Windsurf MCP config (``~/.codeium/windsurf/mcp_config.json``).""" + return Path.home() / ".codeium" / "windsurf" / "mcp_config.json" + + +def mcp_endpoint_url(api_url: str, bank_id: str) -> str: + """The Hindsight MCP endpoint for a bank (bank is the last path segment).""" + return f"{api_url.rstrip('/')}/mcp/{bank_id}/" + + +def build_http_server(api_url: str, api_token: Optional[str], bank_id: str) -> dict[str, Any]: + """Build the ``mcpServers.hindsight`` entry for ``mcp_config.json``. + + A remote MCP server pointing at the Hindsight endpoint via ``serverUrl``, + with a Bearer auth header when a token is set (omitted for an open + self-hosted server). + """ + server: dict[str, Any] = {"serverUrl": mcp_endpoint_url(api_url, bank_id)} + if api_token: + server["headers"] = {"Authorization": f"Bearer {api_token}"} + return server + + +def render_snippet(server: dict[str, Any]) -> str: + """Render the snippet a user can paste into ``mcp_config.json``.""" + return json.dumps({"mcpServers": {SERVER_NAME: server}}, indent=2) + + +@dataclass +class McpResult: + """Outcome of editing ``mcp_config.json``. + + ``action`` is one of ``created``, ``merged``, ``unchanged``, ``removed``, or + ``manual`` (file isn't strict JSON we'll rewrite — ``snippet`` holds what to + paste). + """ + + action: str + path: Path + snippet: Optional[str] = None + + +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_to_mcp(path: Path, server: dict[str, Any]) -> McpResult: + """Add/update ``mcpServers.hindsight`` in ``mcp_config.json`` at ``path``.""" + if not path.is_file(): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps({"mcpServers": {SERVER_NAME: server}}, indent=2) + "\n", encoding="utf-8") + return McpResult("created", path) + + data = _load_strict(path) + if data is None: + return McpResult("manual", path, snippet=render_snippet(server)) + + servers = data.get("mcpServers") + if not isinstance(servers, dict): + servers = {} + if servers.get(SERVER_NAME) == server: + return McpResult("unchanged", path) + servers[SERVER_NAME] = server + data["mcpServers"] = servers + path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8") + return McpResult("merged", path) + + +def remove_from_mcp(path: Path) -> McpResult: + """Remove ``mcpServers.hindsight`` from ``mcp_config.json`` at ``path``.""" + data = _load_strict(path) + if data is None: + return McpResult("manual" if path.is_file() else "unchanged", path) + + servers = data.get("mcpServers") + if not isinstance(servers, dict) or SERVER_NAME not in servers: + return McpResult("unchanged", path) + del servers[SERVER_NAME] + if servers: + data["mcpServers"] = servers + else: + data.pop("mcpServers", None) + path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8") + return McpResult("removed", path) + + +def is_installed(path: Path) -> bool: + """Whether our server is present in ``mcp_config.json`` at ``path``.""" + data = _load_strict(path) + if data is None: + return False + servers = data.get("mcpServers") + return isinstance(servers, dict) and SERVER_NAME in servers diff --git a/hindsight-integrations/windsurf/hindsight_windsurf/py.typed b/hindsight-integrations/windsurf/hindsight_windsurf/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/hindsight-integrations/windsurf/hindsight_windsurf/rules.py b/hindsight-integrations/windsurf/hindsight_windsurf/rules.py new file mode 100644 index 000000000..7c66184ed --- /dev/null +++ b/hindsight-integrations/windsurf/hindsight_windsurf/rules.py @@ -0,0 +1,59 @@ +"""Write Hindsight's recall/retain rule into ``.windsurf/rules/hindsight.md``. + +Windsurf applies workspace rule files under ``.windsurf/rules/``. A file with +``trigger: always_on`` frontmatter is included in every Cascade request in the +workspace, so the rule tells Cascade to use the Hindsight MCP tools — recall +relevant memory at the start of a task, and retain durable facts. + +The rule lives in its own dedicated file, so we own the whole file: a sentinel +comment marks it as ours for idempotent update/removal without touching any +other rule the user has authored. +""" + +from __future__ import annotations + +from pathlib import Path + +SENTINEL = "" + +FRONTMATTER = "---\ntrigger: always_on\n---" + +RULE_TEXT = ( + "You have persistent long-term memory through the Hindsight MCP server " + "(`recall`, `retain`, and `reflect` tools).\n\n" + "- At the start of each task, call `recall` with the user's request to load " + "relevant decisions, preferences, and project context before you act. " + "Use what's relevant and ignore the rest.\n" + "- When you learn a durable fact — an architectural decision, a user " + "preference, a convention, or anything worth remembering across sessions — " + "call `retain` to store it.\n" + "- Do not mention these memory operations unless the user asks about them." +) + + +def default_rules_path() -> Path: + """The workspace ``.windsurf/rules/hindsight.md`` (always-on in Cascade).""" + return Path.cwd() / ".windsurf" / "rules" / "hindsight.md" + + +def render_rule(rule_text: str = RULE_TEXT) -> str: + """The full contents of the dedicated rule file.""" + return f"{FRONTMATTER}\n\n{SENTINEL}\n{rule_text.strip()}\n" + + +def write_rule(path: Path, rule_text: str = RULE_TEXT) -> Path: + """Write (or replace) Hindsight's dedicated rule file at ``path``.""" + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(render_rule(rule_text), encoding="utf-8") + return path + + +def clear_rule(path: Path) -> Path: + """Delete Hindsight's rule file if it's ours (carries our sentinel).""" + if path.is_file() and SENTINEL in path.read_text(encoding="utf-8"): + path.unlink() + return path + + +def is_installed(path: Path) -> bool: + return path.is_file() and SENTINEL in path.read_text(encoding="utf-8") diff --git a/hindsight-integrations/windsurf/pyproject.toml b/hindsight-integrations/windsurf/pyproject.toml new file mode 100644 index 000000000..7acb0693c --- /dev/null +++ b/hindsight-integrations/windsurf/pyproject.toml @@ -0,0 +1,47 @@ +[project] +name = "hindsight-windsurf" +version = "0.1.0" +description = "Windsurf (Codeium) integration for Hindsight - persistent long-term memory via MCP" +readme = "README.md" +requires-python = ">=3.10" +license = { text = "MIT" } +authors = [{ name = "Vectorize", email = "support@vectorize.io" }] +keywords = ["ai", "memory", "windsurf", "codeium", "cascade", "agents", "hindsight", "mcp"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Scientific/Engineering :: Artificial Intelligence", +] +dependencies = [] + +[project.scripts] +hindsight-windsurf = "hindsight_windsurf.cli:main" + +[project.urls] +Homepage = "https://github.com/vectorize-io/hindsight" +Documentation = "https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/windsurf" +Repository = "https://github.com/vectorize-io/hindsight" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["hindsight_windsurf"] + +[tool.ruff] +line-length = 120 + +[tool.pytest.ini_options] +testpaths = ["tests"] +markers = [ + "requires_real_llm: end-to-end test that needs live external services (a running Hindsight server and/or real LLM provider keys). Excluded from the deterministic PR-CI bucket via -m 'not requires_real_llm'; run on its own via -m requires_real_llm.", +] + +[dependency-groups] +dev = ["pytest>=9.0.2", "ruff>=0.8.0"] diff --git a/hindsight-integrations/windsurf/tests/__init__.py b/hindsight-integrations/windsurf/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/hindsight-integrations/windsurf/tests/test_cli.py b/hindsight-integrations/windsurf/tests/test_cli.py new file mode 100644 index 000000000..615b12d2d --- /dev/null +++ b/hindsight-integrations/windsurf/tests/test_cli.py @@ -0,0 +1,71 @@ +"""Tests for the CLI (init/status/uninstall).""" + +import json + +from hindsight_windsurf.cli import build_install, main +from hindsight_windsurf.config import WindsurfConfig +from hindsight_windsurf.mcp_config import SERVER_NAME +from hindsight_windsurf.mcp_config import is_installed as server_installed +from hindsight_windsurf.rules import is_installed as rule_installed + + +class TestBuildInstall: + def test_writes_mcp_and_rule(self, tmp_path): + mcp = tmp_path / "mcp_config.json" + rules = tmp_path / "rules" / "hindsight.md" + cfg = WindsurfConfig( + hindsight_api_url="https://api.hindsight.vectorize.io", hindsight_api_token="k", bank_id="proj" + ) + outcome = build_install(cfg, mcp, rules) + assert outcome.mcp.action == "created" + server = json.loads(mcp.read_text())["mcpServers"][SERVER_NAME] + assert server["serverUrl"] == "https://api.hindsight.vectorize.io/mcp/proj/" + assert server["headers"]["Authorization"] == "Bearer k" + assert rule_installed(rules) + + +class TestMain: + def _common(self, tmp_path): + return [ + "--mcp-path", + str(tmp_path / "mcp_config.json"), + "--rules-path", + str(tmp_path / "rules" / "hindsight.md"), + "--user-config-path", + str(tmp_path / "user.json"), + ] + + def test_init_status_uninstall(self, tmp_path, capsys): + common = self._common(tmp_path) + assert main(["init", "--api-url", "http://localhost:8888", "--bank-id", "b", *common]) == 0 + assert server_installed(tmp_path / "mcp_config.json") + assert rule_installed(tmp_path / "rules" / "hindsight.md") + main(["status", *common]) + assert "installed" in capsys.readouterr().out + main(["uninstall", *common]) + assert not server_installed(tmp_path / "mcp_config.json") + assert not (tmp_path / "rules" / "hindsight.md").exists() + + def test_print_only_writes_nothing(self, tmp_path, capsys): + mcp = tmp_path / "mcp_config.json" + rules = tmp_path / "rules" / "hindsight.md" + rc = main( + [ + "init", + "--print-only", + "--api-url", + "http://localhost:8888", + "--mcp-path", + str(mcp), + "--rules-path", + str(rules), + "--user-config-path", + str(tmp_path / "user.json"), + ] + ) + assert rc == 0 + assert not mcp.exists() and not rules.exists() + assert "mcpServers" in capsys.readouterr().out + + def test_no_command_returns_1(self): + assert main([]) == 1 diff --git a/hindsight-integrations/windsurf/tests/test_config.py b/hindsight-integrations/windsurf/tests/test_config.py new file mode 100644 index 000000000..2233da5ee --- /dev/null +++ b/hindsight-integrations/windsurf/tests/test_config.py @@ -0,0 +1,34 @@ +"""Tests for config loading.""" + +import json + +from hindsight_windsurf.config import DEFAULT_BANK_ID, DEFAULT_HINDSIGHT_API_URL, load_config + + +def test_defaults(tmp_path): + cfg = load_config(config_file=tmp_path / "missing.json", env={}) + assert cfg.hindsight_api_url == DEFAULT_HINDSIGHT_API_URL + assert cfg.hindsight_api_token is None + assert cfg.bank_id == DEFAULT_BANK_ID + + +def test_file_values(tmp_path): + p = tmp_path / "windsurf.json" + p.write_text(json.dumps({"hindsightApiToken": "t", "bankId": "proj"})) + cfg = load_config(config_file=p, env={}) + assert cfg.hindsight_api_token == "t" + assert cfg.bank_id == "proj" + + +def test_env_overrides_file(tmp_path): + p = tmp_path / "windsurf.json" + p.write_text(json.dumps({"bankId": "from-file"})) + cfg = load_config(config_file=p, env={"HINDSIGHT_WINDSURF_BANK_ID": "from-env", "HINDSIGHT_API_TOKEN": "k"}) + assert cfg.bank_id == "from-env" + assert cfg.hindsight_api_token == "k" + + +def test_malformed_file_falls_back(tmp_path): + p = tmp_path / "windsurf.json" + p.write_text("{ broken") + assert load_config(config_file=p, env={}).bank_id == DEFAULT_BANK_ID diff --git a/hindsight-integrations/windsurf/tests/test_e2e.py b/hindsight-integrations/windsurf/tests/test_e2e.py new file mode 100644 index 000000000..3c18311da --- /dev/null +++ b/hindsight-integrations/windsurf/tests/test_e2e.py @@ -0,0 +1,60 @@ +"""Gated MCP-endpoint E2E (requires_real_llm).""" + +from __future__ import annotations + +import json +import os +import urllib.request + +import pytest + +from hindsight_windsurf.mcp_config import mcp_endpoint_url + +HINDSIGHT_API_URL = os.getenv("HINDSIGHT_API_URL", "http://localhost:8888") +HINDSIGHT_API_TOKEN = os.getenv("HINDSIGHT_API_TOKEN") + + +def _reachable() -> bool: + try: + with urllib.request.urlopen(f"{HINDSIGHT_API_URL}/health", timeout=3) as r: + return r.status == 200 + except Exception: + return False + + +pytestmark = [ + pytest.mark.requires_real_llm, + pytest.mark.skipif(not _reachable(), reason=f"Hindsight not reachable at {HINDSIGHT_API_URL}"), +] + + +def _rpc(url, payload, session=None): + req = urllib.request.Request(url, data=json.dumps(payload).encode(), method="POST") + req.add_header("Content-Type", "application/json") + req.add_header("Accept", "application/json, text/event-stream") + if session: + req.add_header("Mcp-Session-Id", session) + if HINDSIGHT_API_TOKEN: + req.add_header("Authorization", f"Bearer {HINDSIGHT_API_TOKEN}") + return urllib.request.urlopen(req, timeout=15) + + +def test_mcp_endpoint_lists_memory_tools(): + url = mcp_endpoint_url(HINDSIGHT_API_URL, "windsurf-e2e") + init = { + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": {"name": "windsurf-e2e", "version": "0"}, + }, + } + resp = _rpc(url, init) + session = resp.headers.get("Mcp-Session-Id") + resp.read() + _rpc(url, {"jsonrpc": "2.0", "method": "notifications/initialized"}, session=session).read() + resp = _rpc(url, {"jsonrpc": "2.0", "id": 2, "method": "tools/list"}, session=session) + text = resp.read().decode("utf-8", "replace") + assert "recall" in text and "retain" in text, f"tools/list missing memory tools: {text[:300]}" diff --git a/hindsight-integrations/windsurf/tests/test_mcp_config.py b/hindsight-integrations/windsurf/tests/test_mcp_config.py new file mode 100644 index 000000000..2c88d83b2 --- /dev/null +++ b/hindsight-integrations/windsurf/tests/test_mcp_config.py @@ -0,0 +1,95 @@ +"""Tests for the mcp_config.json mcpServers writer.""" + +import json + +from hindsight_windsurf.mcp_config import ( + SERVER_NAME, + apply_to_mcp, + build_http_server, + is_installed, + mcp_endpoint_url, + remove_from_mcp, + render_snippet, +) + + +class TestBuildServer: + def test_endpoint_url_embeds_bank(self): + assert mcp_endpoint_url("https://api.hindsight.vectorize.io", "proj") == ( + "https://api.hindsight.vectorize.io/mcp/proj/" + ) + assert mcp_endpoint_url("http://localhost:8888/", "b") == "http://localhost:8888/mcp/b/" + + def test_cloud_server_uses_serverurl_with_auth_header(self): + s = build_http_server("https://api.hindsight.vectorize.io", "hsk_abc", "proj") + assert s["serverUrl"] == "https://api.hindsight.vectorize.io/mcp/proj/" + assert s["headers"] == {"Authorization": "Bearer hsk_abc"} + assert "type" not in s and "url" not in s + + def test_open_server_omits_headers(self): + s = build_http_server("http://localhost:8888", None, "proj") + assert s == {"serverUrl": "http://localhost:8888/mcp/proj/"} + assert "headers" not in s + + +class TestApply: + def test_creates_file(self, tmp_path): + path = tmp_path / "mcp_config.json" + s = build_http_server("https://api.hindsight.vectorize.io", "k", "b") + result = apply_to_mcp(path, s) + assert result.action == "created" + assert json.loads(path.read_text())["mcpServers"][SERVER_NAME] == s + + def test_merges_preserves_other_servers(self, tmp_path): + path = tmp_path / "mcp_config.json" + path.write_text(json.dumps({"mcpServers": {"other": {"command": "x"}}})) + s = build_http_server("https://api.hindsight.vectorize.io", "k", "b") + result = apply_to_mcp(path, s) + assert result.action == "merged" + data = json.loads(path.read_text()) + assert data["mcpServers"]["other"] == {"command": "x"} # untouched + assert data["mcpServers"][SERVER_NAME] == s + + def test_unchanged_when_identical(self, tmp_path): + path = tmp_path / "mcp_config.json" + s = build_http_server("https://api.hindsight.vectorize.io", "k", "b") + apply_to_mcp(path, s) + assert apply_to_mcp(path, s).action == "unchanged" + + def test_non_json_returns_manual(self, tmp_path): + path = tmp_path / "mcp_config.json" + original = "{ not json at all" + path.write_text(original) + s = build_http_server("https://api.hindsight.vectorize.io", "k", "b") + result = apply_to_mcp(path, s) + assert result.action == "manual" + assert result.snippet and SERVER_NAME in result.snippet + assert path.read_text() == original # untouched + + +class TestRemoveAndStatus: + def test_remove_only_our_entry(self, tmp_path): + path = tmp_path / "mcp_config.json" + path.write_text(json.dumps({"mcpServers": {"other": {"command": "x"}, SERVER_NAME: {"serverUrl": "u"}}})) + result = remove_from_mcp(path) + assert result.action == "removed" + servers = json.loads(path.read_text())["mcpServers"] + assert SERVER_NAME not in servers and "other" in servers + + def test_remove_drops_empty_servers(self, tmp_path): + path = tmp_path / "mcp_config.json" + path.write_text(json.dumps({"mcpServers": {SERVER_NAME: {"serverUrl": "u"}}})) + remove_from_mcp(path) + data = json.loads(path.read_text()) + assert "mcpServers" not in data + + def test_is_installed(self, tmp_path): + path = tmp_path / "mcp_config.json" + s = build_http_server("https://api.hindsight.vectorize.io", "k", "b") + assert is_installed(path) is False + apply_to_mcp(path, s) + assert is_installed(path) is True + + def test_render_snippet_valid_json(self): + s = build_http_server("https://api.hindsight.vectorize.io", "k", "b") + assert json.loads(render_snippet(s))["mcpServers"][SERVER_NAME] == s diff --git a/hindsight-integrations/windsurf/tests/test_rules.py b/hindsight-integrations/windsurf/tests/test_rules.py new file mode 100644 index 000000000..a0810ce3e --- /dev/null +++ b/hindsight-integrations/windsurf/tests/test_rules.py @@ -0,0 +1,49 @@ +"""Tests for the .windsurf/rules/hindsight.md rule writer.""" + +from hindsight_windsurf.rules import RULE_TEXT, SENTINEL, clear_rule, is_installed, render_rule, write_rule + + +def test_write_creates_dedicated_file(tmp_path): + path = tmp_path / "hindsight.md" + write_rule(path) + text = path.read_text() + assert SENTINEL in text and "recall" in text and "retain" in text + assert is_installed(path) + + +def test_always_on_frontmatter(tmp_path): + path = tmp_path / "hindsight.md" + write_rule(path) + text = path.read_text() + # Frontmatter must lead the file and declare always-on activation. + assert text.startswith("---\n") + assert "trigger: always_on" in text.split("---", 2)[1] + + +def test_write_is_idempotent(tmp_path): + path = tmp_path / "hindsight.md" + write_rule(path) + first = path.read_text() + write_rule(path) + assert path.read_text() == first + assert path.read_text().count(SENTINEL) == 1 + + +def test_clear_deletes_our_file(tmp_path): + path = tmp_path / "hindsight.md" + write_rule(path) + clear_rule(path) + assert not path.exists() + + +def test_clear_leaves_foreign_file(tmp_path): + path = tmp_path / "hindsight.md" + path.write_text("---\ntrigger: always_on\n---\n\nSomeone else's rule.\n") + clear_rule(path) + assert path.exists() # no sentinel -> not ours -> untouched + + +def test_render_rule_mentions_all_tools(): + rendered = render_rule() + for tool in ("recall", "retain", "reflect"): + assert tool in RULE_TEXT and tool in rendered diff --git a/hindsight-integrations/windsurf/uv.lock b/hindsight-integrations/windsurf/uv.lock new file mode 100644 index 000000000..83b71a1eb --- /dev/null +++ b/hindsight-integrations/windsurf/uv.lock @@ -0,0 +1,185 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "hindsight-windsurf" +version = "0.1.0" +source = { editable = "." } + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=9.0.2" }, + { name = "ruff", specifier = ">=0.8.0" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/98/1295ad5a5aa9bc85bdcdfa5d82fe7b49c61af5657df4f227637ff9de0da6/ruff-0.15.18.tar.gz", hash = "sha256:2698a964c70e8bf402dcb99c8810472d270d141e7aa8c4e13599fd52033a2f33", size = 4761437, upload-time = "2026-06-18T18:25:39.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/d0/686e984941269621e2be72612d5c1e461f8f7b38415a2a7d7a81c8ae6715/ruff-0.15.18-py3-none-linux_armv6l.whl", hash = "sha256:8b6850172348c8381b8b3084c5915a4393c2373b9b54cd5b5e1ea15812bc10df", size = 10887308, upload-time = "2026-06-18T18:25:03.062Z" }, + { url = "https://files.pythonhosted.org/packages/ed/21/bc4123e3f5515ee99f8ce1eb93a14a0628fe4d1678663cd08f933ac16931/ruff-0.15.18-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3fccc153a85417dcd976883160cacce486997b0a0058dd18f54b8aaaac7d1ce2", size = 11281305, upload-time = "2026-06-18T18:25:30.026Z" }, + { url = "https://files.pythonhosted.org/packages/51/93/4769464c25cf7ab2acb3c7dda9cad3d867eb41c59565b3e2a9d17249c90c/ruff-0.15.18-py3-none-macosx_11_0_arm64.whl", hash = "sha256:08d4c86a68f2c3ec2c9d56380a71fb4a4f65373055cbb8caabd645e9102f38d4", size = 10641215, upload-time = "2026-06-18T18:25:15.802Z" }, + { url = "https://files.pythonhosted.org/packages/6c/42/56926d17120db2c208d76bf60a1a019644dd9e91dc27f0f95c9caddb1366/ruff-0.15.18-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37e5108745c2c0705da916d7d4de533ddf547051ef45f62888c31bae73f66318", size = 10957224, upload-time = "2026-06-18T18:25:36.955Z" }, + { url = "https://files.pythonhosted.org/packages/22/4f/d43fab8d8189afde803103022d000a8ef9f230616d436d52a8b2b8d63b50/ruff-0.15.18-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56949a6ce8b3abde54c0bcb22cebfe57e8771cadc84b407ae8b8eaf67ebdcd43", size = 10699024, upload-time = "2026-06-18T18:25:05.707Z" }, + { url = "https://files.pythonhosted.org/packages/63/42/1e3e4c68bd408b9768cf3e439acbe2c78245225faef253f7028a0cdb63e0/ruff-0.15.18-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01a754cd6a1b630d3f97e33eb452cf7a98040482318e870f8bc52a5a30e62657", size = 11491458, upload-time = "2026-06-18T18:25:20.275Z" }, + { url = "https://files.pythonhosted.org/packages/20/77/47a3484bea8521e14a203d98c389c5c97846675e4f02734672da4a69b52a/ruff-0.15.18-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ba7a07e03a44dbf10bb086ee06705b173625014ec99f73a7e6836a5e5590a0c", size = 12383752, upload-time = "2026-06-18T18:25:22.535Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ca/054159590787023d83b658a1a1819c4c8910114e7015069340b71c0961cb/ruff-0.15.18-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a2c40a41a4cadbcf5897b548ab29dfe248b20c540961c0247d98a3973c70403", size = 11577923, upload-time = "2026-06-18T18:25:10.702Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/d353d6b7bbd73cc0ec37f4463d7540e45e894338abdd9964eee0de332708/ruff-0.15.18-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f0480ce690cbb6c4db6e5d08f19fce98e10ba131a8b60c1bcdac42771e3ae2d", size = 11583925, upload-time = "2026-06-18T18:25:32.391Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4a/891f89b9c296ed3e5f3ece1a5629badc989d9a8fdaa30431aaf4774bc1c2/ruff-0.15.18-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2330215f1f393fa8733f55edce04fcf94c36a2c460fcde31f78cc84e4951e9b1", size = 11582834, upload-time = "2026-06-18T18:25:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/32/a3/ed9e370154bf85de360b93c03026157f02d4943b2d01ff4945f4429f8e8a/ruff-0.15.18-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6aa6a3d979e48ae617578183674bf264fbe7d0114a796a26bd678d67963c7ff", size = 10927328, upload-time = "2026-06-18T18:25:34.676Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d1/5cf5909329fedb5d39d555ee818ba5cf4638e1a301b89785d34f2905bfcb/ruff-0.15.18-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a81beadbbff2c9c245561ae3f77b16709d87f35eec650d0501679239d3449b22", size = 10693187, upload-time = "2026-06-18T18:25:08.245Z" }, + { url = "https://files.pythonhosted.org/packages/fd/44/ff6c635cf2c4f4e7b618b6640da057376baa36014695487d88aed4794268/ruff-0.15.18-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2186d9e940ae332ab293623a75b5f4fe49565f449954d50a72a046683aa6b809", size = 11208721, upload-time = "2026-06-18T18:25:41.327Z" }, + { url = "https://files.pythonhosted.org/packages/88/d9/5baa2a30861adfb7022cf33c1e35b2fc18085b08c16f83eff4c7b99a5f48/ruff-0.15.18-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5c2abf140438032bc77b2284a6c9944ecd8a19e5f1c7b52b1b8e4a0a80d19a7a", size = 11678599, upload-time = "2026-06-18T18:25:13.607Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1a/0725a7cfdc32ff769efb96ee782bec882e16448c5d9e3be947ec4c04ce27/ruff-0.15.18-py3-none-win32.whl", hash = "sha256:02299e6e9fa5b297a3f6d5d10d7bcd655c925b028bb8b9d4588214549c6b9ec4", size = 10901903, upload-time = "2026-06-18T18:25:24.755Z" }, + { url = "https://files.pythonhosted.org/packages/f3/51/805d9f6fb7970505c3504794a5ec350f605361b807fef4dcf214ebd35e72/ruff-0.15.18-py3-none-win_amd64.whl", hash = "sha256:dac80dc8d26b2257dbefabed62f5d255c3937b4ccb122da1fc634794fa3578b3", size = 12041189, upload-time = "2026-06-18T18:25:17.915Z" }, + { url = "https://files.pythonhosted.org/packages/29/4c/67bb45e41609eb4726f1bfeb59e083cf91d14c696d4bd14c234a980be93d/ruff-0.15.18-py3-none-win_arm64.whl", hash = "sha256:b2c9257fcbd4a3e5b977a1904e6facca016bafe2edc17df24db67cfaee03b4e4", size = 11329958, upload-time = "2026-06-18T18:25:43.686Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] diff --git a/scripts/release-integration.sh b/scripts/release-integration.sh index 53492c94b..3fd35bf75 100755 --- a/scripts/release-integration.sh +++ b/scripts/release-integration.sh @@ -13,7 +13,7 @@ print_info() { echo -e "${GREEN}[INFO]${NC} $1"; } print_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } print_error() { echo -e "${RED}[ERROR]${NC} $1"; } -VALID_INTEGRATIONS=("ag2" "agent-framework" "agentcore" "agno" "aider" "ai-sdk" "autogen" "chat" "claude-agent-sdk" "claude-code" "cline" "cloudflare-oauth-proxy" "codex" "composio" "continue" "crewai" "cursor" "cursor-cli" "dify" "flowise" "gemini-spark" "google-adk" "haystack" "langgraph" "litellm" "llamaindex" "n8n" "nemoclaw" "obsidian" "omo" "openai-agents" "openclaw" "opencode" "openhands" "paperclip" "pipecat" "pydantic-ai" "roo-code" "smolagents" "strands" "superagent" "vapi" "zed") +VALID_INTEGRATIONS=("ag2" "agent-framework" "agentcore" "agno" "aider" "ai-sdk" "autogen" "chat" "claude-agent-sdk" "claude-code" "cline" "cloudflare-oauth-proxy" "codex" "composio" "continue" "crewai" "cursor" "cursor-cli" "dify" "flowise" "gemini-spark" "google-adk" "haystack" "langgraph" "litellm" "llamaindex" "n8n" "nemoclaw" "obsidian" "omo" "openai-agents" "openclaw" "opencode" "openhands" "paperclip" "pipecat" "pydantic-ai" "roo-code" "smolagents" "strands" "superagent" "vapi" "windsurf" "zed") usage() { print_error "Usage: $0 " From db1ff9984f7ee2c51255b23be2a265570375b672 Mon Sep 17 00:00:00 2001 From: DK09876 Date: Tue, 23 Jun 2026 16:16:42 -0700 Subject: [PATCH 2/3] style(windsurf): apply ruff format to cli.py lint.sh runs 'ruff format'; collapse the --rules-path add_argument to one line so verify-generated-files passes. Co-Authored-By: Claude Opus 4.8 (1M context) --- hindsight-integrations/windsurf/hindsight_windsurf/cli.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/hindsight-integrations/windsurf/hindsight_windsurf/cli.py b/hindsight-integrations/windsurf/hindsight_windsurf/cli.py index 11863a4ee..b145b6dac 100644 --- a/hindsight-integrations/windsurf/hindsight_windsurf/cli.py +++ b/hindsight-integrations/windsurf/hindsight_windsurf/cli.py @@ -131,9 +131,7 @@ def _add_overrides(parser: argparse.ArgumentParser) -> None: parser.add_argument( "--mcp-path", default=None, help="mcp_config.json path (default: ~/.codeium/windsurf/mcp_config.json)" ) - parser.add_argument( - "--rules-path", default=None, help="rule file path (default: ./.windsurf/rules/hindsight.md)" - ) + parser.add_argument("--rules-path", default=None, help="rule file path (default: ./.windsurf/rules/hindsight.md)") parser.add_argument("--user-config-path", default=None, help=argparse.SUPPRESS) From 1bb618eb01e096e48577ccc8993914537c10f394 Mon Sep 17 00:00:00 2001 From: DK09876 Date: Tue, 23 Jun 2026 16:26:14 -0700 Subject: [PATCH 3/3] fix(windsurf): use official Windsurf logo for the integration icon Replace the placeholder abstract mark with the official Windsurf logo (simple-icons, CC0), matching the real-brand-logo convention used by the other integration icons. Co-Authored-By: Claude Opus 4.8 (1M context) --- hindsight-docs/static/img/icons/windsurf.svg | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/hindsight-docs/static/img/icons/windsurf.svg b/hindsight-docs/static/img/icons/windsurf.svg index f675a68d0..7670303f6 100644 --- a/hindsight-docs/static/img/icons/windsurf.svg +++ b/hindsight-docs/static/img/icons/windsurf.svg @@ -1,5 +1 @@ - - - - - +Windsurf