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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user