From 56cf3b1def7f63f8811c2434f70faa87d0c5fef5 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 21 Jul 2026 11:09:33 -0400 Subject: [PATCH] fix(cline): support the Cline CLI's hook directory layout (#2711) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Cline CLI and the VS Code extension discover lifecycle hooks from different directories, but the installer only wrote the extension's paths (`.clinerules/hooks/` / `~/Documents/Cline/Rules/Hooks/`) and told users to flip the extension's Settings → Features → Hooks toggle. On the CLI the hooks therefore landed where the CLI never looks, so they listed under /settings but never fired and the memory bank stayed empty — while a manual invocation of the same script worked, because it was run directly. Add a `--cli` install/uninstall mode that targets the CLI's directories — `~/.cline/hooks` (with `--global`) or `/.cline/hooks` — and prints CLI-appropriate guidance (no toggle; point elsewhere with CLINE_HOOKS_DIR / `cline --hooks-dir`). The hook bundle is already self-contained (each script resolves its `lib/` and `settings.json` relative to its own path), so it works unchanged from the new location. The extension paths and messaging are unchanged when `--cli` is omitted. Tests cover both client layouts for get_hooks_dir and end-to-end install/ uninstall via `--cli` (into `.cline/hooks` and `~/.cline/hooks`), asserting the extension path is not written in CLI mode. README documents the CLI flag, the per-client directory table, and the no-toggle setup. --- hindsight-integrations/cline/README.md | 28 ++++++++--- .../cline/hindsight_cline/cli.py | 18 ++++++- .../cline/hindsight_cline/install.py | 35 +++++++++++--- .../cline/tests/test_install.py | 47 ++++++++++++++++++- 4 files changed, 111 insertions(+), 17 deletions(-) diff --git a/hindsight-integrations/cline/README.md b/hindsight-integrations/cline/README.md index 05d107ef83..7d67fa90dc 100644 --- a/hindsight-integrations/cline/README.md +++ b/hindsight-integrations/cline/README.md @@ -42,14 +42,28 @@ Install globally (applies to all projects): hindsight-cline install --global --api-url https://api.hindsight.vectorize.io --api-token YOUR_KEY ``` -To remove it later: `hindsight-cline uninstall` (add `--global` if you installed globally). +### Cline CLI -This copies four hook scripts (`TaskStart`, `UserPromptSubmit`, `TaskComplete`, `TaskCancel`) plus their `lib/` and `settings.json` into: +The Cline **CLI** discovers hooks from a different directory than the VS Code extension, so add `--cli`: -- `.clinerules/hooks/` (project install — commit it to share with your team), or -- `~/Documents/Cline/Rules/Hooks/` (global install). +```bash +hindsight-cline install --cli --api-url https://api.hindsight.vectorize.io --api-token YOUR_KEY +``` + +To remove it later: `hindsight-cline uninstall` (add `--global` and/or `--cli` to match how you installed). + +This copies four hook scripts (`TaskStart`, `UserPromptSubmit`, `TaskComplete`, `TaskCancel`) plus their `lib/` and `settings.json` into the directory your Cline client reads: + +| Client | Project install (default) | Global install (`--global`) | +| --- | --- | --- | +| VS Code extension | `.clinerules/hooks/` | `~/Documents/Cline/Rules/Hooks/` | +| CLI (`--cli`) | `.cline/hooks/` | `~/.cline/hooks/` | + +Commit the project directory to share the hooks with your team. -**Final step — enable hooks in Cline:** Settings → Features → Hooks. +**Final step:** +- **VS Code extension:** enable hooks in Settings → Features → Hooks. +- **CLI:** nothing — the CLI reads its hooks directory automatically (no toggle). To install the hooks somewhere else, point the CLI at them with `CLINE_HOOKS_DIR=` or `cline --hooks-dir `. ## How It Works @@ -83,8 +97,8 @@ Every key can also be set via `HINDSIGHT_*` environment variables (e.g. `HINDSIG ## Verifying Setup -1. Start Hindsight (`hindsight-api` or Hindsight Cloud) and run `hindsight-cline install` with your URL/key. -2. Enable hooks in Cline (Settings → Features → Hooks). +1. Start Hindsight (`hindsight-api` or Hindsight Cloud) and run `hindsight-cline install` with your URL/key (add `--cli` for the Cline CLI). +2. VS Code extension only: enable hooks in Cline (Settings → Features → Hooks). The CLI needs no toggle. 3. Start a task — recalled memories appear in context as a `` block. 4. Complete a task, then check the `cline` bank (via the API or dashboard) — a memory should appear. diff --git a/hindsight-integrations/cline/hindsight_cline/cli.py b/hindsight-integrations/cline/hindsight_cline/cli.py index 2b9de2ab7b..023ec2eb09 100644 --- a/hindsight-integrations/cline/hindsight_cline/cli.py +++ b/hindsight-integrations/cline/hindsight_cline/cli.py @@ -5,6 +5,7 @@ hindsight-cline install hindsight-cline install --api-url https://api.hindsight.vectorize.io --api-token hsk_... hindsight-cline install --global + hindsight-cline install --cli # Cline CLI instead of the VS Code extension hindsight-cline uninstall """ @@ -21,12 +22,13 @@ def _run_install(args: argparse.Namespace) -> int: api_token=args.api_token, project_dir=Path(args.project_dir), global_install=args.global_install, + cli=args.cli, ) return 0 def _run_uninstall(args: argparse.Namespace) -> int: - run_uninstall(project_dir=Path(args.project_dir), global_install=args.global_install) + run_uninstall(project_dir=Path(args.project_dir), global_install=args.global_install, cli=args.cli) return 0 @@ -36,11 +38,23 @@ def _add_target_args(parser: argparse.ArgumentParser) -> None: default=".", help="Project directory to install into (default: current directory)", ) + parser.add_argument( + "--cli", + action="store_true", + help=( + "Target the Cline CLI instead of the VS Code extension. The CLI reads " + "~/.cline/hooks (with --global) or /.cline/hooks; the extension " + "reads ~/Documents/Cline/Rules/Hooks or /.clinerules/hooks." + ), + ) parser.add_argument( "--global", dest="global_install", action="store_true", - help="Use ~/Documents/Cline/Rules/Hooks/ instead of the project's .clinerules/hooks/", + help=( + "Install globally for all projects. Extension: ~/Documents/Cline/Rules/Hooks/; " + "CLI (with --cli): ~/.cline/hooks. Default is the current project's hooks dir." + ), ) diff --git a/hindsight-integrations/cline/hindsight_cline/install.py b/hindsight-integrations/cline/hindsight_cline/install.py index 3fc7ba673e..f3cb3c2d41 100644 --- a/hindsight-integrations/cline/hindsight_cline/install.py +++ b/hindsight-integrations/cline/hindsight_cline/install.py @@ -30,7 +30,21 @@ def _payload_root(): return resources.files(PACKAGE).joinpath("hooks") -def get_hooks_dir(project_dir: Path, global_install: bool) -> Path: +def get_hooks_dir(project_dir: Path, global_install: bool, cli: bool = False) -> Path: + """Resolve the hooks directory for the target Cline client. + + The Cline CLI and the VS Code extension discover hooks from *different* + directories, so installing into the extension's paths leaves the CLI unable + to find the hooks (they list but never fire — see issue #2711): + + - CLI: ``~/.cline/hooks`` (global) or ``/.cline/hooks`` (project). + - Extension: ``~/Documents/Cline/Rules/Hooks`` (global) or + ``/.clinerules/hooks`` (project). + """ + if cli: + if global_install: + return Path.home() / ".cline" / "hooks" + return project_dir / ".cline" / "hooks" if global_install: return Path.home() / "Documents" / "Cline" / "Rules" / "Hooks" return project_dir / ".clinerules" / "hooks" @@ -89,11 +103,12 @@ def run_install( api_token: str | None = None, project_dir: Path | None = None, global_install: bool = False, + cli: bool = False, ) -> None: """Install the hook scripts into Cline and record connection settings.""" - hooks_dir = get_hooks_dir((project_dir or Path(".")).resolve(), global_install) + hooks_dir = get_hooks_dir((project_dir or Path(".")).resolve(), global_install, cli) - print("Installing Hindsight memory for Cline...") + print(f"Installing Hindsight memory for Cline ({'CLI' if cli else 'VS Code extension'})...") print(f" Hooks dir : {hooks_dir}") print(f" API URL : {api_url or '(set later in ~/.hindsight/cline.json)'}") print() @@ -102,15 +117,21 @@ def run_install( write_user_config(api_url, api_token) print() - print("Done. Final step — enable hooks in Cline:") - print(" Settings → Features → Hooks (toggle on)") + if cli: + # The CLI has no enable toggle — it reads its hooks directory directly. + print("Done. The Cline CLI reads this directory automatically — no toggle needed.") + print("To install elsewhere, point the CLI at these hooks with:") + print(f" export CLINE_HOOKS_DIR={hooks_dir} # or: cline --hooks-dir {hooks_dir}") + else: + print("Done. Final step — enable hooks in Cline:") + print(" Settings → Features → Hooks (toggle on)") print() print("Note: Cline hooks run on macOS and Linux only.") -def run_uninstall(project_dir: Path | None = None, global_install: bool = False) -> None: +def run_uninstall(project_dir: Path | None = None, global_install: bool = False, cli: bool = False) -> None: """Remove the deployed hook scripts, ``lib/`` and ``settings.json``.""" - hooks_dir = get_hooks_dir((project_dir or Path(".")).resolve(), global_install) + hooks_dir = get_hooks_dir((project_dir or Path(".")).resolve(), global_install, cli) removed = False for name in [*HOOK_FILES, "settings.json"]: diff --git a/hindsight-integrations/cline/tests/test_install.py b/hindsight-integrations/cline/tests/test_install.py index 7a6f2a64f6..ac5ba219e7 100644 --- a/hindsight-integrations/cline/tests/test_install.py +++ b/hindsight-integrations/cline/tests/test_install.py @@ -2,9 +2,10 @@ import json import os +from pathlib import Path import pytest -from hindsight_cline import install_hooks, write_user_config +from hindsight_cline import get_hooks_dir, install_hooks, write_user_config from hindsight_cline.cli import main HOOK_FILES = ["TaskStart", "UserPromptSubmit", "TaskComplete", "TaskCancel"] @@ -73,3 +74,47 @@ def test_cli_requires_subcommand(): with pytest.raises(SystemExit) as exc: main([]) assert exc.value.code != 0 + + +# ── Cline CLI hook layout (#2711) ───────────────────────────────────────────── + + +def test_get_hooks_dir_targets_are_client_specific(): + project = Path("/proj") + # Extension (default) — the VS Code paths. + assert get_hooks_dir(project, global_install=False) == project / ".clinerules" / "hooks" + assert get_hooks_dir(project, global_install=True) == Path.home() / "Documents" / "Cline" / "Rules" / "Hooks" + # CLI — the paths the cline CLI actually reads. + assert get_hooks_dir(project, global_install=False, cli=True) == project / ".cline" / "hooks" + assert get_hooks_dir(project, global_install=True, cli=True) == Path.home() / ".cline" / "hooks" + + +def test_cli_flag_installs_into_dot_cline_hooks(tmp_path, monkeypatch): + monkeypatch.setenv("HOME", str(tmp_path)) + rc = main(["install", "--cli", "--project-dir", str(tmp_path)]) + assert rc == 0 + cli_hooks = tmp_path / ".cline" / "hooks" + assert all((cli_hooks / name).exists() for name in HOOK_FILES) + assert (cli_hooks / "lib" / "hooks_impl.py").exists() + assert (cli_hooks / "settings.json").exists() + # The extension path must NOT be written when targeting the CLI. + assert not (tmp_path / ".clinerules" / "hooks").exists() + + +def test_cli_global_flag_installs_into_home_dot_cline_hooks(tmp_path, monkeypatch): + monkeypatch.setenv("HOME", str(tmp_path)) + rc = main(["install", "--cli", "--global"]) + assert rc == 0 + assert (tmp_path / ".cline" / "hooks" / "TaskStart").exists() + + +def test_cli_uninstall_removes_dot_cline_hooks(tmp_path, monkeypatch): + monkeypatch.setenv("HOME", str(tmp_path)) + main(["install", "--cli", "--project-dir", str(tmp_path)]) + cli_hooks = tmp_path / ".cline" / "hooks" + assert (cli_hooks / "TaskStart").exists() + + rc = main(["uninstall", "--cli", "--project-dir", str(tmp_path)]) + assert rc == 0 + assert not (cli_hooks / "TaskStart").exists() + assert not (cli_hooks / "lib").exists()