BroV look: swamp-zombie unibrow character, new app and menu bar icons, BroV naming in UI

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
maksarsanjeev
2026-10-06 19:53:47 +03:00
parent 0f77ec8704
commit 7d76986bc0
22 changed files with 422 additions and 54 deletions
+28 -28
View File
@@ -19,7 +19,7 @@ final class HookServer: @unchecked Sendable {
static var socketPath: String {
#if APPSTORE
// Container home root keeps path ≤ 103 bytes (sun_path limit on macOS is 104 incl. NUL)
// /Users/louis/Library/Containers/fr.louisraille.Coucou/Data/nb.sock = 66 bytes ✓
// /Users/louis/Library/Containers/fr.louisraille.BroV/Data/nb.sock = 66 bytes ✓
return NSHomeDirectory() + "/nb.sock"
#else
return supportDir.appendingPathComponent("nb.sock").path
@@ -1093,7 +1093,7 @@ final class HookServer: @unchecked Sendable {
// MARK: - Outdated hook detection
/// Returns true if settings.json has a Coucou hook that needs updating:
/// Returns true if settings.json has a BroV hook that needs updating:
/// either a PermissionRequest hook with timeout < 120s, or the AskUserQuestion
/// PreToolUse matcher is missing (requires Claude Code 2.1.85+).
static func hooksNeedUpdate() -> Bool {
@@ -1104,8 +1104,8 @@ final class HookServer: @unchecked Sendable {
let hooks = settings["hooks"] as? [String: Any] else {
return false
}
// Track whether any Coucou hook is installed at all
var hasCoucouHooks = false
// Track whether any BroV hook is installed at all
var hasBroVHooks = false
if let permReqHooks = hooks["PermissionRequest"] as? [[String: Any]] {
for matcher in permReqHooks {
@@ -1113,7 +1113,7 @@ final class HookServer: @unchecked Sendable {
for hook in hookList {
if let cmd = hook["command"] as? String,
cmd.contains("NotchBuddy") || cmd.contains("coucou") {
hasCoucouHooks = true
hasBroVHooks = true
if let timeout = hook["timeout"] as? Int, timeout < 120 { return true }
}
}
@@ -1122,7 +1122,7 @@ final class HookServer: @unchecked Sendable {
}
// Check that the AskUserQuestion PreToolUse entry exists
if hasCoucouHooks {
if hasBroVHooks {
let preToolHooks = hooks["PreToolUse"] as? [[String: Any]] ?? []
let hasAskEntry = preToolHooks.contains { m in
(m["matcher"] as? String) == "AskUserQuestion"
@@ -1540,7 +1540,7 @@ final class HookServer: @unchecked Sendable {
label: "~/.gemini/settings.json")
if let raw = settings["hooks"], !(raw is [String: Any]) {
throw NSError(domain: "Coucou", code: 2, userInfo: [
NSLocalizedDescriptionKey: "~/.gemini/settings.json: \"hooks\" has an unexpected type — Coucou has not touched it."
NSLocalizedDescriptionKey: "~/.gemini/settings.json: \"hooks\" has an unexpected type — BroV has not touched it."
])
}
let base = hookBase()
@@ -1557,7 +1557,7 @@ final class HookServer: @unchecked Sendable {
for (geminiEvent, normalizedEvent, timeout) in events {
if let raw = hooks[geminiEvent], !(raw is [[String: Any]]) {
throw NSError(domain: "Coucou", code: 2, userInfo: [
NSLocalizedDescriptionKey: "~/.gemini/settings.json: \"hooks\"[\"\(geminiEvent)\"] has an unexpected type — Coucou has not touched it."
NSLocalizedDescriptionKey: "~/.gemini/settings.json: \"hooks\"[\"\(geminiEvent)\"] has an unexpected type — BroV has not touched it."
])
}
var groups = hooks[geminiEvent] as? [[String: Any]] ?? []
@@ -1581,7 +1581,7 @@ final class HookServer: @unchecked Sendable {
label: "~/.gemini/settings.json")
if let raw = settings["hooks"], !(raw is [String: Any]) {
throw NSError(domain: "Coucou", code: 2, userInfo: [
NSLocalizedDescriptionKey: "~/.gemini/settings.json: \"hooks\" has an unexpected type — Coucou has not touched it."
NSLocalizedDescriptionKey: "~/.gemini/settings.json: \"hooks\" has an unexpected type — BroV has not touched it."
])
}
if var hooks = settings["hooks"] as? [String: Any] {
@@ -1679,12 +1679,12 @@ final class HookServer: @unchecked Sendable {
do { data = try Data(contentsOf: url) }
catch {
throw NSError(domain: "Coucou", code: 2, userInfo: [
NSLocalizedDescriptionKey: "\(label) cannot be read — Coucou has not touched it."
NSLocalizedDescriptionKey: "\(label) cannot be read — BroV has not touched it."
])
}
guard let obj = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] else {
throw NSError(domain: "Coucou", code: 2, userInfo: [
NSLocalizedDescriptionKey: "\(label) is not valid JSON — Coucou has not touched it."
NSLocalizedDescriptionKey: "\(label) is not valid JSON — BroV has not touched it."
])
}
return obj
@@ -1735,7 +1735,7 @@ final class HookServer: @unchecked Sendable {
FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".codex/hooks.json")
}
/// True when ~/.codex/hooks.json already routes Codex events to Coucou's nb-hook.
/// True when ~/.codex/hooks.json already routes Codex events to BroV's nb-hook.
static func codexHooksInstalled() -> Bool {
guard let data = try? Data(contentsOf: codexHooksURL),
let root = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any],
@@ -1790,7 +1790,7 @@ final class HookServer: @unchecked Sendable {
var root = try Self.strictReadJSONObject(at: Self.codexHooksURL, label: "~/.codex/hooks.json")
if let raw = root["hooks"], !(raw is [String: Any]) {
throw NSError(domain: "Coucou", code: 2, userInfo: [
NSLocalizedDescriptionKey: "~/.codex/hooks.json: \"hooks\" has an unexpected type — Coucou has not touched it."
NSLocalizedDescriptionKey: "~/.codex/hooks.json: \"hooks\" has an unexpected type — BroV has not touched it."
])
}
let base = hookBase()
@@ -1800,7 +1800,7 @@ final class HookServer: @unchecked Sendable {
("SessionStart", 10, nil),
("UserPromptSubmit", 10, nil),
("PreToolUse", 10, nil),
("PermissionRequest", 120, "Waiting for your answer in the notch (Coucou)"),
("PermissionRequest", 120, "Waiting for your answer in the notch (BroV)"),
("PostToolUse", 10, nil),
("Stop", 10, nil),
("SubagentStart", 10, nil),
@@ -1812,11 +1812,11 @@ final class HookServer: @unchecked Sendable {
for (event, timeout, statusMsg) in events {
if let raw = hooks[event], !(raw is [[String: Any]]) {
throw NSError(domain: "Coucou", code: 2, userInfo: [
NSLocalizedDescriptionKey: "~/.codex/hooks.json: \"hooks\"[\"\(event)\"] has an unexpected type — Coucou has not touched it."
NSLocalizedDescriptionKey: "~/.codex/hooks.json: \"hooks\"[\"\(event)\"] has an unexpected type — BroV has not touched it."
])
}
var groups = hooks[event] as? [[String: Any]] ?? []
// Remove existing Coucou entries
// Remove existing BroV entries
groups = removeNbHookEntries(from: groups)
var hookEntry: [String: Any] = [
"type": "command",
@@ -1836,7 +1836,7 @@ final class HookServer: @unchecked Sendable {
var root = try Self.strictReadJSONObject(at: Self.codexHooksURL, label: "~/.codex/hooks.json")
if let raw = root["hooks"], !(raw is [String: Any]) {
throw NSError(domain: "Coucou", code: 2, userInfo: [
NSLocalizedDescriptionKey: "~/.codex/hooks.json: \"hooks\" has an unexpected type — Coucou has not touched it."
NSLocalizedDescriptionKey: "~/.codex/hooks.json: \"hooks\" has an unexpected type — BroV has not touched it."
])
}
if var hooks = root["hooks"] as? [String: Any] {
@@ -1874,7 +1874,7 @@ extension Notification.Name {
private let nbHookShellWrapper = """
#!/bin/sh
# Coucou hook relay — always exits 0, never blocks Claude Code
# BroV hook relay — always exits 0, never blocks Claude Code
HOOK_DIR="$(dirname "$0")"
if xcode-select -p >/dev/null 2>&1; then
out=$(/usr/bin/python3 "$HOOK_DIR/nb-hook.py" "$@" 2>/dev/null)
@@ -1890,8 +1890,8 @@ exit 0
private let nbHookPythonGitHub = """
#!/usr/bin/env python3
# nb-hook.py — Coucou hook relay for Claude Code and third-party agents (GitHub version)
# Reads JSON from stdin, forwards to Coucou via Unix socket, translates response.
# nb-hook.py — BroV hook relay for Claude Code and third-party agents (GitHub version)
# Reads JSON from stdin, forwards to BroV via Unix socket, translates response.
import sys, json, os, socket
def normalize_event(name):
@@ -1948,7 +1948,7 @@ def main():
'~/Library/Application Support/NotchBuddy/nb.sock'
)
# --statusline mode: relay rate_limits to Coucou, then delegate to saved previous
# --statusline mode: relay rate_limits to BroV, then delegate to saved previous
if '--statusline' in sys.argv[1:]:
relay = {
'coucou_kind': 'statusline',
@@ -2074,7 +2074,7 @@ def main():
# socket_path is already defined above
if event == 'PermissionRequest':
# Block and wait for Coucou's decision (Claude Code allows up to 120s)
# Block and wait for BroV's decision (Claude Code allows up to 120s)
try:
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.settimeout(118)
@@ -2115,7 +2115,7 @@ def main():
sys.stdout.flush()
sys.exit(0)
elif decision == 'deny':
out = {'hookSpecificOutput': {'hookEventName': 'PermissionRequest', 'decision': {'behavior': 'deny', 'message': 'Denied from Coucou'}}}
out = {'hookSpecificOutput': {'hookEventName': 'PermissionRequest', 'decision': {'behavior': 'deny', 'message': 'Denied from BroV'}}}
sys.stdout.write(json.dumps(out) + '\\n')
sys.stdout.flush()
sys.exit(0)
@@ -2157,7 +2157,7 @@ sys.exit(0)
private let nbHookPythonAppStore = """
#!/usr/bin/env python3
# nb-hook.py — Coucou (App Store) hook relay for Claude Code and third-party agents
# nb-hook.py — BroV (App Store) hook relay for Claude Code and third-party agents
# Socket lives inside the sandboxed container; script runs outside the sandbox.
import sys, json, os, socket
@@ -2212,10 +2212,10 @@ def main():
return
socket_path = os.path.expanduser(
'~/Library/Containers/fr.louisraille.Coucou/Data/nb.sock'
'~/Library/Containers/fr.louisraille.BroV/Data/nb.sock'
)
# --statusline mode: relay rate_limits to Coucou, then delegate to saved previous
# --statusline mode: relay rate_limits to BroV, then delegate to saved previous
if '--statusline' in sys.argv[1:]:
relay = {
'coucou_kind': 'statusline',
@@ -2340,7 +2340,7 @@ def main():
# socket_path is already defined above
if event == 'PermissionRequest':
# Block and wait for Coucou's decision (Claude Code allows up to 120s)
# Block and wait for BroV's decision (Claude Code allows up to 120s)
try:
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.settimeout(118)
@@ -2381,7 +2381,7 @@ def main():
sys.stdout.flush()
sys.exit(0)
elif decision == 'deny':
out = {'hookSpecificOutput': {'hookEventName': 'PermissionRequest', 'decision': {'behavior': 'deny', 'message': 'Denied from Coucou'}}}
out = {'hookSpecificOutput': {'hookEventName': 'PermissionRequest', 'decision': {'behavior': 'deny', 'message': 'Denied from BroV'}}}
sys.stdout.write(json.dumps(out) + '\\n')
sys.stdout.flush()
sys.exit(0)