pjctx 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.
pjctx-0.1.0/.gitignore ADDED
@@ -0,0 +1,20 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .eggs/
7
+ *.egg
8
+ .venv/
9
+ venv/
10
+ .pytest_cache/
11
+ .coverage
12
+ htmlcov/
13
+ *.so
14
+ .DS_Store
15
+ .claude/
16
+ pyproject.toml
17
+ uv.lock
18
+
19
+ # PJContext local data
20
+ .pjctx/
pjctx-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: pjctx
3
+ Version: 0.1.0
4
+ Summary: Capture and restore AI coding context across sessions
5
+ License-Expression: MIT
6
+ Keywords: ai,cli,context,developer-tools
7
+ Requires-Python: >=3.9
8
+ Requires-Dist: click>=8.0
9
+ Requires-Dist: gitpython>=3.1
10
+ Requires-Dist: pyperclip>=1.8
11
+ Requires-Dist: rich>=13.0
12
+ Requires-Dist: watchdog>=3.0
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
15
+ Requires-Dist: pytest>=7.0; extra == 'dev'
16
+ Description-Content-Type: text/markdown
17
+
18
+ # PJContext (pjctx)
19
+
20
+ Capture and restore AI coding context across sessions. Stop wasting 10-15 minutes re-explaining your project every time you start a new AI coding session.
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ pip install pjctx
26
+ # or
27
+ pipx install pjctx
28
+ ```
29
+
30
+ ### Via npm (for Node.js developers)
31
+
32
+ ```bash
33
+ npm install -g pjctx
34
+ ```
35
+
36
+ > Requires Python 3.9+ and pip. The npm package automatically installs the Python package during setup.
37
+
38
+ For development:
39
+
40
+ ```bash
41
+ git clone <repo-url>
42
+ cd pjctx
43
+ pip install -e ".[dev]"
44
+ ```
45
+
46
+ ## Quick Start
47
+
48
+ ```bash
49
+ # Initialize in your git repo
50
+ pjctx init
51
+
52
+ # Save context (interactive)
53
+ pjctx save
54
+
55
+ # Save context (quick, one-liner)
56
+ pjctx save "Refactoring payment service to event sourcing"
57
+
58
+ # Save context (auto-detect from git)
59
+ pjctx save --auto
60
+
61
+ # Resume — copies prompt to clipboard, paste into any AI tool
62
+ pjctx resume
63
+
64
+ # Resume with specific format
65
+ pjctx resume --format xml
66
+ pjctx resume --format compact
67
+ pjctx resume --no-copy # print to stdout
68
+ ```
69
+
70
+ ## Commands
71
+
72
+ ### `pjctx init`
73
+ Initialize `.pjctx/` in the current git repo. Adds `.pjctx/` to `.gitignore`.
74
+
75
+ ### `pjctx save [message]`
76
+ Save current coding context. Three modes:
77
+ - **Interactive** (no args): Rich prompts walk through task, approaches, decisions, next steps
78
+ - **Quick** (`pjctx save "message"`): Auto-fill git data, message only
79
+ - **Auto** (`--auto`): No prompts, git-detected changes, carries forward previous context
80
+
81
+ Options: `--auto`, `-t/--tag TAG` (repeatable)
82
+
83
+ ### `pjctx resume`
84
+ Generate a resume prompt from the latest context and copy it to clipboard. Paste into any AI coding tool (Cursor, Claude Code, Copilot, Windsurf) to instantly restore context.
85
+
86
+ Options: `-f/--format [default|xml|compact]`, `--no-copy`, `-b/--branch BRANCH`
87
+
88
+ ### `pjctx log`
89
+ Show context history as a table.
90
+
91
+ Options: `-b/--branch BRANCH`, `-n/--limit N`, `--all`
92
+
93
+ ### `pjctx diff`
94
+ Show changes since last context save.
95
+
96
+ Options: `--stat` (default), `--full`
97
+
98
+ ### `pjctx handoff [@user]`
99
+ Create a handoff context for another developer. Carries forward all context fields and adds handoff note.
100
+
101
+ ### `pjctx share`
102
+ Remove `.pjctx/` from `.gitignore` and commit it to git for team sharing.
103
+
104
+ ### `pjctx watch`
105
+ Watch for file changes and auto-save context at intervals.
106
+
107
+ Options: `-i/--interval SECONDS` (default: 300)
108
+
109
+ ### `pjctx hook install|uninstall|status`
110
+ Manage git post-commit hook for automatic context saving after each commit.
111
+
112
+ ## Storage
113
+
114
+ Context is stored locally in `.pjctx/` within your repo, scoped by branch:
115
+
116
+ ```
117
+ .pjctx/
118
+ ├── config.json
119
+ ├── contexts/
120
+ │ ├── main/
121
+ │ │ ├── 2024-01-15T10-30-00.json
122
+ │ │ └── latest.json
123
+ │ └── feature/payment-refactor/
124
+ │ └── latest.json
125
+ └── hooks/
126
+ ```
127
+
128
+ ## Prompt Formats
129
+
130
+ - **default**: Markdown briefing document with headers
131
+ - **xml**: Structured XML for tools that parse it
132
+ - **compact**: Dense single paragraph for token-limited contexts
133
+
134
+ ## License
135
+
136
+ MIT
pjctx-0.1.0/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # PJContext (pjctx)
2
+
3
+ Capture and restore AI coding context across sessions. Stop wasting 10-15 minutes re-explaining your project every time you start a new AI coding session.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install pjctx
9
+ # or
10
+ pipx install pjctx
11
+ ```
12
+
13
+ ### Via npm (for Node.js developers)
14
+
15
+ ```bash
16
+ npm install -g pjctx
17
+ ```
18
+
19
+ > Requires Python 3.9+ and pip. The npm package automatically installs the Python package during setup.
20
+
21
+ For development:
22
+
23
+ ```bash
24
+ git clone <repo-url>
25
+ cd pjctx
26
+ pip install -e ".[dev]"
27
+ ```
28
+
29
+ ## Quick Start
30
+
31
+ ```bash
32
+ # Initialize in your git repo
33
+ pjctx init
34
+
35
+ # Save context (interactive)
36
+ pjctx save
37
+
38
+ # Save context (quick, one-liner)
39
+ pjctx save "Refactoring payment service to event sourcing"
40
+
41
+ # Save context (auto-detect from git)
42
+ pjctx save --auto
43
+
44
+ # Resume — copies prompt to clipboard, paste into any AI tool
45
+ pjctx resume
46
+
47
+ # Resume with specific format
48
+ pjctx resume --format xml
49
+ pjctx resume --format compact
50
+ pjctx resume --no-copy # print to stdout
51
+ ```
52
+
53
+ ## Commands
54
+
55
+ ### `pjctx init`
56
+ Initialize `.pjctx/` in the current git repo. Adds `.pjctx/` to `.gitignore`.
57
+
58
+ ### `pjctx save [message]`
59
+ Save current coding context. Three modes:
60
+ - **Interactive** (no args): Rich prompts walk through task, approaches, decisions, next steps
61
+ - **Quick** (`pjctx save "message"`): Auto-fill git data, message only
62
+ - **Auto** (`--auto`): No prompts, git-detected changes, carries forward previous context
63
+
64
+ Options: `--auto`, `-t/--tag TAG` (repeatable)
65
+
66
+ ### `pjctx resume`
67
+ Generate a resume prompt from the latest context and copy it to clipboard. Paste into any AI coding tool (Cursor, Claude Code, Copilot, Windsurf) to instantly restore context.
68
+
69
+ Options: `-f/--format [default|xml|compact]`, `--no-copy`, `-b/--branch BRANCH`
70
+
71
+ ### `pjctx log`
72
+ Show context history as a table.
73
+
74
+ Options: `-b/--branch BRANCH`, `-n/--limit N`, `--all`
75
+
76
+ ### `pjctx diff`
77
+ Show changes since last context save.
78
+
79
+ Options: `--stat` (default), `--full`
80
+
81
+ ### `pjctx handoff [@user]`
82
+ Create a handoff context for another developer. Carries forward all context fields and adds handoff note.
83
+
84
+ ### `pjctx share`
85
+ Remove `.pjctx/` from `.gitignore` and commit it to git for team sharing.
86
+
87
+ ### `pjctx watch`
88
+ Watch for file changes and auto-save context at intervals.
89
+
90
+ Options: `-i/--interval SECONDS` (default: 300)
91
+
92
+ ### `pjctx hook install|uninstall|status`
93
+ Manage git post-commit hook for automatic context saving after each commit.
94
+
95
+ ## Storage
96
+
97
+ Context is stored locally in `.pjctx/` within your repo, scoped by branch:
98
+
99
+ ```
100
+ .pjctx/
101
+ ├── config.json
102
+ ├── contexts/
103
+ │ ├── main/
104
+ │ │ ├── 2024-01-15T10-30-00.json
105
+ │ │ └── latest.json
106
+ │ └── feature/payment-refactor/
107
+ │ └── latest.json
108
+ └── hooks/
109
+ ```
110
+
111
+ ## Prompt Formats
112
+
113
+ - **default**: Markdown briefing document with headers
114
+ - **xml**: Structured XML for tools that parse it
115
+ - **compact**: Dense single paragraph for token-limited contexts
116
+
117
+ ## License
118
+
119
+ MIT
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "pjctx"
7
+ version = "0.1.0"
8
+ description = "Capture and restore AI coding context across sessions"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ keywords = ["ai", "context", "developer-tools", "cli"]
13
+ dependencies = [
14
+ "click>=8.0",
15
+ "rich>=13.0",
16
+ "pyperclip>=1.8",
17
+ "gitpython>=3.1",
18
+ "watchdog>=3.0",
19
+ ]
20
+
21
+ [project.optional-dependencies]
22
+ dev = [
23
+ "pytest>=7.0",
24
+ "pytest-cov>=4.0",
25
+ ]
26
+
27
+ [project.scripts]
28
+ pjctx = "pjctx.cli:cli"
29
+
30
+ [tool.hatch.build.targets.wheel]
31
+ packages = ["src/pjctx"]
32
+
33
+ [tool.pytest.ini_options]
34
+ testpaths = ["tests"]
@@ -0,0 +1,3 @@
1
+ """PJContext — Capture and restore AI coding context."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,128 @@
1
+ """Click CLI group and command wiring."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ from pjctx import __version__
8
+
9
+
10
+ @click.group()
11
+ @click.version_option(version=__version__, prog_name="pjctx")
12
+ @click.option("--verbose", "-v", is_flag=True, help="Enable verbose output.")
13
+ @click.pass_context
14
+ def cli(ctx: click.Context, verbose: bool) -> None:
15
+ """PJContext — Capture and restore AI coding context."""
16
+ ctx.ensure_object(dict)
17
+ ctx.obj["verbose"] = verbose
18
+
19
+
20
+ # Lazy-load commands to keep --help/--version fast
21
+
22
+
23
+ @cli.command()
24
+ @click.pass_context
25
+ def init(ctx: click.Context) -> None:
26
+ """Initialize .pjctx/ in the current repo."""
27
+ from pjctx.commands.init import run_init
28
+ run_init(ctx.obj)
29
+
30
+
31
+ @cli.command()
32
+ @click.argument("message", required=False)
33
+ @click.option("--auto", "auto_mode", is_flag=True, help="Auto-detect from git, no prompts.")
34
+ @click.option("--tag", "-t", multiple=True, help="Add tags.")
35
+ @click.pass_context
36
+ def save(ctx: click.Context, message: str | None, auto_mode: bool, tag: tuple[str, ...]) -> None:
37
+ """Save current context. Interactive if no message given."""
38
+ from pjctx.commands.save import run_save
39
+ run_save(ctx.obj, message=message, auto_mode=auto_mode, tags=list(tag))
40
+
41
+
42
+ @cli.command()
43
+ @click.option("--format", "-f", "fmt", default="default",
44
+ type=click.Choice(["default", "xml", "compact"]),
45
+ help="Prompt format.")
46
+ @click.option("--no-copy", is_flag=True, help="Print to stdout instead of clipboard.")
47
+ @click.option("--branch", "-b", default=None, help="Resume from a specific branch.")
48
+ @click.pass_context
49
+ def resume(ctx: click.Context, fmt: str, no_copy: bool, branch: str | None) -> None:
50
+ """Generate a resume prompt from the latest context."""
51
+ from pjctx.commands.resume import run_resume
52
+ run_resume(ctx.obj, fmt=fmt, no_copy=no_copy, branch=branch)
53
+
54
+
55
+ @cli.command()
56
+ @click.option("--branch", "-b", default=None, help="Show log for a specific branch.")
57
+ @click.option("--limit", "-n", default=10, help="Max entries to show.")
58
+ @click.option("--all", "all_branches", is_flag=True, help="Show logs across all branches.")
59
+ @click.pass_context
60
+ def log(ctx: click.Context, branch: str | None, limit: int, all_branches: bool) -> None:
61
+ """Show context history."""
62
+ from pjctx.commands.log import run_log
63
+ run_log(ctx.obj, branch=branch, limit=limit, all_branches=all_branches)
64
+
65
+
66
+ @cli.command()
67
+ @click.option("--stat/--full", default=True, help="Show stat summary or full diff.")
68
+ @click.pass_context
69
+ def diff(ctx: click.Context, stat: bool) -> None:
70
+ """Show changes since last context save."""
71
+ from pjctx.commands.diff import run_diff
72
+ run_diff(ctx.obj, stat_only=stat)
73
+
74
+
75
+ @cli.command()
76
+ @click.argument("user", required=False)
77
+ @click.pass_context
78
+ def handoff(ctx: click.Context, user: str | None) -> None:
79
+ """Create a handoff context for another user."""
80
+ from pjctx.commands.handoff import run_handoff
81
+ run_handoff(ctx.obj, user=user)
82
+
83
+
84
+ @cli.command()
85
+ @click.pass_context
86
+ def share(ctx: click.Context) -> None:
87
+ """Share .pjctx/ by committing it to git."""
88
+ from pjctx.commands.share import run_share
89
+ run_share(ctx.obj)
90
+
91
+
92
+ @cli.command()
93
+ @click.option("--interval", "-i", default=300, help="Auto-save interval in seconds.")
94
+ @click.pass_context
95
+ def watch(ctx: click.Context, interval: int) -> None:
96
+ """Watch for file changes and auto-save context."""
97
+ from pjctx.commands.watch import run_watch
98
+ run_watch(ctx.obj, interval=interval)
99
+
100
+
101
+ @cli.group()
102
+ def hook() -> None:
103
+ """Manage git hooks for auto-save."""
104
+ pass
105
+
106
+
107
+ @hook.command("install")
108
+ @click.pass_context
109
+ def hook_install(ctx: click.Context) -> None:
110
+ """Install post-commit hook."""
111
+ from pjctx.commands.hook import run_install
112
+ run_install(ctx.obj)
113
+
114
+
115
+ @hook.command("uninstall")
116
+ @click.pass_context
117
+ def hook_uninstall(ctx: click.Context) -> None:
118
+ """Uninstall post-commit hook."""
119
+ from pjctx.commands.hook import run_uninstall
120
+ run_uninstall(ctx.obj)
121
+
122
+
123
+ @hook.command("status")
124
+ @click.pass_context
125
+ def hook_status(ctx: click.Context) -> None:
126
+ """Check hook installation status."""
127
+ from pjctx.commands.hook import run_status
128
+ run_status(ctx.obj)
File without changes
@@ -0,0 +1,48 @@
1
+ """pjctx diff — Show changes since last context save."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pjctx.core.config import find_repo_root, get_pjctx_dir
6
+ from pjctx.core.git_ops import get_current_branch, get_diff_summary, get_full_diff, get_files_changed
7
+ from pjctx.core.storage import load_latest
8
+ from pjctx import ui
9
+
10
+
11
+ def run_diff(obj: dict, stat_only: bool = True) -> None:
12
+ repo_root = find_repo_root()
13
+ if repo_root is None:
14
+ ui.error("Not inside a git repository.")
15
+ raise SystemExit(1)
16
+
17
+ if not get_pjctx_dir(repo_root).exists():
18
+ ui.error("Not initialized. Run 'pjctx init' first.")
19
+ raise SystemExit(1)
20
+
21
+ branch = get_current_branch(repo_root)
22
+ last_ctx = load_latest(repo_root, branch)
23
+
24
+ if last_ctx:
25
+ ui.info(f"Last save: {last_ctx.timestamp[:19]} — {last_ctx.message}")
26
+ else:
27
+ ui.warning("No previous context. Showing all current changes.")
28
+
29
+ if stat_only:
30
+ summary = get_diff_summary(repo_root)
31
+ files = get_files_changed(repo_root)
32
+ if not summary and not files:
33
+ ui.info("No changes since last save.")
34
+ return
35
+ if summary:
36
+ ui._console().print(summary)
37
+ if files:
38
+ ui.info("Changed files:")
39
+ for f in files:
40
+ ui._console().print(f" {f}")
41
+ else:
42
+ full = get_full_diff(repo_root)
43
+ if not full:
44
+ ui.info("No changes since last save.")
45
+ return
46
+ from rich.syntax import Syntax
47
+ syntax = Syntax(full, "diff", theme="monokai")
48
+ ui._console().print(syntax)
@@ -0,0 +1,60 @@
1
+ """pjctx handoff — Create a handoff context for another user."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pjctx.core.config import find_repo_root, get_pjctx_dir
6
+ from pjctx.core.context import Context
7
+ from pjctx.core.git_ops import get_current_branch, get_files_changed, get_diff_summary
8
+ from pjctx.core.storage import save_context, load_latest
9
+ from pjctx import ui
10
+
11
+
12
+ def run_handoff(obj: dict, user: str | None = None) -> None:
13
+ repo_root = find_repo_root()
14
+ if repo_root is None:
15
+ ui.error("Not inside a git repository.")
16
+ raise SystemExit(1)
17
+
18
+ if not get_pjctx_dir(repo_root).exists():
19
+ ui.error("Not initialized. Run 'pjctx init' first.")
20
+ raise SystemExit(1)
21
+
22
+ branch = get_current_branch(repo_root)
23
+
24
+ # Resolve target user
25
+ target = user
26
+ if target and target.startswith("@"):
27
+ target = target[1:]
28
+ if not target:
29
+ target = ui.prompt("Handoff to (username)")
30
+ if not target:
31
+ ui.error("Username required.")
32
+ raise SystemExit(1)
33
+
34
+ # Load previous context to carry forward
35
+ prev = load_latest(repo_root, branch)
36
+
37
+ # Gather handoff note
38
+ handoff_note = ui.prompt("Handoff note (what they need to know)")
39
+
40
+ files_changed = get_files_changed(repo_root)
41
+ diff_summary = get_diff_summary(repo_root)
42
+
43
+ ctx = Context(
44
+ message=f"Handoff to {target}",
45
+ task=prev.task if prev else "",
46
+ approaches_tried=prev.approaches_tried if prev else [],
47
+ current_approach=prev.current_approach if prev else "",
48
+ decisions=prev.decisions if prev else [],
49
+ next_steps=prev.next_steps if prev else [],
50
+ branch=branch,
51
+ files_changed=files_changed,
52
+ git_diff_summary=diff_summary,
53
+ handoff_to=target,
54
+ handoff_note=handoff_note,
55
+ tags=prev.tags if prev else [],
56
+ )
57
+
58
+ path = save_context(repo_root, ctx)
59
+ ui.success(f"Handoff context saved for @{target} → {path.name}")
60
+ ui.info("They can run 'pjctx resume' to pick up the context.")
@@ -0,0 +1,117 @@
1
+ """pjctx hook — Install/uninstall/status for git post-commit hook."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import stat
7
+
8
+ from pjctx.core.config import find_repo_root, get_pjctx_dir
9
+ from pjctx.core.git_ops import get_hooks_dir
10
+ from pjctx import ui
11
+
12
+ HOOK_NAME = "post-commit"
13
+ MARKER_START = "# --- pjctx auto-save start ---"
14
+ MARKER_END = "# --- pjctx auto-save end ---"
15
+ HOOK_BODY = """\
16
+ # --- pjctx auto-save start ---
17
+ if command -v pjctx >/dev/null 2>&1; then
18
+ pjctx save --auto 2>/dev/null || true
19
+ fi
20
+ # --- pjctx auto-save end ---
21
+ """
22
+
23
+
24
+ def run_install(obj: dict) -> None:
25
+ repo_root = find_repo_root()
26
+ if repo_root is None:
27
+ ui.error("Not inside a git repository.")
28
+ raise SystemExit(1)
29
+
30
+ if not get_pjctx_dir(repo_root).exists():
31
+ ui.error("Not initialized. Run 'pjctx init' first.")
32
+ raise SystemExit(1)
33
+
34
+ hooks_dir = get_hooks_dir(repo_root)
35
+ hooks_dir.mkdir(parents=True, exist_ok=True)
36
+ hook_path = hooks_dir / HOOK_NAME
37
+
38
+ if hook_path.exists():
39
+ content = hook_path.read_text()
40
+ if MARKER_START in content:
41
+ ui.warning("Hook already installed.")
42
+ return
43
+ # Append to existing hook
44
+ if not content.endswith("\n"):
45
+ content += "\n"
46
+ content += "\n" + HOOK_BODY
47
+ else:
48
+ content = "#!/bin/sh\n\n" + HOOK_BODY
49
+
50
+ hook_path.write_text(content)
51
+ # Make executable
52
+ hook_path.chmod(hook_path.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
53
+
54
+ ui.success("Post-commit hook installed.")
55
+ ui.info("Context will auto-save after each commit.")
56
+
57
+
58
+ def run_uninstall(obj: dict) -> None:
59
+ repo_root = find_repo_root()
60
+ if repo_root is None:
61
+ ui.error("Not inside a git repository.")
62
+ raise SystemExit(1)
63
+
64
+ hooks_dir = get_hooks_dir(repo_root)
65
+ hook_path = hooks_dir / HOOK_NAME
66
+
67
+ if not hook_path.exists():
68
+ ui.warning("No post-commit hook found.")
69
+ return
70
+
71
+ content = hook_path.read_text()
72
+ if MARKER_START not in content:
73
+ ui.warning("pjctx hook not found in post-commit.")
74
+ return
75
+
76
+ # Remove our section
77
+ lines = content.splitlines(keepends=True)
78
+ new_lines: list[str] = []
79
+ inside = False
80
+ for line in lines:
81
+ if MARKER_START in line:
82
+ inside = True
83
+ continue
84
+ if MARKER_END in line:
85
+ inside = False
86
+ continue
87
+ if not inside:
88
+ new_lines.append(line)
89
+
90
+ remaining = "".join(new_lines).strip()
91
+ if remaining == "#!/bin/sh" or not remaining:
92
+ # Remove the file entirely if only shebang left
93
+ hook_path.unlink()
94
+ ui.success("Post-commit hook removed.")
95
+ else:
96
+ hook_path.write_text(remaining + "\n")
97
+ ui.success("pjctx section removed from post-commit hook.")
98
+
99
+
100
+ def run_status(obj: dict) -> None:
101
+ repo_root = find_repo_root()
102
+ if repo_root is None:
103
+ ui.error("Not inside a git repository.")
104
+ raise SystemExit(1)
105
+
106
+ hooks_dir = get_hooks_dir(repo_root)
107
+ hook_path = hooks_dir / HOOK_NAME
108
+
109
+ if not hook_path.exists():
110
+ ui.info("Post-commit hook: [bold red]not installed[/]")
111
+ return
112
+
113
+ content = hook_path.read_text()
114
+ if MARKER_START in content:
115
+ ui.info("Post-commit hook: [bold green]installed[/]")
116
+ else:
117
+ ui.info("Post-commit hook: [bold yellow]exists but pjctx not found[/]")