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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions hindsight-integrations/cline/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<dir>` or `cline --hooks-dir <dir>`.

## How It Works

Expand Down Expand Up @@ -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 `<hindsight_memories>` block.
4. Complete a task, then check the `cline` bank (via the API or dashboard) — a memory should appear.

Expand Down
18 changes: 16 additions & 2 deletions hindsight-integrations/cline/hindsight_cline/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""

Expand All @@ -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


Expand All @@ -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 <project>/.cline/hooks; the extension "
"reads ~/Documents/Cline/Rules/Hooks or <project>/.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."
),
)


Expand Down
35 changes: 28 additions & 7 deletions hindsight-integrations/cline/hindsight_cline/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ``<project>/.cline/hooks`` (project).
- Extension: ``~/Documents/Cline/Rules/Hooks`` (global) or
``<project>/.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"
Expand Down Expand Up @@ -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()
Expand All @@ -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"]:
Expand Down
47 changes: 46 additions & 1 deletion hindsight-integrations/cline/tests/test_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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()
Loading