@hasna/hooks 0.0.1
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.
- package/.npmrc.example +2 -0
- package/AGENTS.md +54 -0
- package/CLAUDE.md +70 -0
- package/CONTRIBUTING.md +45 -0
- package/README.md +232 -0
- package/bin/index.js +5171 -0
- package/hooks/hook-agentmessages/CLAUDE.md +79 -0
- package/hooks/hook-agentmessages/LICENSE +21 -0
- package/hooks/hook-agentmessages/README.md +107 -0
- package/hooks/hook-agentmessages/package.json +31 -0
- package/hooks/hook-agentmessages/src/check-messages.ts +151 -0
- package/hooks/hook-agentmessages/src/install.ts +126 -0
- package/hooks/hook-agentmessages/src/session-start.ts +255 -0
- package/hooks/hook-agentmessages/src/uninstall.ts +89 -0
- package/hooks/hook-branchprotect/CLAUDE.md +23 -0
- package/hooks/hook-branchprotect/README.md +25 -0
- package/hooks/hook-branchprotect/package.json +42 -0
- package/hooks/hook-branchprotect/src/cli.ts +126 -0
- package/hooks/hook-branchprotect/src/hook.ts +88 -0
- package/hooks/hook-branchprotect/tsconfig.json +25 -0
- package/hooks/hook-checkbugs/LICENSE +21 -0
- package/hooks/hook-checkbugs/README.md +140 -0
- package/hooks/hook-checkbugs/package.json +58 -0
- package/hooks/hook-checkbugs/src/cli.ts +628 -0
- package/hooks/hook-checkbugs/src/hook.ts +335 -0
- package/hooks/hook-checkbugs/tsconfig.json +15 -0
- package/hooks/hook-checkdocs/README.md +137 -0
- package/hooks/hook-checkdocs/package.json +57 -0
- package/hooks/hook-checkdocs/src/cli.ts +628 -0
- package/hooks/hook-checkdocs/src/hook.ts +310 -0
- package/hooks/hook-checkdocs/tsconfig.json +15 -0
- package/hooks/hook-checkfiles/LICENSE +21 -0
- package/hooks/hook-checkfiles/README.md +141 -0
- package/hooks/hook-checkfiles/package.json +56 -0
- package/hooks/hook-checkfiles/src/cli.ts +545 -0
- package/hooks/hook-checkfiles/src/hook.ts +321 -0
- package/hooks/hook-checkfiles/tsconfig.json +15 -0
- package/hooks/hook-checklint/LICENSE +21 -0
- package/hooks/hook-checklint/README.md +147 -0
- package/hooks/hook-checklint/package.json +57 -0
- package/hooks/hook-checklint/src/cli-patch.ts +32 -0
- package/hooks/hook-checklint/src/cli.ts +667 -0
- package/hooks/hook-checklint/src/hook.ts +473 -0
- package/hooks/hook-checklint/tsconfig.json +15 -0
- package/hooks/hook-checkpoint/CLAUDE.md +23 -0
- package/hooks/hook-checkpoint/README.md +37 -0
- package/hooks/hook-checkpoint/package.json +58 -0
- package/hooks/hook-checkpoint/src/cli.ts +191 -0
- package/hooks/hook-checkpoint/src/hook.ts +207 -0
- package/hooks/hook-checkpoint/tsconfig.json +25 -0
- package/hooks/hook-checksecurity/LICENSE +21 -0
- package/hooks/hook-checksecurity/README.md +158 -0
- package/hooks/hook-checksecurity/package.json +57 -0
- package/hooks/hook-checksecurity/src/cli.ts +601 -0
- package/hooks/hook-checksecurity/src/hook.ts +334 -0
- package/hooks/hook-checksecurity/tsconfig.json +15 -0
- package/hooks/hook-checktasks/README.md +144 -0
- package/hooks/hook-checktasks/package.json +55 -0
- package/hooks/hook-checktasks/src/cli.ts +578 -0
- package/hooks/hook-checktasks/src/hook.ts +308 -0
- package/hooks/hook-checktasks/tsconfig.json +20 -0
- package/hooks/hook-checktests/LICENSE +21 -0
- package/hooks/hook-checktests/README.md +137 -0
- package/hooks/hook-checktests/package.json +57 -0
- package/hooks/hook-checktests/src/cli.ts +627 -0
- package/hooks/hook-checktests/src/hook.ts +334 -0
- package/hooks/hook-checktests/tsconfig.json +15 -0
- package/hooks/hook-contextrefresh/CLAUDE.md +23 -0
- package/hooks/hook-contextrefresh/README.md +42 -0
- package/hooks/hook-contextrefresh/package.json +42 -0
- package/hooks/hook-contextrefresh/src/cli.ts +152 -0
- package/hooks/hook-contextrefresh/src/hook.ts +148 -0
- package/hooks/hook-contextrefresh/tsconfig.json +25 -0
- package/hooks/hook-gitguard/CLAUDE.md +22 -0
- package/hooks/hook-gitguard/README.md +30 -0
- package/hooks/hook-gitguard/package.json +57 -0
- package/hooks/hook-gitguard/src/cli.ts +159 -0
- package/hooks/hook-gitguard/src/hook.ts +129 -0
- package/hooks/hook-gitguard/tsconfig.json +25 -0
- package/hooks/hook-packageage/CLAUDE.md +23 -0
- package/hooks/hook-packageage/README.md +33 -0
- package/hooks/hook-packageage/package.json +42 -0
- package/hooks/hook-packageage/src/cli.ts +165 -0
- package/hooks/hook-packageage/src/hook.ts +177 -0
- package/hooks/hook-packageage/tsconfig.json +25 -0
- package/hooks/hook-phonenotify/CLAUDE.md +25 -0
- package/hooks/hook-phonenotify/README.md +44 -0
- package/hooks/hook-phonenotify/package.json +42 -0
- package/hooks/hook-phonenotify/src/cli.ts +196 -0
- package/hooks/hook-phonenotify/src/hook.ts +139 -0
- package/hooks/hook-phonenotify/tsconfig.json +25 -0
- package/hooks/hook-precompact/CLAUDE.md +23 -0
- package/hooks/hook-precompact/README.md +36 -0
- package/hooks/hook-precompact/package.json +42 -0
- package/hooks/hook-precompact/src/cli.ts +168 -0
- package/hooks/hook-precompact/src/hook.ts +122 -0
- package/hooks/hook-precompact/tsconfig.json +25 -0
- package/package.json +61 -0
- package/src/cli/components/App.tsx +191 -0
- package/src/cli/components/CategorySelect.tsx +37 -0
- package/src/cli/components/DataTable.tsx +133 -0
- package/src/cli/components/Header.tsx +18 -0
- package/src/cli/components/HookSelect.tsx +29 -0
- package/src/cli/components/InstallProgress.tsx +105 -0
- package/src/cli/components/SearchView.tsx +86 -0
- package/src/cli/index.tsx +218 -0
- package/src/index.ts +31 -0
- package/src/lib/installer.ts +288 -0
- package/src/lib/registry.ts +205 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Claude Code Hook: phonenotify
|
|
5
|
+
*
|
|
6
|
+
* Stop/Notification hook that sends push notifications to your phone
|
|
7
|
+
* via ntfy.sh when Claude finishes a task or needs your attention.
|
|
8
|
+
*
|
|
9
|
+
* Configure via ~/.claude/settings.json:
|
|
10
|
+
* {
|
|
11
|
+
* "phoneNotifyConfig": {
|
|
12
|
+
* "enabled": true,
|
|
13
|
+
* "topic": "claude-code-YOUR_SECRET",
|
|
14
|
+
* "server": "https://ntfy.sh",
|
|
15
|
+
* "priority": 3
|
|
16
|
+
* }
|
|
17
|
+
* }
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { readFileSync, existsSync } from "fs";
|
|
21
|
+
import { join } from "path";
|
|
22
|
+
import { homedir } from "os";
|
|
23
|
+
|
|
24
|
+
interface HookInput {
|
|
25
|
+
session_id: string;
|
|
26
|
+
cwd: string;
|
|
27
|
+
hook_event_name: string;
|
|
28
|
+
notification_type?: string;
|
|
29
|
+
message?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface HookOutput {
|
|
33
|
+
continue?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface PhoneNotifyConfig {
|
|
37
|
+
enabled?: boolean;
|
|
38
|
+
topic?: string;
|
|
39
|
+
server?: string;
|
|
40
|
+
priority?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const CONFIG_KEY = "phoneNotifyConfig";
|
|
44
|
+
|
|
45
|
+
function readStdinJson(): HookInput | null {
|
|
46
|
+
try {
|
|
47
|
+
const input = readFileSync(0, "utf-8").trim();
|
|
48
|
+
if (!input) return null;
|
|
49
|
+
return JSON.parse(input);
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getConfig(): PhoneNotifyConfig {
|
|
56
|
+
const settingsPath = join(homedir(), ".claude", "settings.json");
|
|
57
|
+
try {
|
|
58
|
+
if (existsSync(settingsPath)) {
|
|
59
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
60
|
+
return settings[CONFIG_KEY] || {};
|
|
61
|
+
}
|
|
62
|
+
} catch {}
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function sendNotification(
|
|
67
|
+
config: PhoneNotifyConfig,
|
|
68
|
+
title: string,
|
|
69
|
+
message: string
|
|
70
|
+
): Promise<void> {
|
|
71
|
+
const server = config.server || "https://ntfy.sh";
|
|
72
|
+
const topic = config.topic;
|
|
73
|
+
|
|
74
|
+
if (!topic) {
|
|
75
|
+
console.error("[hook-phonenotify] No topic configured. Set phoneNotifyConfig.topic in settings.");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const url = `${server}/${topic}`;
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const response = await fetch(url, {
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers: {
|
|
85
|
+
Title: title,
|
|
86
|
+
Priority: String(config.priority || 3),
|
|
87
|
+
Tags: "robot",
|
|
88
|
+
},
|
|
89
|
+
body: message,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
console.error(`[hook-phonenotify] Failed to send: ${response.status}`);
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error(`[hook-phonenotify] Send failed: ${error}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function respond(output: HookOutput): void {
|
|
101
|
+
console.log(JSON.stringify(output));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function run(): Promise<void> {
|
|
105
|
+
const input = readStdinJson();
|
|
106
|
+
|
|
107
|
+
if (!input) {
|
|
108
|
+
respond({ continue: true });
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const config = getConfig();
|
|
113
|
+
|
|
114
|
+
if (!config.enabled) {
|
|
115
|
+
respond({ continue: true });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let title = "Claude Code";
|
|
120
|
+
let message = "";
|
|
121
|
+
|
|
122
|
+
if (input.hook_event_name === "Stop") {
|
|
123
|
+
title = "Claude Code - Done";
|
|
124
|
+
message = "Claude has finished and is waiting for your response.";
|
|
125
|
+
} else if (input.hook_event_name === "Notification") {
|
|
126
|
+
title = "Claude Code - Attention";
|
|
127
|
+
message = input.message || "Claude Code requires your attention.";
|
|
128
|
+
} else {
|
|
129
|
+
respond({ continue: true });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
await sendNotification(config, title, message);
|
|
134
|
+
respond({ continue: true });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (import.meta.main) {
|
|
138
|
+
run();
|
|
139
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": ["ESNext"],
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"allowImportingTsExtensions": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
"noUnusedLocals": true,
|
|
16
|
+
"noUnusedParameters": true,
|
|
17
|
+
"declaration": true,
|
|
18
|
+
"declarationMap": true,
|
|
19
|
+
"outDir": "./dist",
|
|
20
|
+
"rootDir": "./src",
|
|
21
|
+
"types": ["bun-types"]
|
|
22
|
+
},
|
|
23
|
+
"include": ["src/**/*"],
|
|
24
|
+
"exclude": ["node_modules", "dist"]
|
|
25
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
## hook-precompact
|
|
4
|
+
|
|
5
|
+
A PreCompact hook that saves session state before context compaction.
|
|
6
|
+
|
|
7
|
+
### Key Files
|
|
8
|
+
|
|
9
|
+
| File | Purpose |
|
|
10
|
+
|------|---------|
|
|
11
|
+
| `src/hook.ts` | Main hook logic — captures git state, writes handoff files |
|
|
12
|
+
| `src/cli.ts` | CLI — install/uninstall/status/list/latest |
|
|
13
|
+
|
|
14
|
+
### Hook Events
|
|
15
|
+
|
|
16
|
+
- **PreCompact** — fires before context compaction
|
|
17
|
+
|
|
18
|
+
### Behavior
|
|
19
|
+
|
|
20
|
+
- Saves handoff files to `.claude-handoffs/` (auto-gitignored)
|
|
21
|
+
- Captures: session ID, git branch/status/recent commits, timestamp
|
|
22
|
+
- Maintains `latest.json` for quick access to most recent handoff
|
|
23
|
+
- Non-blocking — failures are logged but don't interrupt
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# hook-precompact
|
|
2
|
+
|
|
3
|
+
Claude Code hook that saves session state before context compaction.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
When Claude Code compacts the context (to free up token space), important state can be lost. This hook saves a handoff file with session context, git state, and metadata before compaction occurs.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun install -g @hasnaxyz/hook-precompact
|
|
13
|
+
hook-precompact install
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Commands
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
hook-precompact install # Install to Claude Code settings
|
|
20
|
+
hook-precompact uninstall # Remove hook
|
|
21
|
+
hook-precompact status # Check installation
|
|
22
|
+
hook-precompact list # Show recent handoffs
|
|
23
|
+
hook-precompact latest # Show latest handoff data
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## What Gets Saved
|
|
27
|
+
|
|
28
|
+
Each handoff file (`.claude-handoffs/`) contains:
|
|
29
|
+
- Session ID and timestamp
|
|
30
|
+
- Current working directory
|
|
31
|
+
- Git branch, last commit, status, recent commits
|
|
32
|
+
- Transcript summary (when available)
|
|
33
|
+
|
|
34
|
+
## License
|
|
35
|
+
|
|
36
|
+
MIT
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hasnaxyz/hook-precompact",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Claude Code hook that saves session state before context compaction",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"hook-precompact": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/hook.js",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./dist/hook.js",
|
|
13
|
+
"types": "./dist/hook.d.ts"
|
|
14
|
+
},
|
|
15
|
+
"./cli": {
|
|
16
|
+
"import": "./dist/cli.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": ["dist", "README.md"],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "bun build ./src/cli.ts ./src/hook.ts --outdir ./dist --target node",
|
|
22
|
+
"prepublishOnly": "bun run build",
|
|
23
|
+
"typecheck": "tsc --noEmit"
|
|
24
|
+
},
|
|
25
|
+
"keywords": ["claude-code", "claude", "hook", "precompact", "context", "session", "state", "cli"],
|
|
26
|
+
"author": "Hasna",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/hasnaxyz/hook-precompact.git"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "restricted",
|
|
34
|
+
"registry": "https://registry.npmjs.org/"
|
|
35
|
+
},
|
|
36
|
+
"engines": { "node": ">=18", "bun": ">=1.0" },
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/bun": "^1.3.8",
|
|
39
|
+
"@types/node": "^20",
|
|
40
|
+
"typescript": "^5.0.0"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CLI for hook-precompact
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "fs";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import { homedir } from "os";
|
|
10
|
+
|
|
11
|
+
const HOOK_NAME = "hook-precompact";
|
|
12
|
+
const SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
|
|
13
|
+
const HANDOFF_DIR = ".claude-handoffs";
|
|
14
|
+
|
|
15
|
+
interface ClaudeSettings {
|
|
16
|
+
hooks?: {
|
|
17
|
+
PreCompact?: Array<{
|
|
18
|
+
matcher?: string;
|
|
19
|
+
hooks: Array<{ type: "command"; command: string }>;
|
|
20
|
+
}>;
|
|
21
|
+
};
|
|
22
|
+
[key: string]: unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readSettings(): ClaudeSettings {
|
|
26
|
+
try {
|
|
27
|
+
if (existsSync(SETTINGS_PATH)) {
|
|
28
|
+
return JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
|
|
29
|
+
}
|
|
30
|
+
} catch {}
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function writeSettings(settings: ClaudeSettings): void {
|
|
35
|
+
const dir = join(homedir(), ".claude");
|
|
36
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
37
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function install(): void {
|
|
41
|
+
const settings = readSettings();
|
|
42
|
+
if (!settings.hooks) settings.hooks = {};
|
|
43
|
+
if (!settings.hooks.PreCompact) settings.hooks.PreCompact = [];
|
|
44
|
+
|
|
45
|
+
const existing = settings.hooks.PreCompact.find((h) =>
|
|
46
|
+
h.hooks.some((hook) => hook.command.includes(HOOK_NAME))
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
if (existing) {
|
|
50
|
+
console.log(`${HOOK_NAME} is already installed`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
settings.hooks.PreCompact.push({
|
|
55
|
+
hooks: [{ type: "command", command: `bunx @hasnaxyz/${HOOK_NAME}` }],
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
writeSettings(settings);
|
|
59
|
+
console.log(`${HOOK_NAME} installed successfully`);
|
|
60
|
+
console.log("Hook will save session state before context compaction");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function uninstall(): void {
|
|
64
|
+
const settings = readSettings();
|
|
65
|
+
if (!settings.hooks?.PreCompact) {
|
|
66
|
+
console.log(`${HOOK_NAME} is not installed`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const before = settings.hooks.PreCompact.length;
|
|
71
|
+
settings.hooks.PreCompact = settings.hooks.PreCompact.filter(
|
|
72
|
+
(h) => !h.hooks.some((hook) => hook.command.includes(HOOK_NAME))
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
if (before === settings.hooks.PreCompact.length) {
|
|
76
|
+
console.log(`${HOOK_NAME} is not installed`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
writeSettings(settings);
|
|
81
|
+
console.log(`${HOOK_NAME} uninstalled successfully`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function status(): void {
|
|
85
|
+
const settings = readSettings();
|
|
86
|
+
const installed = settings.hooks?.PreCompact?.some((h) =>
|
|
87
|
+
h.hooks.some((hook) => hook.command.includes(HOOK_NAME))
|
|
88
|
+
);
|
|
89
|
+
console.log(`${HOOK_NAME} is ${installed ? "installed" : "not installed"}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function list(): void {
|
|
93
|
+
const handoffDir = join(process.cwd(), HANDOFF_DIR);
|
|
94
|
+
if (!existsSync(handoffDir)) {
|
|
95
|
+
console.log("No handoffs found in current directory");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const files = readdirSync(handoffDir)
|
|
100
|
+
.filter((f) => f.startsWith("handoff-") && f.endsWith(".json"))
|
|
101
|
+
.sort()
|
|
102
|
+
.reverse();
|
|
103
|
+
|
|
104
|
+
if (files.length === 0) {
|
|
105
|
+
console.log("No handoffs found");
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
console.log(`Recent handoffs (${files.length} total):\n`);
|
|
110
|
+
for (const file of files.slice(0, 10)) {
|
|
111
|
+
try {
|
|
112
|
+
const data = JSON.parse(readFileSync(join(handoffDir, file), "utf-8"));
|
|
113
|
+
console.log(` ${data.timestamp} | session: ${data.session_id} | branch: ${data.git?.branch || "?"}`);
|
|
114
|
+
} catch {
|
|
115
|
+
console.log(` ${file} (unreadable)`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function latest(): void {
|
|
121
|
+
const latestPath = join(process.cwd(), HANDOFF_DIR, "latest.json");
|
|
122
|
+
if (!existsSync(latestPath)) {
|
|
123
|
+
console.log("No handoffs found");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const data = JSON.parse(readFileSync(latestPath, "utf-8"));
|
|
128
|
+
console.log(JSON.stringify(data, null, 2));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function help(): void {
|
|
132
|
+
console.log(`
|
|
133
|
+
${HOOK_NAME} - Save session state before context compaction
|
|
134
|
+
|
|
135
|
+
Usage: ${HOOK_NAME} <command>
|
|
136
|
+
|
|
137
|
+
Commands:
|
|
138
|
+
install Install hook to Claude Code settings
|
|
139
|
+
uninstall Remove hook from Claude Code settings
|
|
140
|
+
status Check if hook is installed
|
|
141
|
+
list Show recent handoffs in current directory
|
|
142
|
+
latest Show the latest handoff data
|
|
143
|
+
help Show this help message
|
|
144
|
+
|
|
145
|
+
Handoff files are saved in .claude-handoffs/ (gitignored).
|
|
146
|
+
`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const command = process.argv[2];
|
|
150
|
+
|
|
151
|
+
switch (command) {
|
|
152
|
+
case "install": install(); break;
|
|
153
|
+
case "uninstall": uninstall(); break;
|
|
154
|
+
case "status": status(); break;
|
|
155
|
+
case "list": list(); break;
|
|
156
|
+
case "latest": latest(); break;
|
|
157
|
+
case "help":
|
|
158
|
+
case "--help":
|
|
159
|
+
case "-h": help(); break;
|
|
160
|
+
default:
|
|
161
|
+
if (!command) {
|
|
162
|
+
import("./hook.ts").then((m) => m.run());
|
|
163
|
+
} else {
|
|
164
|
+
console.error(`Unknown command: ${command}`);
|
|
165
|
+
help();
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Claude Code Hook: precompact
|
|
5
|
+
*
|
|
6
|
+
* PreCompact hook that saves session state/handoff data before
|
|
7
|
+
* context compaction to prevent information loss in long sessions.
|
|
8
|
+
*
|
|
9
|
+
* Creates timestamped handoff files in .claude-handoffs/ with:
|
|
10
|
+
* - Session ID and timestamp
|
|
11
|
+
* - Current working directory
|
|
12
|
+
* - Git branch and recent changes
|
|
13
|
+
* - Task list state (if any)
|
|
14
|
+
* - Environment context
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { readFileSync, existsSync, mkdirSync, writeFileSync, appendFileSync } from "fs";
|
|
18
|
+
import { execSync } from "child_process";
|
|
19
|
+
import { join } from "path";
|
|
20
|
+
|
|
21
|
+
interface HookInput {
|
|
22
|
+
session_id: string;
|
|
23
|
+
cwd: string;
|
|
24
|
+
hook_event_name: string;
|
|
25
|
+
transcript_summary?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface HookOutput {
|
|
29
|
+
continue?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const HANDOFF_DIR = ".claude-handoffs";
|
|
33
|
+
|
|
34
|
+
function readStdinJson(): HookInput | null {
|
|
35
|
+
try {
|
|
36
|
+
const input = readFileSync(0, "utf-8").trim();
|
|
37
|
+
if (!input) return null;
|
|
38
|
+
return JSON.parse(input);
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function safeExec(cmd: string, cwd: string): string {
|
|
45
|
+
try {
|
|
46
|
+
return execSync(cmd, { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
47
|
+
} catch {
|
|
48
|
+
return "";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getGitContext(cwd: string): Record<string, string> {
|
|
53
|
+
const branch = safeExec("git rev-parse --abbrev-ref HEAD", cwd);
|
|
54
|
+
const lastCommit = safeExec("git log -1 --oneline", cwd);
|
|
55
|
+
const status = safeExec("git status --short", cwd);
|
|
56
|
+
const recentCommits = safeExec("git log -5 --oneline", cwd);
|
|
57
|
+
|
|
58
|
+
return { branch, lastCommit, status, recentCommits };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function createHandoff(input: HookInput): void {
|
|
62
|
+
const handoffDir = join(input.cwd, HANDOFF_DIR);
|
|
63
|
+
mkdirSync(handoffDir, { recursive: true });
|
|
64
|
+
|
|
65
|
+
// Ensure it's gitignored
|
|
66
|
+
const gitignorePath = join(input.cwd, ".gitignore");
|
|
67
|
+
if (existsSync(gitignorePath)) {
|
|
68
|
+
const content = readFileSync(gitignorePath, "utf-8");
|
|
69
|
+
if (!content.includes(HANDOFF_DIR)) {
|
|
70
|
+
appendFileSync(gitignorePath, `\n${HANDOFF_DIR}/\n`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const timestamp = new Date().toISOString();
|
|
75
|
+
const gitContext = getGitContext(input.cwd);
|
|
76
|
+
|
|
77
|
+
const handoff = {
|
|
78
|
+
session_id: input.session_id,
|
|
79
|
+
timestamp,
|
|
80
|
+
cwd: input.cwd,
|
|
81
|
+
event: "PreCompact",
|
|
82
|
+
git: gitContext,
|
|
83
|
+
summary: input.transcript_summary || null,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Write timestamped handoff file
|
|
87
|
+
const filename = `handoff-${timestamp.replace(/[:.]/g, "-")}.json`;
|
|
88
|
+
writeFileSync(join(handoffDir, filename), JSON.stringify(handoff, null, 2));
|
|
89
|
+
|
|
90
|
+
// Also write a "latest" file for easy access
|
|
91
|
+
writeFileSync(join(handoffDir, "latest.json"), JSON.stringify(handoff, null, 2));
|
|
92
|
+
|
|
93
|
+
// Append to log
|
|
94
|
+
const logEntry = `[${timestamp}] PreCompact handoff saved (session: ${input.session_id})\n`;
|
|
95
|
+
appendFileSync(join(handoffDir, "handoff.log"), logEntry);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function respond(output: HookOutput): void {
|
|
99
|
+
console.log(JSON.stringify(output));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function run(): void {
|
|
103
|
+
const input = readStdinJson();
|
|
104
|
+
|
|
105
|
+
if (!input) {
|
|
106
|
+
respond({ continue: true });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
createHandoff(input);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
114
|
+
console.error(`[hook-precompact] Warning: handoff save failed: ${errMsg}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
respond({ continue: true });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (import.meta.main) {
|
|
121
|
+
run();
|
|
122
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": ["ESNext"],
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"allowImportingTsExtensions": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
"noUnusedLocals": true,
|
|
16
|
+
"noUnusedParameters": true,
|
|
17
|
+
"declaration": true,
|
|
18
|
+
"declarationMap": true,
|
|
19
|
+
"outDir": "./dist",
|
|
20
|
+
"rootDir": "./src",
|
|
21
|
+
"types": ["bun-types"]
|
|
22
|
+
},
|
|
23
|
+
"include": ["src/**/*"],
|
|
24
|
+
"exclude": ["node_modules", "dist"]
|
|
25
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hasna/hooks",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Open source Claude Code hooks library - Install hooks with a single command",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"hooks": "./bin/index.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "bun build ./src/cli/index.tsx --outdir ./bin --target bun --external ink --external react --external chalk --external conf && bun build ./src/index.ts --outdir ./dist --target bun",
|
|
19
|
+
"dev": "bun run ./src/cli/index.tsx",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"prepublishOnly": "bun run build"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"claude-code",
|
|
25
|
+
"hooks",
|
|
26
|
+
"cli",
|
|
27
|
+
"typescript",
|
|
28
|
+
"bun",
|
|
29
|
+
"git-safety",
|
|
30
|
+
"code-quality",
|
|
31
|
+
"security"
|
|
32
|
+
],
|
|
33
|
+
"author": "Hasna",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/bun": "latest",
|
|
37
|
+
"@types/react": "^18.2.0",
|
|
38
|
+
"typescript": "^5"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"chalk": "^5.3.0",
|
|
42
|
+
"commander": "^12.1.0",
|
|
43
|
+
"conf": "^13.0.1",
|
|
44
|
+
"ink": "^5.0.1",
|
|
45
|
+
"ink-select-input": "^6.0.0",
|
|
46
|
+
"ink-spinner": "^5.0.0",
|
|
47
|
+
"ink-text-input": "^6.0.0",
|
|
48
|
+
"react": "^18.2.0"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"bun": ">=1.0.0"
|
|
52
|
+
},
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"registry": "https://registry.npmjs.org",
|
|
55
|
+
"access": "public"
|
|
56
|
+
},
|
|
57
|
+
"repository": {
|
|
58
|
+
"type": "git",
|
|
59
|
+
"url": "git+https://github.com/hasna/open-hooks.git"
|
|
60
|
+
}
|
|
61
|
+
}
|