@hrushiborhade/pingme 1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hrushikesh Borhade
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,170 @@
1
+ # pingme
2
+
3
+ > My Claude agent pings me when it's stuck. Now I doom scroll guilt-free. Yours is just... stuck.
4
+
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ Get texted when your Claude Code agent needs attention. No more checking terminals.
8
+
9
+ ## The Problem
10
+
11
+ You're running multiple Claude Code instances across tmux panes. One stops because it needs permission or has a question. You think it's still working. Hours later, you find it blocked. Time wasted.
12
+
13
+ ## The Solution
14
+
15
+ ```bash
16
+ npx @hrushiborhade/pingme init
17
+ ```
18
+
19
+ Now your Claude agent texts you when it needs you.
20
+
21
+ ## What You Get
22
+
23
+ SMS notifications when:
24
+ - Agent stops
25
+ - Agent asks a clarifying question
26
+ - Agent needs permission
27
+ - Agent hits rate limits
28
+
29
+ Each message includes:
30
+ - **Project name** - which codebase needs you
31
+ - **tmux context** - which pane to jump to
32
+ - **Reason** - what the agent needs
33
+
34
+ ## Setup
35
+
36
+ ### 1. Get Twilio Credentials (free trial works)
37
+
38
+ 1. Sign up at [twilio.com/console](https://console.twilio.com)
39
+ 2. Get your Account SID and Auth Token from the dashboard
40
+ 3. Get a phone number (or use the trial number)
41
+
42
+ ### 2. Install pingme
43
+
44
+ ```bash
45
+ npx @hrushiborhade/pingme init
46
+ ```
47
+
48
+ Follow the prompts. Done.
49
+
50
+ ## Commands
51
+
52
+ ```bash
53
+ npx @hrushiborhade/pingme init # Setup pingme
54
+ npx @hrushiborhade/pingme test # Send a test SMS
55
+ npx @hrushiborhade/pingme uninstall # Remove pingme
56
+ npx @hrushiborhade/pingme --version # Show version
57
+ npx @hrushiborhade/pingme --help # Show help
58
+ ```
59
+
60
+ ## How It Works
61
+
62
+ pingme uses Claude Code's [hooks system](https://docs.anthropic.com/en/docs/claude-code/hooks) to detect when the agent needs your attention.
63
+
64
+ 1. **Installation**: When you run `npx @hrushiborhade/pingme init`, pingme creates:
65
+ - A bash script at `~/.claude/hooks/pingme.sh` that sends SMS via Twilio
66
+ - Hook entries in `~/.claude/settings.json` that trigger the script
67
+
68
+ 2. **Hook Triggers**: Two hooks are configured:
69
+ - `PostToolUse` with `AskUserQuestion` matcher - triggers when Claude asks you a question
70
+ - `Stop` - triggers when Claude stops execution for any reason
71
+
72
+ 3. **Notification Flow**: When triggered, the hook script:
73
+ - Detects your current project name from the working directory
74
+ - Captures tmux session/window/pane info (if available)
75
+ - Sends an SMS via Twilio's API with context about what needs attention
76
+
77
+ ## Example SMS
78
+
79
+ ```
80
+ [question emoji] agentQ
81
+
82
+ [location emoji] dev:2.1 (main)
83
+ [message emoji] Asking question
84
+
85
+ Do you want me to run npm install?
86
+ ```
87
+
88
+ ## Security
89
+
90
+ - **Credentials are stored locally** in `~/.claude/hooks/pingme.sh`
91
+ - Credentials are never sent to any server except Twilio's API
92
+ - The hook script only runs when Claude Code triggers it
93
+ - Input is sanitized to prevent shell injection
94
+ - SMS requests are made over HTTPS
95
+
96
+ To update or remove credentials, run `npx @hrushiborhade/pingme init` again or `npx @hrushiborhade/pingme uninstall`.
97
+
98
+ ## Troubleshooting
99
+
100
+ ### SMS not sending
101
+
102
+ 1. **Verify credentials**: Run `npx @hrushiborhade/pingme test` to send a test message
103
+ 2. **Check Twilio balance**: Free trial includes $15 credit; ensure it's not exhausted
104
+ 3. **Verify phone numbers**: Both numbers must include country code (e.g., `+1` for US)
105
+ 4. **Trial account limitations**: Twilio trial accounts can only send to verified numbers
106
+
107
+ ### Hook not triggering
108
+
109
+ 1. **Restart Claude Code**: Hooks are loaded on startup
110
+ 2. **Check settings.json**: Verify hooks are present in `~/.claude/settings.json`
111
+ 3. **Check script permissions**: Run `chmod +x ~/.claude/hooks/pingme.sh`
112
+
113
+ ### "curl not found" or no SMS sent
114
+
115
+ The hook script requires `curl`. Install it via your package manager:
116
+ - macOS: `brew install curl` (usually pre-installed)
117
+ - Ubuntu/Debian: `sudo apt install curl`
118
+
119
+ ### Uninstalling
120
+
121
+ ```bash
122
+ npx @hrushiborhade/pingme uninstall
123
+ ```
124
+
125
+ This removes the hook script and settings entries. Your Twilio credentials are deleted locally.
126
+
127
+ ## Requirements
128
+
129
+ - Node.js 18+
130
+ - Twilio account (free trial includes $15 credit)
131
+ - Claude Code CLI
132
+ - `curl` (pre-installed on most systems)
133
+
134
+ ## Contributing
135
+
136
+ Contributions are welcome! Here's how to get started:
137
+
138
+ 1. Fork the repository
139
+ 2. Create a feature branch: `git checkout -b feature/my-feature`
140
+ 3. Make your changes
141
+ 4. Run the build: `npm run build`
142
+ 5. Test locally: `npm start init`
143
+ 6. Commit your changes: `git commit -m 'Add my feature'`
144
+ 7. Push to your fork: `git push origin feature/my-feature`
145
+ 8. Open a Pull Request
146
+
147
+ ### Development
148
+
149
+ ```bash
150
+ git clone https://github.com/HrushiBorhade/pingme-cli.git
151
+ cd pingme-cli
152
+ npm install
153
+ npm run dev # Watch mode for TypeScript
154
+ npm run build # Build for production
155
+ ```
156
+
157
+ ### Ideas for Contribution
158
+
159
+ - Support for other notification providers (Slack, Discord, Pushover)
160
+ - Rate limiting to prevent SMS spam
161
+ - Quiet hours configuration
162
+ - Custom message templates
163
+
164
+ ## License
165
+
166
+ MIT
167
+
168
+ ---
169
+
170
+ Built for developers who run AI agents and want their life back.
package/bin/pingme.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import('../dist/index.js');
@@ -0,0 +1 @@
1
+ export declare function init(): Promise<void>;
@@ -0,0 +1,83 @@
1
+ import * as p from '@clack/prompts';
2
+ import pc from 'picocolors';
3
+ import { installHook } from '../utils/install.js';
4
+ import { sendTestSMS } from '../utils/twilio.js';
5
+ export async function init() {
6
+ p.log.info(pc.dim('Get your Twilio credentials at: ') + pc.cyan('https://console.twilio.com'));
7
+ const credentials = await p.group({
8
+ twilioSid: () => p.text({
9
+ message: 'Twilio Account SID',
10
+ placeholder: 'ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
11
+ validate: (value) => {
12
+ if (!value)
13
+ return 'Required';
14
+ if (!value.startsWith('AC'))
15
+ return 'Should start with AC';
16
+ },
17
+ }),
18
+ twilioToken: () => p.password({
19
+ message: 'Twilio Auth Token',
20
+ validate: (value) => {
21
+ if (!value)
22
+ return 'Required';
23
+ if (value.length < 20)
24
+ return 'Token seems too short';
25
+ },
26
+ }),
27
+ twilioFrom: () => p.text({
28
+ message: 'Twilio Phone Number',
29
+ placeholder: '+14155238886',
30
+ validate: (value) => {
31
+ if (!value)
32
+ return 'Required';
33
+ if (!value.startsWith('+'))
34
+ return 'Include country code (e.g., +1...)';
35
+ },
36
+ }),
37
+ myPhone: () => p.text({
38
+ message: 'Your Phone Number',
39
+ placeholder: '+1234567890',
40
+ validate: (value) => {
41
+ if (!value)
42
+ return 'Required';
43
+ if (!value.startsWith('+'))
44
+ return 'Include country code (e.g., +1...)';
45
+ },
46
+ }),
47
+ }, {
48
+ onCancel: () => {
49
+ p.cancel('Setup cancelled.');
50
+ process.exit(0);
51
+ },
52
+ });
53
+ const s = p.spinner();
54
+ // Install hook
55
+ s.start('Creating hook script');
56
+ try {
57
+ await installHook(credentials);
58
+ s.stop('Hook script created');
59
+ }
60
+ catch (err) {
61
+ s.stop(pc.red('Failed to create hook script'));
62
+ p.log.error(err instanceof Error ? err.message : 'Unknown error');
63
+ p.log.info('Check that you have write permissions to ~/.claude/');
64
+ process.exit(1);
65
+ }
66
+ // Send test SMS
67
+ s.start('Sending test SMS');
68
+ const testResult = await sendTestSMS(credentials);
69
+ if (testResult.success) {
70
+ s.stop('Test SMS sent');
71
+ }
72
+ else {
73
+ s.stop(pc.yellow('Could not send test SMS'));
74
+ p.log.warn('Setup completed, but test SMS failed. Check your Twilio credentials.');
75
+ }
76
+ // Done
77
+ p.note(`Your Claude agent will now ping you when it needs attention.
78
+
79
+ ${pc.dim('Commands:')}
80
+ ${pc.cyan('npx pingme-cli test')} Send a test SMS
81
+ ${pc.cyan('npx pingme-cli uninstall')} Remove pingme`, 'Setup complete');
82
+ p.outro(pc.dim('Now go doom scroll guilt-free ') + '🚀');
83
+ }
@@ -0,0 +1 @@
1
+ export declare function test(): Promise<void>;
@@ -0,0 +1,38 @@
1
+ import * as p from '@clack/prompts';
2
+ import pc from 'picocolors';
3
+ import { existsSync } from 'fs';
4
+ import { homedir } from 'os';
5
+ import path from 'path';
6
+ import { execSync } from 'child_process';
7
+ export async function test() {
8
+ const hookPath = path.join(homedir(), '.claude', 'hooks', 'pingme.sh');
9
+ if (!existsSync(hookPath)) {
10
+ p.log.error('pingme is not installed');
11
+ p.log.info(`Run ${pc.cyan('npx pingme-cli init')} to set up`);
12
+ process.exit(1);
13
+ }
14
+ const s = p.spinner();
15
+ s.start('Sending test SMS');
16
+ try {
17
+ execSync(`echo "🧪 Test ping from pingme-cli" | "${hookPath}" test`, {
18
+ timeout: 15000,
19
+ stdio: 'ignore',
20
+ });
21
+ s.stop('Test SMS sent!');
22
+ p.log.success('Check your phone for the message');
23
+ }
24
+ catch (err) {
25
+ s.stop('Failed to send SMS');
26
+ // Provide more specific error message
27
+ const isTimeout = err instanceof Error && err.message.includes('ETIMEDOUT');
28
+ if (isTimeout) {
29
+ p.log.error('Request timed out - check your network connection');
30
+ }
31
+ else {
32
+ p.log.error('SMS send failed - check your Twilio credentials');
33
+ }
34
+ p.log.info(`Run ${pc.cyan('npx pingme-cli init')} to reconfigure`);
35
+ process.exit(1);
36
+ }
37
+ p.outro(pc.dim('All good!'));
38
+ }
@@ -0,0 +1 @@
1
+ export declare function uninstall(): Promise<void>;
@@ -0,0 +1,46 @@
1
+ import * as p from '@clack/prompts';
2
+ import pc from 'picocolors';
3
+ import { existsSync } from 'fs';
4
+ import { unlink, access, constants } from 'fs/promises';
5
+ import { homedir } from 'os';
6
+ import path from 'path';
7
+ export async function uninstall() {
8
+ const hookPath = path.join(homedir(), '.claude', 'hooks', 'pingme.sh');
9
+ if (!existsSync(hookPath)) {
10
+ p.log.warn('pingme is not installed (hook file not found)');
11
+ p.outro(pc.dim('Nothing to do'));
12
+ return;
13
+ }
14
+ // Check if we have write permission to delete the file
15
+ try {
16
+ await access(hookPath, constants.W_OK);
17
+ }
18
+ catch {
19
+ p.log.error(`Permission denied: Cannot delete ${hookPath}`);
20
+ p.log.info(pc.dim(`Try running: sudo rm "${hookPath}"`));
21
+ process.exit(1);
22
+ }
23
+ const confirm = await p.confirm({
24
+ message: 'Remove pingme?',
25
+ });
26
+ if (p.isCancel(confirm) || !confirm) {
27
+ p.cancel('Cancelled');
28
+ return;
29
+ }
30
+ const s = p.spinner();
31
+ s.start('Removing pingme');
32
+ try {
33
+ await unlink(hookPath);
34
+ s.stop('Hook script removed');
35
+ }
36
+ catch (err) {
37
+ s.stop('Could not remove hook');
38
+ const errorMessage = err instanceof Error ? err.message : 'Unknown error';
39
+ p.log.error(`Failed to delete hook: ${errorMessage}`);
40
+ p.log.info(pc.dim(`Manually delete: rm "${hookPath}"`));
41
+ process.exit(1);
42
+ }
43
+ p.note(pc.dim(`Hook entries in ~/.claude/settings.json remain.
44
+ They're harmless, but you can remove them manually if you want.`), 'Note');
45
+ p.outro(pc.dim('pingme uninstalled'));
46
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,56 @@
1
+ import * as p from '@clack/prompts';
2
+ import pc from 'picocolors';
3
+ import { init } from './commands/init.js';
4
+ import { test } from './commands/test.js';
5
+ import { uninstall } from './commands/uninstall.js';
6
+ const VERSION = '1.0.1';
7
+ async function main() {
8
+ const args = process.argv.slice(2);
9
+ const command = args[0] || 'init';
10
+ // Version flag (no header needed)
11
+ if (command === '--version' || command === '-v') {
12
+ console.log(`pingme-cli v${VERSION}`);
13
+ return;
14
+ }
15
+ // Header
16
+ console.log(); // Add spacing
17
+ p.intro(pc.bgCyan(pc.black(' pingme ')));
18
+ switch (command) {
19
+ case 'init':
20
+ await init();
21
+ break;
22
+ case 'test':
23
+ await test();
24
+ break;
25
+ case 'uninstall':
26
+ await uninstall();
27
+ break;
28
+ case 'help':
29
+ case '--help':
30
+ case '-h':
31
+ showHelp();
32
+ break;
33
+ default:
34
+ p.log.error(`Unknown command: ${command}`);
35
+ showHelp();
36
+ process.exit(1);
37
+ }
38
+ }
39
+ function showHelp() {
40
+ console.log(`
41
+ ${pc.bold('Usage:')} npx pingme-cli ${pc.dim('<command>')}
42
+
43
+ ${pc.bold('Commands:')}
44
+ ${pc.cyan('init')} Setup pingme (default)
45
+ ${pc.cyan('test')} Send a test SMS
46
+ ${pc.cyan('uninstall')} Remove pingme
47
+
48
+ ${pc.bold('Examples:')}
49
+ ${pc.dim('$')} npx pingme-cli init
50
+ ${pc.dim('$')} npx pingme-cli test
51
+ `);
52
+ }
53
+ main().catch((err) => {
54
+ p.log.error(err.message);
55
+ process.exit(1);
56
+ });
@@ -0,0 +1,8 @@
1
+ interface Credentials {
2
+ twilioSid: string;
3
+ twilioToken: string;
4
+ twilioFrom: string;
5
+ myPhone: string;
6
+ }
7
+ export declare function installHook(credentials: Credentials): Promise<void>;
8
+ export {};
@@ -0,0 +1,131 @@
1
+ import { writeFile, readFile, mkdir } from 'fs/promises';
2
+ import { existsSync } from 'fs';
3
+ import path from 'path';
4
+ import { homedir } from 'os';
5
+ // Escape special characters for bash strings (prevents shell injection)
6
+ function escapeForBash(str) {
7
+ // Replace backslashes first, then other special chars
8
+ return str
9
+ .replace(/\\/g, '\\\\')
10
+ .replace(/"/g, '\\"')
11
+ .replace(/\$/g, '\\$')
12
+ .replace(/`/g, '\\`')
13
+ .replace(/!/g, '\\!');
14
+ }
15
+ const HOOK_SCRIPT = `#!/usr/bin/env bash
16
+
17
+ # ┌───────────────────────────────────────────────────────────────┐
18
+ # │ pingme - Get texted when your Claude agent is stuck │
19
+ # │ https://github.com/HrushiBorhade/pingme-cli │
20
+ # └───────────────────────────────────────────────────────────────┘
21
+
22
+ # Config (do not edit manually - use 'npx pingme-cli init' to reconfigure)
23
+ TWILIO_SID="{{TWILIO_SID}}"
24
+ TWILIO_TOKEN="{{TWILIO_TOKEN}}"
25
+ TWILIO_FROM="{{TWILIO_FROM}}"
26
+ MY_PHONE="{{MY_PHONE}}"
27
+
28
+ # Check for curl
29
+ if ! command -v curl &> /dev/null; then
30
+ exit 0 # Silently exit if curl not available
31
+ fi
32
+
33
+ # Context
34
+ EVENT="\${1:-unknown}"
35
+ PROJECT=\$(basename "\$PWD" | tr -cd '[:alnum:]._-') # Sanitize project name
36
+
37
+ # tmux info (if available)
38
+ TMUX_INFO=""
39
+ if [ -n "\$TMUX" ]; then
40
+ TMUX_INFO=\$(tmux display-message -p '#S:#I.#P (#W)' 2>/dev/null || echo "")
41
+ fi
42
+
43
+ # Read context from stdin (limit to 280 chars for SMS)
44
+ CONTEXT=""
45
+ if [ ! -t 0 ]; then
46
+ CONTEXT=\$(head -c 280 | tr -cd '[:print:][:space:]') # Sanitize input
47
+ fi
48
+
49
+ # Message emoji/reason
50
+ case "\$EVENT" in
51
+ question) EMOJI="❓"; REASON="Asking question" ;;
52
+ permission) EMOJI="🔐"; REASON="Needs permission" ;;
53
+ limit) EMOJI="⚠️"; REASON="Hit limit" ;;
54
+ stopped) EMOJI="🛑"; REASON="Agent stopped" ;;
55
+ test) EMOJI="🧪"; REASON="Test ping" ;;
56
+ *) EMOJI="🔔"; REASON="Needs attention" ;;
57
+ esac
58
+
59
+ # Build message
60
+ MESSAGE="\$EMOJI \$PROJECT"
61
+ [ -n "\$TMUX_INFO" ] && MESSAGE="\$MESSAGE
62
+ 📍 \$TMUX_INFO"
63
+ MESSAGE="\$MESSAGE
64
+ 💬 \$REASON"
65
+ [ -n "\$CONTEXT" ] && MESSAGE="\$MESSAGE
66
+
67
+ \$CONTEXT"
68
+
69
+ # Send SMS (background, detached so it survives script exit)
70
+ (
71
+ curl -s -X POST "https://api.twilio.com/2010-04-01/Accounts/\$TWILIO_SID/Messages.json" \\
72
+ --user "\$TWILIO_SID:\$TWILIO_TOKEN" \\
73
+ --data-urlencode "From=\$TWILIO_FROM" \\
74
+ --data-urlencode "To=\$MY_PHONE" \\
75
+ --data-urlencode "Body=\$MESSAGE" \\
76
+ --max-time 10 \\
77
+ > /dev/null 2>&1
78
+ ) &
79
+ disown 2>/dev/null || true
80
+
81
+ exit 0
82
+ `;
83
+ export async function installHook(credentials) {
84
+ const homeDir = homedir();
85
+ const hooksDir = path.join(homeDir, '.claude', 'hooks');
86
+ const hookPath = path.join(hooksDir, 'pingme.sh');
87
+ const configPath = path.join(homeDir, '.claude', 'settings.json');
88
+ // Create hooks directory
89
+ await mkdir(hooksDir, { recursive: true });
90
+ // Create hook script with escaped credentials (prevents shell injection)
91
+ const script = HOOK_SCRIPT
92
+ .replaceAll('{{TWILIO_SID}}', escapeForBash(credentials.twilioSid))
93
+ .replaceAll('{{TWILIO_TOKEN}}', escapeForBash(credentials.twilioToken))
94
+ .replaceAll('{{TWILIO_FROM}}', escapeForBash(credentials.twilioFrom))
95
+ .replaceAll('{{MY_PHONE}}', escapeForBash(credentials.myPhone));
96
+ await writeFile(hookPath, script, { mode: 0o755 });
97
+ // Update Claude config
98
+ let config = {};
99
+ try {
100
+ if (existsSync(configPath)) {
101
+ const existing = await readFile(configPath, 'utf-8');
102
+ config = JSON.parse(existing);
103
+ }
104
+ }
105
+ catch {
106
+ // Start fresh
107
+ }
108
+ // Initialize hooks with Claude Code 2.1+ format
109
+ // Format: { matcher: "ToolName" (regex string), hooks: [{ type: "command", command: "..." }] }
110
+ config.hooks = config.hooks || {};
111
+ const hooks = config.hooks;
112
+ hooks.PostToolUse = hooks.PostToolUse || [];
113
+ hooks.Stop = hooks.Stop || [];
114
+ const postToolHooks = hooks.PostToolUse;
115
+ const stopHooks = hooks.Stop;
116
+ // Check if pingme hook already exists (check in hooks array)
117
+ const hasPingmePostTool = postToolHooks.some((h) => h.hooks?.some((hook) => hook.command?.includes('pingme.sh')));
118
+ const hasPingmeStop = stopHooks.some((h) => h.hooks?.some((hook) => hook.command?.includes('pingme.sh')));
119
+ if (!hasPingmePostTool) {
120
+ postToolHooks.push({
121
+ matcher: 'AskUserQuestion',
122
+ hooks: [{ type: 'command', command: '~/.claude/hooks/pingme.sh question' }],
123
+ });
124
+ }
125
+ if (!hasPingmeStop) {
126
+ stopHooks.push({
127
+ hooks: [{ type: 'command', command: '~/.claude/hooks/pingme.sh stopped' }],
128
+ });
129
+ }
130
+ await writeFile(configPath, JSON.stringify(config, null, 2));
131
+ }
@@ -0,0 +1,18 @@
1
+ interface Credentials {
2
+ twilioSid: string;
3
+ twilioToken: string;
4
+ twilioFrom: string;
5
+ myPhone: string;
6
+ }
7
+ export type TestErrorCode = 'HOOK_NOT_FOUND' | 'TIMEOUT' | 'NETWORK_ERROR' | 'PERMISSION_DENIED' | 'SCRIPT_ERROR' | 'UNKNOWN';
8
+ interface TestResult {
9
+ success: boolean;
10
+ error?: string;
11
+ errorCode?: TestErrorCode;
12
+ }
13
+ /**
14
+ * Sends a test SMS using the installed hook script.
15
+ * This validates that the Twilio credentials are working correctly.
16
+ */
17
+ export declare function sendTestSMS(credentials: Credentials): Promise<TestResult>;
18
+ export {};
@@ -0,0 +1,92 @@
1
+ import { execSync } from 'child_process';
2
+ import { existsSync } from 'fs';
3
+ import { homedir } from 'os';
4
+ import path from 'path';
5
+ const TIMEOUT_MS = 15000;
6
+ /**
7
+ * Determines the error code based on the error type and message
8
+ */
9
+ function getErrorCode(err) {
10
+ if (!(err instanceof Error)) {
11
+ return 'UNKNOWN';
12
+ }
13
+ const message = err.message.toLowerCase();
14
+ // Node.js child_process timeout (ETIMEDOUT or killed due to timeout)
15
+ if (message.includes('etimedout') || message.includes('timedout') || message.includes('timed out')) {
16
+ return 'TIMEOUT';
17
+ }
18
+ // Check for signal-based timeout (execSync kills with SIGTERM on timeout)
19
+ if ('signal' in err && err.signal === 'SIGTERM') {
20
+ return 'TIMEOUT';
21
+ }
22
+ // Network-related errors
23
+ if (message.includes('enotfound') ||
24
+ message.includes('econnrefused') ||
25
+ message.includes('econnreset') ||
26
+ message.includes('network') ||
27
+ message.includes('could not resolve')) {
28
+ return 'NETWORK_ERROR';
29
+ }
30
+ // Permission errors
31
+ if (message.includes('eacces') || message.includes('permission denied')) {
32
+ return 'PERMISSION_DENIED';
33
+ }
34
+ // Script execution errors (non-zero exit code)
35
+ if (message.includes('exited with') || message.includes('exit code')) {
36
+ return 'SCRIPT_ERROR';
37
+ }
38
+ return 'UNKNOWN';
39
+ }
40
+ /**
41
+ * Returns a user-friendly error message based on the error code
42
+ */
43
+ function getErrorMessage(errorCode, originalError) {
44
+ switch (errorCode) {
45
+ case 'HOOK_NOT_FOUND':
46
+ return 'Hook script not found. Run "npx pingme-cli init" to set up.';
47
+ case 'TIMEOUT':
48
+ return `Request timed out after ${TIMEOUT_MS / 1000} seconds. Check your network connection or Twilio service status.`;
49
+ case 'NETWORK_ERROR':
50
+ return 'Network error. Check your internet connection and try again.';
51
+ case 'PERMISSION_DENIED':
52
+ return 'Permission denied. Check that the hook script is executable (chmod +x ~/.claude/hooks/pingme.sh).';
53
+ case 'SCRIPT_ERROR':
54
+ return 'Hook script failed. Check your Twilio credentials with "npx pingme-cli init".';
55
+ case 'UNKNOWN':
56
+ default:
57
+ return originalError || 'An unexpected error occurred while sending SMS.';
58
+ }
59
+ }
60
+ /**
61
+ * Sends a test SMS using the installed hook script.
62
+ * This validates that the Twilio credentials are working correctly.
63
+ */
64
+ export async function sendTestSMS(credentials) {
65
+ const hookPath = path.join(homedir(), '.claude', 'hooks', 'pingme.sh');
66
+ // Pre-flight check: verify hook script exists
67
+ if (!existsSync(hookPath)) {
68
+ return {
69
+ success: false,
70
+ error: getErrorMessage('HOOK_NOT_FOUND'),
71
+ errorCode: 'HOOK_NOT_FOUND',
72
+ };
73
+ }
74
+ const execOptions = {
75
+ timeout: TIMEOUT_MS,
76
+ stdio: ['pipe', 'pipe', 'pipe'], // Capture stdout/stderr for better error diagnosis
77
+ killSignal: 'SIGTERM',
78
+ };
79
+ try {
80
+ execSync(`echo "pingme installed! Your Claude agent can now reach you." | "${hookPath}" test`, execOptions);
81
+ return { success: true };
82
+ }
83
+ catch (err) {
84
+ const errorCode = getErrorCode(err);
85
+ const originalMessage = err instanceof Error ? err.message : String(err);
86
+ return {
87
+ success: false,
88
+ error: getErrorMessage(errorCode, originalMessage),
89
+ errorCode,
90
+ };
91
+ }
92
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@hrushiborhade/pingme",
3
+ "version": "1.0.1",
4
+ "description": "Get texted when your Claude agent is stuck",
5
+ "type": "module",
6
+ "bin": {
7
+ "pingme": "./bin/pingme.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsc --watch",
13
+ "start": "node bin/pingme.js",
14
+ "test": "vitest run",
15
+ "test:watch": "vitest",
16
+ "test:coverage": "vitest run --coverage"
17
+ },
18
+ "files": [
19
+ "bin",
20
+ "dist"
21
+ ],
22
+ "keywords": [
23
+ "claude",
24
+ "claude-code",
25
+ "sms",
26
+ "notifications",
27
+ "twilio",
28
+ "ai-agents",
29
+ "cli",
30
+ "terminal",
31
+ "productivity"
32
+ ],
33
+ "author": "Hrushikesh Borhade",
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/HrushiBorhade/pingme-cli"
38
+ },
39
+ "dependencies": {
40
+ "@clack/prompts": "^0.9.1",
41
+ "picocolors": "^1.1.1"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^20.10.0",
45
+ "@vitest/coverage-v8": "^4.0.18",
46
+ "typescript": "^5.3.0",
47
+ "vitest": "^4.0.18"
48
+ },
49
+ "engines": {
50
+ "node": ">=18"
51
+ }
52
+ }