hindsight-zed 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
@@ -0,0 +1,120 @@
1
+ Metadata-Version: 2.4
2
+ Name: hindsight-zed
3
+ Version: 0.1.0
4
+ Summary: Automatic long-term memory for the Zed editor's AI assistant via Hindsight
5
+ Project-URL: Homepage, https://github.com/vectorize-io/hindsight
6
+ Project-URL: Documentation, https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/zed
7
+ Project-URL: Repository, https://github.com/vectorize-io/hindsight
8
+ Author-email: Vectorize <support@vectorize.io>
9
+ License: MIT
10
+ Keywords: agents,ai,coding,hindsight,memory,zed
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+
22
+ # hindsight-zed
23
+
24
+ Long-term memory for the [Zed](https://zed.dev) editor's AI assistant, powered by
25
+ [Hindsight](https://github.com/vectorize-io/hindsight).
26
+
27
+ `hindsight-zed init` wires Zed's Agent Panel to the Hindsight **MCP server** and
28
+ adds a rule telling the agent to use it — so it recalls relevant memory at the
29
+ start of a task and retains durable facts as it goes. Recall happens at query
30
+ time against your actual message (no lag), and from your seat it's automatic.
31
+
32
+ ## How it works
33
+
34
+ Zed has no pre-prompt hook, but it does support two things this integration uses:
35
+
36
+ - **MCP context servers** — Zed runs MCP servers configured under
37
+ `context_servers` in `settings.json` and exposes their tools in the Agent
38
+ Panel. We register the Hindsight MCP server there, giving the agent
39
+ `recall` / `retain` / `reflect` tools.
40
+ - **A global instructions file** (`~/.config/zed/AGENTS.md`) that Zed includes in
41
+ every conversation. We add a small rule there telling the agent to recall
42
+ first and retain what it learns.
43
+
44
+ Zed doesn't yet have native HTTP-MCP transport, so the server is connected
45
+ through the [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) stdio bridge
46
+ (run via `npx`) — that means you need Node.js installed.
47
+
48
+ ## Install
49
+
50
+ ```bash
51
+ pip install hindsight-zed
52
+ hindsight-zed init --api-token YOUR_HINDSIGHT_API_KEY --bank-id my-memory
53
+ ```
54
+
55
+ `init` adds the `hindsight` MCP server to `~/.config/zed/settings.json` and the
56
+ recall/retain rule to `~/.config/zed/AGENTS.md`. Restart Zed, open the Agent
57
+ Panel, and the `hindsight` server should show a green dot.
58
+
59
+ Use a [Hindsight Cloud](https://hindsight.vectorize.io) key, or point at a
60
+ self-hosted server with `--api-url http://localhost:8888` (no token needed for an
61
+ open local server).
62
+
63
+ > If your `settings.json` contains comments (JSONC), `init` won't rewrite it —
64
+ > it prints the exact `context_servers` entry for you to paste instead. Use
65
+ > `hindsight-zed init --print-only` any time to see the snippet without writing.
66
+
67
+ ## Commands
68
+
69
+ | Command | Description |
70
+ | --- | --- |
71
+ | `hindsight-zed init` | Add the MCP server + recall/retain rule |
72
+ | `hindsight-zed status` | Show whether the server + rule are configured |
73
+ | `hindsight-zed uninstall` | Remove the server + rule |
74
+ | `hindsight-zed init --print-only` | Print the config to add manually |
75
+
76
+ ## What gets written
77
+
78
+ `~/.config/zed/settings.json`:
79
+
80
+ ```jsonc
81
+ {
82
+ "context_servers": {
83
+ "hindsight": {
84
+ "source": "custom",
85
+ "command": "npx",
86
+ "args": [
87
+ "-y", "mcp-remote",
88
+ "https://api.hindsight.vectorize.io/mcp/my-memory/",
89
+ "--header", "Authorization: Bearer YOUR_HINDSIGHT_API_KEY"
90
+ ]
91
+ }
92
+ }
93
+ }
94
+ ```
95
+
96
+ `~/.config/zed/AGENTS.md` (inside a fenced `<!-- HINDSIGHT -->` block that leaves
97
+ the rest of the file untouched): a short rule telling the agent to `recall` at
98
+ the start of each task and `retain` durable facts.
99
+
100
+ ## Configuration
101
+
102
+ | Setting | Env var | Default |
103
+ | --- | --- | --- |
104
+ | API URL | `HINDSIGHT_API_URL` | `https://api.hindsight.vectorize.io` |
105
+ | API token | `HINDSIGHT_API_TOKEN` | _(none; required for Cloud)_ |
106
+ | Bank id | `HINDSIGHT_ZED_BANK_ID` | `zed` |
107
+
108
+ These can also live in `~/.hindsight/zed.json` (written by `init`).
109
+
110
+ ## Development
111
+
112
+ ```bash
113
+ uv sync
114
+ uv run pytest tests -v -m 'not requires_real_llm' # deterministic suite
115
+ uv run pytest tests -v -m requires_real_llm # gated MCP-endpoint check
116
+ ```
117
+
118
+ ## License
119
+
120
+ MIT
@@ -0,0 +1,99 @@
1
+ # hindsight-zed
2
+
3
+ Long-term memory for the [Zed](https://zed.dev) editor's AI assistant, powered by
4
+ [Hindsight](https://github.com/vectorize-io/hindsight).
5
+
6
+ `hindsight-zed init` wires Zed's Agent Panel to the Hindsight **MCP server** and
7
+ adds a rule telling the agent to use it — so it recalls relevant memory at the
8
+ start of a task and retains durable facts as it goes. Recall happens at query
9
+ time against your actual message (no lag), and from your seat it's automatic.
10
+
11
+ ## How it works
12
+
13
+ Zed has no pre-prompt hook, but it does support two things this integration uses:
14
+
15
+ - **MCP context servers** — Zed runs MCP servers configured under
16
+ `context_servers` in `settings.json` and exposes their tools in the Agent
17
+ Panel. We register the Hindsight MCP server there, giving the agent
18
+ `recall` / `retain` / `reflect` tools.
19
+ - **A global instructions file** (`~/.config/zed/AGENTS.md`) that Zed includes in
20
+ every conversation. We add a small rule there telling the agent to recall
21
+ first and retain what it learns.
22
+
23
+ Zed doesn't yet have native HTTP-MCP transport, so the server is connected
24
+ through the [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) stdio bridge
25
+ (run via `npx`) — that means you need Node.js installed.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pip install hindsight-zed
31
+ hindsight-zed init --api-token YOUR_HINDSIGHT_API_KEY --bank-id my-memory
32
+ ```
33
+
34
+ `init` adds the `hindsight` MCP server to `~/.config/zed/settings.json` and the
35
+ recall/retain rule to `~/.config/zed/AGENTS.md`. Restart Zed, open the Agent
36
+ Panel, and the `hindsight` server should show a green dot.
37
+
38
+ Use a [Hindsight Cloud](https://hindsight.vectorize.io) key, or point at a
39
+ self-hosted server with `--api-url http://localhost:8888` (no token needed for an
40
+ open local server).
41
+
42
+ > If your `settings.json` contains comments (JSONC), `init` won't rewrite it —
43
+ > it prints the exact `context_servers` entry for you to paste instead. Use
44
+ > `hindsight-zed init --print-only` any time to see the snippet without writing.
45
+
46
+ ## Commands
47
+
48
+ | Command | Description |
49
+ | --- | --- |
50
+ | `hindsight-zed init` | Add the MCP server + recall/retain rule |
51
+ | `hindsight-zed status` | Show whether the server + rule are configured |
52
+ | `hindsight-zed uninstall` | Remove the server + rule |
53
+ | `hindsight-zed init --print-only` | Print the config to add manually |
54
+
55
+ ## What gets written
56
+
57
+ `~/.config/zed/settings.json`:
58
+
59
+ ```jsonc
60
+ {
61
+ "context_servers": {
62
+ "hindsight": {
63
+ "source": "custom",
64
+ "command": "npx",
65
+ "args": [
66
+ "-y", "mcp-remote",
67
+ "https://api.hindsight.vectorize.io/mcp/my-memory/",
68
+ "--header", "Authorization: Bearer YOUR_HINDSIGHT_API_KEY"
69
+ ]
70
+ }
71
+ }
72
+ }
73
+ ```
74
+
75
+ `~/.config/zed/AGENTS.md` (inside a fenced `<!-- HINDSIGHT -->` block that leaves
76
+ the rest of the file untouched): a short rule telling the agent to `recall` at
77
+ the start of each task and `retain` durable facts.
78
+
79
+ ## Configuration
80
+
81
+ | Setting | Env var | Default |
82
+ | --- | --- | --- |
83
+ | API URL | `HINDSIGHT_API_URL` | `https://api.hindsight.vectorize.io` |
84
+ | API token | `HINDSIGHT_API_TOKEN` | _(none; required for Cloud)_ |
85
+ | Bank id | `HINDSIGHT_ZED_BANK_ID` | `zed` |
86
+
87
+ These can also live in `~/.hindsight/zed.json` (written by `init`).
88
+
89
+ ## Development
90
+
91
+ ```bash
92
+ uv sync
93
+ uv run pytest tests -v -m 'not requires_real_llm' # deterministic suite
94
+ uv run pytest tests -v -m requires_real_llm # gated MCP-endpoint check
95
+ ```
96
+
97
+ ## License
98
+
99
+ MIT
@@ -0,0 +1,3 @@
1
+ """Hindsight memory integration for the Zed editor."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,173 @@
1
+ """CLI for the Hindsight Zed integration.
2
+
3
+ ``hindsight-zed init`` wires Zed's MCP ``context_servers`` to the Hindsight MCP
4
+ endpoint and writes a recall/retain rule into Zed's global instructions file.
5
+ After that, Zed's Agent Panel has ``recall``/``retain``/``reflect`` tools and is
6
+ told (via the rule) to use them automatically. There is no background process.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ import shutil
14
+ import sys
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+ from typing import Optional
18
+
19
+ from . import __version__
20
+ from .config import USER_CONFIG_FILE, ZedConfig, load_config
21
+ from .rules_file import RULE_TEXT, clear_rule, default_rules_path, write_rule
22
+ from .rules_file import is_installed as rule_installed
23
+ from .zed_settings import (
24
+ SettingsResult,
25
+ apply_to_settings,
26
+ build_context_server,
27
+ default_settings_path,
28
+ remove_from_settings,
29
+ render_snippet,
30
+ )
31
+ from .zed_settings import (
32
+ is_installed as server_installed,
33
+ )
34
+
35
+
36
+ @dataclass
37
+ class InstallOutcome:
38
+ """Result of an ``init``: how the settings file changed and where the rule went."""
39
+
40
+ settings: SettingsResult
41
+ rules_path: Path
42
+
43
+
44
+ def build_install(config: ZedConfig, settings_path: Path, rules_path: Path) -> InstallOutcome:
45
+ """Apply the MCP server entry and the recall/retain rule (the testable core)."""
46
+ server = build_context_server(config.hindsight_api_url, config.hindsight_api_token, config.bank_id)
47
+ settings = apply_to_settings(settings_path, server)
48
+ write_rule(rules_path)
49
+ return InstallOutcome(settings=settings, rules_path=rules_path)
50
+
51
+
52
+ def _config_path(args: argparse.Namespace) -> Path:
53
+ return Path(args.config_path) if args.config_path else USER_CONFIG_FILE
54
+
55
+
56
+ def _resolve_config(args: argparse.Namespace) -> ZedConfig:
57
+ """Config from file/env, overridden by any explicitly-passed CLI flags."""
58
+ cfg = load_config(config_file=_config_path(args))
59
+ if args.api_url:
60
+ cfg.hindsight_api_url = args.api_url
61
+ if args.api_token:
62
+ cfg.hindsight_api_token = args.api_token
63
+ if args.bank_id:
64
+ cfg.bank_id = args.bank_id
65
+ return cfg
66
+
67
+
68
+ def _scaffold_config(cfg: ZedConfig, config_path: Path) -> None:
69
+ """Persist the resolved connection settings so re-runs remember them."""
70
+ if config_path.is_file():
71
+ return
72
+ data = {"hindsightApiUrl": cfg.hindsight_api_url, "bankId": cfg.bank_id}
73
+ if cfg.hindsight_api_token:
74
+ data["hindsightApiToken"] = cfg.hindsight_api_token
75
+ config_path.parent.mkdir(parents=True, exist_ok=True)
76
+ config_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
77
+
78
+
79
+ def cmd_init(args: argparse.Namespace) -> None:
80
+ cfg = _resolve_config(args)
81
+ settings_path = Path(args.settings_path) if args.settings_path else default_settings_path()
82
+ rules_path = Path(args.rules_path) if args.rules_path else default_rules_path()
83
+ server = build_context_server(cfg.hindsight_api_url, cfg.hindsight_api_token, cfg.bank_id)
84
+
85
+ if args.print_only:
86
+ print("Add this to your Zed settings.json:\n")
87
+ print(render_snippet(server))
88
+ print("\nAnd add this rule to ~/.config/zed/AGENTS.md:\n")
89
+ print(RULE_TEXT)
90
+ return
91
+
92
+ print("Setting up Hindsight for Zed ...")
93
+ _scaffold_config(cfg, _config_path(args))
94
+ outcome = build_install(cfg, settings_path, rules_path)
95
+
96
+ if outcome.settings.action == "manual":
97
+ print(f" Your {outcome.settings.path} has comments, so I won't rewrite it.")
98
+ print(" Add this `context_servers` entry yourself:\n")
99
+ print(render_snippet(server))
100
+ else:
101
+ verb = {"created": "Created", "merged": "Updated", "unchanged": "Already configured in"}[
102
+ outcome.settings.action
103
+ ]
104
+ print(f" {verb} {outcome.settings.path} (MCP server: hindsight → bank '{cfg.bank_id}')")
105
+ print(f" Wrote recall/retain rule to {outcome.rules_path}")
106
+
107
+ if shutil.which("npx") is None:
108
+ print("\n warning: `npx` (Node.js) was not found on PATH. Zed runs the MCP")
109
+ print(" bridge via `npx mcp-remote`, so install Node.js for the server to start.")
110
+
111
+ print("\nDone. Restart Zed, open the Agent Panel, and the `hindsight` MCP server")
112
+ print("should show a green dot. Memory recall/retain then happen automatically.")
113
+
114
+
115
+ def cmd_status(args: argparse.Namespace) -> None:
116
+ settings_path = Path(args.settings_path) if args.settings_path else default_settings_path()
117
+ rules_path = Path(args.rules_path) if args.rules_path else default_rules_path()
118
+ print(f"MCP server in {settings_path}: {'installed' if server_installed(settings_path) else 'not installed'}")
119
+ print(f"Recall/retain rule in {rules_path}: {'installed' if rule_installed(rules_path) else 'not installed'}")
120
+
121
+
122
+ def cmd_uninstall(args: argparse.Namespace) -> None:
123
+ settings_path = Path(args.settings_path) if args.settings_path else default_settings_path()
124
+ rules_path = Path(args.rules_path) if args.rules_path else default_rules_path()
125
+ result = remove_from_settings(settings_path)
126
+ if result.action == "manual":
127
+ print(f" {settings_path} has comments — remove the `hindsight` context_servers entry yourself.")
128
+ elif result.action == "removed":
129
+ print(f" Removed the hindsight MCP server from {settings_path}")
130
+ else:
131
+ print(f" No hindsight MCP server found in {settings_path}")
132
+ clear_rule(rules_path)
133
+ print(f" Removed the recall/retain rule from {rules_path}")
134
+
135
+
136
+ def _add_path_overrides(parser: argparse.ArgumentParser) -> None:
137
+ # Hidden overrides used by tests and advanced setups.
138
+ parser.add_argument("--settings-path", default=None, help=argparse.SUPPRESS)
139
+ parser.add_argument("--rules-path", default=None, help=argparse.SUPPRESS)
140
+ parser.add_argument("--config-path", default=None, help=argparse.SUPPRESS)
141
+
142
+
143
+ def main(argv: Optional[list] = None) -> int:
144
+ parser = argparse.ArgumentParser(prog="hindsight-zed", description="Hindsight memory for Zed (via MCP)")
145
+ parser.add_argument("--version", action="version", version=f"hindsight-zed {__version__}")
146
+ sub = parser.add_subparsers(dest="command")
147
+
148
+ init_p = sub.add_parser("init", help="Configure Zed's MCP server + recall/retain rule")
149
+ init_p.add_argument("--api-url", default=None, help="Hindsight API URL (default: cloud)")
150
+ init_p.add_argument("--api-token", default=None, help="Hindsight API token (for Cloud)")
151
+ init_p.add_argument("--bank-id", default=None, help="Memory bank for the MCP server (default: zed)")
152
+ init_p.add_argument("--print-only", action="store_true", help="Print the config to add manually; write nothing")
153
+ _add_path_overrides(init_p)
154
+ init_p.set_defaults(func=cmd_init)
155
+
156
+ status_p = sub.add_parser("status", help="Show whether the MCP server + rule are configured")
157
+ _add_path_overrides(status_p)
158
+ status_p.set_defaults(func=cmd_status)
159
+
160
+ uninst_p = sub.add_parser("uninstall", help="Remove the MCP server + rule")
161
+ _add_path_overrides(uninst_p)
162
+ uninst_p.set_defaults(func=cmd_uninstall)
163
+
164
+ args = parser.parse_args(argv)
165
+ if not hasattr(args, "func"):
166
+ parser.print_help()
167
+ return 1
168
+ args.func(args)
169
+ return 0
170
+
171
+
172
+ if __name__ == "__main__":
173
+ sys.exit(main())
@@ -0,0 +1,79 @@
1
+ """Configuration for the Hindsight Zed integration.
2
+
3
+ Settings layer (later wins): built-in defaults → ``~/.hindsight/zed.json`` →
4
+ environment variables. Resolved into a typed :class:`ZedConfig`.
5
+
6
+ The integration is configuration-only: it wires Zed's MCP ``context_servers`` to
7
+ the Hindsight MCP endpoint and writes a recall/retain rule into Zed's global
8
+ instructions file. Memory operations happen through the MCP server at runtime,
9
+ so there is no daemon or direct API client here.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import os
16
+ from dataclasses import dataclass
17
+ from pathlib import Path
18
+ from typing import Optional
19
+
20
+ # Cross-integration cloud-default convention.
21
+ DEFAULT_HINDSIGHT_API_URL = "https://api.hindsight.vectorize.io"
22
+ DEFAULT_BANK_ID = "zed"
23
+
24
+ USER_CONFIG_FILE = Path.home() / ".hindsight" / "zed.json"
25
+
26
+
27
+ @dataclass
28
+ class ZedConfig:
29
+ """Resolved configuration for the Zed MCP setup."""
30
+
31
+ hindsight_api_url: str = DEFAULT_HINDSIGHT_API_URL
32
+ hindsight_api_token: Optional[str] = None
33
+ # The memory bank the Zed MCP server is scoped to (it's the last path
34
+ # segment of the MCP endpoint URL).
35
+ bank_id: str = DEFAULT_BANK_ID
36
+
37
+
38
+ # user-config file key -> attribute
39
+ _FILE_KEYS = {
40
+ "hindsightApiUrl": "hindsight_api_url",
41
+ "hindsightApiToken": "hindsight_api_token",
42
+ "bankId": "bank_id",
43
+ }
44
+
45
+ # env var -> attribute
46
+ _ENV_KEYS = {
47
+ "HINDSIGHT_API_URL": "hindsight_api_url",
48
+ "HINDSIGHT_API_TOKEN": "hindsight_api_token",
49
+ "HINDSIGHT_ZED_BANK_ID": "bank_id",
50
+ }
51
+
52
+
53
+ def load_config(config_file: Optional[Path] = None, env: Optional[dict] = None) -> ZedConfig:
54
+ """Load and resolve configuration from file then environment."""
55
+ cfg = ZedConfig()
56
+ env = os.environ if env is None else env
57
+
58
+ path = config_file if config_file is not None else USER_CONFIG_FILE
59
+ if path.is_file():
60
+ try:
61
+ data = json.loads(path.read_text(encoding="utf-8"))
62
+ except (json.JSONDecodeError, OSError):
63
+ data = {}
64
+ for key, attr in _FILE_KEYS.items():
65
+ value = data.get(key)
66
+ if value:
67
+ setattr(cfg, attr, str(value))
68
+
69
+ for key, attr in _ENV_KEYS.items():
70
+ value = env.get(key)
71
+ if value:
72
+ setattr(cfg, attr, str(value))
73
+
74
+ if not cfg.hindsight_api_url:
75
+ cfg.hindsight_api_url = DEFAULT_HINDSIGHT_API_URL
76
+ if not cfg.bank_id:
77
+ cfg.bank_id = DEFAULT_BANK_ID
78
+
79
+ return cfg
@@ -0,0 +1,97 @@
1
+ """Manage Hindsight's recall/retain rule inside Zed's global instructions file.
2
+
3
+ Zed includes a global instructions file (``~/.config/zed/AGENTS.md`` on macOS and
4
+ Linux) in **every** agent conversation. We write a static rule there telling the
5
+ agent to use the Hindsight MCP tools — recall relevant memory at the start of a
6
+ task, and retain durable facts as it learns them.
7
+
8
+ The rule lives inside a fenced ``<!-- HINDSIGHT:BEGIN -->`` … ``<!-- HINDSIGHT:END -->``
9
+ block so we can update or remove it without touching the user's own rules in the
10
+ same file.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from pathlib import Path
16
+
17
+ BEGIN_MARKER = "<!-- HINDSIGHT:BEGIN -->"
18
+ END_MARKER = "<!-- HINDSIGHT:END -->"
19
+
20
+ # The recall/retain instruction injected into Zed's global rules.
21
+ RULE_TEXT = (
22
+ "You have persistent long-term memory through the Hindsight MCP server "
23
+ "(`recall`, `retain`, and `reflect` tools).\n\n"
24
+ "- At the start of each task, call `recall` with the user's request to load "
25
+ "relevant decisions, preferences, and project context before you answer. "
26
+ "Use what's relevant and ignore the rest.\n"
27
+ "- When you learn a durable fact — an architectural decision, a user "
28
+ "preference, a convention, or anything worth remembering across sessions — "
29
+ "call `retain` to store it.\n"
30
+ "- Do not mention these memory operations unless the user asks about them."
31
+ )
32
+
33
+
34
+ def default_rules_path() -> Path:
35
+ """Zed's global instructions file (``~/.config/zed/AGENTS.md``)."""
36
+ return Path.home() / ".config" / "zed" / "AGENTS.md"
37
+
38
+
39
+ def _strip_block(text: str) -> str:
40
+ """Remove an existing HINDSIGHT block (and its surrounding blank lines)."""
41
+ start = text.find(BEGIN_MARKER)
42
+ if start == -1:
43
+ return text
44
+ end = text.find(END_MARKER, start)
45
+ if end == -1:
46
+ # Malformed (begin without end) — drop from the marker onward.
47
+ return text[:start].rstrip() + "\n"
48
+ end += len(END_MARKER)
49
+ before = text[:start].rstrip()
50
+ after = text[end:].lstrip()
51
+ if before and after:
52
+ return f"{before}\n\n{after}"
53
+ return (before or after).rstrip() + ("\n" if (before or after) else "")
54
+
55
+
56
+ def render_block(rule_text: str = RULE_TEXT) -> str:
57
+ """Render the fenced HINDSIGHT rule block (no trailing newline)."""
58
+ return f"{BEGIN_MARKER}\n{rule_text.strip()}\n{END_MARKER}"
59
+
60
+
61
+ def write_rule(path: Path, rule_text: str = RULE_TEXT) -> Path:
62
+ """Write/replace Hindsight's rule block in the instructions file at ``path``.
63
+
64
+ Preserves any user-authored content and only rewrites our fenced block,
65
+ placing it at the top so the memory rule leads the instructions.
66
+ """
67
+ existing = path.read_text(encoding="utf-8") if path.is_file() else ""
68
+ base = _strip_block(existing).rstrip()
69
+ block = render_block(rule_text)
70
+ new_text = f"{block}\n\n{base}\n" if base else f"{block}\n"
71
+ path.parent.mkdir(parents=True, exist_ok=True)
72
+ path.write_text(new_text, encoding="utf-8")
73
+ return path
74
+
75
+
76
+ def clear_rule(path: Path) -> Path:
77
+ """Remove Hindsight's rule block from the instructions file, if present.
78
+
79
+ Leaves the rest of the file intact. If removing the block empties a file that
80
+ held nothing else, the file is deleted.
81
+ """
82
+ if not path.is_file():
83
+ return path
84
+ existing = path.read_text(encoding="utf-8")
85
+ if BEGIN_MARKER not in existing:
86
+ return path
87
+ stripped = _strip_block(existing).strip()
88
+ if not stripped:
89
+ path.unlink()
90
+ return path
91
+ path.write_text(stripped + "\n", encoding="utf-8")
92
+ return path
93
+
94
+
95
+ def is_installed(path: Path) -> bool:
96
+ """Whether our rule block is present in the instructions file at ``path``."""
97
+ return path.is_file() and BEGIN_MARKER in path.read_text(encoding="utf-8")