Skip to content

Commit 91bf8e4

Browse files
committed
link to changes in readme
also modularize the package AI Prompts: claude-sonnet-4-6: <ide_opened_file>The user opened the file ./tests/test_prepare_repository.py in the IDE. This may or may not be related to the current task.</ide_opened_file> put each hook into its own file, create a common.py file for common functionality claude-sonnet-4-6: <ide_opened_file>The user opened the file ./ai_prompt_auto_commit/unstage.py in the IDE. This may or may not be related to the current task.</ide_opened_file> test unstage() claude-sonnet-4-6: <ide_selection>The user selected the lines 43 to 43 from ./tests/test_unstage.py: patch This may or may not be related to the current task.</ide_selection> put patching into a fixture
1 parent e7dcda1 commit 91bf8e4

12 files changed

Lines changed: 323 additions & 162 deletions

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,7 @@ This pre-commit supports the following AI models:
9898
committed/
9999
2026-04-12T20-10-00_claude-sonnet-4-6_a2e1ca7....md ← archived after commit
100100
```
101+
102+
## Changelog
103+
104+
You can view the versions in the [change log](CHANGES.md).

ai_prompt_auto_commit/append.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""prepare-commit-msg hook: append pending prompts to the commit message."""
2+
3+
from __future__ import annotations
4+
5+
import sys
6+
from pathlib import Path
7+
8+
from . import common
9+
from .common import PROMPTS_DIRECTORY
10+
11+
12+
def append_to_commit_msg(commit_msg_file: Path) -> int:
13+
"""Append all pending .prompts/*.md files to the commit message."""
14+
repo_root = common._repo_root()
15+
prompts_dir = repo_root / PROMPTS_DIRECTORY
16+
17+
pending = sorted(prompts_dir.glob("*.md"))
18+
if not pending:
19+
return 0
20+
21+
lines: list[str] = ["\nAI Prompts:\n"]
22+
for filepath in pending:
23+
stem = filepath.stem
24+
model = stem.split("_", 1)[1] if "_" in stem else stem
25+
raw = filepath.read_text(encoding="utf-8")
26+
content = " ".join(raw.splitlines()).rstrip()
27+
content = content.replace(str(repo_root) + "/", "./")
28+
lines.append(f"{model}: {content}\n")
29+
30+
with commit_msg_file.open("a", encoding="utf-8") as fh:
31+
fh.writelines(lines)
32+
return 0
33+
34+
35+
def main() -> None:
36+
if len(sys.argv) < 2:
37+
print("Usage: append-ai-prompts <commit-msg-file>", file=sys.stderr)
38+
sys.exit(1)
39+
sys.exit(append_to_commit_msg(Path(sys.argv[1])))

ai_prompt_auto_commit/archive.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""post-commit hook: archive pending prompts into .prompts/committed/."""
2+
3+
from __future__ import annotations
4+
5+
import subprocess
6+
import sys
7+
8+
from . import common
9+
from .common import PROMPTS_DIRECTORY
10+
11+
12+
def archive() -> int:
13+
"""Move pending prompts to .prompts/committed/<name>_<hash>.md."""
14+
repo_root = common._repo_root()
15+
prompts_dir = repo_root / PROMPTS_DIRECTORY
16+
committed_dir = prompts_dir / "committed"
17+
18+
pending = sorted(prompts_dir.glob("*.md"))
19+
if not pending:
20+
return 0
21+
22+
commit_hash = subprocess.check_output(
23+
["git", "rev-parse", "HEAD"], text=True
24+
).strip()
25+
committed_dir.mkdir(parents=True, exist_ok=True)
26+
27+
for filepath in pending:
28+
dest = committed_dir / f"{filepath.stem}_{commit_hash}.md"
29+
filepath.rename(dest)
30+
return 0
31+
32+
33+
def main() -> None:
34+
sys.exit(archive())

ai_prompt_auto_commit/common.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""Shared constants and helpers used across all hooks."""
2+
3+
from __future__ import annotations
4+
5+
import subprocess
6+
from pathlib import Path
7+
8+
PROMPTS_DIRECTORY = ".prompts"
9+
10+
11+
def _repo_root() -> Path:
12+
return Path(
13+
subprocess.check_output(
14+
["git", "rev-parse", "--show-toplevel"], text=True
15+
).strip()
16+
)

ai_prompt_auto_commit/hooks.py

Lines changed: 22 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -1,152 +1,22 @@
1-
"""Git hooks for recording AI prompts in commit messages."""
2-
3-
from __future__ import annotations
4-
5-
import importlib.resources
6-
import json
7-
import subprocess
8-
import sys
9-
from pathlib import Path
10-
11-
12-
PROMPTS_DIRECTORY = ".prompts"
13-
14-
15-
def _repo_root() -> Path:
16-
return Path(
17-
subprocess.check_output(
18-
["git", "rev-parse", "--show-toplevel"], text=True
19-
).strip()
20-
)
21-
22-
23-
def prepare_repository() -> int:
24-
"""Set up .prompts/, .gitignore, and .claude/settings.json in the target repo."""
25-
repo_root = _repo_root()
26-
27-
# Initialize PROMPTS_DIRECTORY with a .gitignore that ignores everything
28-
prompts_dir = repo_root / PROMPTS_DIRECTORY
29-
prompts_dir.mkdir(parents=True, exist_ok=True)
30-
prompts_gitignore = prompts_dir / ".gitignore"
31-
existing_patterns = prompts_gitignore.read_text(encoding="utf-8").splitlines() if prompts_gitignore.exists() else []
32-
if "*" not in existing_patterns:
33-
with prompts_gitignore.open("a", encoding="utf-8") as fh:
34-
fh.write("*\n")
35-
print(f"Initialized {prompts_gitignore}")
36-
else:
37-
print(f"{prompts_gitignore} already ignores *")
38-
39-
# Install the UserPromptSubmit hook into .claude/settings.json
40-
ref = importlib.resources.files("ai_prompt_auto_commit.data").joinpath("claude_settings.json")
41-
bundled = json.loads(ref.read_text(encoding="utf-8"))
42-
hook_def = bundled["hooks"]["UserPromptSubmit"][0]["hooks"][0]
43-
hook_id = hook_def["id"]
44-
45-
claude_dest = repo_root / ".claude"
46-
dest_file = claude_dest / "settings.json"
47-
48-
if dest_file.exists():
49-
settings = json.loads(dest_file.read_text(encoding="utf-8"))
50-
else:
51-
settings = {}
52-
53-
# Walk existing UserPromptSubmit matchers to check if hook is already present
54-
matchers = settings.setdefault("hooks", {}).setdefault("UserPromptSubmit", [])
55-
already_installed = any(
56-
h.get("id") == hook_id
57-
for matcher in matchers
58-
for h in matcher.get("hooks", [])
59-
)
60-
61-
if already_installed:
62-
print(f"Hook '{hook_id}' already present in {dest_file}")
63-
else:
64-
if matchers:
65-
matchers[0].setdefault("hooks", []).append(hook_def)
66-
else:
67-
matchers.append({"hooks": [hook_def]})
68-
claude_dest.mkdir(parents=True, exist_ok=True)
69-
dest_file.write_text(json.dumps(settings, indent=2) + "\n", encoding="utf-8")
70-
print(f"Inserted hook '{hook_id}' into {dest_file}")
71-
72-
return 0
73-
74-
75-
def unstage() -> int:
76-
"""pre-commit: remove any .prompts/ files from the git index."""
77-
result = subprocess.run(
78-
["git", "diff", "--cached", "--name-only", "--", f"{PROMPTS_DIRECTORY}/"],
79-
capture_output=True,
80-
text=True,
81-
)
82-
staged = result.stdout.strip()
83-
if not staged:
84-
return 0
85-
for filepath in staged.splitlines():
86-
subprocess.run(["git", "restore", "--staged", "--", filepath], check=True)
87-
return 0
88-
89-
90-
def append_to_commit_msg(commit_msg_file: Path) -> int:
91-
"""prepare-commit-msg: append pending prompts to the commit message."""
92-
repo_root = _repo_root()
93-
prompts_dir = repo_root / PROMPTS_DIRECTORY
94-
95-
pending = sorted(prompts_dir.glob("*.md"))
96-
if not pending:
97-
return 0
98-
99-
lines: list[str] = ["\nAI Prompts:\n"]
100-
for filepath in pending:
101-
stem = filepath.stem
102-
model = stem.split("_", 1)[1] if "_" in stem else stem
103-
raw = filepath.read_text(encoding="utf-8")
104-
# Join lines and strip trailing whitespace
105-
content = " ".join(raw.splitlines()).rstrip()
106-
# Remove absolute repo root from any paths embedded in the prompt
107-
content = content.replace(str(repo_root) + "/", "./")
108-
lines.append(f"{model}: {content}\n")
109-
110-
with commit_msg_file.open("a", encoding="utf-8") as fh:
111-
fh.writelines(lines)
112-
return 0
113-
114-
115-
def archive() -> int:
116-
"""post-commit: move pending prompts to .prompts/committed/<name>_<hash>.md."""
117-
repo_root = _repo_root()
118-
prompts_dir = repo_root / PROMPTS_DIRECTORY
119-
committed_dir = prompts_dir / "committed"
120-
121-
pending = sorted(prompts_dir.glob("*.md"))
122-
if not pending:
123-
return 0
124-
125-
commit_hash = subprocess.check_output(
126-
["git", "rev-parse", "HEAD"], text=True
127-
).strip()
128-
committed_dir.mkdir(parents=True, exist_ok=True)
129-
130-
for filepath in pending:
131-
dest = committed_dir / f"{filepath.stem}_{commit_hash}.md"
132-
filepath.rename(dest)
133-
return 0
134-
135-
136-
def prepare_repository_main() -> None:
137-
sys.exit(prepare_repository())
138-
139-
140-
def unstage_main() -> None:
141-
sys.exit(unstage())
142-
143-
144-
def append_main() -> None:
145-
if len(sys.argv) < 2:
146-
print("Usage: append-ai-prompts <commit-msg-file>", file=sys.stderr)
147-
exit(1)
148-
sys.exit(append_to_commit_msg(Path(sys.argv[1])))
149-
150-
151-
def archive_main() -> None:
152-
sys.exit(archive())
1+
"""Re-exports for backwards compatibility and console-script entry points."""
2+
3+
from .append import append_to_commit_msg, main as append_main
4+
from .archive import archive, main as archive_main
5+
from .common import PROMPTS_DIRECTORY, _repo_root
6+
from .prepare_repository import main as prepare_repository_main
7+
from .prepare_repository import prepare_repository
8+
from .unstage import main as unstage_main
9+
from .unstage import unstage
10+
11+
__all__ = [
12+
"PROMPTS_DIRECTORY",
13+
"_repo_root",
14+
"prepare_repository",
15+
"prepare_repository_main",
16+
"unstage",
17+
"unstage_main",
18+
"append_to_commit_msg",
19+
"append_main",
20+
"archive",
21+
"archive_main",
22+
]
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
"""prepare-ai-repository hook: one-time repository setup."""
2+
3+
from __future__ import annotations
4+
5+
import importlib.resources
6+
import json
7+
import sys
8+
from pathlib import Path
9+
10+
from . import common
11+
from .common import PROMPTS_DIRECTORY
12+
13+
14+
def prepare_repository() -> int:
15+
"""Set up .prompts/, and .claude/settings.json in the target repo."""
16+
repo_root = common._repo_root()
17+
18+
# Initialize PROMPTS_DIRECTORY with a .gitignore that ignores everything
19+
prompts_dir = repo_root / PROMPTS_DIRECTORY
20+
prompts_dir.mkdir(parents=True, exist_ok=True)
21+
prompts_gitignore = prompts_dir / ".gitignore"
22+
existing_patterns = prompts_gitignore.read_text(encoding="utf-8").splitlines() if prompts_gitignore.exists() else []
23+
if "*" not in existing_patterns:
24+
with prompts_gitignore.open("a", encoding="utf-8") as fh:
25+
fh.write("*\n")
26+
print(f"Initialized {prompts_gitignore}")
27+
else:
28+
print(f"{prompts_gitignore} already ignores *")
29+
30+
# Install the UserPromptSubmit hook into .claude/settings.json
31+
ref = importlib.resources.files("ai_prompt_auto_commit.data").joinpath("claude_settings.json")
32+
bundled = json.loads(ref.read_text(encoding="utf-8"))
33+
hook_def = bundled["hooks"]["UserPromptSubmit"][0]["hooks"][0]
34+
hook_id = hook_def["id"]
35+
36+
claude_dest = repo_root / ".claude"
37+
dest_file = claude_dest / "settings.json"
38+
39+
settings = json.loads(dest_file.read_text(encoding="utf-8")) if dest_file.exists() else {}
40+
41+
matchers = settings.setdefault("hooks", {}).setdefault("UserPromptSubmit", [])
42+
already_installed = any(
43+
h.get("id") == hook_id
44+
for matcher in matchers
45+
for h in matcher.get("hooks", [])
46+
)
47+
48+
if already_installed:
49+
print(f"Hook '{hook_id}' already present in {dest_file}")
50+
else:
51+
if matchers:
52+
matchers[0].setdefault("hooks", []).append(hook_def)
53+
else:
54+
matchers.append({"hooks": [hook_def]})
55+
claude_dest.mkdir(parents=True, exist_ok=True)
56+
dest_file.write_text(json.dumps(settings, indent=2) + "\n", encoding="utf-8")
57+
print(f"Inserted hook '{hook_id}' into {dest_file}")
58+
59+
# Ensure prepare-commit-msg and post-commit hooks are installed.
60+
# Derive them from the existing pre-commit hook file by swapping --hook-type.
61+
hooks_dir = repo_root / ".git" / "hooks"
62+
pre_commit_hook = hooks_dir / "pre-commit"
63+
if not pre_commit_hook.exists():
64+
print("Warning: no pre-commit hook found; skipping hook-type installation.", file=sys.stderr)
65+
else:
66+
template = pre_commit_hook.read_text(encoding="utf-8")
67+
for hook_type in ("prepare-commit-msg", "post-commit"):
68+
dest = hooks_dir / hook_type
69+
if dest.exists():
70+
print(f"{hook_type} hook already installed")
71+
else:
72+
content = template.replace(
73+
"ARGS=(hook-impl --config=.pre-commit-config.yaml --hook-type=pre-commit)",
74+
f"ARGS=(hook-impl --config=.pre-commit-config.yaml --hook-type={hook_type})",
75+
)
76+
dest.write_text(content, encoding="utf-8")
77+
dest.chmod(0o755)
78+
print(f"Installed {hook_type} hook to {dest}")
79+
80+
return 0
81+
82+
83+
def main() -> None:
84+
sys.exit(prepare_repository())

ai_prompt_auto_commit/unstage.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""pre-commit hook: remove .prompts/ files from the git index."""
2+
3+
from __future__ import annotations
4+
5+
import subprocess
6+
import sys
7+
8+
from .common import PROMPTS_DIRECTORY
9+
10+
11+
def unstage() -> int:
12+
"""Remove any .prompts/ files from the git index before committing."""
13+
result = subprocess.run(
14+
["git", "diff", "--cached", "--name-only", "--", f"{PROMPTS_DIRECTORY}/"],
15+
capture_output=True,
16+
text=True,
17+
)
18+
staged = result.stdout.strip()
19+
if not staged:
20+
return 0
21+
for filepath in staged.splitlines():
22+
subprocess.run(["git", "restore", "--staged", "--", filepath], check=True)
23+
return 0
24+
25+
26+
def main() -> None:
27+
sys.exit(unstage())

0 commit comments

Comments
 (0)