vagrant-claude-sandbox 0.2.0 → 0.3.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.
- checksums.yaml +4 -4
- data/README.md +8 -0
- data/bin/vagrant-notify-server +12 -0
- data/lib/vagrant-claude-sandbox/command.rb +2 -1
- data/lib/vagrant-claude-sandbox/config.rb +127 -2
- data/lib/vagrant-claude-sandbox/notification_config.yml.example +29 -0
- data/lib/vagrant-claude-sandbox/notification_server.rb +189 -0
- data/lib/vagrant-claude-sandbox/plugin.rb +38 -0
- data/lib/vagrant-claude-sandbox/scripts/claude-notify +300 -0
- data/lib/vagrant-claude-sandbox/version.rb +1 -1
- metadata +8 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 83aa818f90a8658daa51e1666f727910359e687032aa9e0c2686667ab82c5e57
|
|
4
|
+
data.tar.gz: a683a780d3770714c1be586f7066a3dfa102a858c5cc2bb123da9e936d2fb43a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 45117ae9e15c7101215e393ce4bb7e70c05c0e5b37885b648f1c15253176ffd631b5638b8bcd5d860a9d52fadeb9e47ff2cde209c0028afd4f40571889b789d2
|
|
7
|
+
data.tar.gz: 8083c2e00adf8dbc09f6b758a53d64bf9db7527e2dca70d74df3520b8a3719fa0329e439a261dcd27b019ccc952366cc8be0a037c08eb9b0f996f9bfd34ddc75
|
data/README.md
CHANGED
|
@@ -125,6 +125,14 @@ end
|
|
|
125
125
|
|
|
126
126
|
**Security note:** Only use mirrors from trusted sources. The official Ubuntu mirror is always the safest option.
|
|
127
127
|
|
|
128
|
+
## Notifications
|
|
129
|
+
|
|
130
|
+
Vagrant uses the `vagrant-notify-server` gem to provide desktop notifications from Claude.
|
|
131
|
+
To display notifications on your OS, [install `terminal-notifier` on MacOS](https://formulae.brew.sh/formula/terminal-notifier) or `libnotify-bin` on Linux (untested).
|
|
132
|
+
|
|
133
|
+
By default notifications appear when Claude completes tasks or needs input.
|
|
134
|
+
To customize notification behavior: Copy lib/vagrant-claude-sandbox/notification_config.yml.example to ~/.vagrant-claude-sandbox/notification_config.yml to configure which notifications appear.
|
|
135
|
+
|
|
128
136
|
## Development and Testing
|
|
129
137
|
|
|
130
138
|
### Running Tests
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# Add lib directory to load path
|
|
4
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
|
5
|
+
|
|
6
|
+
require 'vagrant-claude-sandbox/notification_server'
|
|
7
|
+
|
|
8
|
+
# If port specified on command line, use it. Otherwise, let auto-detection work
|
|
9
|
+
# (reads from .vagrant-notification-port if present, otherwise uses default)
|
|
10
|
+
port = ARGV[0] ? ARGV[0].to_i : nil
|
|
11
|
+
server = VagrantPlugins::ClaudeSandbox::NotificationServer.new(port)
|
|
12
|
+
server.start
|
|
@@ -27,7 +27,8 @@ module VagrantPlugins
|
|
|
27
27
|
workspace_path = config.workspace_path || "/agent-workspace"
|
|
28
28
|
|
|
29
29
|
# Build the command to run
|
|
30
|
-
|
|
30
|
+
# Source nvm only if needed (claude might already be in PATH)
|
|
31
|
+
command = "cd #{workspace_path}; [ -f ~/.nvm/nvm.sh ] && . ~/.nvm/nvm.sh; exec claude --dangerously-skip-permissions --chrome"
|
|
31
32
|
|
|
32
33
|
# Execute SSH with the command
|
|
33
34
|
machine.action(:ssh_run, ssh_run_command: command, ssh_opts: { extra_args: ["-t"] })
|
|
@@ -33,7 +33,7 @@ module VagrantPlugins
|
|
|
33
33
|
@claude_config_path = File.expand_path("~/.claude/") if @claude_config_path == UNSET_VALUE
|
|
34
34
|
@skip_claude_cli_install = false if @skip_claude_cli_install == UNSET_VALUE
|
|
35
35
|
@additional_packages = [] if @additional_packages == UNSET_VALUE
|
|
36
|
-
@provider = "
|
|
36
|
+
@provider = "docker" if @provider == UNSET_VALUE
|
|
37
37
|
@docker_image = nil if @docker_image == UNSET_VALUE
|
|
38
38
|
@ubuntu_mirror = nil if @ubuntu_mirror == UNSET_VALUE
|
|
39
39
|
end
|
|
@@ -72,7 +72,7 @@ module VagrantPlugins
|
|
|
72
72
|
# Vagrant will choose the appropriate one based on:
|
|
73
73
|
# 1. --provider flag from command line
|
|
74
74
|
# 2. Existing .vagrant directory state
|
|
75
|
-
# 3. Default provider (
|
|
75
|
+
# 3. Default provider (Docker)
|
|
76
76
|
apply_virtualbox_config!(root_config)
|
|
77
77
|
apply_docker_config!(root_config)
|
|
78
78
|
|
|
@@ -96,8 +96,49 @@ module VagrantPlugins
|
|
|
96
96
|
destination: "/tmp/claude-config"
|
|
97
97
|
end
|
|
98
98
|
|
|
99
|
+
# Copy claude-notify script to VM
|
|
100
|
+
claude_notify_script = File.expand_path("../scripts/claude-notify", __FILE__)
|
|
101
|
+
if File.exist?(claude_notify_script)
|
|
102
|
+
root_config.vm.provision "file",
|
|
103
|
+
source: claude_notify_script,
|
|
104
|
+
destination: "/tmp/claude-notify"
|
|
105
|
+
end
|
|
106
|
+
|
|
99
107
|
# Configure SSH port with auto-correction for conflicts
|
|
100
108
|
root_config.vm.network :forwarded_port, guest: 22, host: 2200, id: "ssh", auto_correct: true
|
|
109
|
+
|
|
110
|
+
# Forward notification port from host to guest (guest can send notifications to host)
|
|
111
|
+
# Use auto_correct to handle port conflicts
|
|
112
|
+
root_config.vm.network :forwarded_port, guest: 29325, host: 29325, id: "notifications", host_ip: "127.0.0.1", auto_correct: true
|
|
113
|
+
|
|
114
|
+
# Write the actual notification port to a file after VM is up so scripts can read it
|
|
115
|
+
root_config.trigger.after :up do |trigger|
|
|
116
|
+
trigger.ruby do |env, machine|
|
|
117
|
+
# Get the actual forwarded port for notifications
|
|
118
|
+
ports = machine.provider.driver.read_forwarded_ports rescue []
|
|
119
|
+
notification_port = nil
|
|
120
|
+
|
|
121
|
+
# Find the notification port mapping
|
|
122
|
+
ports.each do |port_info|
|
|
123
|
+
# Format varies by provider, handle both
|
|
124
|
+
if port_info.is_a?(Array) && port_info.length >= 4
|
|
125
|
+
# VirtualBox format: [name, guest_port, host_port, host_ip]
|
|
126
|
+
notification_port = port_info[2] if port_info[1] == 29325
|
|
127
|
+
elsif port_info.is_a?(Hash)
|
|
128
|
+
notification_port = port_info[:host] if port_info[:guest] == 29325
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Default to 29325 if we couldn't detect
|
|
133
|
+
notification_port ||= 29325
|
|
134
|
+
|
|
135
|
+
# Write port to workspace config file
|
|
136
|
+
workspace = machine.config.claude_sandbox.workspace_path rescue "/agent-workspace"
|
|
137
|
+
port_file = File.join(Dir.pwd, ".vagrant-notification-port")
|
|
138
|
+
File.write(port_file, notification_port.to_s)
|
|
139
|
+
machine.ui.info("Notification server port: #{notification_port}")
|
|
140
|
+
end
|
|
141
|
+
end
|
|
101
142
|
end
|
|
102
143
|
|
|
103
144
|
def apply_virtualbox_config!(root_config)
|
|
@@ -172,6 +213,10 @@ module VagrantPlugins
|
|
|
172
213
|
lsb-release \
|
|
173
214
|
git \
|
|
174
215
|
unzip \
|
|
216
|
+
libnotify-bin \
|
|
217
|
+
dbus-x11 \
|
|
218
|
+
inotify-tools \
|
|
219
|
+
netcat-openbsd \
|
|
175
220
|
#{additional_packages}
|
|
176
221
|
|
|
177
222
|
#{docker_install_script}
|
|
@@ -223,6 +268,86 @@ module VagrantPlugins
|
|
|
223
268
|
echo "Claude plugins and skills loaded successfully!"
|
|
224
269
|
fi
|
|
225
270
|
|
|
271
|
+
# Set up notification forwarding to host
|
|
272
|
+
echo "Setting up notification forwarding to host..."
|
|
273
|
+
|
|
274
|
+
# Backup original notify-send if it exists
|
|
275
|
+
if [ -f /usr/bin/notify-send ]; then
|
|
276
|
+
mv /usr/bin/notify-send /usr/bin/notify-send.real
|
|
277
|
+
fi
|
|
278
|
+
|
|
279
|
+
# Create wrapper script that forwards to host
|
|
280
|
+
cat > /usr/bin/notify-send << 'NOTIFY_EOF'
|
|
281
|
+
#!/bin/bash
|
|
282
|
+
# Wrapper script to forward notifications to host
|
|
283
|
+
|
|
284
|
+
TITLE=""
|
|
285
|
+
MESSAGE=""
|
|
286
|
+
|
|
287
|
+
# Read port from config file (written by vagrant trigger), default to 29325
|
|
288
|
+
if [ -f "/agent-workspace/.vagrant-notification-port" ]; then
|
|
289
|
+
HOST_PORT=$(cat /agent-workspace/.vagrant-notification-port)
|
|
290
|
+
else
|
|
291
|
+
HOST_PORT=29325
|
|
292
|
+
fi
|
|
293
|
+
|
|
294
|
+
# Parse notify-send arguments (simplified)
|
|
295
|
+
while [[ $# -gt 0 ]]; do
|
|
296
|
+
case $1 in
|
|
297
|
+
-u|--urgency)
|
|
298
|
+
shift 2
|
|
299
|
+
;;
|
|
300
|
+
-t|--expire-time)
|
|
301
|
+
shift 2
|
|
302
|
+
;;
|
|
303
|
+
-i|--icon)
|
|
304
|
+
shift 2
|
|
305
|
+
;;
|
|
306
|
+
-c|--category)
|
|
307
|
+
shift 2
|
|
308
|
+
;;
|
|
309
|
+
*)
|
|
310
|
+
if [ -z "$TITLE" ]; then
|
|
311
|
+
TITLE="$1"
|
|
312
|
+
elif [ -z "$MESSAGE" ]; then
|
|
313
|
+
MESSAGE="$1"
|
|
314
|
+
fi
|
|
315
|
+
shift
|
|
316
|
+
;;
|
|
317
|
+
esac
|
|
318
|
+
done
|
|
319
|
+
|
|
320
|
+
# Default message if not provided
|
|
321
|
+
[ -z "$MESSAGE" ] && MESSAGE="$TITLE" && TITLE="Notification"
|
|
322
|
+
|
|
323
|
+
# Try host.docker.internal first (Docker), then fall back to 10.0.2.2 (VirtualBox)
|
|
324
|
+
for HOST_IP in host.docker.internal 10.0.2.2; do
|
|
325
|
+
if echo "NOTIFY|$TITLE|$MESSAGE" | nc -w 1 $HOST_IP $HOST_PORT > /dev/null 2>&1; then
|
|
326
|
+
exit 0
|
|
327
|
+
fi
|
|
328
|
+
done
|
|
329
|
+
|
|
330
|
+
# Fall back to local notification if host server not available
|
|
331
|
+
if [ -f /usr/bin/notify-send.real ]; then
|
|
332
|
+
exec /usr/bin/notify-send.real "$TITLE" "$MESSAGE"
|
|
333
|
+
fi
|
|
334
|
+
|
|
335
|
+
# If all else fails, just log it
|
|
336
|
+
logger "Notification: $TITLE - $MESSAGE"
|
|
337
|
+
NOTIFY_EOF
|
|
338
|
+
|
|
339
|
+
chmod +x /usr/bin/notify-send
|
|
340
|
+
echo "Notification forwarding configured!"
|
|
341
|
+
|
|
342
|
+
# Install claude-notify helper script
|
|
343
|
+
if [ -f "/tmp/claude-notify" ]; then
|
|
344
|
+
echo "Installing claude-notify helper script..."
|
|
345
|
+
mv /tmp/claude-notify /usr/local/bin/claude-notify
|
|
346
|
+
chmod +x /usr/local/bin/claude-notify
|
|
347
|
+
chown root:root /usr/local/bin/claude-notify
|
|
348
|
+
echo "claude-notify installed successfully!"
|
|
349
|
+
fi
|
|
350
|
+
|
|
226
351
|
echo "Claude sandbox environment setup complete!"
|
|
227
352
|
SHELL
|
|
228
353
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Notification Configuration for vagrant-claude-sandbox
|
|
2
|
+
# Copy this file to ~/.vagrant-claude-sandbox/notification_config.yml to customize
|
|
3
|
+
|
|
4
|
+
# Which notification types to show
|
|
5
|
+
# Types: info, success, error, warning, needs_input, task_complete, task_start
|
|
6
|
+
show_types:
|
|
7
|
+
- task_complete
|
|
8
|
+
- needs_input
|
|
9
|
+
- error
|
|
10
|
+
- warning
|
|
11
|
+
|
|
12
|
+
# Default timeout in seconds (0 = permanent until dismissed)
|
|
13
|
+
default_timeout: 0
|
|
14
|
+
|
|
15
|
+
# Override timeout for specific types (in seconds)
|
|
16
|
+
type_timeouts:
|
|
17
|
+
info: 10
|
|
18
|
+
success: 15
|
|
19
|
+
error: 0 # Errors stay until dismissed
|
|
20
|
+
warning: 0 # Warnings stay until dismissed
|
|
21
|
+
needs_input: 0 # User input needed stays until dismissed
|
|
22
|
+
task_complete: 0 # Task completions stay until dismissed
|
|
23
|
+
task_start: 5 # Task starts auto-dismiss after 5s
|
|
24
|
+
|
|
25
|
+
# Enable sound for notifications
|
|
26
|
+
enable_sound: false
|
|
27
|
+
|
|
28
|
+
# Only show notifications with URLs (e.g., link to conversation)
|
|
29
|
+
require_url: false
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
require 'socket'
|
|
2
|
+
require 'json'
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
module VagrantPlugins
|
|
6
|
+
module ClaudeSandbox
|
|
7
|
+
class NotificationServer
|
|
8
|
+
DEFAULT_PORT = 29325
|
|
9
|
+
PORT_FILE = ".vagrant-notification-port"
|
|
10
|
+
DEFAULT_CONFIG = {
|
|
11
|
+
'show_types' => ['task_complete', 'needs_input', 'error', 'warning'],
|
|
12
|
+
'default_timeout' => 0,
|
|
13
|
+
'type_timeouts' => {
|
|
14
|
+
'info' => 10,
|
|
15
|
+
'success' => 15,
|
|
16
|
+
'error' => 0,
|
|
17
|
+
'warning' => 0,
|
|
18
|
+
'needs_input' => 0,
|
|
19
|
+
'task_complete' => 0,
|
|
20
|
+
'task_start' => 5
|
|
21
|
+
},
|
|
22
|
+
'enable_sound' => false,
|
|
23
|
+
'require_url' => false
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
def initialize(port = nil)
|
|
27
|
+
@port = port || detect_port
|
|
28
|
+
@server = nil
|
|
29
|
+
@config = load_config
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def detect_port
|
|
33
|
+
# Check for port file in current directory (written by vagrant trigger)
|
|
34
|
+
if File.exist?(PORT_FILE)
|
|
35
|
+
port = File.read(PORT_FILE).strip.to_i
|
|
36
|
+
return port if port > 0
|
|
37
|
+
end
|
|
38
|
+
DEFAULT_PORT
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def load_config
|
|
42
|
+
config_path = File.expand_path('~/.vagrant-claude-sandbox/notification_config.yml')
|
|
43
|
+
if File.exist?(config_path)
|
|
44
|
+
begin
|
|
45
|
+
loaded = YAML.load_file(config_path)
|
|
46
|
+
DEFAULT_CONFIG.merge(loaded || {})
|
|
47
|
+
rescue => e
|
|
48
|
+
puts "Warning: Failed to load config from #{config_path}: #{e.message}"
|
|
49
|
+
puts "Using default configuration"
|
|
50
|
+
DEFAULT_CONFIG
|
|
51
|
+
end
|
|
52
|
+
else
|
|
53
|
+
DEFAULT_CONFIG
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def start
|
|
58
|
+
@server = TCPServer.new('127.0.0.1', @port)
|
|
59
|
+
puts "Notification server listening on port #{@port}..."
|
|
60
|
+
puts "VM can now send notifications to this host."
|
|
61
|
+
puts ""
|
|
62
|
+
puts "Configuration:"
|
|
63
|
+
puts " Show types: #{@config['show_types'].join(', ')}"
|
|
64
|
+
puts " Default timeout: #{@config['default_timeout'] == 0 ? 'permanent' : "#{@config['default_timeout']}s"}"
|
|
65
|
+
puts " Require URL: #{@config['require_url']}"
|
|
66
|
+
puts ""
|
|
67
|
+
puts "Press Ctrl+C to stop."
|
|
68
|
+
|
|
69
|
+
trap("INT") do
|
|
70
|
+
puts "\nShutting down notification server..."
|
|
71
|
+
@server.close if @server
|
|
72
|
+
exit
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
loop do
|
|
76
|
+
client = @server.accept
|
|
77
|
+
handle_client(client)
|
|
78
|
+
end
|
|
79
|
+
rescue Errno::EADDRINUSE
|
|
80
|
+
puts "Error: Port #{@port} is already in use."
|
|
81
|
+
puts "Another notification server may already be running."
|
|
82
|
+
exit 1
|
|
83
|
+
rescue => e
|
|
84
|
+
puts "Error starting notification server: #{e.message}"
|
|
85
|
+
exit 1
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def handle_client(client)
|
|
91
|
+
request = client.gets
|
|
92
|
+
return unless request
|
|
93
|
+
|
|
94
|
+
# Parse protocol: NOTIFY|title|message|url|timeout|type (url, timeout, type optional)
|
|
95
|
+
parts = request.strip.split('|', 6)
|
|
96
|
+
if parts[0] == 'NOTIFY' && parts.length >= 3
|
|
97
|
+
title = parts[1]
|
|
98
|
+
message = parts[2]
|
|
99
|
+
url = parts[3] if parts.length >= 4 && !parts[3].to_s.strip.empty?
|
|
100
|
+
timeout_str = parts[4] if parts.length >= 5
|
|
101
|
+
timeout = timeout_str.to_i if timeout_str && !timeout_str.strip.empty?
|
|
102
|
+
type = parts[5] if parts.length >= 6
|
|
103
|
+
type = 'info' if !type || type.strip.empty?
|
|
104
|
+
type = type.strip if type
|
|
105
|
+
|
|
106
|
+
# Filter based on configuration
|
|
107
|
+
if should_show_notification?(type, url)
|
|
108
|
+
# Apply configured timeout
|
|
109
|
+
timeout = get_timeout_for_type(type) if timeout.nil?
|
|
110
|
+
send_notification(title, message, url, timeout, type)
|
|
111
|
+
client.puts "OK"
|
|
112
|
+
else
|
|
113
|
+
client.puts "FILTERED"
|
|
114
|
+
end
|
|
115
|
+
else
|
|
116
|
+
client.puts "ERROR: Invalid format. Use: NOTIFY|title|message|url|timeout|type"
|
|
117
|
+
end
|
|
118
|
+
ensure
|
|
119
|
+
client.close
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def should_show_notification?(type, url)
|
|
123
|
+
# Check if this type should be shown
|
|
124
|
+
return false unless @config['show_types'].include?(type)
|
|
125
|
+
|
|
126
|
+
# Check if URL is required
|
|
127
|
+
return false if @config['require_url'] && (url.nil? || url.empty?)
|
|
128
|
+
|
|
129
|
+
true
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def get_timeout_for_type(type)
|
|
133
|
+
@config['type_timeouts'][type] || @config['default_timeout']
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def send_notification(title, message, url = nil, timeout = nil, type = 'info')
|
|
137
|
+
# Detect OS and send notification
|
|
138
|
+
case RbConfig::CONFIG['host_os']
|
|
139
|
+
when /darwin|mac os/
|
|
140
|
+
send_macos_notification(title, message, url, timeout, type)
|
|
141
|
+
when /linux/
|
|
142
|
+
send_linux_notification(title, message, url, timeout, type)
|
|
143
|
+
else
|
|
144
|
+
puts "[#{type}] #{title}: #{message}"
|
|
145
|
+
puts " URL: #{url}" if url
|
|
146
|
+
puts " Timeout: #{timeout}s" if timeout
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def send_macos_notification(title, message, url = nil, timeout = nil, type = 'info')
|
|
151
|
+
# Try terminal-notifier first (more features including clickable notifications)
|
|
152
|
+
if system('which terminal-notifier > /dev/null 2>&1')
|
|
153
|
+
args = ['-title', title, '-message', message]
|
|
154
|
+
args += ['-sound', 'default'] if @config['enable_sound']
|
|
155
|
+
args += ['-open', url] if url
|
|
156
|
+
# terminal-notifier timeout is in seconds, 0 means "stick around until dismissed"
|
|
157
|
+
args += ['-timeout', timeout.to_s] if timeout && timeout > 0
|
|
158
|
+
system('terminal-notifier', *args)
|
|
159
|
+
else
|
|
160
|
+
# Fall back to osascript (not clickable, no timeout control)
|
|
161
|
+
escaped_title = title.gsub('"', '\"')
|
|
162
|
+
escaped_message = message.gsub('"', '\"')
|
|
163
|
+
system("osascript -e 'display notification \"#{escaped_message}\" with title \"#{escaped_title}\"'")
|
|
164
|
+
puts " (Install terminal-notifier for clickable notifications and timeout control: brew install terminal-notifier)" if url || timeout
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def send_linux_notification(title, message, url = nil, timeout = nil, type = 'info')
|
|
169
|
+
# notify-send supports -t for timeout in milliseconds
|
|
170
|
+
args = [title]
|
|
171
|
+
|
|
172
|
+
# Add URL to message if provided
|
|
173
|
+
if url
|
|
174
|
+
full_message = "#{message}\n\n🔗 #{url}"
|
|
175
|
+
args << full_message
|
|
176
|
+
else
|
|
177
|
+
args << message
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Add timeout if specified (convert seconds to milliseconds, 0 means no timeout)
|
|
181
|
+
if timeout && timeout > 0
|
|
182
|
+
args += ['-t', (timeout * 1000).to_s]
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
system('notify-send', *args)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
require_relative "path_fixer"
|
|
2
|
+
require 'fileutils'
|
|
3
|
+
|
|
4
|
+
# Set Docker as the default provider unless already specified
|
|
5
|
+
ENV['VAGRANT_DEFAULT_PROVIDER'] ||= 'docker'
|
|
2
6
|
|
|
3
7
|
# Fix Docker PATH issues immediately when plugin loads (before Vagrant initialization)
|
|
4
8
|
VagrantPlugins::ClaudeSandbox::PathFixer.fix_docker_path!
|
|
@@ -18,6 +22,40 @@ module VagrantPlugins
|
|
|
18
22
|
require_relative "command"
|
|
19
23
|
Command
|
|
20
24
|
end
|
|
25
|
+
|
|
26
|
+
# Check for required plugins on environment load
|
|
27
|
+
action_hook(:check_dependencies, :environment_load) do |hook|
|
|
28
|
+
hook.after(Vagrant::Action::Builtin::HandleBox, Action::CheckDependencies)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Action to check for vagrant-notify-forwarder plugin
|
|
33
|
+
module Action
|
|
34
|
+
class CheckDependencies
|
|
35
|
+
def initialize(app, env)
|
|
36
|
+
@app = app
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def call(env)
|
|
40
|
+
# Check if vagrant-notify-forwarder is installed
|
|
41
|
+
unless plugin_installed?("vagrant-notify-forwarder")
|
|
42
|
+
env[:ui].warn("vagrant-notify-forwarder plugin is not installed.")
|
|
43
|
+
env[:ui].warn("This plugin enables real-time filesystem change notifications from host to guest,")
|
|
44
|
+
env[:ui].warn("improving performance for development tools like webpack, nodemon, etc.")
|
|
45
|
+
env[:ui].warn("")
|
|
46
|
+
env[:ui].info("Install it with: vagrant plugin install vagrant-notify-forwarder")
|
|
47
|
+
env[:ui].warn("")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
@app.call(env)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def plugin_installed?(plugin_name)
|
|
56
|
+
Vagrant::Plugin::Manager.instance.installed_plugins.key?(plugin_name)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
21
59
|
end
|
|
22
60
|
end
|
|
23
61
|
end
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Claude Code notification helper
|
|
3
|
+
# Sends rich notifications with project context and optional URLs
|
|
4
|
+
|
|
5
|
+
# Don't use set -e because we handle non-zero exit codes explicitly
|
|
6
|
+
set +e
|
|
7
|
+
|
|
8
|
+
# Configuration
|
|
9
|
+
# Read port from config file (written by vagrant trigger), default to 29325
|
|
10
|
+
if [ -f "/agent-workspace/.vagrant-notification-port" ]; then
|
|
11
|
+
HOST_PORT=$(cat /agent-workspace/.vagrant-notification-port)
|
|
12
|
+
else
|
|
13
|
+
HOST_PORT=29325
|
|
14
|
+
fi
|
|
15
|
+
NOTIFICATION_TIMEOUT=1
|
|
16
|
+
|
|
17
|
+
# Colors for terminal output
|
|
18
|
+
RED='\033[0;31m'
|
|
19
|
+
GREEN='\033[0;32m'
|
|
20
|
+
YELLOW='\033[1;33m'
|
|
21
|
+
NC='\033[0m' # No Color
|
|
22
|
+
|
|
23
|
+
# Usage information
|
|
24
|
+
usage() {
|
|
25
|
+
cat << EOF
|
|
26
|
+
Usage: claude-notify [OPTIONS] TITLE [MESSAGE]
|
|
27
|
+
|
|
28
|
+
Send a notification from Claude Code to the host machine.
|
|
29
|
+
|
|
30
|
+
OPTIONS:
|
|
31
|
+
-u, --url URL Add clickable URL to notification
|
|
32
|
+
-s, --session ID Add session/conversation ID
|
|
33
|
+
-p, --project PATH Project path (default: current directory)
|
|
34
|
+
-t, --timeout SECONDS How long notification stays visible (default: from config)
|
|
35
|
+
--type TYPE Notification type (default: auto-detect)
|
|
36
|
+
-h, --help Show this help message
|
|
37
|
+
|
|
38
|
+
NOTIFICATION TYPES:
|
|
39
|
+
info - General information (default)
|
|
40
|
+
success - Success messages
|
|
41
|
+
error - Error messages (high priority)
|
|
42
|
+
warning - Warning messages (high priority)
|
|
43
|
+
needs_input - Claude needs user input (high priority)
|
|
44
|
+
task_complete - Task completed (default shown)
|
|
45
|
+
task_start - Task started (default filtered)
|
|
46
|
+
|
|
47
|
+
DEFAULT BEHAVIOR:
|
|
48
|
+
- Only task_complete, needs_input, error, warning shown
|
|
49
|
+
- Notifications are permanent (stay until dismissed)
|
|
50
|
+
- Configure in ~/.vagrant-claude-sandbox/notification_config.yml
|
|
51
|
+
|
|
52
|
+
EXAMPLES:
|
|
53
|
+
# Task completion (shown by default)
|
|
54
|
+
claude-notify "Build Complete" "All tests passed!"
|
|
55
|
+
|
|
56
|
+
# Task completion with URL
|
|
57
|
+
claude-notify --type task_complete -u "https://claude.ai/chat/abc" "Done" "Ready for review"
|
|
58
|
+
|
|
59
|
+
# Needs input (shown by default)
|
|
60
|
+
claude-notify --type needs_input "Input Required" "Which API endpoint to use?"
|
|
61
|
+
|
|
62
|
+
# Task start (filtered by default)
|
|
63
|
+
claude-notify --type task_start "Building" "Starting build process..."
|
|
64
|
+
|
|
65
|
+
# Error (shown by default, permanent)
|
|
66
|
+
claude-notify --type error "Build Failed" "Check line 42"
|
|
67
|
+
|
|
68
|
+
ENVIRONMENT VARIABLES:
|
|
69
|
+
CLAUDE_SESSION_ID Current Claude conversation ID
|
|
70
|
+
CLAUDE_SESSION_URL Direct URL to current conversation
|
|
71
|
+
CLAUDE_PROJECT_PATH Current project directory
|
|
72
|
+
|
|
73
|
+
EOF
|
|
74
|
+
exit 0
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# Parse arguments
|
|
78
|
+
TITLE=""
|
|
79
|
+
MESSAGE=""
|
|
80
|
+
URL=""
|
|
81
|
+
SESSION_ID=""
|
|
82
|
+
TIMEOUT=""
|
|
83
|
+
TYPE=""
|
|
84
|
+
|
|
85
|
+
# Default to workspace directory if we're in /home/vagrant
|
|
86
|
+
if [ "$PWD" = "/home/vagrant" ] && [ -d "/agent-workspace" ]; then
|
|
87
|
+
PROJECT_PATH="${CLAUDE_PROJECT_PATH:-/agent-workspace}"
|
|
88
|
+
else
|
|
89
|
+
PROJECT_PATH="${CLAUDE_PROJECT_PATH:-$PWD}"
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
while [[ $# -gt 0 ]]; do
|
|
93
|
+
case $1 in
|
|
94
|
+
-h|--help)
|
|
95
|
+
usage
|
|
96
|
+
;;
|
|
97
|
+
-u|--url)
|
|
98
|
+
URL="$2"
|
|
99
|
+
shift 2
|
|
100
|
+
;;
|
|
101
|
+
-s|--session)
|
|
102
|
+
SESSION_ID="$2"
|
|
103
|
+
shift 2
|
|
104
|
+
;;
|
|
105
|
+
-p|--project)
|
|
106
|
+
PROJECT_PATH="$2"
|
|
107
|
+
shift 2
|
|
108
|
+
;;
|
|
109
|
+
-t|--timeout)
|
|
110
|
+
TIMEOUT="$2"
|
|
111
|
+
shift 2
|
|
112
|
+
;;
|
|
113
|
+
--type)
|
|
114
|
+
TYPE="$2"
|
|
115
|
+
shift 2
|
|
116
|
+
;;
|
|
117
|
+
*)
|
|
118
|
+
if [ -z "$TITLE" ]; then
|
|
119
|
+
TITLE="$1"
|
|
120
|
+
elif [ -z "$MESSAGE" ]; then
|
|
121
|
+
MESSAGE="$1"
|
|
122
|
+
else
|
|
123
|
+
echo -e "${RED}Error: Too many arguments${NC}" >&2
|
|
124
|
+
echo "Run 'claude-notify --help' for usage information" >&2
|
|
125
|
+
exit 1
|
|
126
|
+
fi
|
|
127
|
+
shift
|
|
128
|
+
;;
|
|
129
|
+
esac
|
|
130
|
+
done
|
|
131
|
+
|
|
132
|
+
# Validate required arguments
|
|
133
|
+
if [ -z "$TITLE" ]; then
|
|
134
|
+
echo -e "${RED}Error: TITLE is required${NC}" >&2
|
|
135
|
+
echo "Run 'claude-notify --help' for usage information" >&2
|
|
136
|
+
exit 1
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
# Use title as message if no message provided
|
|
140
|
+
if [ -z "$MESSAGE" ]; then
|
|
141
|
+
MESSAGE="$TITLE"
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
# Try to get session info from environment if not provided
|
|
145
|
+
if [ -z "$URL" ]; then
|
|
146
|
+
if [ -n "$CLAUDE_SESSION_URL" ]; then
|
|
147
|
+
URL="$CLAUDE_SESSION_URL"
|
|
148
|
+
elif [ -n "$SESSION_ID" ]; then
|
|
149
|
+
# Construct URL from session ID
|
|
150
|
+
URL="https://claude.ai/chat/${SESSION_ID}"
|
|
151
|
+
elif [ -n "$CLAUDE_SESSION_ID" ]; then
|
|
152
|
+
URL="https://claude.ai/chat/${CLAUDE_SESSION_ID}"
|
|
153
|
+
fi
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
# Detect project name intelligently
|
|
157
|
+
detect_project_name() {
|
|
158
|
+
local path="$1"
|
|
159
|
+
local name=""
|
|
160
|
+
|
|
161
|
+
# Try git remote first
|
|
162
|
+
if [ -d "$path/.git" ]; then
|
|
163
|
+
name=$(cd "$path" && git remote get-url origin 2>/dev/null | sed -E 's/.*\/(.+)(\.git)?$/\1/' | sed 's/\.git$//')
|
|
164
|
+
fi
|
|
165
|
+
|
|
166
|
+
# Try package.json
|
|
167
|
+
if [ -z "$name" ] && [ -f "$path/package.json" ]; then
|
|
168
|
+
name=$(grep -o '"name"[[:space:]]*:[[:space:]]*"[^"]*"' "$path/package.json" 2>/dev/null | head -1 | sed 's/"name"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/')
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
# Try Cargo.toml
|
|
172
|
+
if [ -z "$name" ] && [ -f "$path/Cargo.toml" ]; then
|
|
173
|
+
name=$(grep -o 'name[[:space:]]*=[[:space:]]*"[^"]*"' "$path/Cargo.toml" 2>/dev/null | head -1 | sed 's/name[[:space:]]*=[[:space:]]*"\([^"]*\)"/\1/')
|
|
174
|
+
fi
|
|
175
|
+
|
|
176
|
+
# Fall back to directory name
|
|
177
|
+
if [ -z "$name" ]; then
|
|
178
|
+
name=$(basename "$path")
|
|
179
|
+
fi
|
|
180
|
+
|
|
181
|
+
echo "$name"
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
# Auto-detect notification type from message content
|
|
185
|
+
detect_type() {
|
|
186
|
+
local title="$1"
|
|
187
|
+
local message="$2"
|
|
188
|
+
local combined="$title $message"
|
|
189
|
+
|
|
190
|
+
# Check for needs input keywords
|
|
191
|
+
if echo "$combined" | grep -qiE "(input|choose|select|confirm|approve|review|check|question)"; then
|
|
192
|
+
echo "needs_input"
|
|
193
|
+
return
|
|
194
|
+
fi
|
|
195
|
+
|
|
196
|
+
# Check for error keywords
|
|
197
|
+
if echo "$combined" | grep -qiE "(error|fail|failed|exception|critical|crash)"; then
|
|
198
|
+
echo "error"
|
|
199
|
+
return
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
# Check for warning keywords
|
|
203
|
+
if echo "$combined" | grep -qiE "(warning|warn|deprecated|caution|alert)"; then
|
|
204
|
+
echo "warning"
|
|
205
|
+
return
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
# Check for task complete keywords
|
|
209
|
+
if echo "$combined" | grep -qiE "(complete|completed|done|finished|success|passed|ready)"; then
|
|
210
|
+
echo "task_complete"
|
|
211
|
+
return
|
|
212
|
+
fi
|
|
213
|
+
|
|
214
|
+
# Check for task start keywords
|
|
215
|
+
if echo "$combined" | grep -qiE "(start|starting|begin|beginning|running|processing)"; then
|
|
216
|
+
echo "task_start"
|
|
217
|
+
return
|
|
218
|
+
fi
|
|
219
|
+
|
|
220
|
+
# Check for success keywords
|
|
221
|
+
if echo "$combined" | grep -qiE "(success|successful|ok|okay)"; then
|
|
222
|
+
echo "success"
|
|
223
|
+
return
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
# Default to info
|
|
227
|
+
echo "info"
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
# Enhance message with project context
|
|
231
|
+
PROJECT_NAME=$(detect_project_name "$PROJECT_PATH")
|
|
232
|
+
# Don't use newlines - they don't render properly
|
|
233
|
+
# Add project name to the title instead
|
|
234
|
+
ENHANCED_TITLE="[${PROJECT_NAME}] ${TITLE}"
|
|
235
|
+
ENHANCED_MESSAGE="${MESSAGE}"
|
|
236
|
+
|
|
237
|
+
# Auto-detect type if not specified
|
|
238
|
+
if [ -z "$TYPE" ]; then
|
|
239
|
+
TYPE=$(detect_type "$TITLE" "$MESSAGE")
|
|
240
|
+
fi
|
|
241
|
+
|
|
242
|
+
# Timeout defaults to empty (server will use configured default)
|
|
243
|
+
if [ -z "$TIMEOUT" ]; then
|
|
244
|
+
TIMEOUT=""
|
|
245
|
+
fi
|
|
246
|
+
|
|
247
|
+
# Determine host IP (Docker vs VirtualBox)
|
|
248
|
+
detect_host() {
|
|
249
|
+
# Try host.docker.internal first (Docker)
|
|
250
|
+
if getent hosts host.docker.internal > /dev/null 2>&1; then
|
|
251
|
+
echo "host.docker.internal"
|
|
252
|
+
return 0
|
|
253
|
+
fi
|
|
254
|
+
# Fall back to VirtualBox gateway
|
|
255
|
+
echo "10.0.2.2"
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
HOST_IP=$(detect_host)
|
|
259
|
+
|
|
260
|
+
# Send notification
|
|
261
|
+
send_notification() {
|
|
262
|
+
local payload="NOTIFY|${ENHANCED_TITLE}|${ENHANCED_MESSAGE}|${URL:-}|${TIMEOUT}|${TYPE}"
|
|
263
|
+
|
|
264
|
+
local response=$(echo "$payload" | nc -w "$NOTIFICATION_TIMEOUT" "$HOST_IP" "$HOST_PORT" 2>&1)
|
|
265
|
+
|
|
266
|
+
if [ "$response" = "OK" ]; then
|
|
267
|
+
return 0
|
|
268
|
+
elif [ "$response" = "FILTERED" ]; then
|
|
269
|
+
return 2 # Filtered by server config
|
|
270
|
+
else
|
|
271
|
+
return 1 # Connection failed
|
|
272
|
+
fi
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
# Try to send notification
|
|
276
|
+
send_notification
|
|
277
|
+
result=$?
|
|
278
|
+
|
|
279
|
+
if [ $result -eq 0 ]; then
|
|
280
|
+
echo -e "${GREEN}✓${NC} Notification sent to host [${TYPE}]"
|
|
281
|
+
[ -n "$URL" ] && echo -e " ${YELLOW}🔗${NC} Clickable URL: $URL"
|
|
282
|
+
[ -n "$TIMEOUT" ] && echo -e " ⏱ Timeout: ${TIMEOUT}s"
|
|
283
|
+
exit 0
|
|
284
|
+
elif [ $result -eq 2 ]; then
|
|
285
|
+
echo -e "${YELLOW}○${NC} Notification filtered by server config [${TYPE}]"
|
|
286
|
+
exit 0
|
|
287
|
+
else
|
|
288
|
+
echo -e "${YELLOW}⚠${NC} Could not reach notification server on host" >&2
|
|
289
|
+
echo -e " Make sure ${GREEN}vagrant-notify-server${NC} is running on your host machine" >&2
|
|
290
|
+
echo -e " Notification: ${TITLE} - ${MESSAGE}" >&2
|
|
291
|
+
|
|
292
|
+
# Fall back to local notification if available
|
|
293
|
+
if [ -f /usr/bin/notify-send.real ]; then
|
|
294
|
+
/usr/bin/notify-send.real "$TITLE" "$MESSAGE" 2>/dev/null || true
|
|
295
|
+
fi
|
|
296
|
+
|
|
297
|
+
# Log to syslog as last resort
|
|
298
|
+
logger "Claude Notification: $TITLE - $MESSAGE"
|
|
299
|
+
exit 1
|
|
300
|
+
fi
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: vagrant-claude-sandbox
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bero
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-02-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rake
|
|
@@ -41,18 +41,23 @@ dependencies:
|
|
|
41
41
|
description: Provides a pre-configured sandbox environment for running Claude Code
|
|
42
42
|
in an isolated VM with full plugin and skills support
|
|
43
43
|
email:
|
|
44
|
-
executables:
|
|
44
|
+
executables:
|
|
45
|
+
- vagrant-notify-server
|
|
45
46
|
extensions: []
|
|
46
47
|
extra_rdoc_files: []
|
|
47
48
|
files:
|
|
48
49
|
- LICENSE
|
|
49
50
|
- README.md
|
|
51
|
+
- bin/vagrant-notify-server
|
|
50
52
|
- lib/docker/Dockerfile
|
|
51
53
|
- lib/vagrant-claude-sandbox.rb
|
|
52
54
|
- lib/vagrant-claude-sandbox/command.rb
|
|
53
55
|
- lib/vagrant-claude-sandbox/config.rb
|
|
56
|
+
- lib/vagrant-claude-sandbox/notification_config.yml.example
|
|
57
|
+
- lib/vagrant-claude-sandbox/notification_server.rb
|
|
54
58
|
- lib/vagrant-claude-sandbox/path_fixer.rb
|
|
55
59
|
- lib/vagrant-claude-sandbox/plugin.rb
|
|
60
|
+
- lib/vagrant-claude-sandbox/scripts/claude-notify
|
|
56
61
|
- lib/vagrant-claude-sandbox/version.rb
|
|
57
62
|
homepage: https://github.com/bgrgicak/vagrant-claude-sandbox
|
|
58
63
|
licenses:
|