diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 8caadf058..fbfb2f4fe 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -38,6 +38,7 @@ jobs:
integrations-claude-code: ${{ steps.filter.outputs.integrations-claude-code }}
integrations-cline: ${{ steps.filter.outputs.integrations-cline }}
integrations-codex: ${{ steps.filter.outputs.integrations-codex }}
+ integrations-github-copilot: ${{ steps.filter.outputs.integrations-github-copilot }}
integrations-continue: ${{ steps.filter.outputs.integrations-continue }}
integrations-cursor-cli: ${{ steps.filter.outputs.integrations-cursor-cli }}
integrations-crewai: ${{ steps.filter.outputs.integrations-crewai }}
@@ -142,6 +143,8 @@ jobs:
- 'hindsight-integrations/cline/**'
integrations-codex:
- 'hindsight-integrations/codex/**'
+ integrations-github-copilot:
+ - 'hindsight-integrations/github-copilot/**'
integrations-continue:
- 'hindsight-integrations/continue/**'
integrations-cursor-cli:
@@ -588,6 +591,45 @@ jobs:
working-directory: ./hindsight-integrations/cline
run: uv run pytest tests -v
+ test-github-copilot-integration:
+ needs: [detect-changes]
+ if: >-
+ (github.event_name == 'workflow_dispatch' ||
+ needs.detect-changes.outputs.integrations-github-copilot == '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 github-copilot integration
+ working-directory: ./hindsight-integrations/github-copilot
+ run: uv build
+
+ - name: Install dependencies
+ working-directory: ./hindsight-integrations/github-copilot
+ run: uv sync --frozen
+
+ - name: Run tests
+ working-directory: ./hindsight-integrations/github-copilot
+ # 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-codex-integration:
needs: [detect-changes]
if: >-
@@ -4746,6 +4788,7 @@ jobs:
- test-claude-code-integration
- test-cursor-integration
- test-cline-integration
+ - test-github-copilot-integration
- test-codex-integration
- test-cursor-cli-integration
- build-ai-sdk-integration
diff --git a/hindsight-dev/hindsight_dev/generate_changelog.py b/hindsight-dev/hindsight_dev/generate_changelog.py
index 2cbc336d0..99f4bdee3 100644
--- a/hindsight-dev/hindsight_dev/generate_changelog.py
+++ b/hindsight-dev/hindsight_dev/generate_changelog.py
@@ -53,6 +53,7 @@ class IntegrationMeta:
"claude-agent-sdk": IntegrationMeta("hindsight-claude-agent-sdk", "Claude Agent SDK"),
"llamaindex": IntegrationMeta("hindsight-llamaindex", "LlamaIndex"),
"codex": IntegrationMeta("hindsight-codex", "Codex"),
+ "github-copilot": IntegrationMeta("hindsight-copilot", "GitHub Copilot"),
"cline": IntegrationMeta("hindsight-cline", "Cline"),
"cursor-cli": IntegrationMeta("hindsight-cursor-cli", "Cursor CLI"),
"cursor": IntegrationMeta("hindsight-cursor", "Cursor"),
diff --git a/hindsight-docs/docs-integrations/github-copilot.md b/hindsight-docs/docs-integrations/github-copilot.md
new file mode 100644
index 000000000..5d33ee801
--- /dev/null
+++ b/hindsight-docs/docs-integrations/github-copilot.md
@@ -0,0 +1,51 @@
+---
+sidebar_position: 39
+title: "GitHub Copilot Persistent Memory with Hindsight | Integration"
+description: "Add long-term memory to GitHub Copilot in VS Code with Hindsight via MCP. One command wires up the Hindsight MCP server plus a recall/retain rule."
+---
+
+# GitHub Copilot
+
+Long-term memory for [GitHub Copilot](https://github.com/features/copilot) in VS Code, powered by [Hindsight](https://vectorize.io/hindsight). One command connects Copilot's agent mode to the Hindsight MCP server and adds a recall/retain rule — so Copilot recalls relevant memory at the start of a task and retains durable facts as it works.
+
+## How It Works
+
+VS Code Copilot supports two things this integration uses:
+
+- **MCP servers** via `.vscode/mcp.json` (agent mode), including **HTTP servers** with headers — so the Hindsight MCP endpoint connects directly:
+
+ ```json
+ {
+ "servers": {
+ "hindsight": {
+ "type": "http",
+ "url": "https://api.hindsight.vectorize.io/mcp/my-project/",
+ "headers": { "Authorization": "Bearer hsk_..." }
+ }
+ }
+ }
+ ```
+
+- **`.github/copilot-instructions.md`**, which Copilot applies to every chat in the workspace — that's where the recall/retain rule lives.
+
+## Setup
+
+```bash
+pip install hindsight-copilot
+cd your-project
+hindsight-copilot init --api-token YOUR_HINDSIGHT_API_KEY --bank-id my-project
+```
+
+`init` merges the `servers` entry into `./.vscode/mcp.json` and writes the rule into `./.github/copilot-instructions.md`. Reload VS Code, open Copilot Chat in **agent mode**, and start the `hindsight` MCP server from the chat's tools menu.
+
+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.json` has comments, `init` prints the snippet to paste instead — or run `hindsight-copilot init --print-only` anytime.
+
+## Commands
+
+| Command | Description |
+| --- | --- |
+| `hindsight-copilot init` | Add the MCP server + recall/retain rule |
+| `hindsight-copilot status` | Show whether the server + rule are configured |
+| `hindsight-copilot uninstall` | Remove the server + rule |
+
+See the [package README](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/github-copilot) for full configuration options.
diff --git a/hindsight-docs/src/data/integrations.json b/hindsight-docs/src/data/integrations.json
index 162c7d10a..f9ea5eebd 100644
--- a/hindsight-docs/src/data/integrations.json
+++ b/hindsight-docs/src/data/integrations.json
@@ -160,6 +160,16 @@
"link": "/sdks/integrations/codex",
"icon": "/img/icons/codex.svg"
},
+ {
+ "id": "github-copilot",
+ "name": "GitHub Copilot",
+ "description": "Long-term memory for GitHub Copilot in VS Code. One command wires the Hindsight MCP server into .vscode/mcp.json plus a recall/retain rule in .github/copilot-instructions.md.",
+ "type": "official",
+ "by": "hindsight",
+ "category": "tool",
+ "link": "/sdks/integrations/github-copilot",
+ "icon": "/img/icons/github-copilot.svg"
+ },
{
"id": "continue",
"name": "Continue",
diff --git a/hindsight-docs/static/img/icons/github-copilot.svg b/hindsight-docs/static/img/icons/github-copilot.svg
new file mode 100644
index 000000000..201ecf408
--- /dev/null
+++ b/hindsight-docs/static/img/icons/github-copilot.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/hindsight-integrations/README.md b/hindsight-integrations/README.md
index 46118c6c1..c66416cba 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` |
+| [**GitHub Copilot**](./github-copilot) | MCP server config (`.vscode/mcp.json`) + a recall/retain rule for VS Code Copilot's agent mode. | `pip install hindsight-copilot` |
| [**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/github-copilot/README.md b/hindsight-integrations/github-copilot/README.md
new file mode 100644
index 000000000..54a72510a
--- /dev/null
+++ b/hindsight-integrations/github-copilot/README.md
@@ -0,0 +1,76 @@
+# hindsight-copilot
+
+Long-term memory for **GitHub Copilot** in VS Code, powered by [Hindsight](https://github.com/vectorize-io/hindsight).
+
+`hindsight-copilot init` wires the Hindsight **MCP server** into VS Code's
+`.vscode/mcp.json` and adds a recall/retain rule to `.github/copilot-instructions.md`.
+Copilot's agent mode 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
+
+VS Code Copilot supports two things this integration uses:
+
+- **MCP servers** in `.vscode/mcp.json` (agent mode), including **HTTP servers**
+ with headers — so the Hindsight MCP endpoint connects directly:
+
+ ```json
+ {
+ "servers": {
+ "hindsight": {
+ "type": "http",
+ "url": "https://api.hindsight.vectorize.io/mcp/my-project/",
+ "headers": { "Authorization": "Bearer hsk_..." }
+ }
+ }
+ }
+ ```
+
+- **`.github/copilot-instructions.md`**, which Copilot applies to every chat in
+ the workspace — that's where the recall/retain rule lives.
+
+## Install
+
+```bash
+pip install hindsight-copilot
+cd your-project
+hindsight-copilot init --api-token YOUR_HINDSIGHT_API_KEY --bank-id my-project
+```
+
+`init` merges the `servers` entry into `./.vscode/mcp.json` and writes the rule
+into `./.github/copilot-instructions.md`. Reload VS Code, open Copilot Chat in
+**agent mode**, and start the `hindsight` MCP server from the chat's tools menu.
+
+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.json` has comments, `init` prints the snippet to paste instead
+of touching the file — or run `hindsight-copilot init --print-only` anytime.
+
+## Commands
+
+| Command | Description |
+| --- | --- |
+| `hindsight-copilot init` | Add the MCP server + recall/retain rule |
+| `hindsight-copilot status` | Show whether the server + rule are configured |
+| `hindsight-copilot 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_COPILOT_BANK_ID` | `copilot` |
+
+## 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/github-copilot/hindsight_copilot/__init__.py b/hindsight-integrations/github-copilot/hindsight_copilot/__init__.py
new file mode 100644
index 000000000..a0d307ac7
--- /dev/null
+++ b/hindsight-integrations/github-copilot/hindsight_copilot/__init__.py
@@ -0,0 +1,12 @@
+"""Hindsight memory integration for GitHub Copilot (VS Code).
+
+Wires the Hindsight MCP server into VS Code's ``.vscode/mcp.json`` and writes a
+recall/retain rule into ``.github/copilot-instructions.md``, so Copilot's agent
+mode has ``recall``/``retain``/``reflect`` tools and uses them automatically.
+
+CLI::
+
+ hindsight-copilot init --api-token hsk_... --bank-id my-project
+"""
+
+__version__ = "0.1.0"
diff --git a/hindsight-integrations/github-copilot/hindsight_copilot/cli.py b/hindsight-integrations/github-copilot/hindsight_copilot/cli.py
new file mode 100644
index 000000000..397035044
--- /dev/null
+++ b/hindsight-integrations/github-copilot/hindsight_copilot/cli.py
@@ -0,0 +1,172 @@
+"""CLI for the Hindsight GitHub Copilot integration.
+
+``hindsight-copilot init`` wires the Hindsight MCP server into VS Code's
+``.vscode/mcp.json`` and writes a recall/retain rule into
+``.github/copilot-instructions.md``. Copilot's agent mode 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, CopilotConfig, load_config
+from .instructions import RULE_TEXT, clear_rule, default_instructions_path, write_rule
+from .instructions import is_installed as rule_installed
+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
+
+
+@dataclass
+class InstallOutcome:
+ mcp: McpResult
+ instructions_path: Path
+
+
+def build_install(config: CopilotConfig, mcp_path: Path, instructions_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(instructions_path)
+ return InstallOutcome(mcp=mcp, instructions_path=instructions_path)
+
+
+def _resolve_config(args: argparse.Namespace) -> CopilotConfig:
+ 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 _instructions_path(args: argparse.Namespace) -> Path:
+ return Path(args.instructions_path) if args.instructions_path else default_instructions_path()
+
+
+def _scaffold_user_config(cfg: CopilotConfig, 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)
+ instructions_path = _instructions_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 .vscode/mcp.json:\n")
+ print(render_snippet(server))
+ print("\nAnd add this rule to .github/copilot-instructions.md:\n")
+ print(RULE_TEXT)
+ return
+
+ print("Setting up Hindsight for GitHub Copilot ...")
+ _scaffold_user_config(cfg, _user_config_path(args))
+ outcome = build_install(cfg, mcp_path, instructions_path)
+
+ if outcome.mcp.action == "manual":
+ print(f" Your {outcome.mcp.path} has comments, so I won't rewrite it.")
+ print(" Add this `servers` 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 recall/retain rule to {outcome.instructions_path}")
+ print("\nDone. Reload VS Code, open Copilot Chat in agent mode, 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)
+ instructions_path = _instructions_path(args)
+ print(f"MCP server in {mcp_path}: {'installed' if server_installed(mcp_path) else 'not installed'}")
+ print(
+ f"Recall/retain rule in {instructions_path}: {'installed' if rule_installed(instructions_path) else 'not installed'}"
+ )
+
+
+def cmd_uninstall(args: argparse.Namespace) -> None:
+ mcp_path = _mcp_path(args)
+ instructions_path = _instructions_path(args)
+ result = remove_from_mcp(mcp_path)
+ if result.action == "manual":
+ print(f" {mcp_path} has comments — 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(instructions_path)
+ print(f" Removed the recall/retain rule from {instructions_path}")
+
+
+def _add_overrides(parser: argparse.ArgumentParser) -> None:
+ parser.add_argument("--mcp-path", default=None, help=".vscode/mcp.json path (default: ./.vscode/mcp.json)")
+ parser.add_argument(
+ "--instructions-path", default=None, help="copilot-instructions.md path (default: ./.github/...)"
+ )
+ parser.add_argument("--user-config-path", default=None, help=argparse.SUPPRESS)
+
+
+def main(argv: Optional[list[str]] = None) -> int:
+ parser = argparse.ArgumentParser(
+ prog="hindsight-copilot", description="Hindsight memory for GitHub Copilot (VS Code, via MCP)"
+ )
+ parser.add_argument("--version", action="version", version=f"hindsight-copilot {__version__}")
+ sub = parser.add_subparsers(dest="command")
+
+ init_p = sub.add_parser("init", help="Configure Copilot'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: copilot)")
+ 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/github-copilot/hindsight_copilot/config.py b/hindsight-integrations/github-copilot/hindsight_copilot/config.py
new file mode 100644
index 000000000..9473fe99f
--- /dev/null
+++ b/hindsight-integrations/github-copilot/hindsight_copilot/config.py
@@ -0,0 +1,76 @@
+"""Configuration for the Hindsight GitHub Copilot integration.
+
+Settings layer (later wins): built-in defaults -> ``~/.hindsight/copilot.json``
+-> environment variables. Resolved into a typed :class:`CopilotConfig`.
+
+The integration is configuration-only: it wires the Hindsight MCP server into
+VS Code's ``.vscode/mcp.json`` and writes a recall/retain rule into
+``.github/copilot-instructions.md`` (which Copilot applies to every chat 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 = "copilot"
+
+USER_CONFIG_FILE = Path.home() / ".hindsight" / "copilot.json"
+
+
+@dataclass
+class CopilotConfig:
+ """Resolved configuration for the Copilot 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_COPILOT_BANK_ID": "bank_id",
+}
+
+
+def load_config(config_file: Optional[Path] = None, env: Optional[dict] = None) -> CopilotConfig:
+ """Load and resolve configuration from file then environment."""
+ cfg = CopilotConfig()
+ 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/github-copilot/hindsight_copilot/instructions.py b/hindsight-integrations/github-copilot/hindsight_copilot/instructions.py
new file mode 100644
index 000000000..d1cb2cc2d
--- /dev/null
+++ b/hindsight-integrations/github-copilot/hindsight_copilot/instructions.py
@@ -0,0 +1,87 @@
+"""Write Hindsight's recall/retain rule into ``.github/copilot-instructions.md``.
+
+VS Code Copilot automatically applies ``.github/copilot-instructions.md`` to
+every chat request in the workspace, so a rule there tells Copilot to use the
+Hindsight MCP tools — recall relevant memory at the start of a task, and retain
+durable facts.
+
+The rule lives inside a fenced ```` ... ````
+block so we can update or remove it without disturbing the user's own content.
+"""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+BEGIN_MARKER = ""
+END_MARKER = ""
+
+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 answer. "
+ "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_instructions_path() -> Path:
+ """The workspace ``.github/copilot-instructions.md`` (always-on in Copilot)."""
+ return Path.cwd() / ".github" / "copilot-instructions.md"
+
+
+def _strip_block(text: str) -> str:
+ start = text.find(BEGIN_MARKER)
+ if start == -1:
+ return text
+ end = text.find(END_MARKER, start)
+ if end == -1:
+ return text[:start].rstrip() + "\n"
+ end += len(END_MARKER)
+ before = text[:start].rstrip()
+ after = text[end:].lstrip()
+ if before and after:
+ return f"{before}\n\n{after}"
+ return (before or after).rstrip() + ("\n" if (before or after) else "")
+
+
+def render_block(rule_text: str = RULE_TEXT) -> str:
+ return f"{BEGIN_MARKER}\n{rule_text.strip()}\n{END_MARKER}"
+
+
+def write_rule(path: Path, rule_text: str = RULE_TEXT) -> Path:
+ """Write/replace Hindsight's rule block in the instructions file at ``path``.
+
+ Preserves any user-authored content; only rewrites our fenced block, placing
+ it at the top so the memory rule leads the instructions.
+ """
+ existing = path.read_text(encoding="utf-8") if path.is_file() else ""
+ base = _strip_block(existing).rstrip()
+ block = render_block(rule_text)
+ new_text = f"{block}\n\n{base}\n" if base else f"{block}\n"
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(new_text, encoding="utf-8")
+ return path
+
+
+def clear_rule(path: Path) -> Path:
+ """Remove Hindsight's rule block from the instructions file; delete if empty."""
+ if not path.is_file():
+ return path
+ existing = path.read_text(encoding="utf-8")
+ if BEGIN_MARKER not in existing:
+ return path
+ stripped = _strip_block(existing).strip()
+ if not stripped:
+ path.unlink()
+ return path
+ path.write_text(stripped + "\n", encoding="utf-8")
+ return path
+
+
+def is_installed(path: Path) -> bool:
+ return path.is_file() and BEGIN_MARKER in path.read_text(encoding="utf-8")
diff --git a/hindsight-integrations/github-copilot/hindsight_copilot/mcp_config.py b/hindsight-integrations/github-copilot/hindsight_copilot/mcp_config.py
new file mode 100644
index 000000000..534272af3
--- /dev/null
+++ b/hindsight-integrations/github-copilot/hindsight_copilot/mcp_config.py
@@ -0,0 +1,128 @@
+"""Wire Hindsight into VS Code's MCP config (``.vscode/mcp.json``).
+
+VS Code's Copilot agent mode reads MCP servers from ``.vscode/mcp.json`` under
+the ``servers`` key, and supports HTTP servers directly — so the Hindsight MCP
+endpoint connects with no bridge::
+
+ {
+ "servers": {
+ "hindsight": {
+ "type": "http",
+ "url": "https://api.hindsight.vectorize.io/mcp//",
+ "headers": { "Authorization": "Bearer hsk_..." }
+ }
+ }
+ }
+
+``.vscode/mcp.json`` may contain comments (JSONC), which the stdlib JSON parser
+can't round-trip. So we only edit in place when the file 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 workspace ``.vscode/mcp.json`` (project-shared MCP config)."""
+ return Path.cwd() / ".vscode" / "mcp.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 ``servers.hindsight`` entry for ``.vscode/mcp.json``.
+
+ An HTTP MCP server pointing at the Hindsight endpoint, with a Bearer auth
+ header when a token is set (omitted for an open self-hosted server).
+ """
+ server: dict[str, Any] = {"type": "http", "url": 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 ``.vscode/mcp.json``."""
+ return json.dumps({"servers": {SERVER_NAME: server}}, indent=2)
+
+
+@dataclass
+class McpResult:
+ """Outcome of editing ``.vscode/mcp.json``.
+
+ ``action`` is one of ``created``, ``merged``, ``unchanged``, ``removed``, or
+ ``manual`` (file is JSONC we won't 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 ``servers.hindsight`` in ``.vscode/mcp.json`` at ``path``."""
+ if not path.is_file():
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(json.dumps({"servers": {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("servers")
+ if not isinstance(servers, dict):
+ servers = {}
+ if servers.get(SERVER_NAME) == server:
+ return McpResult("unchanged", path)
+ servers[SERVER_NAME] = server
+ data["servers"] = 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 ``servers.hindsight`` from ``.vscode/mcp.json`` at ``path``."""
+ data = _load_strict(path)
+ if data is None:
+ return McpResult("manual" if path.is_file() else "unchanged", path)
+
+ servers = data.get("servers")
+ if not isinstance(servers, dict) or SERVER_NAME not in servers:
+ return McpResult("unchanged", path)
+ del servers[SERVER_NAME]
+ if servers:
+ data["servers"] = servers
+ else:
+ data.pop("servers", 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 ``.vscode/mcp.json`` at ``path``."""
+ data = _load_strict(path)
+ if data is None:
+ return False
+ servers = data.get("servers")
+ return isinstance(servers, dict) and SERVER_NAME in servers
diff --git a/hindsight-integrations/github-copilot/hindsight_copilot/py.typed b/hindsight-integrations/github-copilot/hindsight_copilot/py.typed
new file mode 100644
index 000000000..e69de29bb
diff --git a/hindsight-integrations/github-copilot/pyproject.toml b/hindsight-integrations/github-copilot/pyproject.toml
new file mode 100644
index 000000000..4526c45e2
--- /dev/null
+++ b/hindsight-integrations/github-copilot/pyproject.toml
@@ -0,0 +1,47 @@
+[project]
+name = "hindsight-copilot"
+version = "0.1.0"
+description = "GitHub Copilot (VS Code) 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", "github-copilot", "copilot", "vscode", "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-copilot = "hindsight_copilot.cli:main"
+
+[project.urls]
+Homepage = "https://github.com/vectorize-io/hindsight"
+Documentation = "https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/github-copilot"
+Repository = "https://github.com/vectorize-io/hindsight"
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[tool.hatch.build.targets.wheel]
+packages = ["hindsight_copilot"]
+
+[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/github-copilot/tests/__init__.py b/hindsight-integrations/github-copilot/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/hindsight-integrations/github-copilot/tests/test_cli.py b/hindsight-integrations/github-copilot/tests/test_cli.py
new file mode 100644
index 000000000..d70acb431
--- /dev/null
+++ b/hindsight-integrations/github-copilot/tests/test_cli.py
@@ -0,0 +1,68 @@
+"""Tests for the CLI (init/status/uninstall)."""
+
+import json
+
+from hindsight_copilot.cli import build_install, main
+from hindsight_copilot.config import CopilotConfig
+from hindsight_copilot.mcp_config import SERVER_NAME, is_installed as server_installed
+
+
+class TestBuildInstall:
+ def test_writes_mcp_and_rule(self, tmp_path):
+ mcp = tmp_path / "mcp.json"
+ instr = tmp_path / "copilot-instructions.md"
+ cfg = CopilotConfig(
+ hindsight_api_url="https://api.hindsight.vectorize.io", hindsight_api_token="k", bank_id="proj"
+ )
+ outcome = build_install(cfg, mcp, instr)
+ assert outcome.mcp.action == "created"
+ server = json.loads(mcp.read_text())["servers"][SERVER_NAME]
+ assert server["url"] == "https://api.hindsight.vectorize.io/mcp/proj/"
+ assert server["headers"]["Authorization"] == "Bearer k"
+ assert "HINDSIGHT:BEGIN" in instr.read_text()
+
+
+class TestMain:
+ def _common(self, tmp_path):
+ return [
+ "--mcp-path",
+ str(tmp_path / "mcp.json"),
+ "--instructions-path",
+ str(tmp_path / "copilot-instructions.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.json")
+ main(["status", *common])
+ assert "installed" in capsys.readouterr().out
+ main(["uninstall", *common])
+ assert not server_installed(tmp_path / "mcp.json")
+ assert not (tmp_path / "copilot-instructions.md").exists()
+
+ def test_print_only_writes_nothing(self, tmp_path, capsys):
+ mcp = tmp_path / "mcp.json"
+ instr = tmp_path / "copilot-instructions.md"
+ rc = main(
+ [
+ "init",
+ "--print-only",
+ "--api-url",
+ "http://localhost:8888",
+ "--mcp-path",
+ str(mcp),
+ "--instructions-path",
+ str(instr),
+ "--user-config-path",
+ str(tmp_path / "user.json"),
+ ]
+ )
+ assert rc == 0
+ assert not mcp.exists() and not instr.exists()
+ assert "servers" in capsys.readouterr().out
+
+ def test_no_command_returns_1(self):
+ assert main([]) == 1
diff --git a/hindsight-integrations/github-copilot/tests/test_config.py b/hindsight-integrations/github-copilot/tests/test_config.py
new file mode 100644
index 000000000..3d71d0fcd
--- /dev/null
+++ b/hindsight-integrations/github-copilot/tests/test_config.py
@@ -0,0 +1,34 @@
+"""Tests for config loading."""
+
+import json
+
+from hindsight_copilot.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 / "copilot.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 / "copilot.json"
+ p.write_text(json.dumps({"bankId": "from-file"}))
+ cfg = load_config(config_file=p, env={"HINDSIGHT_COPILOT_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 / "copilot.json"
+ p.write_text("{ broken")
+ assert load_config(config_file=p, env={}).bank_id == DEFAULT_BANK_ID
diff --git a/hindsight-integrations/github-copilot/tests/test_e2e.py b/hindsight-integrations/github-copilot/tests/test_e2e.py
new file mode 100644
index 000000000..5909c6c48
--- /dev/null
+++ b/hindsight-integrations/github-copilot/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_copilot.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, "copilot-e2e")
+ init = {
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "initialize",
+ "params": {
+ "protocolVersion": "2025-03-26",
+ "capabilities": {},
+ "clientInfo": {"name": "copilot-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/github-copilot/tests/test_instructions.py b/hindsight-integrations/github-copilot/tests/test_instructions.py
new file mode 100644
index 000000000..310c0f402
--- /dev/null
+++ b/hindsight-integrations/github-copilot/tests/test_instructions.py
@@ -0,0 +1,47 @@
+"""Tests for the copilot-instructions.md rule writer."""
+
+from hindsight_copilot.instructions import BEGIN_MARKER, RULE_TEXT, clear_rule, is_installed, write_rule
+
+
+def test_write_creates_with_block(tmp_path):
+ path = tmp_path / "copilot-instructions.md"
+ write_rule(path)
+ text = path.read_text()
+ assert BEGIN_MARKER in text and "recall" in text and "retain" in text
+ assert is_installed(path)
+
+
+def test_write_preserves_user_content_block_leads(tmp_path):
+ path = tmp_path / "copilot-instructions.md"
+ path.write_text("# Project\n\nUse TypeScript.\n")
+ write_rule(path)
+ text = path.read_text()
+ assert "Use TypeScript." in text
+ assert text.index(BEGIN_MARKER) < text.index("Use TypeScript.")
+
+
+def test_write_replaces_existing_block(tmp_path):
+ path = tmp_path / "copilot-instructions.md"
+ write_rule(path)
+ write_rule(path)
+ assert path.read_text().count(BEGIN_MARKER) == 1
+
+
+def test_clear_keeps_user_content(tmp_path):
+ path = tmp_path / "copilot-instructions.md"
+ path.write_text("Keep me.\n")
+ write_rule(path)
+ clear_rule(path)
+ assert "Keep me." in path.read_text() and BEGIN_MARKER not in path.read_text()
+
+
+def test_clear_deletes_if_only_block(tmp_path):
+ path = tmp_path / "copilot-instructions.md"
+ write_rule(path)
+ clear_rule(path)
+ assert not path.exists()
+
+
+def test_rule_mentions_all_tools():
+ for tool in ("recall", "retain", "reflect"):
+ assert tool in RULE_TEXT
diff --git a/hindsight-integrations/github-copilot/tests/test_mcp_config.py b/hindsight-integrations/github-copilot/tests/test_mcp_config.py
new file mode 100644
index 000000000..d8f1197a2
--- /dev/null
+++ b/hindsight-integrations/github-copilot/tests/test_mcp_config.py
@@ -0,0 +1,97 @@
+"""Tests for the .vscode/mcp.json servers writer."""
+
+import json
+
+from hindsight_copilot.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_http_with_auth_header(self):
+ s = build_http_server("https://api.hindsight.vectorize.io", "hsk_abc", "proj")
+ assert s["type"] == "http"
+ assert s["url"] == "https://api.hindsight.vectorize.io/mcp/proj/"
+ assert s["headers"] == {"Authorization": "Bearer hsk_abc"}
+
+ def test_open_server_omits_headers(self):
+ s = build_http_server("http://localhost:8888", None, "proj")
+ assert s == {"type": "http", "url": "http://localhost:8888/mcp/proj/"}
+ assert "headers" not in s
+
+
+class TestApply:
+ def test_creates_file(self, tmp_path):
+ path = tmp_path / "mcp.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())["servers"][SERVER_NAME] == s
+
+ def test_merges_preserves_other_servers_and_inputs(self, tmp_path):
+ path = tmp_path / "mcp.json"
+ path.write_text(json.dumps({"inputs": [{"id": "tok"}], "servers": {"other": {"type": "stdio"}}}))
+ 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["inputs"] == [{"id": "tok"}] # untouched
+ assert data["servers"]["other"] == {"type": "stdio"} # untouched
+ assert data["servers"][SERVER_NAME] == s
+
+ def test_unchanged_when_identical(self, tmp_path):
+ path = tmp_path / "mcp.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_jsonc_returns_manual(self, tmp_path):
+ path = tmp_path / "mcp.json"
+ original = '{\n // comment\n "servers": {}\n}\n'
+ 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.json"
+ path.write_text(json.dumps({"servers": {"other": {"type": "stdio"}, SERVER_NAME: {"type": "http"}}}))
+ result = remove_from_mcp(path)
+ assert result.action == "removed"
+ servers = json.loads(path.read_text())["servers"]
+ assert SERVER_NAME not in servers and "other" in servers
+
+ def test_remove_drops_empty_servers(self, tmp_path):
+ path = tmp_path / "mcp.json"
+ path.write_text(json.dumps({"inputs": [], "servers": {SERVER_NAME: {"type": "http"}}}))
+ remove_from_mcp(path)
+ data = json.loads(path.read_text())
+ assert "servers" not in data
+ assert "inputs" in data
+
+ def test_is_installed(self, tmp_path):
+ path = tmp_path / "mcp.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))["servers"][SERVER_NAME] == s
diff --git a/hindsight-integrations/github-copilot/uv.lock b/hindsight-integrations/github-copilot/uv.lock
new file mode 100644
index 000000000..221e00889
--- /dev/null
+++ b/hindsight-integrations/github-copilot/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-copilot"
+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.0"
+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/84/0e/b5858858d74958632c49b72cb25a3976ff9f632397626715be71c89d3971/pytest-9.1.0.tar.gz", hash = "sha256:41dd9148c08072446394cefd3d79701701335a9f4cae69ba92e39f6c7f5c061c", size = 1634181, upload-time = "2026-06-13T18:52:45.983Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8b/5a/ba30a81239b909821b3153e303e7def45178bf353da4f72380e6c5e8793b/pytest-9.1.0-py3-none-any.whl", hash = "sha256:8ebb0e7888bdf2bdfc602ec51f8f62d50200af37356c74e503c79a94f5c81f32", size = 386453, upload-time = "2026-06-13T18:52:44.045Z" },
+]
+
+[[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 d6de647f9..c9c16600e 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" "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" "ai-sdk" "autogen" "chat" "claude-agent-sdk" "claude-code" "cline" "cloudflare-oauth-proxy" "codex" "composio" "continue" "crewai" "cursor" "cursor-cli" "dify" "flowise" "gemini-spark" "github-copilot" "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")
usage() {
print_error "Usage: $0 "