Skip to content

Commit 993cc79

Browse files
committed
Significantly simplify this
1 parent 670f026 commit 993cc79

44 files changed

Lines changed: 481 additions & 6579 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

ARCHITECTURE.md

Lines changed: 136 additions & 336 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 24 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
11
# claude-code-ntfy
22

3-
A transparent wrapper for Claude Code that monitors output and sends notifications via [ntfy.sh](https://ntfy.sh) based on configurable patterns and user activity.
3+
A transparent wrapper for Claude Code that sends a notification when Claude needs your attention.
44

55
## Features
66

7-
- = Smart notifications based on output patterns
8-
- >� Transparent wrapping - preserves all Claude Code functionality
9-
- =� User idle detection to avoid interruptions
10-
- <� Configurable regex patterns for triggers
11-
- =� Rate limiting and notification batching
12-
- =' Cross-platform support (Linux/macOS)
7+
- 🔔 Single notification when Claude needs attention
8+
- 🔄 Transparent wrapping - preserves all Claude Code functionality
9+
- 💤 Intelligent inactivity detection
10+
- 🖥️ Cross-platform support (Linux/macOS)
11+
12+
### Intelligent Inactivity Detection
13+
14+
The backstop timer provides smart notifications when Claude might need your attention:
15+
16+
- **While Claude is outputting**: Timer is continuously reset
17+
- **When Claude stops**: A 30-second countdown begins
18+
- **If you start typing**: Timer is permanently disabled
19+
- **If Claude sent a bell**: Timer is disabled (you're already notified)
20+
- **After 30 seconds of inactivity**: ONE notification is sent
21+
22+
This ensures you're notified when Claude needs input, but not when you're actively working.
1323

1424
## Installation
1525

@@ -76,7 +86,7 @@ let
7686
claude-code-ntfy = pkgs.callPackage (pkgs.fetchFromGitHub {
7787
owner = "Veraticus";
7888
repo = "claude-code-ntfy";
79-
rev = "ba76a6ce3b0bce2b17e5b9d528b8f4f80ec93cf8";
89+
rev = "main";
8090
sha256 = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
8191
} + "/default.nix") { };
8292
in
@@ -98,18 +108,7 @@ For user-level installation with configuration management:
98108
settings = {
99109
ntfy_topic = "my-claude-notifications";
100110
ntfy_server = "https://ntfy.sh";
101-
idle_timeout = "2m";
102-
patterns = [
103-
{
104-
name = "error";
105-
regex = "(?i)(error|failed|exception)";
106-
enabled = true;
107-
}
108-
];
109-
rate_limit = {
110-
window = "1m";
111-
max_messages = 5;
112-
};
111+
backstop_timeout = "30s";
113112
};
114113
};
115114
}
@@ -143,33 +142,18 @@ Configure via environment variables:
143142
144143
- `CLAUDE_NOTIFY_TOPIC` - Ntfy topic for notifications (required)
145144
- `CLAUDE_NOTIFY_SERVER` - Ntfy server URL (default: https://ntfy.sh)
146-
- `CLAUDE_NOTIFY_IDLE_TIMEOUT` - User idle timeout (default: 2m)
145+
- `CLAUDE_NOTIFY_BACKSTOP_TIMEOUT` - Inactivity timeout (default: 30s)
147146
- `CLAUDE_NOTIFY_QUIET` - Disable notifications (true/false)
148-
- `CLAUDE_NOTIFY_FORCE` - Force notifications even when active (true/false)
147+
- `CLAUDE_NOTIFY_CLAUDE_PATH` - Path to the real claude binary
149148
150149
Or use a config file at `~/.config/claude-code-ntfy/config.yaml`:
151150
152151
```yaml
153152
ntfy_topic: "my-claude-notifications"
154153
ntfy_server: "https://ntfy.sh"
155-
idle_timeout: "2m"
156-
157-
patterns:
158-
- name: "bell"
159-
regex: '\x07'
160-
enabled: true
161-
- name: "question"
162-
regex: '\?\s*$'
163-
enabled: true
164-
- name: "error"
165-
regex: '(?i)(error|failed|exception)'
166-
enabled: true
167-
168-
rate_limit:
169-
window: "1m"
170-
max_messages: 5
171-
172-
batch_window: "5s"
154+
backstop_timeout: "30s"
155+
quiet: false
156+
claude_path: "/usr/local/bin/claude"
173157
```
174158
175159
## Development

cmd/claude-code-ntfy/app.go

Lines changed: 19 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,21 @@ package main
33
import (
44
"fmt"
55
"os"
6-
"time"
76

87
"github.com/Veraticus/claude-code-ntfy/pkg/config"
9-
"github.com/Veraticus/claude-code-ntfy/pkg/idle"
108
"github.com/Veraticus/claude-code-ntfy/pkg/interfaces"
119
"github.com/Veraticus/claude-code-ntfy/pkg/monitor"
1210
"github.com/Veraticus/claude-code-ntfy/pkg/notification"
1311
"github.com/Veraticus/claude-code-ntfy/pkg/process"
14-
"github.com/Veraticus/claude-code-ntfy/pkg/status"
1512
)
1613

1714
// Dependencies holds all the dependencies for the application
1815
type Dependencies struct {
19-
Config *config.Config
20-
IdleDetector interfaces.IdleDetector
21-
Notifier notification.Notifier
22-
RateLimiter interfaces.RateLimiter
23-
PatternMatcher monitor.PatternMatcher
24-
NotificationManager *notification.Manager
25-
OutputMonitor interfaces.DataHandler
26-
ProcessManager *process.Manager
27-
StatusIndicator *status.Indicator
28-
StatusReporter *status.Reporter
29-
stopChan chan struct{}
16+
Config *config.Config
17+
Notifier notification.Notifier
18+
OutputMonitor interfaces.DataHandler
19+
ProcessManager *process.Manager
20+
stopChan chan struct{}
3021
}
3122

3223
// NewDependencies creates all dependencies with the given configuration
@@ -36,59 +27,32 @@ func NewDependencies(cfg *config.Config) (*Dependencies, error) {
3627
stopChan: make(chan struct{}),
3728
}
3829

39-
// Create idle detector
40-
deps.IdleDetector = idle.NewIdleDetector()
41-
42-
// Create status indicator (only enabled if we have a terminal and notifications are enabled)
43-
// The indicator will only flash briefly when notifications are sent
44-
isTerminal := isatty(os.Stderr.Fd())
45-
statusEnabled := isTerminal && !cfg.Quiet && cfg.NtfyTopic != ""
46-
deps.StatusIndicator = status.NewIndicator(os.Stderr, statusEnabled)
47-
deps.StatusReporter = status.NewReporter(deps.StatusIndicator)
48-
49-
// Start auto-refresh to keep the indicator visible despite Claude's screen clears
50-
deps.StatusIndicator.StartAutoRefresh(deps.stopChan)
51-
52-
// Create pattern matcher and output monitor first (needed for terminal title)
53-
deps.PatternMatcher = monitor.NewSimplePatternMatcher(cfg.Patterns)
54-
outputMonitor := monitor.NewOutputMonitor(cfg, deps.PatternMatcher, deps.IdleDetector, nil) // nil notifier for now
55-
deps.OutputMonitor = outputMonitor
56-
5730
// Create notification components
5831
baseNotifier := notification.NewNtfyClient(cfg.NtfyServer, cfg.NtfyTopic)
5932

60-
// Wrap with context notifier
61-
contextNotifier := notification.NewContextNotifier(baseNotifier, func() string {
62-
return outputMonitor.GetTerminalTitle()
63-
})
64-
6533
// Wrap with backstop notifier if configured
66-
var finalNotifier notification.Notifier = contextNotifier
34+
var finalNotifier notification.Notifier = baseNotifier
6735
if cfg.BackstopTimeout > 0 {
68-
finalNotifier = notification.NewBackstopNotifier(contextNotifier, cfg.BackstopTimeout)
36+
finalNotifier = notification.NewBackstopNotifier(baseNotifier, cfg.BackstopTimeout)
6937
}
7038
deps.Notifier = finalNotifier
7139

72-
deps.RateLimiter = notification.NewTokenBucketRateLimiter(cfg.RateLimit.MaxMessages, cfg.RateLimit.Window)
73-
deps.NotificationManager = notification.NewManager(cfg, deps.Notifier, deps.RateLimiter)
74-
75-
// Connect status reporter to notification manager
76-
deps.NotificationManager.SetStatusReporter(deps.StatusReporter)
77-
78-
// Now set the notification manager on the output monitor
79-
outputMonitor.SetNotifier(deps.NotificationManager)
40+
// Create output monitor with the notifier
41+
outputMonitor := monitor.NewOutputMonitor(cfg, deps.Notifier)
42+
deps.OutputMonitor = outputMonitor
8043

81-
// Connect status indicator to output monitor for screen clear detection
82-
if statusEnabled {
83-
outputMonitor.SetScreenEventHandler(deps.StatusIndicator)
84-
// Enable focus reporting display if configured
85-
if cfg.EnableFocusDetection {
86-
deps.StatusIndicator.SetFocusReportingEnabled(true)
44+
// Create input handler that disables backstop timer
45+
inputHandler := func() {
46+
if backstopNotifier, ok := deps.Notifier.(*notification.BackstopNotifier); ok {
47+
backstopNotifier.DisableBackstopTimer()
48+
if os.Getenv("CLAUDE_NOTIFY_DEBUG") == "true" {
49+
fmt.Fprintf(os.Stderr, "claude-code-ntfy: user input detected, disabling backstop timer\n")
50+
}
8751
}
8852
}
8953

9054
// Create process manager
91-
deps.ProcessManager = process.NewManager(cfg, deps.OutputMonitor)
55+
deps.ProcessManager = process.NewManager(cfg, deps.OutputMonitor, inputHandler)
9256

9357
return deps, nil
9458
}
@@ -106,20 +70,11 @@ func (d *Dependencies) Close() {
10670
d.stopChan = nil
10771
}
10872

109-
// Clean up status indicator
110-
if d.StatusIndicator != nil {
111-
_ = d.StatusIndicator.Clear() // Best effort
112-
}
113-
11473
// Close notifiers
11574
// First try to close as backstop notifier
11675
if backstopNotifier, ok := d.Notifier.(*notification.BackstopNotifier); ok {
11776
_ = backstopNotifier.Close()
11877
}
119-
120-
if d.NotificationManager != nil {
121-
_ = d.NotificationManager.Close()
122-
}
12378
}
12479

12580
// Application represents the main application
@@ -136,23 +91,7 @@ func NewApplication(deps *Dependencies) *Application {
13691

13792
// Run starts the application with the given command and arguments
13893
func (a *Application) Run(command string, args []string) error {
139-
// Send startup notification if configured
140-
if a.deps.Config.StartupNotify && !a.deps.Config.Quiet && a.deps.NotificationManager != nil {
141-
pwd, _ := os.Getwd()
142-
startupNotification := notification.Notification{
143-
Title: "Claude Code Session Started",
144-
Message: fmt.Sprintf("Working directory: %s", pwd),
145-
Time: time.Now(),
146-
Pattern: "startup",
147-
}
148-
_ = a.deps.NotificationManager.Send(startupNotification)
149-
150-
// After sending startup notification, update the startup time in output monitor
151-
// to suppress subsequent notifications for StartupGracePeriod
152-
if om, ok := a.deps.OutputMonitor.(*monitor.OutputMonitor); ok {
153-
om.ResetStartTime()
154-
}
155-
}
94+
// No startup notification - we only notify when Claude needs attention
15695

15796
if err := a.deps.ProcessManager.Start(command, args); err != nil {
15897
return err

0 commit comments

Comments
 (0)