@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.
Files changed (110) hide show
  1. package/.npmrc.example +2 -0
  2. package/AGENTS.md +54 -0
  3. package/CLAUDE.md +70 -0
  4. package/CONTRIBUTING.md +45 -0
  5. package/README.md +232 -0
  6. package/bin/index.js +5171 -0
  7. package/hooks/hook-agentmessages/CLAUDE.md +79 -0
  8. package/hooks/hook-agentmessages/LICENSE +21 -0
  9. package/hooks/hook-agentmessages/README.md +107 -0
  10. package/hooks/hook-agentmessages/package.json +31 -0
  11. package/hooks/hook-agentmessages/src/check-messages.ts +151 -0
  12. package/hooks/hook-agentmessages/src/install.ts +126 -0
  13. package/hooks/hook-agentmessages/src/session-start.ts +255 -0
  14. package/hooks/hook-agentmessages/src/uninstall.ts +89 -0
  15. package/hooks/hook-branchprotect/CLAUDE.md +23 -0
  16. package/hooks/hook-branchprotect/README.md +25 -0
  17. package/hooks/hook-branchprotect/package.json +42 -0
  18. package/hooks/hook-branchprotect/src/cli.ts +126 -0
  19. package/hooks/hook-branchprotect/src/hook.ts +88 -0
  20. package/hooks/hook-branchprotect/tsconfig.json +25 -0
  21. package/hooks/hook-checkbugs/LICENSE +21 -0
  22. package/hooks/hook-checkbugs/README.md +140 -0
  23. package/hooks/hook-checkbugs/package.json +58 -0
  24. package/hooks/hook-checkbugs/src/cli.ts +628 -0
  25. package/hooks/hook-checkbugs/src/hook.ts +335 -0
  26. package/hooks/hook-checkbugs/tsconfig.json +15 -0
  27. package/hooks/hook-checkdocs/README.md +137 -0
  28. package/hooks/hook-checkdocs/package.json +57 -0
  29. package/hooks/hook-checkdocs/src/cli.ts +628 -0
  30. package/hooks/hook-checkdocs/src/hook.ts +310 -0
  31. package/hooks/hook-checkdocs/tsconfig.json +15 -0
  32. package/hooks/hook-checkfiles/LICENSE +21 -0
  33. package/hooks/hook-checkfiles/README.md +141 -0
  34. package/hooks/hook-checkfiles/package.json +56 -0
  35. package/hooks/hook-checkfiles/src/cli.ts +545 -0
  36. package/hooks/hook-checkfiles/src/hook.ts +321 -0
  37. package/hooks/hook-checkfiles/tsconfig.json +15 -0
  38. package/hooks/hook-checklint/LICENSE +21 -0
  39. package/hooks/hook-checklint/README.md +147 -0
  40. package/hooks/hook-checklint/package.json +57 -0
  41. package/hooks/hook-checklint/src/cli-patch.ts +32 -0
  42. package/hooks/hook-checklint/src/cli.ts +667 -0
  43. package/hooks/hook-checklint/src/hook.ts +473 -0
  44. package/hooks/hook-checklint/tsconfig.json +15 -0
  45. package/hooks/hook-checkpoint/CLAUDE.md +23 -0
  46. package/hooks/hook-checkpoint/README.md +37 -0
  47. package/hooks/hook-checkpoint/package.json +58 -0
  48. package/hooks/hook-checkpoint/src/cli.ts +191 -0
  49. package/hooks/hook-checkpoint/src/hook.ts +207 -0
  50. package/hooks/hook-checkpoint/tsconfig.json +25 -0
  51. package/hooks/hook-checksecurity/LICENSE +21 -0
  52. package/hooks/hook-checksecurity/README.md +158 -0
  53. package/hooks/hook-checksecurity/package.json +57 -0
  54. package/hooks/hook-checksecurity/src/cli.ts +601 -0
  55. package/hooks/hook-checksecurity/src/hook.ts +334 -0
  56. package/hooks/hook-checksecurity/tsconfig.json +15 -0
  57. package/hooks/hook-checktasks/README.md +144 -0
  58. package/hooks/hook-checktasks/package.json +55 -0
  59. package/hooks/hook-checktasks/src/cli.ts +578 -0
  60. package/hooks/hook-checktasks/src/hook.ts +308 -0
  61. package/hooks/hook-checktasks/tsconfig.json +20 -0
  62. package/hooks/hook-checktests/LICENSE +21 -0
  63. package/hooks/hook-checktests/README.md +137 -0
  64. package/hooks/hook-checktests/package.json +57 -0
  65. package/hooks/hook-checktests/src/cli.ts +627 -0
  66. package/hooks/hook-checktests/src/hook.ts +334 -0
  67. package/hooks/hook-checktests/tsconfig.json +15 -0
  68. package/hooks/hook-contextrefresh/CLAUDE.md +23 -0
  69. package/hooks/hook-contextrefresh/README.md +42 -0
  70. package/hooks/hook-contextrefresh/package.json +42 -0
  71. package/hooks/hook-contextrefresh/src/cli.ts +152 -0
  72. package/hooks/hook-contextrefresh/src/hook.ts +148 -0
  73. package/hooks/hook-contextrefresh/tsconfig.json +25 -0
  74. package/hooks/hook-gitguard/CLAUDE.md +22 -0
  75. package/hooks/hook-gitguard/README.md +30 -0
  76. package/hooks/hook-gitguard/package.json +57 -0
  77. package/hooks/hook-gitguard/src/cli.ts +159 -0
  78. package/hooks/hook-gitguard/src/hook.ts +129 -0
  79. package/hooks/hook-gitguard/tsconfig.json +25 -0
  80. package/hooks/hook-packageage/CLAUDE.md +23 -0
  81. package/hooks/hook-packageage/README.md +33 -0
  82. package/hooks/hook-packageage/package.json +42 -0
  83. package/hooks/hook-packageage/src/cli.ts +165 -0
  84. package/hooks/hook-packageage/src/hook.ts +177 -0
  85. package/hooks/hook-packageage/tsconfig.json +25 -0
  86. package/hooks/hook-phonenotify/CLAUDE.md +25 -0
  87. package/hooks/hook-phonenotify/README.md +44 -0
  88. package/hooks/hook-phonenotify/package.json +42 -0
  89. package/hooks/hook-phonenotify/src/cli.ts +196 -0
  90. package/hooks/hook-phonenotify/src/hook.ts +139 -0
  91. package/hooks/hook-phonenotify/tsconfig.json +25 -0
  92. package/hooks/hook-precompact/CLAUDE.md +23 -0
  93. package/hooks/hook-precompact/README.md +36 -0
  94. package/hooks/hook-precompact/package.json +42 -0
  95. package/hooks/hook-precompact/src/cli.ts +168 -0
  96. package/hooks/hook-precompact/src/hook.ts +122 -0
  97. package/hooks/hook-precompact/tsconfig.json +25 -0
  98. package/package.json +61 -0
  99. package/src/cli/components/App.tsx +191 -0
  100. package/src/cli/components/CategorySelect.tsx +37 -0
  101. package/src/cli/components/DataTable.tsx +133 -0
  102. package/src/cli/components/Header.tsx +18 -0
  103. package/src/cli/components/HookSelect.tsx +29 -0
  104. package/src/cli/components/InstallProgress.tsx +105 -0
  105. package/src/cli/components/SearchView.tsx +86 -0
  106. package/src/cli/index.tsx +218 -0
  107. package/src/index.ts +31 -0
  108. package/src/lib/installer.ts +288 -0
  109. package/src/lib/registry.ts +205 -0
  110. package/tsconfig.json +17 -0
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * CLI for hook-packageage
5
+ */
6
+
7
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
8
+ import { join } from "path";
9
+ import { homedir } from "os";
10
+
11
+ const HOOK_NAME = "hook-packageage";
12
+ const SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
13
+
14
+ interface ClaudeSettings {
15
+ hooks?: {
16
+ PreToolUse?: Array<{
17
+ matcher: string;
18
+ hooks: Array<{ type: "command"; command: string }>;
19
+ }>;
20
+ };
21
+ [key: string]: unknown;
22
+ }
23
+
24
+ function readSettings(): ClaudeSettings {
25
+ try {
26
+ if (existsSync(SETTINGS_PATH)) {
27
+ return JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
28
+ }
29
+ } catch {}
30
+ return {};
31
+ }
32
+
33
+ function writeSettings(settings: ClaudeSettings): void {
34
+ const dir = join(homedir(), ".claude");
35
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
36
+ writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
37
+ }
38
+
39
+ function install(): void {
40
+ const settings = readSettings();
41
+ if (!settings.hooks) settings.hooks = {};
42
+ if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
43
+
44
+ const existing = settings.hooks.PreToolUse.find((h) =>
45
+ h.hooks.some((hook) => hook.command.includes(HOOK_NAME))
46
+ );
47
+
48
+ if (existing) {
49
+ console.log(`${HOOK_NAME} is already installed`);
50
+ return;
51
+ }
52
+
53
+ settings.hooks.PreToolUse.push({
54
+ matcher: "Bash",
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 check package age before npm/bun install commands");
61
+ }
62
+
63
+ function uninstall(): void {
64
+ const settings = readSettings();
65
+ if (!settings.hooks?.PreToolUse) {
66
+ console.log(`${HOOK_NAME} is not installed`);
67
+ return;
68
+ }
69
+
70
+ const before = settings.hooks.PreToolUse.length;
71
+ settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
72
+ (h) => !h.hooks.some((hook) => hook.command.includes(HOOK_NAME))
73
+ );
74
+
75
+ if (before === settings.hooks.PreToolUse.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?.PreToolUse?.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
+ async function check(packageName: string): Promise<void> {
93
+ console.log(`Checking ${packageName}...`);
94
+ try {
95
+ const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}`);
96
+ if (!response.ok) {
97
+ console.error(`Package not found: ${packageName}`);
98
+ return;
99
+ }
100
+ const data = await response.json() as Record<string, unknown>;
101
+ const time = data.time as Record<string, string> | undefined;
102
+ const modified = time?.modified;
103
+ if (modified) {
104
+ const days = Math.floor((Date.now() - new Date(modified).getTime()) / (1000 * 60 * 60 * 24));
105
+ const status = days > 730 ? "ABANDONED" : days > 365 ? "STALE" : "ACTIVE";
106
+ console.log(` Last updated: ${modified} (${days} days ago) — ${status}`);
107
+ }
108
+
109
+ const distTags = data["dist-tags"] as Record<string, string> | undefined;
110
+ const latestVersion = distTags?.latest;
111
+ const versions = data.versions as Record<string, Record<string, unknown>> | undefined;
112
+ if (latestVersion && versions?.[latestVersion]?.deprecated) {
113
+ console.log(` DEPRECATED: ${versions[latestVersion].deprecated}`);
114
+ }
115
+ } catch (error) {
116
+ console.error(`Error checking ${packageName}:`, error);
117
+ }
118
+ }
119
+
120
+ function help(): void {
121
+ console.log(`
122
+ ${HOOK_NAME} - Check package age before install
123
+
124
+ Usage: ${HOOK_NAME} <command>
125
+
126
+ Commands:
127
+ install Install hook to Claude Code settings
128
+ uninstall Remove hook from Claude Code settings
129
+ status Check if hook is installed
130
+ check <pkg> Manually check a package's age
131
+ help Show this help message
132
+
133
+ Thresholds:
134
+ > 1 year since last publish: STALE warning
135
+ > 2 years since last publish: ABANDONED warning
136
+ Deprecated packages: always warned
137
+ `);
138
+ }
139
+
140
+ const command = process.argv[2];
141
+
142
+ switch (command) {
143
+ case "install": install(); break;
144
+ case "uninstall": uninstall(); break;
145
+ case "status": status(); break;
146
+ case "check":
147
+ if (process.argv[3]) {
148
+ check(process.argv[3]);
149
+ } else {
150
+ console.error("Usage: hook-packageage check <package-name>");
151
+ process.exit(1);
152
+ }
153
+ break;
154
+ case "help":
155
+ case "--help":
156
+ case "-h": help(); break;
157
+ default:
158
+ if (!command) {
159
+ import("./hook.ts").then((m) => m.run());
160
+ } else {
161
+ console.error(`Unknown command: ${command}`);
162
+ help();
163
+ process.exit(1);
164
+ }
165
+ }
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * Claude Code Hook: packageage
5
+ *
6
+ * PreToolUse hook that checks package age before npm/bun install commands.
7
+ * Warns on packages that haven't been updated in over a year (potentially
8
+ * abandoned) or have known deprecation markers.
9
+ *
10
+ * Checks the npm registry for last publish date and warns accordingly.
11
+ */
12
+
13
+ import { readFileSync } from "fs";
14
+
15
+ interface HookInput {
16
+ session_id: string;
17
+ cwd: string;
18
+ tool_name: string;
19
+ tool_input: Record<string, unknown>;
20
+ }
21
+
22
+ interface HookOutput {
23
+ decision?: "approve" | "block";
24
+ reason?: string;
25
+ }
26
+
27
+ const INSTALL_PATTERNS = [
28
+ /(?:npm|bun|yarn|pnpm)\s+(?:install|add|i)\s+/,
29
+ ];
30
+
31
+ const STALE_THRESHOLD_DAYS = 365; // 1 year
32
+ const ABANDONED_THRESHOLD_DAYS = 730; // 2 years
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
+ /**
45
+ * Extract package names from an install command
46
+ */
47
+ function extractPackageNames(command: string): string[] {
48
+ // Match: npm install pkg1 pkg2 / bun add pkg1 / etc.
49
+ const match = command.match(/(?:npm|bun|yarn|pnpm)\s+(?:install|add|i)\s+(.*)/);
50
+ if (!match) return [];
51
+
52
+ return match[1]
53
+ .split(/\s+/)
54
+ .filter((arg) => !arg.startsWith("-") && !arg.startsWith("--"))
55
+ .map((pkg) => pkg.replace(/@[\^~><=\d].*$/, "")) // strip version specifier
56
+ .filter((pkg) => pkg.length > 0 && !pkg.startsWith(".")); // filter paths
57
+ }
58
+
59
+ /**
60
+ * Check package age via npm registry
61
+ */
62
+ async function checkPackageAge(packageName: string): Promise<{
63
+ name: string;
64
+ lastPublish: Date | null;
65
+ daysSincePublish: number;
66
+ deprecated: boolean;
67
+ error?: string;
68
+ }> {
69
+ try {
70
+ const response = await fetch(
71
+ `https://registry.npmjs.org/${encodeURIComponent(packageName)}`,
72
+ { signal: AbortSignal.timeout(5000) }
73
+ );
74
+
75
+ if (!response.ok) {
76
+ return { name: packageName, lastPublish: null, daysSincePublish: 0, deprecated: false, error: `HTTP ${response.status}` };
77
+ }
78
+
79
+ const data = await response.json() as Record<string, unknown>;
80
+
81
+ // Check deprecation
82
+ const distTags = data["dist-tags"] as Record<string, string> | undefined;
83
+ const latestVersion = distTags?.latest;
84
+ const versions = data.versions as Record<string, Record<string, unknown>> | undefined;
85
+ const deprecated = latestVersion && versions?.[latestVersion]?.deprecated ? true : false;
86
+
87
+ // Get last publish date
88
+ const time = data.time as Record<string, string> | undefined;
89
+ const modified = time?.modified;
90
+
91
+ if (!modified) {
92
+ return { name: packageName, lastPublish: null, daysSincePublish: 0, deprecated };
93
+ }
94
+
95
+ const lastPublish = new Date(modified);
96
+ const daysSincePublish = Math.floor(
97
+ (Date.now() - lastPublish.getTime()) / (1000 * 60 * 60 * 24)
98
+ );
99
+
100
+ return { name: packageName, lastPublish, daysSincePublish, deprecated };
101
+ } catch (error) {
102
+ return {
103
+ name: packageName,
104
+ lastPublish: null,
105
+ daysSincePublish: 0,
106
+ deprecated: false,
107
+ error: error instanceof Error ? error.message : String(error),
108
+ };
109
+ }
110
+ }
111
+
112
+ function respond(output: HookOutput): void {
113
+ console.log(JSON.stringify(output));
114
+ }
115
+
116
+ export async function run(): Promise<void> {
117
+ const input = readStdinJson();
118
+
119
+ if (!input) {
120
+ respond({ decision: "approve" });
121
+ return;
122
+ }
123
+
124
+ if (input.tool_name !== "Bash") {
125
+ respond({ decision: "approve" });
126
+ return;
127
+ }
128
+
129
+ const command = input.tool_input?.command as string;
130
+ if (!command || typeof command !== "string") {
131
+ respond({ decision: "approve" });
132
+ return;
133
+ }
134
+
135
+ // Check if this is a package install command
136
+ const isInstall = INSTALL_PATTERNS.some((p) => p.test(command));
137
+ if (!isInstall) {
138
+ respond({ decision: "approve" });
139
+ return;
140
+ }
141
+
142
+ const packages = extractPackageNames(command);
143
+ if (packages.length === 0) {
144
+ respond({ decision: "approve" });
145
+ return;
146
+ }
147
+
148
+ const warnings: string[] = [];
149
+
150
+ // Check each package (in parallel, with timeout)
151
+ const results = await Promise.all(packages.map(checkPackageAge));
152
+
153
+ for (const result of results) {
154
+ if (result.deprecated) {
155
+ warnings.push(`${result.name}: DEPRECATED`);
156
+ }
157
+ if (result.daysSincePublish > ABANDONED_THRESHOLD_DAYS) {
158
+ warnings.push(`${result.name}: possibly abandoned (last updated ${result.daysSincePublish} days ago)`);
159
+ } else if (result.daysSincePublish > STALE_THRESHOLD_DAYS) {
160
+ warnings.push(`${result.name}: stale (last updated ${result.daysSincePublish} days ago)`);
161
+ }
162
+ }
163
+
164
+ if (warnings.length > 0) {
165
+ const reason = `Package age warnings:\n${warnings.map((w) => ` - ${w}`).join("\n")}\n\nConsider using more actively maintained alternatives.`;
166
+ console.error(`[hook-packageage] ${reason}`);
167
+ // Warn but don't block — just inject the warning
168
+ respond({ decision: "approve", reason });
169
+ return;
170
+ }
171
+
172
+ respond({ decision: "approve" });
173
+ }
174
+
175
+ if (import.meta.main) {
176
+ run();
177
+ }
@@ -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,25 @@
1
+ # CLAUDE.md
2
+
3
+ ## hook-phonenotify
4
+
5
+ A Stop/Notification hook that sends push notifications to phone via ntfy.sh.
6
+
7
+ ### Key Files
8
+
9
+ | File | Purpose |
10
+ |------|---------|
11
+ | `src/hook.ts` | Main hook logic — reads stdin, sends ntfy.sh notification |
12
+ | `src/cli.ts` | CLI — install/uninstall/status/test |
13
+
14
+ ### Hook Events
15
+
16
+ - **Stop** — notifies when Claude finishes
17
+ - **Notification** — notifies when Claude needs attention
18
+
19
+ ### Configuration
20
+
21
+ Reads `phoneNotifyConfig` from `~/.claude/settings.json`:
22
+ - `topic` — ntfy.sh topic name (required)
23
+ - `server` — ntfy server URL (default: https://ntfy.sh)
24
+ - `priority` — notification priority 1-5 (default: 3)
25
+ - `enabled` — toggle on/off
@@ -0,0 +1,44 @@
1
+ # hook-phonenotify
2
+
3
+ Claude Code hook that sends push notifications to your phone via ntfy.sh.
4
+
5
+ ## Overview
6
+
7
+ Get notified on your phone when Claude finishes a task or needs your attention. Uses [ntfy.sh](https://ntfy.sh) for free, no-account-required push notifications.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ bun install -g @hasnaxyz/hook-phonenotify
13
+ hook-phonenotify install my-secret-topic
14
+ ```
15
+
16
+ Then subscribe to `my-secret-topic` in the ntfy app on your phone.
17
+
18
+ ## Commands
19
+
20
+ ```bash
21
+ hook-phonenotify install [topic] # Install with ntfy topic
22
+ hook-phonenotify uninstall # Remove hook
23
+ hook-phonenotify status # Show config
24
+ hook-phonenotify test # Send test notification
25
+ ```
26
+
27
+ ## Configuration
28
+
29
+ In `~/.claude/settings.json`:
30
+
31
+ ```json
32
+ {
33
+ "phoneNotifyConfig": {
34
+ "enabled": true,
35
+ "topic": "your-secret-topic",
36
+ "server": "https://ntfy.sh",
37
+ "priority": 3
38
+ }
39
+ }
40
+ ```
41
+
42
+ ## License
43
+
44
+ MIT
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@hasnaxyz/hook-phonenotify",
3
+ "version": "0.1.0",
4
+ "description": "Claude Code hook that sends push notifications to phone via ntfy.sh",
5
+ "type": "module",
6
+ "bin": {
7
+ "hook-phonenotify": "./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", "notifications", "phone", "ntfy", "push", "cli"],
26
+ "author": "Hasna",
27
+ "license": "MIT",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/hasnaxyz/hook-phonenotify.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,196 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * CLI for hook-phonenotify
5
+ */
6
+
7
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
8
+ import { join } from "path";
9
+ import { homedir } from "os";
10
+
11
+ const HOOK_NAME = "hook-phonenotify";
12
+ const SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
13
+
14
+ interface ClaudeSettings {
15
+ hooks?: {
16
+ Stop?: Array<{ matcher?: string; hooks: Array<{ type: "command"; command: string }> }>;
17
+ Notification?: Array<{ matcher?: string; hooks: Array<{ type: "command"; command: string }> }>;
18
+ };
19
+ phoneNotifyConfig?: {
20
+ enabled?: boolean;
21
+ topic?: string;
22
+ server?: string;
23
+ priority?: number;
24
+ };
25
+ [key: string]: unknown;
26
+ }
27
+
28
+ function readSettings(): ClaudeSettings {
29
+ try {
30
+ if (existsSync(SETTINGS_PATH)) {
31
+ return JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
32
+ }
33
+ } catch {}
34
+ return {};
35
+ }
36
+
37
+ function writeSettings(settings: ClaudeSettings): void {
38
+ const dir = join(homedir(), ".claude");
39
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
40
+ writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
41
+ }
42
+
43
+ function install(topic?: string): void {
44
+ const settings = readSettings();
45
+ if (!settings.hooks) settings.hooks = {};
46
+ if (!settings.hooks.Stop) settings.hooks.Stop = [];
47
+ if (!settings.hooks.Notification) settings.hooks.Notification = [];
48
+
49
+ const hookCommand = `bunx @hasnaxyz/${HOOK_NAME}`;
50
+
51
+ const existing = settings.hooks.Stop.find((h) =>
52
+ h.hooks.some((hook) => hook.command.includes(HOOK_NAME))
53
+ );
54
+
55
+ if (existing) {
56
+ console.log(`${HOOK_NAME} is already installed`);
57
+ return;
58
+ }
59
+
60
+ settings.hooks.Stop.push({
61
+ hooks: [{ type: "command", command: hookCommand }],
62
+ });
63
+ settings.hooks.Notification.push({
64
+ hooks: [{ type: "command", command: hookCommand }],
65
+ });
66
+
67
+ if (!settings.phoneNotifyConfig) {
68
+ settings.phoneNotifyConfig = {
69
+ enabled: true,
70
+ topic: topic || "claude-code",
71
+ server: "https://ntfy.sh",
72
+ priority: 3,
73
+ };
74
+ }
75
+
76
+ writeSettings(settings);
77
+ console.log(`${HOOK_NAME} installed successfully`);
78
+ console.log(`Topic: ${settings.phoneNotifyConfig.topic}`);
79
+ console.log(`\nTo receive notifications, subscribe to your topic in the ntfy app:`);
80
+ console.log(` https://ntfy.sh/${settings.phoneNotifyConfig.topic}`);
81
+ }
82
+
83
+ function uninstall(): void {
84
+ const settings = readSettings();
85
+ let removed = false;
86
+
87
+ if (settings.hooks?.Stop) {
88
+ const before = settings.hooks.Stop.length;
89
+ settings.hooks.Stop = settings.hooks.Stop.filter(
90
+ (h) => !h.hooks.some((hook) => hook.command.includes(HOOK_NAME))
91
+ );
92
+ if (before !== settings.hooks.Stop.length) removed = true;
93
+ }
94
+
95
+ if (settings.hooks?.Notification) {
96
+ const before = settings.hooks.Notification.length;
97
+ settings.hooks.Notification = settings.hooks.Notification.filter(
98
+ (h) => !h.hooks.some((hook) => hook.command.includes(HOOK_NAME))
99
+ );
100
+ if (before !== settings.hooks.Notification.length) removed = true;
101
+ }
102
+
103
+ if (!removed) {
104
+ console.log(`${HOOK_NAME} is not installed`);
105
+ return;
106
+ }
107
+
108
+ writeSettings(settings);
109
+ console.log(`${HOOK_NAME} uninstalled successfully`);
110
+ }
111
+
112
+ function status(): void {
113
+ const settings = readSettings();
114
+ const installed = settings.hooks?.Stop?.some((h) =>
115
+ h.hooks.some((hook) => hook.command.includes(HOOK_NAME))
116
+ );
117
+ console.log(`${HOOK_NAME} is ${installed ? "installed" : "not installed"}`);
118
+
119
+ if (settings.phoneNotifyConfig) {
120
+ console.log(`\nConfig:`);
121
+ console.log(` Enabled: ${settings.phoneNotifyConfig.enabled !== false}`);
122
+ console.log(` Topic: ${settings.phoneNotifyConfig.topic || "(not set)"}`);
123
+ console.log(` Server: ${settings.phoneNotifyConfig.server || "https://ntfy.sh"}`);
124
+ console.log(` Priority: ${settings.phoneNotifyConfig.priority || 3}`);
125
+ }
126
+ }
127
+
128
+ async function test(): Promise<void> {
129
+ console.log("Sending test notification...");
130
+ const settings = readSettings();
131
+ const config = settings.phoneNotifyConfig;
132
+
133
+ if (!config?.topic) {
134
+ console.error("No topic configured. Run: hook-phonenotify install <topic>");
135
+ process.exit(1);
136
+ }
137
+
138
+ const server = config.server || "https://ntfy.sh";
139
+ const url = `${server}/${config.topic}`;
140
+
141
+ const response = await fetch(url, {
142
+ method: "POST",
143
+ headers: {
144
+ Title: "Claude Code - Test",
145
+ Priority: String(config.priority || 3),
146
+ Tags: "robot,test_tube",
147
+ },
148
+ body: "This is a test notification from hook-phonenotify.",
149
+ });
150
+
151
+ if (response.ok) {
152
+ console.log("Test notification sent!");
153
+ } else {
154
+ console.error(`Failed: ${response.status} ${response.statusText}`);
155
+ }
156
+ }
157
+
158
+ function help(): void {
159
+ console.log(`
160
+ ${HOOK_NAME} - Push notifications to phone via ntfy.sh
161
+
162
+ Usage: ${HOOK_NAME} <command>
163
+
164
+ Commands:
165
+ install [topic] Install hook (optional: set ntfy topic)
166
+ uninstall Remove hook from Claude Code settings
167
+ status Check if hook is installed and show config
168
+ test Send a test notification
169
+ help Show this help message
170
+
171
+ Setup:
172
+ 1. Install the ntfy app on your phone (iOS/Android)
173
+ 2. Subscribe to your chosen topic
174
+ 3. Run: ${HOOK_NAME} install my-secret-topic
175
+ `);
176
+ }
177
+
178
+ const command = process.argv[2];
179
+
180
+ switch (command) {
181
+ case "install": install(process.argv[3]); break;
182
+ case "uninstall": uninstall(); break;
183
+ case "status": status(); break;
184
+ case "test": test(); break;
185
+ case "help":
186
+ case "--help":
187
+ case "-h": help(); break;
188
+ default:
189
+ if (!command) {
190
+ import("./hook.ts").then((m) => m.run());
191
+ } else {
192
+ console.error(`Unknown command: ${command}`);
193
+ help();
194
+ process.exit(1);
195
+ }
196
+ }