helix-memory 4.0.1__py3-none-any.whl

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.
helix/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from importlib.metadata import version
2
+
3
+ __version__ = version("helix-memory")
helix/cli/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ from .app import app
2
+ from .commands import (
3
+ cmd_forget,
4
+ cmd_install,
5
+ cmd_list,
6
+ cmd_recall,
7
+ cmd_remember,
8
+ cmd_uninstall,
9
+ )
10
+
11
+ __all__ = [
12
+ "app",
13
+ "cmd_forget",
14
+ "cmd_install",
15
+ "cmd_list",
16
+ "cmd_recall",
17
+ "cmd_remember",
18
+ "cmd_uninstall",
19
+ ]
helix/cli/app.py ADDED
@@ -0,0 +1,21 @@
1
+ import typer
2
+
3
+ from helix.cli.commands import COMMANDS
4
+ from helix.core import Brain
5
+
6
+ app = typer.Typer()
7
+
8
+
9
+ @app.callback(invoke_without_command=True)
10
+ def main(ctx: typer.Context) -> None:
11
+ Brain().initialize()
12
+ if ctx.invoked_subcommand is None:
13
+ typer.echo("Helix — global convention memory. Run `helix --help` for commands.")
14
+
15
+
16
+ for name, command in COMMANDS.items():
17
+ app.command(name)(command)
18
+
19
+
20
+ if __name__ == "__main__": # pragma: no cover
21
+ app()
helix/cli/commands.py ADDED
@@ -0,0 +1,136 @@
1
+ import shutil
2
+ from collections.abc import Callable
3
+ from pathlib import Path
4
+ from typing import Annotated
5
+
6
+ import typer
7
+
8
+ from helix.core import Brain, Scope, detect_snippet_blocks, install, uninstall
9
+ from helix.core.installer import clients as all_clients
10
+ from helix.core.installer import detect_installed_clients
11
+ from helix.utils import parse_csv
12
+
13
+ from .prompts import pick, pick_many
14
+
15
+
16
+ def cmd_remember(
17
+ name: Annotated[str, typer.Argument(help="Convention name (kebab-case).")],
18
+ body: Annotated[str, typer.Argument(help="Convention body text.")],
19
+ tags: Annotated[str | None, typer.Option(help="Comma-separated tags.")] = None,
20
+ applies_to: Annotated[
21
+ str | None, typer.Option(help="Comma-separated stacks/scopes.")
22
+ ] = None,
23
+ ) -> None:
24
+ path = Brain().remember(
25
+ name=name,
26
+ body=body,
27
+ tags=parse_csv(tags),
28
+ applies_to=parse_csv(applies_to),
29
+ )
30
+ typer.echo(f"Saved as {path.name}")
31
+
32
+
33
+ def cmd_list(
34
+ tags: Annotated[
35
+ str | None, typer.Option(help="Filter by comma-separated tags.")
36
+ ] = None,
37
+ ) -> None:
38
+ lines = Brain().list_conventions(tags=parse_csv(tags))
39
+ if not lines:
40
+ typer.echo("No conventions found.")
41
+ return
42
+ for line in lines:
43
+ typer.echo(line)
44
+
45
+
46
+ def cmd_recall(
47
+ query: Annotated[str, typer.Argument(help="Substring to search for.")],
48
+ tags: Annotated[
49
+ str | None, typer.Option(help="Filter by comma-separated tags.")
50
+ ] = None,
51
+ ) -> None:
52
+ results: list[str] = Brain().recall(query=query, tags=parse_csv(tags))
53
+ if not results:
54
+ typer.echo("No matches found.")
55
+ return
56
+ for line in results:
57
+ typer.echo(line)
58
+
59
+
60
+ def cmd_forget(
61
+ name: Annotated[str, typer.Argument(help="Convention name to remove.")],
62
+ ) -> None:
63
+ if Brain().forget(name):
64
+ typer.echo(f"Removed {name}")
65
+ else:
66
+ typer.echo(f"Convention '{name}' not found.", err=True)
67
+ raise typer.Exit(1)
68
+
69
+
70
+ def cmd_install() -> None:
71
+ detected = detect_installed_clients()
72
+ available = detected or all_clients()
73
+ if not detected:
74
+ typer.echo("No client config directories found; showing all known clients.")
75
+
76
+ typer.echo("Pick client(s):")
77
+ selected = [available[i] for i in pick_many("Clients", [c.name for c in available])]
78
+
79
+ project_root = Path.cwd()
80
+ typer.echo("Pick a scope:")
81
+ scope = (Scope.GLOBAL, Scope.PROJECT)[
82
+ pick("Scope", ["global (per-user config dir)", "project (this repo)"])
83
+ ]
84
+
85
+ written: set[Path] = set()
86
+ for client in selected:
87
+ path = client.path_for(scope, project_root)
88
+ if path in written: # pragma: no cover
89
+ typer.echo(f"Skipped {client.name}: {path} already written this run")
90
+ continue
91
+ install(client, scope, project_root)
92
+ written.add(path)
93
+ typer.echo(f"Wrote helix block to {path} ({client.name})")
94
+
95
+ if not shutil.which("helix"): # pragma: no cover
96
+ typer.echo(
97
+ "\nWarning: 'helix' is not on PATH. The installed snippet tells agents "
98
+ "to run `helix list`, which will fail until the CLI is installed on "
99
+ "PATH (e.g. `pipx install helix` or `uv tool install helix`).",
100
+ err=True,
101
+ )
102
+
103
+
104
+ def cmd_uninstall() -> None:
105
+ project_root = Path.cwd()
106
+ installed = detect_snippet_blocks(project_root)
107
+ if not installed:
108
+ typer.echo("No helix blocks found.")
109
+ return
110
+
111
+ typer.echo("Pick block(s) to remove:")
112
+ labels = [
113
+ f"{block.client.name} [{block.scope}] — {block.path}" for block in installed
114
+ ]
115
+ selected = [installed[i] for i in pick_many("Blocks", labels)]
116
+
117
+ removed_any = False
118
+ for block in selected:
119
+ if uninstall(block.client, block.scope, project_root):
120
+ typer.echo(f"Removed helix block from {block.path}")
121
+ removed_any = True
122
+ else:
123
+ typer.echo(f"Nothing to remove from {block.path}", err=True)
124
+
125
+ if not removed_any:
126
+ raise typer.Exit(1)
127
+
128
+
129
+ COMMANDS: dict[str, Callable[..., None]] = {
130
+ "forget": cmd_forget,
131
+ "install": cmd_install,
132
+ "list": cmd_list,
133
+ "recall": cmd_recall,
134
+ "remember": cmd_remember,
135
+ "uninstall": cmd_uninstall,
136
+ }
helix/cli/prompts.py ADDED
@@ -0,0 +1,33 @@
1
+ """Interactive selection helpers for CLI commands."""
2
+
3
+ import typer
4
+
5
+
6
+ def pick(prompt: str, options: list[str]) -> int: # pragma: no cover
7
+ """Prompt for a single choice and return its zero-based index."""
8
+ for index, label in enumerate(options, 1):
9
+ typer.echo(f" {index}) {label}")
10
+ choice = typer.prompt(prompt, type=int, default=1)
11
+ if not 1 <= choice <= len(options):
12
+ typer.echo("Invalid choice.", err=True)
13
+ raise typer.Exit(1)
14
+ return int(choice) - 1
15
+
16
+
17
+ def pick_many(prompt: str, options: list[str]) -> list[int]: # pragma: no cover
18
+ """Prompt for one or more choices and return their zero-based indices."""
19
+ for index, label in enumerate(options, 1):
20
+ typer.echo(f" {index}) {label}")
21
+ raw = typer.prompt(f"{prompt} (comma-separated, or 'all')", default="all")
22
+ if raw.strip().lower() == "all":
23
+ return list(range(len(options)))
24
+ chosen: list[int] = []
25
+ for raw_part in raw.split(","):
26
+ part = raw_part.strip()
27
+ if not part.isdigit() or not 1 <= int(part) <= len(options):
28
+ typer.echo(f"Invalid choice: {part!r}", err=True)
29
+ raise typer.Exit(1)
30
+ index = int(part) - 1
31
+ if index not in chosen:
32
+ chosen.append(index)
33
+ return chosen
helix/core/__init__.py ADDED
@@ -0,0 +1,35 @@
1
+ # pylint: disable=duplicate-code
2
+ from .conventions import Brain, Convention
3
+ from .installer import (
4
+ BLOCK_PATTERN,
5
+ END_MARKER,
6
+ SNIPPET,
7
+ START_MARKER,
8
+ Client,
9
+ Scope,
10
+ SnippetBlock,
11
+ clients,
12
+ detect_installed_clients,
13
+ detect_snippet_blocks,
14
+ install,
15
+ uninstall,
16
+ )
17
+ from .settings import Settings
18
+
19
+ __all__ = [
20
+ "BLOCK_PATTERN",
21
+ "END_MARKER",
22
+ "SNIPPET",
23
+ "START_MARKER",
24
+ "Brain",
25
+ "Client",
26
+ "Convention",
27
+ "Scope",
28
+ "Settings",
29
+ "SnippetBlock",
30
+ "clients",
31
+ "detect_installed_clients",
32
+ "detect_snippet_blocks",
33
+ "install",
34
+ "uninstall",
35
+ ]
@@ -0,0 +1,7 @@
1
+ from helix.core.conventions.brain import Brain
2
+ from helix.core.conventions.convention import Convention
3
+
4
+ __all__ = [
5
+ "Brain",
6
+ "Convention",
7
+ ]
@@ -0,0 +1,127 @@
1
+ from pathlib import Path
2
+
3
+ from helix.core.settings import Settings
4
+
5
+ from .convention import Convention
6
+
7
+
8
+ class Brain:
9
+ @property
10
+ def index(self) -> Path:
11
+ return Settings.HELIX_INDEX
12
+
13
+ @property
14
+ def conventions(self) -> Path:
15
+ return Settings.HELIX_CONVENTIONS
16
+
17
+ @property
18
+ def is_initialized(self) -> bool:
19
+ return self.index.exists()
20
+
21
+ @property
22
+ def content(self) -> str:
23
+ return self.index.read_text()
24
+
25
+ @property
26
+ def index_lines(self) -> list[str]:
27
+ return self.content.splitlines(keepends=True)
28
+
29
+ def initialize(self) -> None:
30
+ self.conventions.mkdir(parents=True, exist_ok=True)
31
+ if not self.is_initialized:
32
+ self.index.write_text("# Helix Convention Index\n\n")
33
+
34
+ def remember(
35
+ self,
36
+ *,
37
+ name: str,
38
+ body: str,
39
+ tags: list[str],
40
+ applies_to: list[str] | None = None,
41
+ ) -> Path:
42
+ convention = Convention(
43
+ name=name, body=body, tags=tags, applies_to=applies_to or []
44
+ )
45
+ convention.file_path.write_text(convention.to_markdown())
46
+ self._add_convention_to_index(convention)
47
+
48
+ return convention.file_path
49
+
50
+ def index_line_for(self, name: str) -> str | None:
51
+ return next(
52
+ (line for line in self.index_lines if line.startswith(f"- [{name}](")),
53
+ None,
54
+ )
55
+
56
+ def convention_for(self, name: str) -> Convention | None:
57
+ path = self.conventions / f"{name}.md"
58
+ if not path.exists():
59
+ return None
60
+ try:
61
+ return Convention.from_markdown(path.read_text())
62
+ except ValueError:
63
+ return None
64
+
65
+ def _add_convention_to_index(self, convention: Convention) -> None:
66
+ lines = [
67
+ line
68
+ for line in self.index_lines
69
+ if not line.startswith(f"- [{convention.name}](")
70
+ ]
71
+ content = "".join(lines).rstrip("\n") + "\n"
72
+ content += convention.index_line() + "\n"
73
+ self.index.write_text(content)
74
+
75
+ def list_conventions(self, tags: list[str] | None = None) -> list[str]:
76
+ if not self.is_initialized:
77
+ return []
78
+ lines = [line for line in self.index_lines if line.startswith("- [")]
79
+ if not tags:
80
+ return lines
81
+
82
+ return self._filter_index_lines_by_tags(lines, tags)
83
+
84
+ @staticmethod
85
+ def _filter_index_lines_by_tags(lines: list[str], tags: list[str]) -> list[str]:
86
+ tags_set = set(tags)
87
+ return [
88
+ line for line in lines if Convention.tags_from_index_line(line) & tags_set
89
+ ]
90
+
91
+ def recall(self, query: str, tags: list[str] | None = None) -> list[str]:
92
+ lines = [
93
+ f"{path}:{line_number}:{line}"
94
+ for path in self.conventions.glob("*.md")
95
+ for line_number, line in enumerate(path.read_text().splitlines(), 1)
96
+ if query.lower() in line.lower()
97
+ ]
98
+
99
+ if not tags:
100
+ return lines
101
+
102
+ tags_set = set(tags)
103
+ allowed_names = {
104
+ convention.name
105
+ for convention in self._load_conventions()
106
+ if tags_set & set(convention.tags)
107
+ }
108
+ return [line for line in lines if any(name in line for name in allowed_names)]
109
+
110
+ def _load_conventions(self) -> list[Convention]:
111
+ conventions = []
112
+ for path in self.conventions.glob("*.md"):
113
+ try:
114
+ conventions.append(Convention.from_markdown(path.read_text()))
115
+ except ValueError:
116
+ pass
117
+ return conventions
118
+
119
+ def forget(self, name: str) -> bool:
120
+ file_path = self.conventions / f"{name}.md"
121
+ if not file_path.exists():
122
+ return False
123
+ file_path.unlink()
124
+ existing_line = self.index_line_for(name)
125
+ if existing_line:
126
+ self.index.write_text(self.content.replace(existing_line, ""))
127
+ return True
@@ -0,0 +1,53 @@
1
+ import re
2
+ from pathlib import Path
3
+ from typing import Self
4
+
5
+ import frontmatter
6
+ from pydantic import BaseModel, ConfigDict, Field
7
+
8
+ from helix.core.settings import Settings
9
+
10
+ CONVENTION_REGEX = r"\[([^\]]*)\] —"
11
+
12
+
13
+ class Convention(BaseModel):
14
+ name: str
15
+ body: str
16
+ tags: list[str] = Field(default_factory=list)
17
+ applies_to: list[str] = Field(default_factory=list)
18
+
19
+ model_config = ConfigDict(extra="ignore")
20
+
21
+ @property
22
+ def file_path(self) -> Path:
23
+ return Settings.HELIX_CONVENTIONS / f"{self.name}.md"
24
+
25
+ def to_markdown(self) -> str:
26
+ post = frontmatter.Post(
27
+ self.body,
28
+ name=self.name,
29
+ tags=self.tags,
30
+ applies_to=self.applies_to,
31
+ )
32
+ return str(frontmatter.dumps(post)) + "\n"
33
+
34
+ @classmethod
35
+ def from_markdown(cls, text: str) -> Self:
36
+ post = frontmatter.loads(text)
37
+ if not post.metadata.get("name"):
38
+ raise ValueError("Invalid convention file: missing 'name' field")
39
+ return cls.model_validate({"body": post.content, **post.metadata})
40
+
41
+ def index_line(self) -> str:
42
+ first_line = self.body.strip().splitlines()[0] if self.body.strip() else ""
43
+ if len(first_line) > 80:
44
+ first_line = first_line[:77] + "..."
45
+ tags_string = ",".join(self.tags)
46
+ return f"- [{self.name}](conventions/{self.name}.md) [{tags_string}] — {first_line}"
47
+
48
+ @staticmethod
49
+ def tags_from_index_line(line: str) -> set[str]:
50
+ match = re.search(CONVENTION_REGEX, line)
51
+ if not match:
52
+ return set()
53
+ return {tag.strip() for tag in match.group(1).split(",") if tag.strip()}
@@ -0,0 +1,30 @@
1
+ # pylint: disable=duplicate-code
2
+ from helix.core.installer.models import Client, Scope, SnippetBlock
3
+ from helix.core.installer.operations import (
4
+ clients,
5
+ detect_installed_clients,
6
+ detect_snippet_blocks,
7
+ install,
8
+ uninstall,
9
+ )
10
+ from helix.core.installer.snippet import (
11
+ BLOCK_PATTERN,
12
+ END_MARKER,
13
+ SNIPPET,
14
+ START_MARKER,
15
+ )
16
+
17
+ __all__ = [
18
+ "BLOCK_PATTERN",
19
+ "END_MARKER",
20
+ "SNIPPET",
21
+ "START_MARKER",
22
+ "Client",
23
+ "Scope",
24
+ "SnippetBlock",
25
+ "clients",
26
+ "detect_installed_clients",
27
+ "detect_snippet_blocks",
28
+ "install",
29
+ "uninstall",
30
+ ]
@@ -0,0 +1,35 @@
1
+ from enum import StrEnum
2
+ from pathlib import Path
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+
7
+ class Scope(StrEnum):
8
+ GLOBAL = "global"
9
+ PROJECT = "project"
10
+
11
+
12
+ class Client(BaseModel):
13
+ key: str = Field(description="Stable identifier for the client (e.g. 'claude').")
14
+ name: str = Field(description="Human-readable client name shown in CLI prompts.")
15
+ global_path: Path = Field(
16
+ description="Absolute path to the client's per-user (global) config file."
17
+ )
18
+ project_relative_path: Path = Field(
19
+ description="Config file path relative to a project root, for project scope."
20
+ )
21
+
22
+ def path_for(self, scope: Scope, project_root: Path) -> Path:
23
+ if scope == Scope.GLOBAL:
24
+ return self.global_path
25
+ return project_root / self.project_relative_path
26
+
27
+
28
+ class SnippetBlock(BaseModel):
29
+ client: Client = Field(description="The client whose config file holds the block.")
30
+ scope: Scope = Field(
31
+ description="Whether the block was found in global or project scope."
32
+ )
33
+ path: Path = Field(
34
+ description="Resolved config file where the helix block was detected."
35
+ )
@@ -0,0 +1,114 @@
1
+ from pathlib import Path
2
+
3
+ from helix.core.settings import Settings
4
+
5
+ from .models import Client, Scope, SnippetBlock
6
+ from .snippet import BLOCK_PATTERN, END_MARKER, SNIPPET, START_MARKER
7
+
8
+
9
+ def clients() -> list[Client]:
10
+ home = Settings.HOME_DIRECTORY
11
+ return [
12
+ Client(
13
+ key="claude",
14
+ name="Claude Code",
15
+ global_path=home / ".claude" / "CLAUDE.md",
16
+ project_relative_path=Path("CLAUDE.md"),
17
+ ),
18
+ Client(
19
+ key="cursor",
20
+ name="Cursor",
21
+ global_path=home / ".cursor" / "rules" / "helix.mdc",
22
+ project_relative_path=Path(".cursor") / "rules" / "helix.mdc",
23
+ ),
24
+ Client(
25
+ key="codex",
26
+ name="Codex CLI",
27
+ global_path=home / ".codex" / "AGENTS.md",
28
+ project_relative_path=Path("AGENTS.md"),
29
+ ),
30
+ Client(
31
+ key="opencode",
32
+ name="Opencode",
33
+ global_path=home / ".config" / "opencode" / "AGENTS.md",
34
+ project_relative_path=Path("AGENTS.md"),
35
+ ),
36
+ ]
37
+
38
+
39
+ def detect_installed_clients() -> list[Client]:
40
+ return [client for client in clients() if client.global_path.parent.exists()]
41
+
42
+
43
+ def detect_snippet_blocks(project_root: Path) -> list[SnippetBlock]:
44
+ blocks: list[SnippetBlock] = []
45
+ for client in clients():
46
+ for scope in Scope:
47
+ path = client.path_for(scope, project_root)
48
+ if not path.exists():
49
+ continue
50
+
51
+ if START_MARKER not in path.read_text():
52
+ continue
53
+
54
+ config_block = SnippetBlock(client=client, scope=scope, path=path)
55
+ blocks.append(config_block)
56
+ return blocks
57
+
58
+
59
+ def _insert_snippet_block(existing: str, block: str) -> str:
60
+ """Return ``existing`` with the Helix snippet block inserted or refreshed.
61
+
62
+ If a snippet block is already present, it is replaced in place
63
+ so reinstalling updates the snippet rather than duplicating it.
64
+
65
+ If there is other content but no existing block, the
66
+ block is appended after a blank-line separator.
67
+
68
+ Otherwise the block becomes the entire content.
69
+ """
70
+ if BLOCK_PATTERN.search(existing):
71
+ return BLOCK_PATTERN.sub(block.rstrip("\n"), existing)
72
+ if existing.strip():
73
+ return existing.rstrip("\n") + "\n\n" + block
74
+ return block
75
+
76
+
77
+ def install(client: Client, scope: Scope, project_root: Path) -> Path:
78
+ """Write (or refresh) the Helix snippet block in the client's config file.
79
+
80
+ Creates the file and parent directories if needed, and returns the path
81
+ that was written.
82
+ """
83
+ path = client.path_for(scope, project_root)
84
+ path.parent.mkdir(parents=True, exist_ok=True)
85
+
86
+ existing = path.read_text() if path.exists() else ""
87
+ block = f"{START_MARKER}\n{SNIPPET}{END_MARKER}\n"
88
+ new_text = _insert_snippet_block(existing, block)
89
+ path.write_text(new_text)
90
+
91
+ return path
92
+
93
+
94
+ def uninstall(client: Client, scope: Scope, project_root: Path) -> bool:
95
+ """Remove the Helix snippet block from the client's config file.
96
+
97
+ Deletes the file if nothing else remains. Returns ``True`` if a block was
98
+ removed, or ``False`` if the file or block was not present.
99
+ """
100
+ path = client.path_for(scope, project_root)
101
+ if not path.exists():
102
+ return False
103
+
104
+ text = path.read_text()
105
+ if START_MARKER not in text:
106
+ return False
107
+
108
+ remaining = BLOCK_PATTERN.sub("", text).strip("\n")
109
+ if not remaining:
110
+ path.unlink()
111
+ return True
112
+
113
+ path.write_text(remaining + "\n")
114
+ return True
@@ -0,0 +1,18 @@
1
+ import re
2
+
3
+ START_MARKER = "<!-- helix:start -->"
4
+ END_MARKER = "<!-- helix:end -->"
5
+
6
+ SNIPPET = (
7
+ "## Helix — Global Conventions\n"
8
+ "\n"
9
+ "If the `helix` CLI is available on PATH, run `helix list` at the start of "
10
+ "every session and treat the output as global coding conventions that apply "
11
+ "across projects. Use `helix recall <query>` to load the full text of any "
12
+ "convention that looks relevant.\n"
13
+ )
14
+
15
+ BLOCK_PATTERN = re.compile(
16
+ re.escape(START_MARKER) + r".*?" + re.escape(END_MARKER),
17
+ re.DOTALL,
18
+ )
helix/core/settings.py ADDED
@@ -0,0 +1,23 @@
1
+ from pathlib import Path
2
+
3
+ from pydantic import Field
4
+ from pydantic_settings import BaseSettings
5
+
6
+
7
+ class AppSettings(BaseSettings):
8
+ HOME_DIRECTORY: Path = Field(default=Path.home())
9
+
10
+ @property
11
+ def HELIX_BRAIN(self) -> Path:
12
+ return self.HOME_DIRECTORY / ".dev_brain"
13
+
14
+ @property
15
+ def HELIX_CONVENTIONS(self) -> Path:
16
+ return self.HELIX_BRAIN / "conventions"
17
+
18
+ @property
19
+ def HELIX_INDEX(self) -> Path:
20
+ return self.HELIX_BRAIN / "INDEX.md"
21
+
22
+
23
+ Settings = AppSettings()
@@ -0,0 +1,3 @@
1
+ from .parsing import parse_csv
2
+
3
+ __all__ = ["parse_csv"]
helix/utils/parsing.py ADDED
@@ -0,0 +1,4 @@
1
+ def parse_csv(value: str | None) -> list[str]:
2
+ if not value:
3
+ return []
4
+ return [item.strip() for item in value.split(",")]
@@ -0,0 +1,74 @@
1
+ Metadata-Version: 2.4
2
+ Name: helix-memory
3
+ Version: 4.0.1
4
+ Summary: A lightweight, multi-client memory layer that persists cross-project coding conventions
5
+ Author-email: Matias Gimenez <matiasgimenez.dev@gmail.com>
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.13
8
+ Requires-Dist: loguru>=0.7.3
9
+ Requires-Dist: pydantic-settings>=2.14.1
10
+ Requires-Dist: pydantic>=2.13.4
11
+ Requires-Dist: python-frontmatter>=1.1.0
12
+ Requires-Dist: typer>=0.25.1
13
+ Description-Content-Type: text/markdown
14
+
15
+ # Helix
16
+
17
+ Global convention memory for AI coding agents — persist your coding preferences once, surface them in every Claude Code, Cursor, or MCP-compatible session.
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ # With uv (recommended)
23
+ uv tool install helix-memory
24
+
25
+ # Or with pip
26
+ pip install helix-memory
27
+ ```
28
+
29
+ ## Quick start
30
+
31
+ ```bash
32
+ # 1. Hook Helix into your agent (Claude Code, Cursor, …).
33
+ helix install
34
+
35
+ # 2. Save your first convention.
36
+ helix remember pydantic-validation \
37
+ "Prefer Pydantic v2 for any external-boundary validation." \
38
+ --tags python,validation
39
+ ```
40
+
41
+ From there, your next agent session will see the conventions automatically.
42
+
43
+ ## CLI
44
+
45
+ ```bash
46
+ helix remember <name> "<body>" --tags <comma,separated>
47
+ helix list [--tags <tag>]
48
+ helix recall "<query>" [--tags <tag>]
49
+ helix forget <name>
50
+ helix install # wire Helix into your agent
51
+ helix uninstall # remove the integration
52
+ helix serve # start the MCP server
53
+ ```
54
+
55
+ ## MCP server
56
+
57
+ `helix serve` exposes four tools to any MCP-compatible client: `remember`, `recall`, `list_conventions`, `forget`.
58
+
59
+ Add to your client's MCP config (e.g. `~/.claude/claude_desktop_config.json`):
60
+
61
+ ```json
62
+ {
63
+ "mcpServers": {
64
+ "helix": {
65
+ "command": "helix",
66
+ "args": ["serve"]
67
+ }
68
+ }
69
+ }
70
+ ```
71
+
72
+ ## License
73
+
74
+ MIT
@@ -0,0 +1,21 @@
1
+ helix/__init__.py,sha256=5g2kqKvprvMTvEJlJb5hCmx3gPH-GNXGaAQi2wUSmp4,78
2
+ helix/cli/__init__.py,sha256=zraDVOm11vw3MjqGS_VVugBKCwG3vbn2s1yGtSWvYpA,285
3
+ helix/cli/app.py,sha256=lF7hPodHgrG2me5Ex8HeA9YZT5mjeVD73fmn9e23pWE,473
4
+ helix/cli/commands.py,sha256=OJwo-Nn9C1mGBMdwpRShyvzrnCBL2fG3qLTSMPkMT1w,4220
5
+ helix/cli/prompts.py,sha256=iDBwni87NYsUgt3Z2urK2dLhLN9M3l6xFQhMlJMaLf0,1275
6
+ helix/core/__init__.py,sha256=a_ongXXQoRWpTQFc3iPVnNKv7uB_ng4IrV7VHGHCCUw,630
7
+ helix/core/settings.py,sha256=i5SVBCqAM9ARUYZ1eCiUGD68O9_rReLNcFBXzB_Nvi0,511
8
+ helix/core/conventions/__init__.py,sha256=CKyUUGf1X3CpJuf_PWx9SUBCx9jmk-NAGOud0hIiBok,150
9
+ helix/core/conventions/brain.py,sha256=P-Ei9wIlSYkgQTDcdxbZlrTFPY_R1rJDVndcjJJMD4s,3929
10
+ helix/core/conventions/convention.py,sha256=M6zmbjw3zi7AxBg2UJB3SAUYSTQ9JZrMSFr0IbT0SDM,1667
11
+ helix/core/installer/__init__.py,sha256=iMC2xycD9CfD1XmP5e0D29NHjYhDxfqz9am2pS2o2xw,602
12
+ helix/core/installer/models.py,sha256=onqU7KaX-bP3wwfYxGWePk7nR9vDda9FbvW72RPKRnk,1145
13
+ helix/core/installer/operations.py,sha256=N26kV2kqTMyTGyE4BPNl8VcWk2cbbLHxWCVqXHD7634,3580
14
+ helix/core/installer/snippet.py,sha256=o0ACtehFPdJA-J2mxbnmJpraN8c6dpPE-3MdmsOcvaU,538
15
+ helix/utils/__init__.py,sha256=9BnbE-Egv-kDjwRLLZJAgR5bfeMRxb0CPbIOj-Z2U5s,56
16
+ helix/utils/parsing.py,sha256=xleJFLV96sLxznFqI6lVj-aPadscX966BK3EGXg1JpA,138
17
+ helix_memory-4.0.1.dist-info/METADATA,sha256=k1NFTJ22FBSs4gColM7berG8cdEO9Pg5rZP_36RLiWw,1709
18
+ helix_memory-4.0.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
19
+ helix_memory-4.0.1.dist-info/entry_points.txt,sha256=eNgBVu2lbzlXnYzKBtj167gLyI9_B6buR9-5udLzyG0,44
20
+ helix_memory-4.0.1.dist-info/licenses/LICENSE,sha256=yY8ZbpGnAb7AQA9D-zSLN6X78WcUvXA34GRr8urUbKY,1073
21
+ helix_memory-4.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ helix = helix.cli.app:app
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Matías Giménez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.