Skip to content

Commit c514e1a

Browse files
committed
fix: add --quiet/--json CLI flags, fix Claude Code hooks template
- Add -q/--quiet flag to build, update, and status commands - Add --json flag to status command for machine-readable output - Fix generate_hooks_config() to use correct Claude Code schema: nested "hooks" arrays with "type": "command" on each entry - Replace invalid PreCommit hook event with PreToolUse + if: "Bash(git commit*)" filter - Update docs and tests to match
1 parent afae1c4 commit c514e1a

7 files changed

Lines changed: 115 additions & 55 deletions

File tree

code_review_graph/cli.py

Lines changed: 48 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -226,11 +226,13 @@ def main() -> None:
226226
# build
227227
build_cmd = sub.add_parser("build", help="Full graph build (re-parse all files)")
228228
build_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)")
229+
build_cmd.add_argument("-q", "--quiet", action="store_true", help="Suppress output")
229230

230231
# update
231232
update_cmd = sub.add_parser("update", help="Incremental update (only changed files)")
232233
update_cmd.add_argument("--base", default="HEAD~1", help="Git diff base (default: HEAD~1)")
233234
update_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)")
235+
update_cmd.add_argument("-q", "--quiet", action="store_true", help="Suppress output")
234236

235237
# watch
236238
watch_cmd = sub.add_parser("watch", help="Watch for changes and auto-update")
@@ -239,6 +241,11 @@ def main() -> None:
239241
# status
240242
status_cmd = sub.add_parser("status", help="Show graph statistics")
241243
status_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)")
244+
status_cmd.add_argument("-q", "--quiet", action="store_true", help="Suppress output")
245+
status_cmd.add_argument(
246+
"--json", action="store_true", dest="json_output",
247+
help="Output as JSON",
248+
)
242249

243250
# visualize
244251
vis_cmd = sub.add_parser("visualize", help="Generate interactive HTML graph visualization")
@@ -421,43 +428,62 @@ def main() -> None:
421428
try:
422429
if args.command == "build":
423430
result = full_build(repo_root, store)
424-
print(
425-
f"Full build: {result['files_parsed']} files, "
426-
f"{result['total_nodes']} nodes, {result['total_edges']} edges"
427-
)
428-
if result["errors"]:
429-
print(f"Errors: {len(result['errors'])}")
431+
if not args.quiet:
432+
print(
433+
f"Full build: {result['files_parsed']} files, "
434+
f"{result['total_nodes']} nodes, {result['total_edges']} edges"
435+
)
436+
if result["errors"]:
437+
print(f"Errors: {len(result['errors'])}")
430438

431439
elif args.command == "update":
432440
result = incremental_update(repo_root, store, base=args.base)
433-
print(
434-
f"Incremental: {result['files_updated']} files updated, "
435-
f"{result['total_nodes']} nodes, {result['total_edges']} edges"
436-
)
441+
if not args.quiet:
442+
print(
443+
f"Incremental: {result['files_updated']} files updated, "
444+
f"{result['total_nodes']} nodes, {result['total_edges']} edges"
445+
)
437446

438447
elif args.command == "status":
448+
import json as json_mod
439449
stats = store.get_stats()
440-
print(f"Nodes: {stats.total_nodes}")
441-
print(f"Edges: {stats.total_edges}")
442-
print(f"Files: {stats.files_count}")
443-
print(f"Languages: {', '.join(stats.languages)}")
444-
print(f"Last updated: {stats.last_updated or 'never'}")
445-
# Show branch info and warn if stale
446450
stored_branch = store.get_metadata("git_branch")
447451
stored_sha = store.get_metadata("git_head_sha")
448-
if stored_branch:
449-
print(f"Built on branch: {stored_branch}")
450-
if stored_sha:
451-
print(f"Built at commit: {stored_sha[:12]}")
452452
from .incremental import _git_branch_info
453453
current_branch, current_sha = _git_branch_info(repo_root)
454+
stale_warning = None
454455
if stored_branch and current_branch and stored_branch != current_branch:
455-
print(
456-
f"WARNING: Graph was built on '{stored_branch}' "
456+
stale_warning = (
457+
f"Graph was built on '{stored_branch}' "
457458
f"but you are now on '{current_branch}'. "
458459
f"Run 'code-review-graph build' to rebuild."
459460
)
460461

462+
if getattr(args, "json_output", False):
463+
data = {
464+
"nodes": stats.total_nodes,
465+
"edges": stats.total_edges,
466+
"files": stats.files_count,
467+
"languages": list(stats.languages),
468+
"last_updated": stats.last_updated,
469+
"branch": stored_branch,
470+
"commit": stored_sha[:12] if stored_sha else None,
471+
"stale": stale_warning,
472+
}
473+
print(json_mod.dumps(data))
474+
elif not args.quiet:
475+
print(f"Nodes: {stats.total_nodes}")
476+
print(f"Edges: {stats.total_edges}")
477+
print(f"Files: {stats.files_count}")
478+
print(f"Languages: {', '.join(stats.languages)}")
479+
print(f"Last updated: {stats.last_updated or 'never'}")
480+
if stored_branch:
481+
print(f"Built on branch: {stored_branch}")
482+
if stored_sha:
483+
print(f"Built at commit: {stored_sha[:12]}")
484+
if stale_warning:
485+
print(f"WARNING: {stale_warning}")
486+
461487
elif args.command == "watch":
462488
watch(repo_root, store)
463489

code_review_graph/skills.py

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ def generate_hooks_config() -> dict[str, Any]:
307307
"""Generate Claude Code hooks configuration.
308308
309309
Returns a hooks config dict with PostToolUse, SessionStart, and
310-
PreCommit hooks for automatic graph updates.
310+
PreToolUse hooks for automatic graph updates and pre-commit analysis.
311311
312312
Returns:
313313
Dict with hooks configuration suitable for .claude/settings.json.
@@ -317,20 +317,38 @@ def generate_hooks_config() -> dict[str, Any]:
317317
"PostToolUse": [
318318
{
319319
"matcher": "Edit|Write|Bash",
320-
"command": "code-review-graph update --quiet",
321-
"timeout": 5000,
320+
"hooks": [
321+
{
322+
"type": "command",
323+
"command": "code-review-graph update --quiet",
324+
"timeout": 5000,
325+
}
326+
],
322327
},
323328
],
324329
"SessionStart": [
325330
{
326-
"command": "code-review-graph status --json",
327-
"timeout": 3000,
331+
"matcher": "",
332+
"hooks": [
333+
{
334+
"type": "command",
335+
"command": "code-review-graph status --json",
336+
"timeout": 3000,
337+
}
338+
],
328339
},
329340
],
330-
"PreCommit": [
341+
"PreToolUse": [
331342
{
332-
"command": "code-review-graph detect-changes --brief",
333-
"timeout": 10000,
343+
"matcher": "Bash",
344+
"hooks": [
345+
{
346+
"type": "command",
347+
"if": "Bash(git commit*)",
348+
"command": "code-review-graph detect-changes --brief",
349+
"timeout": 10000,
350+
}
351+
],
334352
},
335353
],
336354
}

docs/COMMANDS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,11 +229,14 @@ code-review-graph install --dry-run # Preview without writing files
229229

230230
# Build and update
231231
code-review-graph build # Full build
232+
code-review-graph build --quiet # Full build, no output
232233
code-review-graph update # Incremental update
234+
code-review-graph update --quiet # Incremental update, no output
233235
code-review-graph update --base origin/main # Custom base ref
234236

235237
# Monitor and inspect
236238
code-review-graph status # Graph statistics
239+
code-review-graph status --json # Machine-readable JSON output
237240
code-review-graph watch # Auto-update on file changes
238241
code-review-graph visualize # Generate interactive HTML graph
239242

docs/FEATURES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@
106106
- **6 MCP tools** for full graph interaction
107107
- **3 review-first skills**: build-graph, review-delta, review-pr
108108
- **PostToolUse hooks** (Write|Edit|Bash) for automatic background updates
109+
- **PreToolUse hooks** for pre-commit change analysis via `detect-changes`
109110
- **FastMCP 3.0 compatible** stdio MCP server
110111

111112
## Privacy & Data

docs/LLM-OPTIMIZED-REFERENCE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ MIT license. 100% local. No telemetry. DB file: .code-review-graph/graph.db
3737
<section name="watch">
3838
Run: code-review-graph watch (auto-updates graph on file save via watchdog)
3939
Or use PostToolUse (Write|Edit|Bash) hooks for automatic background updates.
40+
PreToolUse hooks with `if: "Bash(git commit*)"` run detect-changes before commits.
4041
</section>
4142

4243
<section name="embeddings">

docs/architecture.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010
┌──────────────────────────────────────────────────────────────┐
1111
│ Claude Code │
1212
│ │
13-
│ Skills (SKILL.md) Hooks (hooks.json)
14-
│ ├── build-graph ── PostToolUse (Write|Edit|Bash) │
15-
│ ├── review-delta → incremental update │
16-
│ └── review-pr
17-
│ │
13+
│ Skills (SKILL.md) Hooks (settings.json) │
14+
│ ├── build-graph ── PostToolUse (Write|Edit|Bash) │
15+
│ ├── review-delta → incremental update │
16+
│ └── review-pr └── PreToolUse (git commit)
17+
│ │ → detect-changes
1818
│ ▼ ▼ │
1919
│ ┌────────────────────────────────────────────┐ │
2020
│ │ MCP Server (stdio) │ │

tests/test_skills.py

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -66,32 +66,43 @@ def test_returns_dict_with_hooks(self):
6666
def test_has_post_tool_use(self):
6767
config = generate_hooks_config()
6868
assert "PostToolUse" in config["hooks"]
69-
hooks = config["hooks"]["PostToolUse"]
70-
assert len(hooks) >= 1
71-
assert hooks[0]["matcher"] == "Edit|Write|Bash"
72-
assert "update" in hooks[0]["command"]
73-
assert hooks[0]["timeout"] == 5000
69+
entries = config["hooks"]["PostToolUse"]
70+
assert len(entries) >= 1
71+
assert entries[0]["matcher"] == "Edit|Write|Bash"
72+
inner = entries[0]["hooks"]
73+
assert len(inner) >= 1
74+
assert inner[0]["type"] == "command"
75+
assert "update" in inner[0]["command"]
76+
assert inner[0]["timeout"] == 5000
7477

7578
def test_has_session_start(self):
7679
config = generate_hooks_config()
7780
assert "SessionStart" in config["hooks"]
78-
hooks = config["hooks"]["SessionStart"]
79-
assert len(hooks) >= 1
80-
assert "status" in hooks[0]["command"]
81-
assert hooks[0]["timeout"] == 3000
82-
83-
def test_has_pre_commit(self):
81+
entries = config["hooks"]["SessionStart"]
82+
assert len(entries) >= 1
83+
inner = entries[0]["hooks"]
84+
assert len(inner) >= 1
85+
assert inner[0]["type"] == "command"
86+
assert "status" in inner[0]["command"]
87+
assert inner[0]["timeout"] == 3000
88+
89+
def test_has_pre_tool_use(self):
8490
config = generate_hooks_config()
85-
assert "PreCommit" in config["hooks"]
86-
hooks = config["hooks"]["PreCommit"]
87-
assert len(hooks) >= 1
88-
assert "detect-changes" in hooks[0]["command"]
89-
assert hooks[0]["timeout"] == 10000
90-
91-
def test_has_all_three_hook_types(self):
91+
assert "PreToolUse" in config["hooks"]
92+
entries = config["hooks"]["PreToolUse"]
93+
assert len(entries) >= 1
94+
assert entries[0]["matcher"] == "Bash"
95+
inner = entries[0]["hooks"]
96+
assert len(inner) >= 1
97+
assert inner[0]["type"] == "command"
98+
assert inner[0]["if"] == "Bash(git commit*)"
99+
assert "detect-changes" in inner[0]["command"]
100+
assert inner[0]["timeout"] == 10000
101+
102+
def test_has_all_hook_types(self):
92103
config = generate_hooks_config()
93104
hook_types = set(config["hooks"].keys())
94-
assert hook_types == {"PostToolUse", "SessionStart", "PreCommit"}
105+
assert hook_types == {"PostToolUse", "SessionStart", "PreToolUse"}
95106

96107

97108
class TestInstallHooks:
@@ -114,7 +125,7 @@ def test_merges_with_existing(self, tmp_path):
114125
assert data["customSetting"] is True
115126
assert "PostToolUse" in data["hooks"]
116127
assert "SessionStart" in data["hooks"]
117-
assert "PreCommit" in data["hooks"]
128+
assert "PreToolUse" in data["hooks"]
118129

119130
def test_creates_claude_directory(self, tmp_path):
120131
install_hooks(tmp_path)

0 commit comments

Comments
 (0)