Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:42:05 +08:00
commit 2a050efe7f
17 changed files with 1342 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
{
"name": "git",
"description": "Git intelligence for Claude Code - session context, commit history, and smart commits with Conventional Commits",
"version": "1.0.0",
"author": {
"name": "Nathan Vale",
"email": "hi@nathanvale.com"
},
"skills": [
"./skills"
],
"commands": [
"./commands"
],
"hooks": [
"./hooks"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# git
Git intelligence for Claude Code - session context, commit history, and smart commits with Conventional Commits

54
commands/checkpoint.md Normal file
View File

@@ -0,0 +1,54 @@
---
description: Create a quick WIP checkpoint commit to save your current work
model: claude-haiku-4-5-20251001
allowed-tools: Bash(git add:*), Bash(git commit:*), mcp__plugin_git_git-intelligence__get_diff_summary
argument-hint: [description]
---
# Quick Checkpoint Commit
Create a quick WIP checkpoint commit to save your current work.
## Instructions
Create a quick checkpoint commit to save work-in-progress. This is useful for:
- Saving state before risky operations
- Creating restore points during development
- Quick saves when switching context
### Workflow
1. **Check current status** - Use `get_status` MCP tool
- Quickly see staged, modified, and untracked files
- Check for any files that shouldn't be committed (secrets, etc.)
2. **Stage all changes** (unless there are files with secrets):
```bash
git add -A
```
3. **Create checkpoint commit**:
```bash
git commit -m "$(cat <<'EOF'
chore(wip): checkpoint - <brief description>
Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
EOF
)"
```
### Notes
- Checkpoints can be squashed later with `git rebase -i`
- The description should be brief (e.g., "before refactor", "auth working", "halfway done")
- Skip files that shouldn't be committed (secrets, large binaries)
### Arguments
If the user provides a description after the command, use it:
- `/git:checkpoint before api changes` -> `chore(wip): checkpoint - before api changes`
- `/git:checkpoint` -> Ask for a brief description or use current context
Now create a checkpoint commit.

122
commands/commit.md Normal file
View File

@@ -0,0 +1,122 @@
---
description: Create well-formatted commits using Conventional Commits specification
model: claude-sonnet-4-5-20250929
allowed-tools: Bash(git add:*), Bash(git commit:*), mcp__plugin_git_git-intelligence__get_recent_commits, mcp__plugin_git_git-intelligence__get_diff_summary
---
# Smart Commit
Create well-formatted commits using Conventional Commits specification.
## Instructions
You are a git commit specialist. Create atomic, well-documented commits following these rules:
### Commit Format
```
<type>(<scope>): <subject>
```
### Types (REQUIRED - use exactly these)
| Type | Description |
|------|-------------|
| feat | A new feature |
| fix | A bug fix |
| docs | Documentation changes |
| style | Code style changes (formatting, etc) |
| refactor | Code refactoring |
| perf | Performance improvements |
| test | Adding or updating tests |
| build | Build system changes |
| ci | CI/CD configuration changes |
| chore | Maintenance tasks |
| revert | Revert changes |
### Rules
- Subject line max 100 characters
- Use lowercase for type and scope
- No period at end of subject
- Scope should describe the area of change (e.g., auth, api, config)
- Subject should be imperative ("add" not "added", "fix" not "fixed")
### Workflow
1. **Check status and diff** (use MCP tools for efficiency):
- Use `get_status` tool to see all changes (staged, modified, untracked)
- Use `get_diff_summary` tool to review what will be committed
- Use `get_recent_commits` tool with `limit: 5` to see recent commit style
- For detailed diff content, use Bash: `git diff` or `git diff --cached`
2. **Analyze the changes**:
- Determine if changes should be one commit or split into multiple
- Identify the primary type of change (feat, fix, refactor, etc.)
- Identify the scope (which area of the codebase)
3. **Stage files** (if not already staged):
- Use `git add <files>` for specific files
- NEVER use `git add .` without reviewing what will be added
- Skip files that contain secrets (.env, credentials, etc.)
4. **Create the commit**:
```bash
git commit -m "$(cat <<'EOF'
<type>(<scope>): <subject>
[optional body explaining what and why]
Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
EOF
)"
```
5. **Handle pre-commit hook failures**:
- If hooks modify files, stage the changes and amend: `git add . && git commit --amend --no-edit`
- Only amend if: (1) you're the author, (2) commit not pushed
- Check authorship: `git log -1 --format='%an %ae'`
### Examples
**Feature commit:**
```
feat(auth): add OAuth2 login support
Implement OAuth2 flow with Google and GitHub providers.
Includes token refresh and session management.
Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
```
**Bug fix commit:**
```
fix(api): handle null response in user endpoint
The /api/users endpoint was crashing when the database
returned null for deleted users.
Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
```
**Chore commit:**
```
chore(deps): update dependencies to latest versions
Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
```
### Important Notes
- NEVER skip hooks with --no-verify unless explicitly asked
- NEVER force push to main/master
- NEVER commit files containing secrets
- If changes are too large, suggest splitting into multiple commits
- Ask user before committing if anything is unclear
Now analyze the current changes and create an appropriate commit.

152
commands/create-pr.md Normal file
View File

@@ -0,0 +1,152 @@
---
description: Create pull requests using GitHub CLI with Conventional Commits format
model: claude-sonnet-4-5-20250929
allowed-tools: Bash(git push:*), Bash(gh pr:*), mcp__plugin_git_git-intelligence__get_recent_commits, mcp__plugin_git_git-intelligence__get_diff_summary
---
# Create Pull Request
Create well-formatted pull requests using GitHub CLI with Conventional Commits specification.
## Prerequisites
Check if `gh` is installed and authenticated:
```bash
gh auth status
```
If not installed:
```bash
brew install gh
gh auth login
```
## Workflow
### 1. Gather Context
Use MCP tools for efficient data gathering:
- **Status** - Use `get_status` MCP tool
- **Recent commits** - Use `get_recent_commits` MCP tool
- **Diff summary** - Use `get_diff_summary` MCP tool
For branch info and comparison, use Bash:
```bash
# Check current branch
git branch --show-current
# Check if we need to push
git log --oneline @{u}..HEAD 2>/dev/null || echo "No upstream"
# All commits on this branch vs main
git log --oneline main..HEAD
```
### 2. Analyze Changes
Review all commits that will be in the PR (not just the latest):
```bash
git diff main...HEAD --stat
```
### 3. Determine PR Title
Use Conventional Commits format matching the primary change type:
| Type | Example |
|------|---------|
| feat | `feat(auth): add OAuth2 login support` |
| fix | `fix(api): handle null response` |
| docs | `docs(readme): update installation guide` |
| style | `style(ui): improve button styling` |
| refactor | `refactor(core): simplify data flow` |
| perf | `perf(queries): optimize database calls` |
| test | `test(auth): add login integration tests` |
| build | `build(deps): upgrade to Node 20` |
| ci | `ci(actions): add deployment workflow` |
| chore | `chore(deps): update dependencies` |
### 4. Create the PR
```bash
# Push branch if needed
git push -u origin HEAD
# Create PR with HEREDOC for body
gh pr create --title "<type>(<scope>): <subject>" --body "$(cat <<'EOF'
## Summary
<1-3 bullet points describing what this PR does>
## Changes
<Brief description of the changes made>
## Test Plan
- [ ] <Testing checklist item>
- [ ] <Another testing item>
---
Generated with [Claude Code](https://claude.ai/code)
EOF
)"
```
### 5. Draft vs Ready
- Use `--draft` flag if work is in progress
- Convert to ready: `gh pr ready`
## PR Body Template
```markdown
## Summary
- <Primary change/feature>
- <Secondary change if applicable>
## Changes
<Describe what changed and why>
## Test Plan
- [ ] Unit tests pass
- [ ] Manual testing completed
- [ ] No regressions
---
Generated with [Claude Code](https://claude.ai/code)
```
## Useful Commands
```bash
# List your open PRs
gh pr list --author "@me"
# Check PR status
gh pr status
# View specific PR
gh pr view <PR-NUMBER>
# Add reviewers
gh pr edit <PR-NUMBER> --add-reviewer username
# Merge PR (squash)
gh pr merge <PR-NUMBER> --squash
```
## Important Notes
- NEVER force push to main/master
- Always analyze ALL commits in the branch, not just the latest
- If there's a PR template at `.github/pull_request_template.md`, use it
- Return the PR URL when done so user can access it
Now analyze the current branch and create an appropriate PR.

62
commands/history.md Normal file
View File

@@ -0,0 +1,62 @@
---
description: Explore git commit history using git-intelligence MCP tools
model: claude-haiku-4-5-20251001
allowed-tools: mcp__plugin_git_git-intelligence__get_recent_commits, mcp__plugin_git_git-intelligence__search_commits, mcp__plugin_git_git-intelligence__get_diff_summary
argument-hint: [search-query]
---
# Git History Explorer
Interactively explore git commit history using the git-intelligence MCP tools.
## Instructions
Help the user explore git history to understand past changes. Use the MCP tools for efficient queries.
### Available MCP Tools
Based on the user's request, use the appropriate tool:
1. **Recent commits** → Use `get_recent_commits` tool
- Default: 10 commits, adjust `limit` as needed
2. **Search by message** → Use `search_commits` tool
- Set `query` to the search term
- Set `search_code: false` (default)
3. **Search by code change** → Use `search_commits` tool
- Set `query` to the code snippet
- Set `search_code: true` (like git log -S)
4. **Current status** → Use `get_status` tool
- Shows branch, staged/modified/untracked files
### Fallback to Bash
For queries not covered by MCP tools, use Bash:
- **File history**: `git log --oneline -10 -- <filepath>`
- **Branch info**: `git branch -a`
- **Show specific commit**: `git show <commit-hash> --stat`
- **Compare branches**: `git log --oneline main..HEAD`
- **Who changed what**: `git blame <filepath>`
- **Time-based queries**: `git log --oneline --since="last week"`
### Arguments
Parse the user's query to determine intent:
- `/git:history` → Use `get_recent_commits`
- `/git:history auth` → Use `search_commits` with query "auth"
- `/git:history src/api.ts` → Use Bash: `git log --oneline -10 -- src/api.ts`
- `/git:history -S function` → Use `search_commits` with search_code: true
- `/git:history last week` → Use Bash: `git log --oneline --since="last week"`
### Output
Present results clearly with:
- Commit hash (short)
- Subject line
- Author and relative time
- Optionally show diff for specific commits
Now explore the history based on the query: $ARGUMENTS

56
commands/session-log.md Normal file
View File

@@ -0,0 +1,56 @@
---
description: Show git activity during this Claude session
model: claude-haiku-4-5-20251001
allowed-tools: mcp__plugin_git_git-intelligence__get_recent_commits, mcp__plugin_git_git-intelligence__get_diff_summary
---
# Session Activity Log
Show what git activity has happened during this session using the git-intelligence MCP tools.
## Instructions
Display a summary of git activity during the current Claude session, including:
- Commits made
- Files changed
- Current uncommitted work
### Workflow
1. **Get recent commits** → Use `get_recent_commits` tool with `limit: 10`
- Shows commits with hash, message, author, and relative time
2. **Get current status** → Use `get_status` tool
- Returns branch, staged/modified/untracked counts, and file lists
3. **Get diff summary** → Use `get_diff_summary` tool
- Returns files changed with lines added/deleted
4. **Check for session summary file** (Bash fallback):
```bash
cat .claude-session-summary 2>/dev/null || echo "No session summary found"
```
### Output Format
Present the information in a clear summary:
```
## Session Activity
### Commits Made (recent)
- abc1234 feat(auth): add login endpoint (5 minutes ago)
- def5678 fix(api): handle null case (20 minutes ago)
### Current Changes
Staged: 2 files (+45, -12)
Modified: 3 files
Untracked: 1 file
### Uncommitted Files
M src/auth.ts
M src/api.ts
?? src/new-file.ts
```
Now show the session activity.

39
hooks/bun.lock Normal file
View File

@@ -0,0 +1,39 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "git-hooks",
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "latest",
},
},
},
"packages": {
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.1.50", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.33.5", "@img/sharp-darwin-x64": "^0.33.5", "@img/sharp-linux-arm": "^0.33.5", "@img/sharp-linux-arm64": "^0.33.5", "@img/sharp-linux-x64": "^0.33.5", "@img/sharp-win32-x64": "^0.33.5" }, "peerDependencies": { "zod": "^3.24.1" } }, "sha512-vHOLohUeiVadWl4eTAbw12ACIG1wZ/NN4ScLe8P/yrsldT1QkYwn6ndkoilaFBB2gIHECEx7wRAtSfCLefge4Q=="],
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="],
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="],
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="],
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="],
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="],
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="],
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="],
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="],
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="],
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="],
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="],
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
}
}

View File

@@ -0,0 +1,46 @@
import { describe, expect, test } from "bun:test";
import { parseGitStatus } from "./git-context-loader";
describe("git-context-loader", () => {
test("parseGitStatus parses clean status", () => {
const output = "## main...origin/main";
const result = parseGitStatus(output);
expect(result.branch).toBe("main");
expect(result.status).toEqual({ staged: 0, modified: 0, untracked: 0 });
});
test("parseGitStatus parses dirty status", () => {
const output = `## feature/test...origin/feature/test [ahead 1]
M modified-file.ts
A staged-file.ts
?? untracked-file.ts
D deleted-file.ts`;
const result = parseGitStatus(output);
expect(result.branch).toBe("feature/test");
// M (modified), A (staged), ?? (untracked), D (unstaged delete -> modified)
// M -> index: M, worktree: space -> staged
// A -> index: A, worktree: space -> staged
// ?? -> untracked
// D -> index: space, worktree: D -> modified
// Let's check the logic in parseGitStatus:
// if (code.startsWith("?") || code === "??") untracked++
// else:
// if (code[0] !== " " && code[0] !== "?") staged++
// if (code[1] !== " " && code[1] !== "?") modified++
// M -> code="M " -> staged++
// A -> code="A " -> staged++
// ?? -> untracked++
// D -> code=" D" -> modified++
expect(result.status).toEqual({ staged: 2, modified: 1, untracked: 1 });
});
test("parseGitStatus handles detached head", () => {
const output = "## HEAD (no branch)";
const result = parseGitStatus(output);
expect(result.branch).toBe("HEAD (no branch)");
});
});

182
hooks/git-context-loader.ts Normal file
View File

@@ -0,0 +1,182 @@
#!/usr/bin/env bun
/**
* Git Context Loader Hook
*
* SessionStart hook that loads git context at the beginning of a session.
* Outputs recent commits, status, and open issues for Claude's awareness.
*/
import type { SessionStartHookInput } from "@anthropic-ai/claude-agent-sdk";
interface GitContext {
branch: string;
status: {
staged: number;
modified: number;
untracked: number;
};
recentCommits: string[];
openIssues?: string[];
}
async function exec(
command: string,
cwd: string,
): Promise<{ stdout: string; exitCode: number }> {
const proc = Bun.spawn(["sh", "-c", command], {
cwd,
stdout: "pipe",
stderr: "pipe",
});
const stdout = await new Response(proc.stdout).text();
const exitCode = await proc.exited;
return { stdout: stdout.trim(), exitCode };
}
async function isGitRepo(cwd: string): Promise<boolean> {
const { exitCode } = await exec("git rev-parse --git-dir", cwd);
return exitCode === 0;
}
export function parseGitStatus(statusOut: string) {
const lines = statusOut.split("\n");
const branchLine = lines.find((l) => l.startsWith("##"));
let branch = "(detached)";
if (branchLine) {
const parsed = branchLine.substring(3).split("...")[0];
if (parsed) branch = parsed.trim();
}
let staged = 0;
let modified = 0;
let untracked = 0;
for (const line of lines) {
if (line.startsWith("##") || !line.trim()) continue;
const code = line.substring(0, 2);
if (code.startsWith("?") || code === "??") {
untracked++;
} else {
if (code[0] !== " " && code[0] !== "?") staged++;
if (code[1] !== " " && code[1] !== "?") modified++;
}
}
return { branch, status: { staged, modified, untracked } };
}
async function getGitContext(cwd: string): Promise<GitContext | null> {
if (!(await isGitRepo(cwd))) {
return null;
}
// Get status and branch in one go
const { stdout: statusOut } = await exec(
"git status --porcelain -b 2>/dev/null",
cwd,
);
const { branch, status } = parseGitStatus(statusOut);
// Get recent commits
const { stdout: commitsOut } = await exec(
'git log --oneline -5 --format="%h %s (%ar)" 2>/dev/null',
cwd,
);
const recentCommits = commitsOut
.split("\n")
.filter((line) => line.trim() !== "");
const context: GitContext = {
branch: branch || "(detached)",
status,
recentCommits,
};
// Check for open issues if gh is available and authenticated
const { exitCode: ghAuthCheck } = await exec("gh auth status", cwd);
if (ghAuthCheck === 0) {
const { stdout: issuesOut, exitCode: issuesCode } = await exec(
"gh issue list --limit 3 --state open 2>/dev/null",
cwd,
);
if (issuesCode === 0 && issuesOut.trim()) {
context.openIssues = issuesOut
.split("\n")
.filter((line) => line.trim() !== "");
}
}
return context;
}
function formatContext(context: GitContext): string {
const { branch, status, recentCommits, openIssues } = context;
let output = "Git Context:\n";
output += ` Branch: ${branch}\n`;
output += ` Status: ${status.staged} staged, ${status.modified} modified, ${status.untracked} untracked\n`;
output += "\nRecent commits:\n";
if (recentCommits.length > 0) {
recentCommits.forEach((commit) => {
output += ` ${commit}\n`;
});
} else {
output += " (no commits yet)\n";
}
if (openIssues && openIssues.length > 0) {
output += "\nOpen issues (top 3):\n";
openIssues.forEach((issue) => {
output += ` ${issue}\n`;
});
}
return output;
}
function formatSystemMessage(context: GitContext): string {
const { branch, status, recentCommits } = context;
const totalChanges = status.staged + status.modified + status.untracked;
const changesStr = totalChanges > 0 ? `, ${totalChanges} changes` : "";
const lastCommit =
recentCommits[0]?.split(" ").slice(1).join(" ") || "no commits";
return `Git: ${branch}${changesStr} | Last: ${lastCommit}`;
}
interface HookOutput {
systemMessage?: string;
hookSpecificOutput: {
hookEventName: string;
additionalContext: string;
};
}
// Main execution
if (import.meta.main) {
const input = (await Bun.stdin.json()) as SessionStartHookInput;
const { cwd, source } = input;
// Only run on startup, not on resume/clear/compact
if (source === "startup") {
const context = await getGitContext(cwd);
if (context) {
const output: HookOutput = {
systemMessage: formatSystemMessage(context),
hookSpecificOutput: {
hookEventName: "SessionStart",
additionalContext: formatContext(context),
},
};
console.log(JSON.stringify(output));
}
}
process.exit(0);
}

32
hooks/hooks.json Normal file
View File

@@ -0,0 +1,32 @@
{
"description": "Git intelligence hooks for session context and summaries",
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/install-deps.sh",
"timeout": 30
},
{
"type": "command",
"command": "bun run ${CLAUDE_PLUGIN_ROOT}/hooks/git-context-loader.ts",
"timeout": 10
}
]
}
],
"PreCompact": [
{
"hooks": [
{
"type": "command",
"command": "bun run ${CLAUDE_PLUGIN_ROOT}/hooks/session-summary.ts",
"timeout": 5
}
]
}
]
}
}

200
hooks/hooks.test.ts Normal file
View File

@@ -0,0 +1,200 @@
import { describe, expect, test } from "bun:test";
import type {
PreCompactHookInput,
SessionStartHookInput,
} from "@anthropic-ai/claude-agent-sdk";
// Test the hook input types and basic functionality
describe("SessionStartHookInput type", () => {
test("has correct shape", () => {
const input: SessionStartHookInput = {
session_id: "test-session-123",
transcript_path: "/tmp/transcript",
cwd: "/Users/test/code",
hook_event_name: "SessionStart",
source: "startup",
};
expect(input.session_id).toBe("test-session-123");
expect(input.cwd).toBe("/Users/test/code");
expect(input.hook_event_name).toBe("SessionStart");
expect(input.source).toBe("startup");
});
test("source can be startup, resume, clear, or compact", () => {
const sources: SessionStartHookInput["source"][] = [
"startup",
"resume",
"clear",
"compact",
];
sources.forEach((source) => {
const input: SessionStartHookInput = {
session_id: "test",
transcript_path: "/tmp",
cwd: "/tmp",
hook_event_name: "SessionStart",
source,
};
expect(input.source).toBe(source);
});
});
});
describe("PreCompactHookInput type", () => {
test("has correct shape", () => {
const input: PreCompactHookInput = {
session_id: "test-session-123",
transcript_path: "/tmp/transcript",
cwd: "/Users/test/code",
hook_event_name: "PreCompact",
trigger: "auto",
custom_instructions: null,
};
expect(input.session_id).toBe("test-session-123");
expect(input.hook_event_name).toBe("PreCompact");
expect(input.trigger).toBe("auto");
expect(input.custom_instructions).toBeNull();
});
test("trigger can be manual or auto", () => {
const triggers: PreCompactHookInput["trigger"][] = ["manual", "auto"];
triggers.forEach((trigger) => {
const input: PreCompactHookInput = {
session_id: "test",
transcript_path: "/tmp",
cwd: "/tmp",
hook_event_name: "PreCompact",
trigger,
custom_instructions: null,
};
expect(input.trigger).toBe(trigger);
});
});
test("custom_instructions can be string or null", () => {
const withInstructions: PreCompactHookInput = {
session_id: "test",
transcript_path: "/tmp",
cwd: "/tmp",
hook_event_name: "PreCompact",
trigger: "manual",
custom_instructions: "Focus on the auth changes",
};
const withoutInstructions: PreCompactHookInput = {
session_id: "test",
transcript_path: "/tmp",
cwd: "/tmp",
hook_event_name: "PreCompact",
trigger: "auto",
custom_instructions: null,
};
expect(withInstructions.custom_instructions).toBe(
"Focus on the auth changes",
);
expect(withoutInstructions.custom_instructions).toBeNull();
});
});
describe("git-context-loader", () => {
test("can be run with valid input", async () => {
const input: SessionStartHookInput = {
session_id: "test-123",
transcript_path: "/tmp/transcript",
cwd: import.meta.dir, // Use the hooks directory (which is in a git repo)
hook_event_name: "SessionStart",
source: "startup",
};
// Run the actual hook script using echo to pipe input
const proc = Bun.spawn(
[
"sh",
"-c",
`echo '${JSON.stringify(input)}' | bun run git-context-loader.ts`,
],
{
cwd: import.meta.dir,
stdout: "pipe",
stderr: "pipe",
},
);
// Wait for process to complete
const exitCode = await proc.exited;
const stdout = await new Response(proc.stdout).text();
expect(exitCode).toBe(0);
// Should output git context
expect(stdout).toContain("Git Context:");
expect(stdout).toContain("Branch:");
});
test("skips non-startup sources", async () => {
const input: SessionStartHookInput = {
session_id: "test-123",
transcript_path: "/tmp/transcript",
cwd: import.meta.dir,
hook_event_name: "SessionStart",
source: "resume", // Not startup
};
const proc = Bun.spawn(
[
"sh",
"-c",
`echo '${JSON.stringify(input)}' | bun run git-context-loader.ts`,
],
{
cwd: import.meta.dir,
stdout: "pipe",
stderr: "pipe",
},
);
const exitCode = await proc.exited;
const stdout = await new Response(proc.stdout).text();
expect(exitCode).toBe(0);
// Should NOT output git context for resume
expect(stdout).not.toContain("Git Context:");
});
});
describe("session-summary", () => {
test("can be run with valid input", async () => {
const input: PreCompactHookInput = {
session_id: "test-123",
transcript_path: "/tmp/transcript",
cwd: import.meta.dir,
hook_event_name: "PreCompact",
trigger: "manual",
custom_instructions: null,
};
const proc = Bun.spawn(
[
"sh",
"-c",
`echo '${JSON.stringify(input)}' | bun run session-summary.ts`,
],
{
cwd: import.meta.dir,
stdout: "pipe",
stderr: "pipe",
},
);
const exitCode = await proc.exited;
const stdout = await new Response(proc.stdout).text();
expect(exitCode).toBe(0);
expect(stdout).toContain("Session summary saved");
});
});

23
hooks/install-deps.sh Executable file
View File

@@ -0,0 +1,23 @@
#!/bin/bash
# Install MCP server and hooks dependencies on session start
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PLUGIN_ROOT="$(dirname "$SCRIPT_DIR")"
MCP_DIR="$PLUGIN_ROOT/mcp-servers/git-intelligence"
HOOKS_DIR="$PLUGIN_ROOT/hooks"
# Install MCP server dependencies
if [ ! -d "$MCP_DIR/node_modules" ]; then
cd "$MCP_DIR"
bun install --silent 2>/dev/null || bun install
fi
# Install hooks dependencies (for TypeScript types)
if [ ! -d "$HOOKS_DIR/node_modules" ]; then
cd "$HOOKS_DIR"
bun install --silent 2>/dev/null || bun install
fi
exit 0

15
hooks/package.json Normal file
View File

@@ -0,0 +1,15 @@
{
"name": "git-hooks",
"version": "1.0.0",
"type": "module",
"scripts": {
"test": "bun test",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "latest"
},
"devDependencies": {
"bun-types": "^1.3.3"
}
}

158
hooks/session-summary.ts Normal file
View File

@@ -0,0 +1,158 @@
#!/usr/bin/env bun
/**
* Session Summary Hook
*
* PreCompact hook that saves a session summary before context compaction.
* Helps maintain continuity across context windows.
*/
import { existsSync, mkdirSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import type { PreCompactHookInput } from "@anthropic-ai/claude-agent-sdk";
interface SessionSummary {
timestamp: string;
branch: string;
trigger: "manual" | "auto";
sessionCommits: string[];
uncommittedChanges: {
staged: string;
modified: string;
};
}
async function exec(
command: string,
cwd: string,
): Promise<{ stdout: string; exitCode: number }> {
const proc = Bun.spawn(["sh", "-c", command], {
cwd,
stdout: "pipe",
stderr: "pipe",
});
const stdout = await new Response(proc.stdout).text();
const exitCode = await proc.exited;
return { stdout: stdout.trim(), exitCode };
}
async function isGitRepo(cwd: string): Promise<boolean> {
const { exitCode } = await exec("git rev-parse --git-dir", cwd);
return exitCode === 0;
}
async function getGitRoot(cwd: string): Promise<string | null> {
const { stdout, exitCode } = await exec("git rev-parse --show-toplevel", cwd);
return exitCode === 0 ? stdout : null;
}
async function getSessionSummary(
cwd: string,
trigger: "manual" | "auto",
): Promise<SessionSummary | null> {
if (!(await isGitRepo(cwd))) {
return null;
}
// Get branch
const { stdout: branch } = await exec(
"git branch --show-current 2>/dev/null || echo '(detached)'",
cwd,
);
// Get commits from the last hour (approximate session length)
const { stdout: commitsOut } = await exec(
'git log --oneline --since="1 hour ago" 2>/dev/null | head -10',
cwd,
);
const sessionCommits = commitsOut
.split("\n")
.filter((line) => line.trim() !== "");
// Get staged changes summary
const { stdout: stagedStat } = await exec(
"git diff --cached --stat 2>/dev/null | tail -1",
cwd,
);
// Get modified changes summary
const { stdout: modifiedStat } = await exec(
"git diff --stat 2>/dev/null | tail -1",
cwd,
);
return {
timestamp: new Date().toISOString(),
branch: branch || "(detached)",
trigger,
sessionCommits,
uncommittedChanges: {
staged: stagedStat || "none",
modified: modifiedStat || "none",
},
};
}
function formatSummary(summary: SessionSummary): string {
let output = "# Claude Session Summary\n";
output += `# Generated: ${summary.timestamp}\n`;
output += `# Branch: ${summary.branch}\n`;
output += `# Trigger: ${summary.trigger}\n\n`;
output += "## Session Activity\n\n";
if (summary.sessionCommits.length > 0) {
output += "### Commits this session:\n";
summary.sessionCommits.forEach((commit) => {
output += `- ${commit}\n`;
});
output += "\n";
}
if (
summary.uncommittedChanges.staged !== "none" ||
summary.uncommittedChanges.modified !== "none"
) {
output += "### Uncommitted changes:\n";
if (summary.uncommittedChanges.staged !== "none") {
output += `Staged: ${summary.uncommittedChanges.staged}\n`;
}
if (summary.uncommittedChanges.modified !== "none") {
output += `Modified: ${summary.uncommittedChanges.modified}\n`;
}
output += "\n";
}
return output;
}
// Main execution
const input = (await Bun.stdin.json()) as PreCompactHookInput;
const { cwd, trigger } = input;
const gitRoot = await getGitRoot(cwd);
if (gitRoot) {
const summary = await getSessionSummary(cwd, trigger);
if (summary) {
// Write to ~/.claude/session-summaries/ to avoid polluting user repos
const claudeDir = join(homedir(), ".claude", "session-summaries");
if (!existsSync(claudeDir)) {
mkdirSync(claudeDir, { recursive: true });
}
// Use repo name as filename to keep summaries separate per project
const repoName = gitRoot.split("/").pop() || "unknown";
const summaryPath = join(claudeDir, `${repoName}.md`);
const content = formatSummary(summary);
await Bun.write(summaryPath, content);
console.log(`Session summary saved to ${summaryPath}`);
}
}
process.exit(0);

97
plugin.lock.json Normal file
View File

@@ -0,0 +1,97 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:nathanvale/side-quest-marketplace:plugins/git",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "d928f3dcf67e95244c1c89af5bc6c128c9c1f431",
"treeHash": "530f81ef6118809acaf022e840c4de5c9f6301021f6176971c33a37827567dbd",
"generatedAt": "2025-11-28T10:27:14.576270Z",
"toolVersion": "publish_plugins.py@0.2.0"
},
"origin": {
"remote": "git@github.com:zhongweili/42plugin-data.git",
"branch": "master",
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
},
"manifest": {
"name": "git",
"description": "Git intelligence for Claude Code - session context, commit history, and smart commits with Conventional Commits",
"version": "1.0.0"
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "0268c0aff442dc0bcbca8102c649d1903859525b59d26d4a5a1fe58230928daa"
},
{
"path": "hooks/git-context-loader.ts",
"sha256": "f4b0e0eb11cb8398b6b6db856f8e14f8a3f0e4e5bc7dabe9ed4b459ea319b165"
},
{
"path": "hooks/bun.lock",
"sha256": "5ef31882354fa9dcc22799527b8e6ccb0292e33f6d85c202605dc02a85f7c51f"
},
{
"path": "hooks/package.json",
"sha256": "3c6f46756b0ccf6bda00514b17178d78c52b5c37dfa6548222155c3e177bbadc"
},
{
"path": "hooks/hooks.test.ts",
"sha256": "38d7b10a461910fd02b428705497b3396ebb7eea52c504250a9b665d0f3026ad"
},
{
"path": "hooks/git-context-loader.test.ts",
"sha256": "845e6e8bb21627fcc1ec91e2ce9a34ad8488d818173b74c8b1662e414db2fdbd"
},
{
"path": "hooks/hooks.json",
"sha256": "b23bc6fd5500fd83d47d215025c76a49251e7cf49dd17dccf61443cfe06b0084"
},
{
"path": "hooks/session-summary.ts",
"sha256": "7895cf7f075ebe6da65bc8dee42a7ba91f9202efc79e3eda632039c2b43eec75"
},
{
"path": "hooks/install-deps.sh",
"sha256": "e47946c4b285189db9ced87561ade597a60f6dcdde516cbeced9331984cfd0d3"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "d6aef1074c5d2b985dfe23b565b01aa00de6d4d63fd68aa43d58c8746ddf52fd"
},
{
"path": "commands/history.md",
"sha256": "dd8864684e6ffc10cc50809cce988139d24497e7c842e2d1f4bb49ad2d7aa0c3"
},
{
"path": "commands/checkpoint.md",
"sha256": "e6dba1faf3ec8cfce9c1f2178d2bfa07c5d28305d3395a76940c510f4c493173"
},
{
"path": "commands/create-pr.md",
"sha256": "4319905290ed92a99103e9a8797d3c2e99899cc25a54edcce9a708b0df10e492"
},
{
"path": "commands/commit.md",
"sha256": "84da3a89debb5af49dc417f02dd67e59b79d0904f8ac6df8370c81f3b0da3da8"
},
{
"path": "commands/session-log.md",
"sha256": "a7a3f91aa93d98335950f7896a88098b2605837e4f52c36567f1840a5cc7b35c"
},
{
"path": "skills/git-expert/SKILL.md",
"sha256": "1477e0db66d80387769adbc2bf1fddbc6faf4e5a0b3fb0f83c4bf6826a4e6436"
}
],
"dirSha256": "530f81ef6118809acaf022e840c4de5c9f6301021f6176971c33a37827567dbd"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

View File

@@ -0,0 +1,83 @@
---
name: git-expert
description: Git operations, history exploration, and intelligent commit management. Use when users ask about git history ("what did we change", "when did we add"), want to understand past changes, need help with commits ("commit this", "save my work", "checkpoint"), ask about branches, or mention recent work.
---
# Git Expert Skill
Git operations, history exploration, and intelligent commit management.
## Capabilities
### History Analysis
- Search commit messages and code changes
- Trace file history to understand evolution
- Find when specific code was introduced or removed
- Identify who made changes and why
### Smart Commits
- Create well-formatted Conventional Commits
- Split large changes into atomic commits
- Handle pre-commit hooks gracefully
- Never commit secrets or sensitive data
### Session Awareness
- Track what was done during the current session
- Show uncommitted work
- Create checkpoints before risky operations
## Tools Available
Use the git-intelligence MCP server tools:
- `get_recent_commits` - Recent commit history
- `search_commits` - Search by message or code
- `get_status` - Current repository state
- `get_diff_summary` - Summary of changes
For file history and branch info, use Bash:
- `git log --oneline -10 -- <filepath>` - File-specific history
- `git branch -a` - Branch information
## Commit Format
Always use Conventional Commits format:
```
<type>(<scope>): <subject>
```
Type mappings:
| Type | Use for |
|------|---------|
| feat | New feature |
| fix | Bug fix |
| docs | Documentation |
| style | Formatting |
| refactor | Refactoring |
| perf | Performance |
| test | Tests |
| build | Build system |
| ci | CI/CD |
| chore | Maintenance |
| revert | Revert |
## Best Practices
1. **Before editing**: Check recent commits to understand context
2. **Before committing**: Review all changes, never blind commit
3. **Large changes**: Suggest splitting into atomic commits
4. **Unclear intent**: Ask user for commit scope/description
5. **Secrets detected**: Warn and refuse to commit
## Example Interactions
**User**: "What changed in the auth module recently?"
- Use `search_commits` with query "auth" or Bash `git log --oneline -10 -- src/auth/`
**User**: "Commit my changes"
- Run `get_status`, review diffs, create appropriate conventional commit
**User**: "What did we do this session?"
- Check recent commits and current diff, summarize activity
**User**: "Save my work before I try this"
- Create a checkpoint commit with current context