hiiro 0.1.360 → 0.1.362
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/CHANGELOG.md +12 -0
- data/README.md +24 -17
- data/bin/h-app +4 -6
- data/bin/h-bg +4 -9
- data/bin/h-branch +21 -21
- data/bin/h-claude +6 -6
- data/bin/h-config +2 -2
- data/bin/h-date +48 -0
- data/bin/h-notify +92 -257
- data/bin/h-pane +96 -137
- data/bin/h-pr +16 -10
- data/bin/h-project +7 -19
- data/bin/h-session +52 -106
- data/bin/h-time +48 -0
- data/bin/h-title +12 -44
- data/bin/h-window +24 -135
- data/bin/h-wtree +2 -2
- data/docs/README.md +10 -13
- data/docs/docker-testing-guide.md +6 -7
- data/docs/h-app.md +2 -2
- data/docs/h-bg.md +11 -59
- data/docs/h-branch.md +3 -4
- data/docs/h-claude.md +25 -25
- data/docs/h-config.md +4 -5
- data/docs/h-notify.md +33 -106
- data/docs/h-pane.md +36 -148
- data/docs/h-pr.md +2 -3
- data/docs/h-project.md +3 -3
- data/docs/h-queue.md +24 -24
- data/docs/h-service.md +8 -8
- data/docs/h-session.md +29 -126
- data/docs/h-subtask.md +3 -3
- data/docs/h-task.md +16 -28
- data/docs/h-title.md +5 -25
- data/docs/h-window.md +19 -118
- data/docs/h-wtree.md +3 -4
- data/docs/h.md +10 -12
- data/exe/h +1 -1
- data/lib/hiiro/background.rb +17 -14
- data/lib/hiiro/branch.rb +24 -3
- data/lib/hiiro/db.rb +4 -1
- data/lib/hiiro/effects.rb +2 -2
- data/lib/hiiro/git/pr.rb +13 -6
- data/lib/hiiro/herdr.rb +610 -0
- data/lib/hiiro/invocation.rb +16 -19
- data/lib/hiiro/options.rb +5 -1
- data/lib/hiiro/pinned_pr.rb +16 -3
- data/lib/hiiro/pinned_pr_manager.rb +1 -1
- data/lib/hiiro/queue.rb +93 -74
- data/lib/hiiro/service_manager.rb +54 -80
- data/lib/hiiro/tasks.rb +53 -91
- data/lib/hiiro/version.rb +1 -1
- data/lib/hiiro.rb +5 -6
- data/merge +47 -0
- data/plugins/notify.rb +2 -7
- data/plugins/project.rb +2 -2
- metadata +10 -17
- data/bin/h-buffer +0 -123
- data/bin/h-jumplist +0 -287
- data/docs/h-buffer.md +0 -133
- data/docs/h-jumplist.md +0 -111
- data/lib/hiiro/task_colors.rb +0 -38
- data/lib/hiiro/tmux/buffer.rb +0 -63
- data/lib/hiiro/tmux/buffers.rb +0 -58
- data/lib/hiiro/tmux/pane.rb +0 -122
- data/lib/hiiro/tmux/panes.rb +0 -81
- data/lib/hiiro/tmux/session.rb +0 -112
- data/lib/hiiro/tmux/sessions.rb +0 -61
- data/lib/hiiro/tmux/window.rb +0 -94
- data/lib/hiiro/tmux/windows.rb +0 -73
- data/lib/hiiro/tmux.rb +0 -384
data/bin/h-notify
CHANGED
|
@@ -1,52 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'hiiro'
|
|
5
|
+
require 'yaml'
|
|
6
6
|
|
|
7
7
|
TYPE_PRESETS = {
|
|
8
|
-
'success' => { prefix: '[OK]',
|
|
9
|
-
'error'
|
|
10
|
-
'info'
|
|
11
|
-
'warning' => { prefix: '[WARN]',
|
|
12
|
-
}
|
|
8
|
+
'success' => { prefix: '[OK]', title: 'Success', sound: 'done' },
|
|
9
|
+
'error' => { prefix: '[ERR]', title: 'Error', sound: 'request' },
|
|
10
|
+
'info' => { prefix: '[INFO]', title: 'Info', sound: 'none' },
|
|
11
|
+
'warning' => { prefix: '[WARN]', title: 'Warning', sound: 'request' },
|
|
12
|
+
}.freeze
|
|
13
13
|
|
|
14
14
|
LOG_FILE = Hiiro::Config.data_path('notify_log.yml')
|
|
15
|
-
|
|
16
|
-
TMUX_CONF = File.join(Dir.home, '.tmux.conf')
|
|
17
|
-
NOTIFY_CONF = File.join(Dir.home, '.config', 'tmux', 'h-notify.tmux.conf')
|
|
18
15
|
CLAUDE_SETTINGS = File.join(Dir.home, '.claude', 'settings.json')
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
NOTIFY_TMUX_CONF_CONTENT = <<~'CONF'
|
|
23
|
-
# --- Notify hooks (clean up notifications when panes/windows/sessions close) ---
|
|
24
|
-
set-hook -g after-kill-pane "run-shell -b 'h notify remove_pane \#{hook_pane_id}'"
|
|
25
|
-
set-hook -g session-closed "run-shell -b 'h notify remove_session \#{hook_session_name}'"
|
|
26
|
-
# set-hook -g window-unlinked "run-shell -b 'h notify remove_window \#{hook_window_id}'"
|
|
27
|
-
|
|
28
|
-
# --- Notify keybinding (prefix + N to open notification menu) ---
|
|
29
|
-
bind-key N run-shell "h notify menu"
|
|
30
|
-
CONF
|
|
31
|
-
|
|
32
|
-
CLAUDE_NOTIFICATION_CMD = %q(MSG=$(cat | jq -r '.message // "Needs input"'); h alert -t 'Claude Code' -m "$MSG" -s k -c "echo switchc -t '$TMUX_PANE' | pbcopy"; h notify push -t info "$MSG")
|
|
33
|
-
CLAUDE_STOP_CMD = %q(h alert -t 'Claude Code' -m 'Work completed' -s b -c "echo switchc -t '$TMUX_PANE' | pbcopy"; h notify push -t success 'Work completed')
|
|
16
|
+
CLAUDE_NOTIFICATION_CMD = %q(MSG=$(cat | jq -r '.message // "Needs input"'); h notify push -t info "$MSG")
|
|
17
|
+
CLAUDE_STOP_CMD = %q(h notify push -t success 'Work completed')
|
|
34
18
|
|
|
35
19
|
def read_log
|
|
36
20
|
return [] unless File.exist?(LOG_FILE)
|
|
21
|
+
|
|
37
22
|
data = YAML.safe_load(File.read(LOG_FILE))
|
|
38
|
-
if data.is_a?(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
migrated = data.values.flatten.compact.sort_by { |e| -(e['timestamp'] || 0) }
|
|
43
|
-
write_log(migrated)
|
|
44
|
-
migrated
|
|
45
|
-
elsif data.is_a?(Array)
|
|
46
|
-
data
|
|
47
|
-
else
|
|
48
|
-
[]
|
|
49
|
-
end
|
|
23
|
+
return data if data.is_a?(Array)
|
|
24
|
+
return [] unless data.is_a?(Hash)
|
|
25
|
+
|
|
26
|
+
data.values.flatten.compact.sort_by { |entry| -(entry['timestamp'] || 0) }
|
|
50
27
|
rescue
|
|
51
28
|
[]
|
|
52
29
|
end
|
|
@@ -56,304 +33,162 @@ def write_log(data)
|
|
|
56
33
|
File.write(LOG_FILE, data.to_yaml)
|
|
57
34
|
end
|
|
58
35
|
|
|
59
|
-
def current_pane
|
|
60
|
-
# Prefer $TMUX_PANE env var — it's reliably set even in hook subprocesses
|
|
61
|
-
env_id = ENV['TMUX_PANE'].to_s
|
|
62
|
-
if !env_id.empty?
|
|
63
|
-
Hiiro::Tmux::Panes.fetch(all: true).find_by_id(env_id)
|
|
64
|
-
else
|
|
65
|
-
Hiiro::Tmux::Pane.current
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def pane_exists?(pane_id)
|
|
70
|
-
Hiiro::Tmux::Panes.fetch(all: true).find_by_id(pane_id)
|
|
71
|
-
end
|
|
72
|
-
|
|
73
36
|
Hiiro.run(*ARGV, tasks: true) do
|
|
74
37
|
@opts = Hiiro::Options.parse(args) do
|
|
75
38
|
option(:type, short: 't', desc: 'Notification type (success, error, info, warning)', default: 'info')
|
|
76
39
|
end
|
|
77
|
-
|
|
78
40
|
@preset = TYPE_PRESETS[@opts.type] || TYPE_PRESETS['info']
|
|
79
41
|
|
|
42
|
+
live_panes = -> { herdr_client.panes(all: true) }
|
|
43
|
+
|
|
80
44
|
add_subcmd(:push) do
|
|
81
45
|
message = @opts.args.join(' ')
|
|
82
46
|
if message.empty?
|
|
83
|
-
puts
|
|
84
|
-
next
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
pane = current_pane
|
|
88
|
-
session = pane&.session_name || Hiiro::Tmux::Session.current&.name
|
|
89
|
-
|
|
90
|
-
unless pane && session
|
|
91
|
-
$stderr.puts "h notify push: not in a tmux session"
|
|
47
|
+
puts 'Usage: h notify push [-t TYPE] "message"'
|
|
92
48
|
next
|
|
93
49
|
end
|
|
94
50
|
|
|
51
|
+
pane = herdr_client.current_pane
|
|
95
52
|
entry = {
|
|
96
|
-
'
|
|
97
|
-
'
|
|
98
|
-
'
|
|
53
|
+
'workspace_id' => pane&.workspace_id || ENV['HERDR_WORKSPACE_ID'],
|
|
54
|
+
'tab_id' => pane&.tab_id || ENV['HERDR_TAB_ID'],
|
|
55
|
+
'pane_id' => pane&.id || ENV['HERDR_PANE_ID'],
|
|
56
|
+
'agent' => pane&.agent,
|
|
99
57
|
'timestamp' => Time.now.to_i,
|
|
100
|
-
'
|
|
101
|
-
'
|
|
102
|
-
|
|
103
|
-
}
|
|
58
|
+
'message' => message,
|
|
59
|
+
'type' => @opts.type,
|
|
60
|
+
}.compact
|
|
104
61
|
|
|
105
62
|
data = read_log
|
|
106
|
-
|
|
107
|
-
data.reject! { |e| e['pane_id'] == pane.id }
|
|
63
|
+
data.reject! { |existing| entry['pane_id'] && existing['pane_id'] == entry['pane_id'] }
|
|
108
64
|
data.unshift(entry)
|
|
109
65
|
write_log(data)
|
|
110
|
-
|
|
111
|
-
binpath = `command -v terminal-notifier`
|
|
112
|
-
system(binpath,
|
|
113
|
-
'-title', @preset[:title],
|
|
114
|
-
'-message', message,
|
|
115
|
-
'-sound', @preset[:sound])
|
|
66
|
+
herdr_client.notify(@preset[:title], body: message, sound: @preset[:sound])
|
|
116
67
|
end
|
|
117
68
|
|
|
118
69
|
add_subcmd(:ls) do
|
|
119
70
|
data = read_log
|
|
71
|
+
live_ids = live_panes.call.ids
|
|
72
|
+
current = data.select { |entry| !entry['pane_id'] || live_ids.include?(entry['pane_id']) }
|
|
73
|
+
removed = data.size - current.size
|
|
74
|
+
write_log(current) if removed.positive?
|
|
120
75
|
|
|
121
|
-
if
|
|
122
|
-
puts "No notifications."
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
puts "(#{pruned.size} stale entries removed.)" if pruned.any?
|
|
133
|
-
data.each_with_index do |e, i|
|
|
134
|
-
time_str = Time.at(e['timestamp']).strftime('%H:%M:%S')
|
|
135
|
-
prefix = TYPE_PRESETS[e['type']]&.dig(:prefix) || '[INFO]'
|
|
136
|
-
puts "%3d) %s [%s/%s] %s: %s (%s)" % [i, prefix, e['session'], e['pane_id'], e['pane_cmd'], e['message'], time_str]
|
|
137
|
-
end
|
|
138
|
-
end
|
|
76
|
+
if current.empty?
|
|
77
|
+
puts(removed.positive? ? "No notifications (#{removed} stale entries removed)." : 'No notifications.')
|
|
78
|
+
next
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
current.each_with_index do |entry, index|
|
|
82
|
+
time = Time.at(entry['timestamp']).strftime('%H:%M:%S')
|
|
83
|
+
prefix = TYPE_PRESETS[entry['type']]&.dig(:prefix) || '[INFO]'
|
|
84
|
+
location = [entry['workspace_id'], entry['tab_id'], entry['pane_id']].compact.join('/')
|
|
85
|
+
actor = entry['agent'] || 'pane'
|
|
86
|
+
puts "%3d) %s [%s] %s: %s (%s)" % [index, prefix, location, actor, entry['message'], time]
|
|
139
87
|
end
|
|
140
88
|
end
|
|
141
89
|
|
|
142
90
|
add_subcmd(:prune) do
|
|
143
|
-
data
|
|
144
|
-
live_ids =
|
|
145
|
-
|
|
146
|
-
write_log(
|
|
147
|
-
puts "Removed #{
|
|
91
|
+
data = read_log
|
|
92
|
+
live_ids = live_panes.call.ids
|
|
93
|
+
current = data.select { |entry| !entry['pane_id'] || live_ids.include?(entry['pane_id']) }
|
|
94
|
+
write_log(current)
|
|
95
|
+
puts "Removed #{data.size - current.size} stale notification(s). #{current.size} remaining."
|
|
148
96
|
end
|
|
149
97
|
|
|
150
98
|
add_subcmd(:menu) do
|
|
151
|
-
|
|
152
|
-
live_ids = Hiiro::Tmux::Panes.fetch(all: true).ids.to_set
|
|
153
|
-
entries = data.select { |e| live_ids.include?(e['pane_id']) }
|
|
154
|
-
write_log(entries) if entries.size != data.size
|
|
155
|
-
|
|
99
|
+
entries = read_log
|
|
156
100
|
if entries.empty?
|
|
101
|
+
puts 'No notifications.'
|
|
157
102
|
next
|
|
158
103
|
end
|
|
159
104
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
msg = e['message']
|
|
164
|
-
msg = msg[0..46] + '...' if msg.length > 50
|
|
165
|
-
prefix = TYPE_PRESETS[e['type']]&.dig(:prefix) || '[INFO]'
|
|
166
|
-
label = "#{idx}: #{prefix} [#{e['pane_id']}] #{e['pane_cmd']}: #{msg}"
|
|
167
|
-
menu_args.push(label, idx.to_s, "run-shell 'h notify jump #{idx}'")
|
|
105
|
+
mapping = entries.each_with_index.each_with_object({}) do |(entry, index), choices|
|
|
106
|
+
prefix = TYPE_PRESETS[entry['type']]&.dig(:prefix) || '[INFO]'
|
|
107
|
+
choices["#{prefix} #{entry['message']} [#{entry['pane_id'] || 'no pane'}]"] = index
|
|
168
108
|
end
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
system('tmux', 'display-menu', *menu_args)
|
|
109
|
+
index = fuzzyfind_from_map(mapping)
|
|
110
|
+
run_subcmd(:jump, index.to_s) if index
|
|
172
111
|
end
|
|
173
112
|
|
|
174
|
-
add_subcmd(:jump) do |
|
|
175
|
-
|
|
176
|
-
puts
|
|
113
|
+
add_subcmd(:jump) do |index_string = nil|
|
|
114
|
+
unless index_string
|
|
115
|
+
puts 'Usage: h notify jump INDEX'
|
|
177
116
|
next
|
|
178
117
|
end
|
|
179
118
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
next if index >= data.length
|
|
184
|
-
|
|
119
|
+
data = read_log
|
|
120
|
+
index = index_string.to_i
|
|
185
121
|
entry = data[index]
|
|
122
|
+
next unless entry
|
|
186
123
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
write_log(data)
|
|
190
|
-
next
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
system('tmux', 'switch-client', '-t', entry['session'])
|
|
194
|
-
system('tmux', 'select-window', '-t', entry['window_id'])
|
|
195
|
-
system('tmux', 'select-pane', '-t', entry['pane_id'])
|
|
196
|
-
|
|
124
|
+
herdr_client.focus_workspace(entry['workspace_id']) if entry['workspace_id']
|
|
125
|
+
herdr_client.focus_tab(entry['tab_id']) if entry['tab_id']
|
|
197
126
|
data.delete_at(index)
|
|
198
127
|
write_log(data)
|
|
199
128
|
end
|
|
200
129
|
|
|
201
|
-
add_subcmd(:clear)
|
|
202
|
-
write_log([])
|
|
203
|
-
end
|
|
130
|
+
add_subcmd(:clear) { write_log([]) }
|
|
204
131
|
|
|
205
|
-
# Called by tmux hook: after-kill-pane
|
|
206
132
|
add_subcmd(:remove_pane) do |pane_id = nil|
|
|
207
133
|
next unless pane_id
|
|
208
|
-
|
|
209
|
-
data.reject! { |e| e['pane_id'] == pane_id }
|
|
210
|
-
write_log(data)
|
|
134
|
+
write_log(read_log.reject { |entry| entry['pane_id'] == pane_id })
|
|
211
135
|
end
|
|
212
136
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
data = read_log
|
|
217
|
-
data.reject! { |e| e['window_id'] == window_id }
|
|
218
|
-
write_log(data)
|
|
137
|
+
add_subcmd(:remove_window, :remove_tab) do |tab_id = nil|
|
|
138
|
+
next unless tab_id
|
|
139
|
+
write_log(read_log.reject { |entry| entry['tab_id'] == tab_id })
|
|
219
140
|
end
|
|
220
141
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
data = read_log
|
|
225
|
-
data.reject! { |e| e['session'] == sess }
|
|
226
|
-
write_log(data)
|
|
142
|
+
add_subcmd(:remove_session, :remove_workspace) do |workspace_id = nil|
|
|
143
|
+
next unless workspace_id
|
|
144
|
+
write_log(read_log.reject { |entry| entry['workspace_id'] == workspace_id })
|
|
227
145
|
end
|
|
228
146
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
add_subcmd(:setup) do
|
|
234
|
-
FileUtils.mkdir_p(File.dirname(NOTIFY_CONF))
|
|
235
|
-
File.write(NOTIFY_CONF, NOTIFY_TMUX_CONF_CONTENT)
|
|
236
|
-
puts "Wrote #{NOTIFY_CONF}"
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
add_subcmd(:add_hooks) do
|
|
240
|
-
source_line = "source-file #{NOTIFY_CONF}"
|
|
241
|
-
|
|
242
|
-
if File.exist?(TMUX_CONF)
|
|
243
|
-
existing = File.read(TMUX_CONF)
|
|
244
|
-
if existing.include?(source_line)
|
|
245
|
-
puts "#{TMUX_CONF} already sources #{NOTIFY_CONF}"
|
|
246
|
-
else
|
|
247
|
-
File.open(TMUX_CONF, 'a') { |f| f.puts; f.puts source_line }
|
|
248
|
-
puts "Added '#{source_line}' to #{TMUX_CONF}"
|
|
249
|
-
end
|
|
250
|
-
else
|
|
251
|
-
File.write(TMUX_CONF, source_line + "\n")
|
|
252
|
-
puts "Created #{TMUX_CONF} with source line"
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
system('tmux', 'source-file', TMUX_CONF)
|
|
256
|
-
puts "Reloaded #{TMUX_CONF}"
|
|
257
|
-
end
|
|
258
|
-
|
|
259
|
-
add_subcmd(:reset_hooks) do
|
|
260
|
-
NOTIFY_TMUX_HOOKS.each do |hook|
|
|
261
|
-
system('tmux', 'set-hook', '-gu', hook)
|
|
262
|
-
puts "Unset tmux hook: #{hook}"
|
|
263
|
-
end
|
|
147
|
+
add_subcmd(:herdr) do
|
|
148
|
+
run_child(:herdr) do |child|
|
|
149
|
+
child.add_subcmd(:setup) do
|
|
150
|
+
puts 'No hooks are required. Hiiro uses Herdr notifications and live pane IDs directly.'
|
|
264
151
|
end
|
|
265
|
-
|
|
266
|
-
add_subcmd(:load_hooks) do
|
|
267
|
-
system('tmux', 'source-file', TMUX_CONF)
|
|
268
|
-
puts "Sourced #{TMUX_CONF}"
|
|
269
|
-
end
|
|
270
|
-
}.run
|
|
152
|
+
end
|
|
271
153
|
end
|
|
272
154
|
|
|
273
|
-
# --- h notify claude ---
|
|
274
|
-
|
|
275
155
|
add_subcmd(:claude) do
|
|
276
156
|
require 'json'
|
|
277
157
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
JSON.parse(File.read(CLAUDE_SETTINGS))
|
|
158
|
+
read_settings = lambda do
|
|
159
|
+
File.exist?(CLAUDE_SETTINGS) ? JSON.parse(File.read(CLAUDE_SETTINGS)) : {}
|
|
281
160
|
rescue JSON::ParserError
|
|
282
161
|
{}
|
|
283
162
|
end
|
|
163
|
+
write_settings = ->(settings) { File.write(CLAUDE_SETTINGS, JSON.pretty_generate(settings)) }
|
|
284
164
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
make_child {
|
|
290
|
-
add_subcmd(:setup) do
|
|
291
|
-
settings = read_claude_settings.call
|
|
165
|
+
run_child(:claude) do |child|
|
|
166
|
+
child.add_subcmd(:setup, :add_hooks) do
|
|
167
|
+
settings = read_settings.call
|
|
292
168
|
settings['hooks'] ||= {}
|
|
293
169
|
settings['hooks']['Notification'] = [{ 'hooks' => [{ 'type' => 'command', 'command' => CLAUDE_NOTIFICATION_CMD }] }]
|
|
294
|
-
settings['hooks']['Stop']
|
|
295
|
-
|
|
296
|
-
puts "Updated #{CLAUDE_SETTINGS}"
|
|
297
|
-
puts " Notification -> h alert + h notify push -t info"
|
|
298
|
-
puts " Stop -> h alert + h notify push -t success"
|
|
299
|
-
end
|
|
300
|
-
|
|
301
|
-
add_subcmd(:add_hooks) do
|
|
302
|
-
settings = read_claude_settings.call
|
|
303
|
-
unless File.exist?(CLAUDE_SETTINGS)
|
|
304
|
-
puts "#{CLAUDE_SETTINGS} not found"
|
|
305
|
-
next
|
|
306
|
-
end
|
|
307
|
-
|
|
308
|
-
settings['hooks'] ||= {}
|
|
309
|
-
|
|
310
|
-
# Inject h notify push into existing commands if not already present,
|
|
311
|
-
# otherwise write fresh hooks
|
|
312
|
-
%w[Notification Stop].each do |event|
|
|
313
|
-
existing_hooks = settings.dig('hooks', event, 0, 'hooks') || []
|
|
314
|
-
if existing_hooks.any? { |h| h['command']&.include?('h notify push') }
|
|
315
|
-
puts "#{event} hook already includes h notify push — skipping"
|
|
316
|
-
next
|
|
317
|
-
end
|
|
318
|
-
|
|
319
|
-
cmd = event == 'Notification' ? CLAUDE_NOTIFICATION_CMD : CLAUDE_STOP_CMD
|
|
320
|
-
settings['hooks'][event] = [{ 'hooks' => [{ 'type' => 'command', 'command' => cmd }] }]
|
|
321
|
-
puts "Set #{event} hook -> h alert + h notify push"
|
|
322
|
-
end
|
|
323
|
-
|
|
324
|
-
write_claude_settings.call(settings)
|
|
170
|
+
settings['hooks']['Stop'] = [{ 'hooks' => [{ 'type' => 'command', 'command' => CLAUDE_STOP_CMD }] }]
|
|
171
|
+
write_settings.call(settings)
|
|
172
|
+
puts "Updated #{CLAUDE_SETTINGS} with Herdr-backed Hiiro notifications"
|
|
325
173
|
end
|
|
326
174
|
|
|
327
|
-
add_subcmd(:reset_hooks) do
|
|
175
|
+
child.add_subcmd(:reset_hooks) do
|
|
328
176
|
unless File.exist?(CLAUDE_SETTINGS)
|
|
329
177
|
puts "#{CLAUDE_SETTINGS} not found"
|
|
330
178
|
next
|
|
331
179
|
end
|
|
332
180
|
|
|
333
|
-
settings =
|
|
334
|
-
settings['hooks'] ||= {}
|
|
335
|
-
|
|
181
|
+
settings = read_settings.call
|
|
336
182
|
%w[Notification Stop].each do |event|
|
|
337
183
|
hooks = settings.dig('hooks', event, 0, 'hooks') || []
|
|
338
|
-
hooks.
|
|
339
|
-
next unless h['command']&.include?('h notify push')
|
|
340
|
-
# Strip the h notify push portion from the command
|
|
341
|
-
h['command'] = h['command']
|
|
342
|
-
.split(';')
|
|
343
|
-
.reject { |part| part.strip.start_with?('h notify push') }
|
|
344
|
-
.join(';')
|
|
345
|
-
.strip
|
|
346
|
-
end
|
|
347
|
-
puts "Stripped h notify push from #{event} hook"
|
|
184
|
+
hooks.reject! { |hook| hook['command']&.include?('h notify push') }
|
|
348
185
|
end
|
|
349
|
-
|
|
350
|
-
write_claude_settings.call(settings)
|
|
186
|
+
write_settings.call(settings)
|
|
351
187
|
end
|
|
352
188
|
|
|
353
|
-
add_subcmd(:load_hooks) do
|
|
354
|
-
puts
|
|
355
|
-
puts "Restart claude to pick up changes to #{CLAUDE_SETTINGS}"
|
|
189
|
+
child.add_subcmd(:load_hooks) do
|
|
190
|
+
puts 'Claude Code loads hook settings on startup; restart it to pick up changes.'
|
|
356
191
|
end
|
|
357
|
-
|
|
192
|
+
end
|
|
358
193
|
end
|
|
359
194
|
end
|