@hasna/hooks 0.2.5 → 0.2.7
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/bin/index.js +749 -37
- package/dist/index.js +4 -4
- package/hooks/hook-agentmessages/bin/cli.ts +125 -0
- package/hooks/hook-checkdocs/bun.lock +25 -0
- package/hooks/hook-commandlog/src/hook.ts +15 -38
- package/hooks/hook-costwatch/src/hook.ts +39 -42
- package/hooks/hook-errornotify/src/hook.ts +20 -65
- package/hooks/hook-sessionlog/src/hook.ts +11 -52
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -412,7 +412,7 @@ var HOOKS = [
|
|
|
412
412
|
{
|
|
413
413
|
name: "sessionlog",
|
|
414
414
|
displayName: "Session Log",
|
|
415
|
-
description: "Logs every tool call to .
|
|
415
|
+
description: "Logs every tool call to SQLite (~/.hooks/hooks.db)",
|
|
416
416
|
version: "0.1.0",
|
|
417
417
|
category: "Observability",
|
|
418
418
|
event: "PostToolUse",
|
|
@@ -422,7 +422,7 @@ var HOOKS = [
|
|
|
422
422
|
{
|
|
423
423
|
name: "commandlog",
|
|
424
424
|
displayName: "Command Log",
|
|
425
|
-
description: "Logs every
|
|
425
|
+
description: "Logs every Bash command to SQLite (~/.hooks/hooks.db)",
|
|
426
426
|
version: "0.1.0",
|
|
427
427
|
category: "Observability",
|
|
428
428
|
event: "PostToolUse",
|
|
@@ -432,7 +432,7 @@ var HOOKS = [
|
|
|
432
432
|
{
|
|
433
433
|
name: "costwatch",
|
|
434
434
|
displayName: "Cost Watch",
|
|
435
|
-
description: "Estimates session token usage and warns
|
|
435
|
+
description: "Estimates session token usage, persists cost history to SQLite, and warns on budget overrun",
|
|
436
436
|
version: "0.1.0",
|
|
437
437
|
category: "Observability",
|
|
438
438
|
event: "Stop",
|
|
@@ -442,7 +442,7 @@ var HOOKS = [
|
|
|
442
442
|
{
|
|
443
443
|
name: "errornotify",
|
|
444
444
|
displayName: "Error Notify",
|
|
445
|
-
description: "Detects tool failures and logs errors to
|
|
445
|
+
description: "Detects tool failures and logs errors to SQLite (~/.hooks/hooks.db)",
|
|
446
446
|
version: "0.1.0",
|
|
447
447
|
category: "Observability",
|
|
448
448
|
event: "PostToolUse",
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* CLI for hook-agentmessages
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* hook-agentmessages install - Install hooks into Claude Code
|
|
7
|
+
* hook-agentmessages uninstall - Remove hooks from Claude Code
|
|
8
|
+
* hook-agentmessages status - Show hook status
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { homedir } from 'os';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
|
|
14
|
+
const CLAUDE_SETTINGS_FILE = join(homedir(), '.claude', 'settings.json');
|
|
15
|
+
|
|
16
|
+
const args = process.argv.slice(2);
|
|
17
|
+
const command = args[0];
|
|
18
|
+
|
|
19
|
+
async function showStatus() {
|
|
20
|
+
console.log('hook-agentmessages status\n');
|
|
21
|
+
|
|
22
|
+
// Check if hooks are installed
|
|
23
|
+
try {
|
|
24
|
+
const file = Bun.file(CLAUDE_SETTINGS_FILE);
|
|
25
|
+
if (await file.exists()) {
|
|
26
|
+
const settings = await file.json();
|
|
27
|
+
|
|
28
|
+
if (settings.hooks) {
|
|
29
|
+
let installed = false;
|
|
30
|
+
|
|
31
|
+
if (settings.hooks.SessionStart) {
|
|
32
|
+
const hasHook = settings.hooks.SessionStart.some((h: any) =>
|
|
33
|
+
h.hooks?.some((hook: any) => hook.command?.includes('hook-agentmessages'))
|
|
34
|
+
);
|
|
35
|
+
if (hasHook) {
|
|
36
|
+
console.log(' SessionStart hook: installed');
|
|
37
|
+
installed = true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (settings.hooks.Stop) {
|
|
42
|
+
const hasHook = settings.hooks.Stop.some((h: any) =>
|
|
43
|
+
h.hooks?.some((hook: any) => hook.command?.includes('hook-agentmessages'))
|
|
44
|
+
);
|
|
45
|
+
if (hasHook) {
|
|
46
|
+
console.log(' Stop hook: installed');
|
|
47
|
+
installed = true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!installed) {
|
|
52
|
+
console.log(' No hook-agentmessages hooks installed');
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
console.log(' No hooks configured in Claude Code');
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
console.log(' Claude Code settings not found');
|
|
59
|
+
}
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.log(' Error reading settings:', (err as Error).message);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check service-message status
|
|
65
|
+
const serviceDir = join(homedir(), '.service', 'service-message');
|
|
66
|
+
const configFile = Bun.file(join(serviceDir, 'config.json'));
|
|
67
|
+
|
|
68
|
+
console.log('\nservice-message integration:');
|
|
69
|
+
if (await configFile.exists()) {
|
|
70
|
+
const config = await configFile.json();
|
|
71
|
+
console.log(` Agent ID: ${config.agentId || 'not set'}`);
|
|
72
|
+
console.log(` Data dir: ${serviceDir}`);
|
|
73
|
+
} else {
|
|
74
|
+
console.log(' Not configured');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function showHelp() {
|
|
79
|
+
console.log(`
|
|
80
|
+
hook-agentmessages - Claude Code hook for service-message integration
|
|
81
|
+
|
|
82
|
+
Usage:
|
|
83
|
+
hook-agentmessages <command>
|
|
84
|
+
|
|
85
|
+
Commands:
|
|
86
|
+
install Install hooks into Claude Code settings
|
|
87
|
+
uninstall Remove hooks from Claude Code settings
|
|
88
|
+
status Show current hook status
|
|
89
|
+
|
|
90
|
+
Examples:
|
|
91
|
+
hook-agentmessages install
|
|
92
|
+
hook-agentmessages status
|
|
93
|
+
`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function main() {
|
|
97
|
+
if (!command || command === '--help' || command === '-h') {
|
|
98
|
+
showHelp();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
switch (command) {
|
|
103
|
+
case 'install':
|
|
104
|
+
await import('../src/install.ts');
|
|
105
|
+
break;
|
|
106
|
+
|
|
107
|
+
case 'uninstall':
|
|
108
|
+
await import('../src/uninstall.ts');
|
|
109
|
+
break;
|
|
110
|
+
|
|
111
|
+
case 'status':
|
|
112
|
+
await showStatus();
|
|
113
|
+
break;
|
|
114
|
+
|
|
115
|
+
default:
|
|
116
|
+
console.error(`Unknown command: ${command}`);
|
|
117
|
+
showHelp();
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
main().catch((err) => {
|
|
123
|
+
console.error('Error:', err.message);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"configVersion": 1,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"name": "@hasnaxyz/hook-checkfiles",
|
|
7
|
+
"devDependencies": {
|
|
8
|
+
"@types/bun": "^1.3.8",
|
|
9
|
+
"@types/node": "^20",
|
|
10
|
+
"typescript": "^5.0.0",
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
"packages": {
|
|
15
|
+
"@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
|
|
16
|
+
|
|
17
|
+
"@types/node": ["@types/node@20.19.30", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g=="],
|
|
18
|
+
|
|
19
|
+
"bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="],
|
|
20
|
+
|
|
21
|
+
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
22
|
+
|
|
23
|
+
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -3,15 +3,11 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Claude Code Hook: commandlog
|
|
5
5
|
*
|
|
6
|
-
* PostToolUse hook that logs every
|
|
7
|
-
* .claude/commands.log in the project directory.
|
|
8
|
-
*
|
|
9
|
-
* Format: [ISO timestamp] <exit_code> <command>
|
|
10
|
-
* One command per line.
|
|
6
|
+
* PostToolUse hook that logs every Bash command to SQLite (~/.hooks/hooks.db).
|
|
11
7
|
*/
|
|
12
8
|
|
|
13
|
-
import { readFileSync
|
|
14
|
-
import {
|
|
9
|
+
import { readFileSync } from "fs";
|
|
10
|
+
import { writeHookEvent } from "../../../src/lib/db-writer";
|
|
15
11
|
|
|
16
12
|
interface HookInput {
|
|
17
13
|
session_id: string;
|
|
@@ -38,31 +34,6 @@ function respond(output: HookOutput): void {
|
|
|
38
34
|
console.log(JSON.stringify(output));
|
|
39
35
|
}
|
|
40
36
|
|
|
41
|
-
function logCommand(input: HookInput): void {
|
|
42
|
-
const claudeDir = join(input.cwd, ".claude");
|
|
43
|
-
|
|
44
|
-
// Create .claude/ directory if it doesn't exist
|
|
45
|
-
if (!existsSync(claudeDir)) {
|
|
46
|
-
mkdirSync(claudeDir, { recursive: true });
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const logFile = join(claudeDir, "commands.log");
|
|
50
|
-
const timestamp = new Date().toISOString();
|
|
51
|
-
const command = (input.tool_input.command as string) || "(unknown command)";
|
|
52
|
-
const exitCode = input.tool_input.exit_code;
|
|
53
|
-
|
|
54
|
-
// Format: [timestamp] exit_code command
|
|
55
|
-
// If exit_code is available, include it; otherwise just log the command
|
|
56
|
-
let logLine: string;
|
|
57
|
-
if (exitCode !== undefined && exitCode !== null) {
|
|
58
|
-
logLine = `[${timestamp}] exit=${exitCode} ${command}\n`;
|
|
59
|
-
} else {
|
|
60
|
-
logLine = `[${timestamp}] ${command}\n`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
appendFileSync(logFile, logLine);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
37
|
export function run(): void {
|
|
67
38
|
const input = readStdinJson();
|
|
68
39
|
|
|
@@ -77,12 +48,18 @@ export function run(): void {
|
|
|
77
48
|
return;
|
|
78
49
|
}
|
|
79
50
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
51
|
+
const command = (input.tool_input.command as string) || "(unknown command)";
|
|
52
|
+
const exitCode = input.tool_input.exit_code;
|
|
53
|
+
|
|
54
|
+
writeHookEvent({
|
|
55
|
+
session_id: input.session_id,
|
|
56
|
+
hook_name: "commandlog",
|
|
57
|
+
event_type: "PostToolUse",
|
|
58
|
+
tool_name: "Bash",
|
|
59
|
+
tool_input: command,
|
|
60
|
+
project_dir: input.cwd,
|
|
61
|
+
metadata: exitCode !== undefined && exitCode !== null ? JSON.stringify({ exit_code: exitCode }) : null,
|
|
62
|
+
});
|
|
86
63
|
|
|
87
64
|
respond({ continue: true });
|
|
88
65
|
}
|
|
@@ -3,24 +3,19 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Claude Code Hook: costwatch
|
|
5
5
|
*
|
|
6
|
-
* Stop hook that estimates session token usage and
|
|
7
|
-
*
|
|
6
|
+
* Stop hook that estimates session token usage and persists cost data to
|
|
7
|
+
* SQLite (~/.hooks/hooks.db) for cross-session history queries.
|
|
8
8
|
*
|
|
9
9
|
* Configuration:
|
|
10
10
|
* - Environment variable: COST_WATCH_BUDGET (max $ per session, e.g. "5.00")
|
|
11
11
|
* - If not set, no budget enforcement (just logs a reminder)
|
|
12
12
|
*
|
|
13
|
-
* Token estimation
|
|
14
|
-
* - ~4 characters per token (English text average)
|
|
15
|
-
* - Claude Opus pricing: ~$15/M input tokens, ~$75/M output tokens
|
|
16
|
-
* - We estimate a blended rate of ~$30/M tokens for simplicity
|
|
17
|
-
*
|
|
18
|
-
* Since the Stop event provides limited session info, this hook
|
|
19
|
-
* primarily serves as a reminder to check actual usage.
|
|
13
|
+
* Token estimation: ~4 chars/token, blended rate ~$30/M tokens.
|
|
20
14
|
*/
|
|
21
15
|
|
|
22
16
|
import { readFileSync, existsSync, readdirSync, statSync } from "fs";
|
|
23
17
|
import { join } from "path";
|
|
18
|
+
import { writeHookEvent } from "../../../src/lib/db-writer";
|
|
24
19
|
|
|
25
20
|
interface HookInput {
|
|
26
21
|
session_id: string;
|
|
@@ -33,10 +28,7 @@ interface HookOutput {
|
|
|
33
28
|
continue: boolean;
|
|
34
29
|
}
|
|
35
30
|
|
|
36
|
-
/** Approximate cost per million tokens (blended input/output estimate) */
|
|
37
31
|
const BLENDED_COST_PER_MILLION_TOKENS = 30;
|
|
38
|
-
|
|
39
|
-
/** Average characters per token */
|
|
40
32
|
const CHARS_PER_TOKEN = 4;
|
|
41
33
|
|
|
42
34
|
function readStdinJson(): HookInput | null {
|
|
@@ -59,8 +51,8 @@ function getBudget(): number | null {
|
|
|
59
51
|
|
|
60
52
|
const budget = parseFloat(budgetStr);
|
|
61
53
|
if (isNaN(budget) || budget <= 0) {
|
|
62
|
-
|
|
63
|
-
`[hook-costwatch] Invalid COST_WATCH_BUDGET value: "${budgetStr}". Must be a positive number
|
|
54
|
+
process.stderr.write(
|
|
55
|
+
`[hook-costwatch] Invalid COST_WATCH_BUDGET value: "${budgetStr}". Must be a positive number.\n`
|
|
64
56
|
);
|
|
65
57
|
return null;
|
|
66
58
|
}
|
|
@@ -74,12 +66,10 @@ function estimateTranscriptCost(transcriptPath: string): {
|
|
|
74
66
|
} | null {
|
|
75
67
|
try {
|
|
76
68
|
if (!existsSync(transcriptPath)) return null;
|
|
77
|
-
|
|
78
69
|
const stat = statSync(transcriptPath);
|
|
79
70
|
const charCount = stat.size;
|
|
80
71
|
const estimatedTokens = Math.ceil(charCount / CHARS_PER_TOKEN);
|
|
81
72
|
const estimatedCost = (estimatedTokens / 1_000_000) * BLENDED_COST_PER_MILLION_TOKENS;
|
|
82
|
-
|
|
83
73
|
return { charCount, estimatedTokens, estimatedCost };
|
|
84
74
|
} catch {
|
|
85
75
|
return null;
|
|
@@ -87,7 +77,6 @@ function estimateTranscriptCost(transcriptPath: string): {
|
|
|
87
77
|
}
|
|
88
78
|
|
|
89
79
|
function findSessionTranscript(cwd: string, sessionId: string): string | null {
|
|
90
|
-
// Check common transcript locations
|
|
91
80
|
const possibleDirs = [
|
|
92
81
|
join(cwd, ".claude"),
|
|
93
82
|
join(process.env.HOME || "", ".claude", "projects"),
|
|
@@ -98,9 +87,8 @@ function findSessionTranscript(cwd: string, sessionId: string): string | null {
|
|
|
98
87
|
try {
|
|
99
88
|
const files = readdirSync(dir, { recursive: true }) as string[];
|
|
100
89
|
for (const file of files) {
|
|
101
|
-
const filePath = join(dir, file);
|
|
102
90
|
if (typeof file === "string" && file.includes(sessionId)) {
|
|
103
|
-
return
|
|
91
|
+
return join(dir, file);
|
|
104
92
|
}
|
|
105
93
|
}
|
|
106
94
|
} catch {
|
|
@@ -121,19 +109,13 @@ export function run(): void {
|
|
|
121
109
|
|
|
122
110
|
const budget = getBudget();
|
|
123
111
|
|
|
124
|
-
|
|
125
|
-
let estimate: {
|
|
126
|
-
charCount: number;
|
|
127
|
-
estimatedTokens: number;
|
|
128
|
-
estimatedCost: number;
|
|
129
|
-
} | null = null;
|
|
112
|
+
let estimate: { charCount: number; estimatedTokens: number; estimatedCost: number } | null = null;
|
|
130
113
|
|
|
131
114
|
if (input.transcript_path) {
|
|
132
115
|
estimate = estimateTranscriptCost(input.transcript_path);
|
|
133
116
|
}
|
|
134
117
|
|
|
135
118
|
if (!estimate) {
|
|
136
|
-
// Try to find transcript by session ID
|
|
137
119
|
const transcriptPath = findSessionTranscript(input.cwd, input.session_id);
|
|
138
120
|
if (transcriptPath) {
|
|
139
121
|
estimate = estimateTranscriptCost(transcriptPath);
|
|
@@ -143,31 +125,46 @@ export function run(): void {
|
|
|
143
125
|
if (estimate) {
|
|
144
126
|
const costStr = estimate.estimatedCost.toFixed(2);
|
|
145
127
|
const tokensStr = (estimate.estimatedTokens / 1000).toFixed(1);
|
|
128
|
+
const budgetExceeded = budget !== null && estimate.estimatedCost > budget;
|
|
146
129
|
|
|
147
|
-
|
|
130
|
+
process.stderr.write(`[hook-costwatch] Session estimate: ~${tokensStr}K tokens, ~$${costStr}\n`);
|
|
148
131
|
|
|
149
|
-
if (
|
|
150
|
-
|
|
151
|
-
`[hook-costwatch] WARNING: Estimated cost ($${costStr}) exceeds budget ($${budget
|
|
152
|
-
);
|
|
153
|
-
console.error(
|
|
154
|
-
`[hook-costwatch] Check your actual usage at https://console.anthropic.com/`
|
|
132
|
+
if (budgetExceeded) {
|
|
133
|
+
process.stderr.write(
|
|
134
|
+
`[hook-costwatch] WARNING: Estimated cost ($${costStr}) exceeds budget ($${budget!.toFixed(2)})!\n`
|
|
155
135
|
);
|
|
136
|
+
process.stderr.write(`[hook-costwatch] Check your actual usage at https://console.anthropic.com/\n`);
|
|
156
137
|
}
|
|
138
|
+
|
|
139
|
+
writeHookEvent({
|
|
140
|
+
session_id: input.session_id,
|
|
141
|
+
hook_name: "costwatch",
|
|
142
|
+
event_type: "Stop",
|
|
143
|
+
project_dir: input.cwd,
|
|
144
|
+
metadata: JSON.stringify({
|
|
145
|
+
char_count: estimate.charCount,
|
|
146
|
+
estimated_tokens: estimate.estimatedTokens,
|
|
147
|
+
estimated_cost_usd: estimate.estimatedCost,
|
|
148
|
+
budget_usd: budget,
|
|
149
|
+
budget_exceeded: budgetExceeded,
|
|
150
|
+
}),
|
|
151
|
+
});
|
|
157
152
|
} else {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
153
|
+
process.stderr.write(`[hook-costwatch] Could not estimate session cost (no transcript found).\n`);
|
|
154
|
+
|
|
155
|
+
writeHookEvent({
|
|
156
|
+
session_id: input.session_id,
|
|
157
|
+
hook_name: "costwatch",
|
|
158
|
+
event_type: "Stop",
|
|
159
|
+
project_dir: input.cwd,
|
|
160
|
+
metadata: JSON.stringify({ error: "no_transcript", budget_usd: budget }),
|
|
161
|
+
});
|
|
161
162
|
}
|
|
162
163
|
|
|
163
164
|
if (budget !== null) {
|
|
164
|
-
|
|
165
|
-
`[hook-costwatch] Budget: $${budget.toFixed(2)}/session. Remember to check actual usage.`
|
|
166
|
-
);
|
|
165
|
+
process.stderr.write(`[hook-costwatch] Budget: $${budget.toFixed(2)}/session. Remember to check actual usage.\n`);
|
|
167
166
|
} else {
|
|
168
|
-
|
|
169
|
-
`[hook-costwatch] No budget set. Set COST_WATCH_BUDGET env var to enable budget warnings.`
|
|
170
|
-
);
|
|
167
|
+
process.stderr.write(`[hook-costwatch] No budget set. Set COST_WATCH_BUDGET env var to enable budget warnings.\n`);
|
|
171
168
|
}
|
|
172
169
|
|
|
173
170
|
respond({ continue: true });
|
|
@@ -3,16 +3,13 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Claude Code Hook: errornotify
|
|
5
5
|
*
|
|
6
|
-
* PostToolUse hook that detects tool failures and logs errors.
|
|
7
|
-
*
|
|
8
|
-
* error messages) and logs warnings to stderr. Optionally writes
|
|
9
|
-
* to a .claude/errors.log file for persistent error tracking.
|
|
10
|
-
*
|
|
6
|
+
* PostToolUse hook that detects tool failures and logs errors to SQLite (~/.hooks/hooks.db).
|
|
7
|
+
* Also writes warnings to stderr for immediate terminal visibility.
|
|
11
8
|
* Never blocks — always outputs { continue: true }.
|
|
12
9
|
*/
|
|
13
10
|
|
|
14
|
-
import { readFileSync
|
|
15
|
-
import {
|
|
11
|
+
import { readFileSync } from "fs";
|
|
12
|
+
import { writeHookEvent } from "../../../src/lib/db-writer";
|
|
16
13
|
|
|
17
14
|
interface HookInput {
|
|
18
15
|
session_id: string;
|
|
@@ -26,9 +23,6 @@ interface HookOutput {
|
|
|
26
23
|
continue: true;
|
|
27
24
|
}
|
|
28
25
|
|
|
29
|
-
/**
|
|
30
|
-
* Read and parse JSON from stdin
|
|
31
|
-
*/
|
|
32
26
|
function readStdinJson(): HookInput | null {
|
|
33
27
|
try {
|
|
34
28
|
const input = readFileSync(0, "utf-8").trim();
|
|
@@ -39,31 +33,19 @@ function readStdinJson(): HookInput | null {
|
|
|
39
33
|
}
|
|
40
34
|
}
|
|
41
35
|
|
|
42
|
-
/**
|
|
43
|
-
* Check if the tool output indicates a failure
|
|
44
|
-
*/
|
|
45
36
|
function detectError(input: HookInput): { isError: boolean; message: string } {
|
|
46
37
|
const output = input.tool_output || {};
|
|
47
38
|
|
|
48
|
-
// Check for explicit exit code
|
|
49
39
|
const exitCode = output.exit_code ?? output.exitCode ?? output.code;
|
|
50
40
|
if (exitCode !== undefined && exitCode !== null && exitCode !== 0) {
|
|
51
41
|
const stderr = (output.stderr as string) || (output.output as string) || "unknown error";
|
|
52
|
-
return {
|
|
53
|
-
isError: true,
|
|
54
|
-
message: `Exit code ${exitCode}: ${truncate(stderr, 200)}`,
|
|
55
|
-
};
|
|
42
|
+
return { isError: true, message: `Exit code ${exitCode}: ${truncate(stderr, 200)}` };
|
|
56
43
|
}
|
|
57
44
|
|
|
58
|
-
// Check for error field
|
|
59
45
|
if (output.error && typeof output.error === "string") {
|
|
60
|
-
return {
|
|
61
|
-
isError: true,
|
|
62
|
-
message: `Error: ${truncate(output.error, 200)}`,
|
|
63
|
-
};
|
|
46
|
+
return { isError: true, message: `Error: ${truncate(output.error, 200)}` };
|
|
64
47
|
}
|
|
65
48
|
|
|
66
|
-
// Check output text for common error indicators
|
|
67
49
|
const outputText =
|
|
68
50
|
(output.stderr as string) ||
|
|
69
51
|
(output.output as string) ||
|
|
@@ -72,7 +54,6 @@ function detectError(input: HookInput): { isError: boolean; message: string } {
|
|
|
72
54
|
"";
|
|
73
55
|
|
|
74
56
|
if (typeof outputText === "string" && outputText.length > 0) {
|
|
75
|
-
// Check for common error patterns in output
|
|
76
57
|
const errorPatterns = [
|
|
77
58
|
/^error:/im,
|
|
78
59
|
/^fatal:/im,
|
|
@@ -98,13 +79,9 @@ function detectError(input: HookInput): { isError: boolean; message: string } {
|
|
|
98
79
|
|
|
99
80
|
for (const pattern of errorPatterns) {
|
|
100
81
|
if (pattern.test(outputText)) {
|
|
101
|
-
// Extract the first relevant line
|
|
102
82
|
const lines = outputText.split("\n").filter((l: string) => l.trim());
|
|
103
83
|
const errorLine = lines.find((l: string) => pattern.test(l)) || lines[0] || "";
|
|
104
|
-
return {
|
|
105
|
-
isError: true,
|
|
106
|
-
message: truncate(errorLine.trim(), 200),
|
|
107
|
-
};
|
|
84
|
+
return { isError: true, message: truncate(errorLine.trim(), 200) };
|
|
108
85
|
}
|
|
109
86
|
}
|
|
110
87
|
}
|
|
@@ -112,17 +89,11 @@ function detectError(input: HookInput): { isError: boolean; message: string } {
|
|
|
112
89
|
return { isError: false, message: "" };
|
|
113
90
|
}
|
|
114
91
|
|
|
115
|
-
/**
|
|
116
|
-
* Truncate a string to a maximum length
|
|
117
|
-
*/
|
|
118
92
|
function truncate(str: string, maxLen: number): string {
|
|
119
93
|
if (str.length <= maxLen) return str;
|
|
120
94
|
return str.slice(0, maxLen) + "...";
|
|
121
95
|
}
|
|
122
96
|
|
|
123
|
-
/**
|
|
124
|
-
* Get a human-readable description of what was being executed
|
|
125
|
-
*/
|
|
126
97
|
function getToolContext(input: HookInput): string {
|
|
127
98
|
const toolName = input.tool_name;
|
|
128
99
|
const toolInput = input.tool_input || {};
|
|
@@ -140,36 +111,11 @@ function getToolContext(input: HookInput): string {
|
|
|
140
111
|
}
|
|
141
112
|
}
|
|
142
113
|
|
|
143
|
-
/**
|
|
144
|
-
* Write error to .claude/errors.log
|
|
145
|
-
*/
|
|
146
|
-
function writeErrorLog(cwd: string, toolContext: string, errorMessage: string, sessionId: string): void {
|
|
147
|
-
try {
|
|
148
|
-
const claudeDir = join(cwd, ".claude");
|
|
149
|
-
if (!existsSync(claudeDir)) {
|
|
150
|
-
mkdirSync(claudeDir, { recursive: true });
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const logFile = join(claudeDir, "errors.log");
|
|
154
|
-
const timestamp = new Date().toISOString();
|
|
155
|
-
const entry = `[${timestamp}] [session:${sessionId.slice(0, 8)}] ${toolContext} — ${errorMessage}\n`;
|
|
156
|
-
appendFileSync(logFile, entry);
|
|
157
|
-
} catch {
|
|
158
|
-
// Silently fail — logging should never cause issues
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Output hook response
|
|
164
|
-
*/
|
|
165
114
|
function respond(): void {
|
|
166
115
|
const output: HookOutput = { continue: true };
|
|
167
116
|
console.log(JSON.stringify(output));
|
|
168
117
|
}
|
|
169
118
|
|
|
170
|
-
/**
|
|
171
|
-
* Main hook execution
|
|
172
|
-
*/
|
|
173
119
|
export function run(): void {
|
|
174
120
|
const input = readStdinJson();
|
|
175
121
|
|
|
@@ -182,11 +128,20 @@ export function run(): void {
|
|
|
182
128
|
|
|
183
129
|
if (isError) {
|
|
184
130
|
const toolContext = getToolContext(input);
|
|
185
|
-
console.error(`[hook-errornotify] FAILURE in ${toolContext}`);
|
|
186
|
-
console.error(`[hook-errornotify] ${message}`);
|
|
187
131
|
|
|
188
|
-
//
|
|
189
|
-
|
|
132
|
+
// Keep stderr warnings for immediate terminal visibility
|
|
133
|
+
process.stderr.write(`[hook-errornotify] FAILURE in ${toolContext}\n`);
|
|
134
|
+
process.stderr.write(`[hook-errornotify] ${message}\n`);
|
|
135
|
+
|
|
136
|
+
writeHookEvent({
|
|
137
|
+
session_id: input.session_id,
|
|
138
|
+
hook_name: "errornotify",
|
|
139
|
+
event_type: "PostToolUse",
|
|
140
|
+
tool_name: input.tool_name,
|
|
141
|
+
tool_input: JSON.stringify(input.tool_input),
|
|
142
|
+
error: message,
|
|
143
|
+
project_dir: input.cwd,
|
|
144
|
+
});
|
|
190
145
|
}
|
|
191
146
|
|
|
192
147
|
respond();
|
|
@@ -3,18 +3,11 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Claude Code Hook: sessionlog
|
|
5
5
|
*
|
|
6
|
-
* PostToolUse hook that logs every tool call to
|
|
7
|
-
* Creates .claude/session-log-<date>.jsonl in the project directory.
|
|
8
|
-
*
|
|
9
|
-
* Each line is a JSON object with:
|
|
10
|
-
* - timestamp: ISO string
|
|
11
|
-
* - tool_name: name of the tool that was called
|
|
12
|
-
* - tool_input: first 500 characters of the stringified tool input
|
|
13
|
-
* - session_id: current session ID
|
|
6
|
+
* PostToolUse hook that logs every tool call to SQLite (~/.hooks/hooks.db).
|
|
14
7
|
*/
|
|
15
8
|
|
|
16
|
-
import { readFileSync
|
|
17
|
-
import {
|
|
9
|
+
import { readFileSync } from "fs";
|
|
10
|
+
import { writeHookEvent } from "../../../src/lib/db-writer";
|
|
18
11
|
|
|
19
12
|
interface HookInput {
|
|
20
13
|
session_id: string;
|
|
@@ -41,42 +34,6 @@ function respond(output: HookOutput): void {
|
|
|
41
34
|
console.log(JSON.stringify(output));
|
|
42
35
|
}
|
|
43
36
|
|
|
44
|
-
function getDateString(): string {
|
|
45
|
-
const now = new Date();
|
|
46
|
-
const year = now.getFullYear();
|
|
47
|
-
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
48
|
-
const day = String(now.getDate()).padStart(2, "0");
|
|
49
|
-
return `${year}-${month}-${day}`;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function truncate(str: string, maxLength: number): string {
|
|
53
|
-
if (str.length <= maxLength) return str;
|
|
54
|
-
return str.slice(0, maxLength) + "...";
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function logToolCall(input: HookInput): void {
|
|
58
|
-
const claudeDir = join(input.cwd, ".claude");
|
|
59
|
-
|
|
60
|
-
// Create .claude/ directory if it doesn't exist
|
|
61
|
-
if (!existsSync(claudeDir)) {
|
|
62
|
-
mkdirSync(claudeDir, { recursive: true });
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const dateStr = getDateString();
|
|
66
|
-
const logFile = join(claudeDir, `session-log-${dateStr}.jsonl`);
|
|
67
|
-
|
|
68
|
-
const toolInputStr = truncate(JSON.stringify(input.tool_input), 500);
|
|
69
|
-
|
|
70
|
-
const logEntry = {
|
|
71
|
-
timestamp: new Date().toISOString(),
|
|
72
|
-
tool_name: input.tool_name,
|
|
73
|
-
tool_input: toolInputStr,
|
|
74
|
-
session_id: input.session_id,
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
appendFileSync(logFile, JSON.stringify(logEntry) + "\n");
|
|
78
|
-
}
|
|
79
|
-
|
|
80
37
|
export function run(): void {
|
|
81
38
|
const input = readStdinJson();
|
|
82
39
|
|
|
@@ -85,12 +42,14 @@ export function run(): void {
|
|
|
85
42
|
return;
|
|
86
43
|
}
|
|
87
44
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
45
|
+
writeHookEvent({
|
|
46
|
+
session_id: input.session_id,
|
|
47
|
+
hook_name: "sessionlog",
|
|
48
|
+
event_type: "PostToolUse",
|
|
49
|
+
tool_name: input.tool_name,
|
|
50
|
+
tool_input: JSON.stringify(input.tool_input),
|
|
51
|
+
project_dir: input.cwd,
|
|
52
|
+
});
|
|
94
53
|
|
|
95
54
|
respond({ continue: true });
|
|
96
55
|
}
|
package/package.json
CHANGED