ai-agent-session-center 1.0.0
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.
- package/README.md +618 -0
- package/bin/cli.js +20 -0
- package/hooks/dashboard-hook-codex.sh +67 -0
- package/hooks/dashboard-hook-gemini.sh +102 -0
- package/hooks/dashboard-hook.ps1 +147 -0
- package/hooks/dashboard-hook.sh +142 -0
- package/hooks/dashboard-hooks-backup.json +103 -0
- package/hooks/install-hooks.js +543 -0
- package/hooks/reset.js +357 -0
- package/hooks/setup-wizard.js +156 -0
- package/package.json +52 -0
- package/public/css/dashboard.css +10200 -0
- package/public/index.html +915 -0
- package/public/js/analyticsPanel.js +467 -0
- package/public/js/app.js +1148 -0
- package/public/js/browserDb.js +806 -0
- package/public/js/chartUtils.js +383 -0
- package/public/js/historyPanel.js +298 -0
- package/public/js/movementManager.js +155 -0
- package/public/js/navController.js +32 -0
- package/public/js/robotManager.js +526 -0
- package/public/js/sceneManager.js +7 -0
- package/public/js/sessionPanel.js +2477 -0
- package/public/js/settingsManager.js +924 -0
- package/public/js/soundManager.js +249 -0
- package/public/js/statsPanel.js +118 -0
- package/public/js/terminalManager.js +391 -0
- package/public/js/timelinePanel.js +278 -0
- package/public/js/wsClient.js +88 -0
- package/server/apiRouter.js +321 -0
- package/server/config.js +120 -0
- package/server/hookProcessor.js +55 -0
- package/server/hookRouter.js +18 -0
- package/server/hookStats.js +107 -0
- package/server/index.js +314 -0
- package/server/logger.js +67 -0
- package/server/mqReader.js +218 -0
- package/server/serverConfig.js +27 -0
- package/server/sessionStore.js +1049 -0
- package/server/sshManager.js +339 -0
- package/server/wsManager.js +83 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# AI Agent Session Center - Gemini CLI hook relay (macOS / Linux)
|
|
3
|
+
# Receives Gemini event name as $1, reads hook JSON from stdin.
|
|
4
|
+
# Maps Gemini events to dashboard-compatible format, enriches with env info,
|
|
5
|
+
# and delivers to the dashboard server via file-based MQ or HTTP.
|
|
6
|
+
#
|
|
7
|
+
# Key difference from Claude hook:
|
|
8
|
+
# - Gemini hooks are SYNCHRONOUS — must print response to stdout immediately
|
|
9
|
+
# - Event name comes as $1 argument (registered per-event in settings.json)
|
|
10
|
+
# - Session ID from GEMINI_SESSION_ID env var
|
|
11
|
+
# - CWD from GEMINI_CWD env var
|
|
12
|
+
|
|
13
|
+
GEMINI_EVENT="${1:-unknown}"
|
|
14
|
+
SENT_AT=$(date +%s)
|
|
15
|
+
INPUT=$(cat)
|
|
16
|
+
|
|
17
|
+
# Gemini hooks are blocking — respond immediately to allow execution
|
|
18
|
+
echo '{"decision":"allow"}'
|
|
19
|
+
|
|
20
|
+
# --- Everything below runs in background so the hook returns instantly ---
|
|
21
|
+
{
|
|
22
|
+
|
|
23
|
+
# ── Map Gemini events to dashboard-compatible event names ──
|
|
24
|
+
case "$GEMINI_EVENT" in
|
|
25
|
+
SessionStart) MAPPED_EVENT="SessionStart" ;;
|
|
26
|
+
BeforeAgent) MAPPED_EVENT="UserPromptSubmit" ;;
|
|
27
|
+
BeforeTool) MAPPED_EVENT="PreToolUse" ;;
|
|
28
|
+
AfterTool) MAPPED_EVENT="PostToolUse" ;;
|
|
29
|
+
AfterAgent) MAPPED_EVENT="Stop" ;;
|
|
30
|
+
SessionEnd) MAPPED_EVENT="SessionEnd" ;;
|
|
31
|
+
Notification) MAPPED_EVENT="Notification" ;;
|
|
32
|
+
*) MAPPED_EVENT="$GEMINI_EVENT" ;;
|
|
33
|
+
esac
|
|
34
|
+
|
|
35
|
+
# ── Session/CWD from Gemini env vars ──
|
|
36
|
+
SESSION_ID="${GEMINI_SESSION_ID:-}"
|
|
37
|
+
CWD="${GEMINI_CWD:-$(pwd)}"
|
|
38
|
+
|
|
39
|
+
# ── TTY detection (cached per PID) ──
|
|
40
|
+
HOOK_TTY=""
|
|
41
|
+
if [ -n "$PPID" ] && [ "$PPID" != "0" ]; then
|
|
42
|
+
TTY_CACHE="/tmp/gemini-tty-cache"
|
|
43
|
+
TTY_CACHE_FILE="$TTY_CACHE/$PPID"
|
|
44
|
+
if [ -f "$TTY_CACHE_FILE" ]; then
|
|
45
|
+
HOOK_TTY=$(cat "$TTY_CACHE_FILE" 2>/dev/null)
|
|
46
|
+
else
|
|
47
|
+
RAW_TTY=$(ps -o tty= -p "$PPID" 2>/dev/null | tr -d ' ')
|
|
48
|
+
if [ -n "$RAW_TTY" ] && [ "$RAW_TTY" != "??" ] && [ "$RAW_TTY" != "?" ]; then
|
|
49
|
+
HOOK_TTY="/dev/${RAW_TTY}"
|
|
50
|
+
mkdir -p "$TTY_CACHE" 2>/dev/null
|
|
51
|
+
echo "$HOOK_TTY" > "$TTY_CACHE_FILE" 2>/dev/null
|
|
52
|
+
fi
|
|
53
|
+
fi
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# ── Single jq pass: build enriched JSON ──
|
|
57
|
+
ENRICHED=$(echo "$INPUT" | jq -c \
|
|
58
|
+
--arg event "$MAPPED_EVENT" \
|
|
59
|
+
--arg session_id "$SESSION_ID" \
|
|
60
|
+
--arg cwd "$CWD" \
|
|
61
|
+
--arg pid "$PPID" \
|
|
62
|
+
--arg tty "$HOOK_TTY" \
|
|
63
|
+
--arg sent_at "$SENT_AT" \
|
|
64
|
+
--arg agent_terminal_id "${AGENT_MANAGER_TERMINAL_ID:-}" \
|
|
65
|
+
--arg gemini_event "$GEMINI_EVENT" \
|
|
66
|
+
'
|
|
67
|
+
{
|
|
68
|
+
hook_event_name: $event,
|
|
69
|
+
session_id: (if $session_id != "" then $session_id else (.session_id // null) end),
|
|
70
|
+
cwd: (if $cwd != "" then $cwd else (.cwd // null) end),
|
|
71
|
+
claude_pid: ($pid | tonumber),
|
|
72
|
+
hook_sent_at: (($sent_at | tonumber) * 1000),
|
|
73
|
+
tty_path: (if $tty != "" then $tty else null end),
|
|
74
|
+
agent_terminal_id: (if $agent_terminal_id != "" then $agent_terminal_id else null end),
|
|
75
|
+
tool_name: (.tool_name // null),
|
|
76
|
+
tool_input: (.tool_input // null),
|
|
77
|
+
prompt: (.prompt // .llm_request // null),
|
|
78
|
+
response: (.response // .llm_response // .prompt_response // null),
|
|
79
|
+
model: (.model // null),
|
|
80
|
+
source: "gemini",
|
|
81
|
+
gemini_event: $gemini_event
|
|
82
|
+
}
|
|
83
|
+
' 2>/dev/null)
|
|
84
|
+
|
|
85
|
+
[ -z "$ENRICHED" ] && ENRICHED="{\"hook_event_name\":\"$MAPPED_EVENT\",\"session_id\":\"$SESSION_ID\",\"cwd\":\"$CWD\",\"source\":\"gemini\"}"
|
|
86
|
+
|
|
87
|
+
# ── Deliver to dashboard via file-based MQ (primary) or HTTP (fallback) ──
|
|
88
|
+
MQ_DIR="/tmp/claude-session-center"
|
|
89
|
+
MQ_FILE="$MQ_DIR/queue.jsonl"
|
|
90
|
+
|
|
91
|
+
if [ -d "$MQ_DIR" ]; then
|
|
92
|
+
echo "$ENRICHED" >> "$MQ_FILE" 2>/dev/null
|
|
93
|
+
else
|
|
94
|
+
echo "$ENRICHED" | curl -s --connect-timeout 1 -m 3 -X POST \
|
|
95
|
+
-H "Content-Type: application/json" \
|
|
96
|
+
--data-binary @- \
|
|
97
|
+
http://localhost:3333/api/hooks &>/dev/null
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
} &>/dev/null &
|
|
101
|
+
disown
|
|
102
|
+
exit 0
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# AI Agent Session Center - Hook relay (Windows)
|
|
2
|
+
# Reads hook JSON from stdin, enriches with process/env info, POSTs to dashboard server
|
|
3
|
+
# Runs in background, fails silently if server is not running
|
|
4
|
+
|
|
5
|
+
$ErrorActionPreference = 'SilentlyContinue'
|
|
6
|
+
$input_json = [Console]::In.ReadToEnd()
|
|
7
|
+
|
|
8
|
+
if (-not $input_json) { exit 0 }
|
|
9
|
+
|
|
10
|
+
# Gather environment info
|
|
11
|
+
$claude_pid = (Get-Process -Id $PID).Parent.Id # Parent of PowerShell = Claude process
|
|
12
|
+
$vscode_pid = $env:VSCODE_PID
|
|
13
|
+
$term_program = $env:TERM_PROGRAM
|
|
14
|
+
$wt_session = $env:WT_SESSION # Windows Terminal session GUID
|
|
15
|
+
$wt_profile = $env:WT_PROFILE_ID # Windows Terminal profile ID
|
|
16
|
+
$conemu_pid = $env:ConEmuPID # ConEmu/Cmder PID
|
|
17
|
+
$term = $env:TERM
|
|
18
|
+
|
|
19
|
+
# Build enrichment object
|
|
20
|
+
$enrich = @{
|
|
21
|
+
claude_pid = if ($claude_pid) { [int]$claude_pid } else { $null }
|
|
22
|
+
term_program = if ($term_program) { $term_program } else { $null }
|
|
23
|
+
vscode_pid = if ($vscode_pid) { [int]$vscode_pid } else { $null }
|
|
24
|
+
term = if ($term) { $term } else { $null }
|
|
25
|
+
tab_id = if ($wt_session) { "wt:$wt_session" } elseif ($conemu_pid) { "conemu:$conemu_pid" } else { $null }
|
|
26
|
+
wt_profile = if ($wt_profile) { $wt_profile } else { $null }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# Merge enrichment into the hook JSON
|
|
30
|
+
try {
|
|
31
|
+
$data = $input_json | ConvertFrom-Json
|
|
32
|
+
foreach ($key in $enrich.Keys) {
|
|
33
|
+
if ($null -ne $enrich[$key]) {
|
|
34
|
+
$data | Add-Member -NotePropertyName $key -NotePropertyValue $enrich[$key] -Force
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
$enriched = $data | ConvertTo-Json -Compress -Depth 10
|
|
38
|
+
} catch {
|
|
39
|
+
$enriched = $input_json
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# ---- Tab title management ----
|
|
43
|
+
# Keep the terminal tab/window title set to "Claude: <project>" so the dashboard can find it.
|
|
44
|
+
# On SessionStart: resolve project name and cache to temp file.
|
|
45
|
+
# On every other event: read cache and refresh the title.
|
|
46
|
+
# Uses console title (works on Windows Terminal, ConEmu, cmd, PowerShell, VS Code, JetBrains).
|
|
47
|
+
# Works in all terminals including VS Code and JetBrains integrated terminals.
|
|
48
|
+
try {
|
|
49
|
+
$hookEvent = ($input_json | ConvertFrom-Json).hook_event_name
|
|
50
|
+
$sessionId = ($input_json | ConvertFrom-Json).session_id
|
|
51
|
+
} catch {
|
|
52
|
+
$hookEvent = $null
|
|
53
|
+
$sessionId = $null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
$cacheDir = "$env:TEMP\claude-tab-titles"
|
|
57
|
+
if ($sessionId) {
|
|
58
|
+
$cacheFile = "$cacheDir\$sessionId"
|
|
59
|
+
|
|
60
|
+
if ($hookEvent -eq 'SessionStart') {
|
|
61
|
+
# Resolve project name from cwd in JSON or from Claude process cwd
|
|
62
|
+
$project = $null
|
|
63
|
+
try {
|
|
64
|
+
$cwd = ($input_json | ConvertFrom-Json).cwd
|
|
65
|
+
if ($cwd) { $project = Split-Path $cwd -Leaf }
|
|
66
|
+
} catch {}
|
|
67
|
+
if (-not $project -and $claude_pid) {
|
|
68
|
+
try {
|
|
69
|
+
$proc = Get-Process -Id $claude_pid -ErrorAction Stop
|
|
70
|
+
$project = Split-Path $proc.Path -Leaf
|
|
71
|
+
# Try to get actual working directory via CIM
|
|
72
|
+
$wmiProc = Get-CimInstance Win32_Process -Filter "ProcessId=$claude_pid" -ErrorAction Stop
|
|
73
|
+
if ($wmiProc.CommandLine -match '([A-Z]:\\[^\s"]+)') {
|
|
74
|
+
$possiblePath = $Matches[1]
|
|
75
|
+
if (Test-Path $possiblePath -PathType Container) {
|
|
76
|
+
$project = Split-Path $possiblePath -Leaf
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch {}
|
|
80
|
+
}
|
|
81
|
+
if ($project) {
|
|
82
|
+
if (-not (Test-Path $cacheDir)) { New-Item -ItemType Directory -Path $cacheDir -Force | Out-Null }
|
|
83
|
+
$project | Out-File -FilePath $cacheFile -Encoding utf8 -NoNewline
|
|
84
|
+
}
|
|
85
|
+
} elseif ($hookEvent -eq 'SessionEnd') {
|
|
86
|
+
# Clean up
|
|
87
|
+
if (Test-Path $cacheFile) { Remove-Item $cacheFile -Force }
|
|
88
|
+
} else {
|
|
89
|
+
# Read cached project name
|
|
90
|
+
$project = $null
|
|
91
|
+
if (Test-Path $cacheFile) { $project = Get-Content $cacheFile -Raw -ErrorAction SilentlyContinue }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Set/refresh the console window title on every event (except SessionEnd)
|
|
95
|
+
if ($hookEvent -ne 'SessionEnd' -and $project) {
|
|
96
|
+
$Host.UI.RawUI.WindowTitle = "Claude: $project"
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Deliver to dashboard via file-based MQ (primary) or HTTP (fallback)
|
|
101
|
+
$mqDir = "$env:TEMP\claude-session-center"
|
|
102
|
+
$mqFile = "$mqDir\queue.jsonl"
|
|
103
|
+
|
|
104
|
+
if (Test-Path $mqDir -PathType Container) {
|
|
105
|
+
# File-based MQ: atomic append via .NET StreamWriter (no process spawn)
|
|
106
|
+
try {
|
|
107
|
+
$fs = [System.IO.File]::Open($mqFile,
|
|
108
|
+
[System.IO.FileMode]::Append,
|
|
109
|
+
[System.IO.FileAccess]::Write,
|
|
110
|
+
[System.IO.FileShare]::ReadWrite)
|
|
111
|
+
$writer = New-Object System.IO.StreamWriter($fs)
|
|
112
|
+
$writer.WriteLine($enriched)
|
|
113
|
+
$writer.Flush()
|
|
114
|
+
$writer.Close()
|
|
115
|
+
$fs.Close()
|
|
116
|
+
} catch {
|
|
117
|
+
# Fall through to HTTP on file error
|
|
118
|
+
try {
|
|
119
|
+
$job = Start-Job -ScriptBlock {
|
|
120
|
+
param($body)
|
|
121
|
+
try {
|
|
122
|
+
Invoke-RestMethod -Uri 'http://localhost:3333/api/hooks' `
|
|
123
|
+
-Method POST `
|
|
124
|
+
-ContentType 'application/json' `
|
|
125
|
+
-Body $body `
|
|
126
|
+
-TimeoutSec 5 | Out-Null
|
|
127
|
+
} catch {}
|
|
128
|
+
} -ArgumentList $enriched
|
|
129
|
+
} catch {}
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
# Fallback: HTTP POST when MQ dir doesn't exist (server not started yet)
|
|
133
|
+
try {
|
|
134
|
+
$job = Start-Job -ScriptBlock {
|
|
135
|
+
param($body)
|
|
136
|
+
try {
|
|
137
|
+
Invoke-RestMethod -Uri 'http://localhost:3333/api/hooks' `
|
|
138
|
+
-Method POST `
|
|
139
|
+
-ContentType 'application/json' `
|
|
140
|
+
-Body $body `
|
|
141
|
+
-TimeoutSec 5 | Out-Null
|
|
142
|
+
} catch {}
|
|
143
|
+
} -ArgumentList $enriched
|
|
144
|
+
} catch {}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
exit 0
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# AI Agent Session Center - Hook relay (macOS / Linux)
|
|
3
|
+
# Reads hook JSON from stdin, enriches with process/env info, POSTs to dashboard server
|
|
4
|
+
#
|
|
5
|
+
# Performance notes:
|
|
6
|
+
# - stdin read (cat) is synchronous; everything else runs in a background subshell
|
|
7
|
+
# - Single jq invocation for all JSON parsing + enrichment
|
|
8
|
+
# - TTY lookup cached per PID in /tmp to avoid `ps` on every event
|
|
9
|
+
# - Tab title only refreshed on state-changing events, not rapid tool calls
|
|
10
|
+
# - curl fails fast (1s connect timeout) so dead server doesn't pile up processes
|
|
11
|
+
# - hook_sent_at timestamp lets the server measure delivery latency
|
|
12
|
+
|
|
13
|
+
SENT_AT=$(date +%s)
|
|
14
|
+
INPUT=$(cat)
|
|
15
|
+
|
|
16
|
+
# --- Everything below runs in background so the hook returns instantly ---
|
|
17
|
+
{
|
|
18
|
+
|
|
19
|
+
# ── TTY detection (cached per Claude PID) ──
|
|
20
|
+
HOOK_TTY=""
|
|
21
|
+
if [ -n "$PPID" ] && [ "$PPID" != "0" ]; then
|
|
22
|
+
TTY_CACHE="/tmp/claude-tty-cache"
|
|
23
|
+
TTY_CACHE_FILE="$TTY_CACHE/$PPID"
|
|
24
|
+
if [ -f "$TTY_CACHE_FILE" ]; then
|
|
25
|
+
HOOK_TTY=$(cat "$TTY_CACHE_FILE" 2>/dev/null)
|
|
26
|
+
else
|
|
27
|
+
RAW_TTY=$(ps -o tty= -p "$PPID" 2>/dev/null | tr -d ' ')
|
|
28
|
+
if [ -n "$RAW_TTY" ] && [ "$RAW_TTY" != "??" ] && [ "$RAW_TTY" != "?" ]; then
|
|
29
|
+
HOOK_TTY="/dev/${RAW_TTY}"
|
|
30
|
+
mkdir -p "$TTY_CACHE" 2>/dev/null
|
|
31
|
+
echo "$HOOK_TTY" > "$TTY_CACHE_FILE" 2>/dev/null
|
|
32
|
+
fi
|
|
33
|
+
fi
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# ── Single jq pass: enrich JSON + extract event/session_id/cwd ──
|
|
37
|
+
JQ_OUT=$(echo "$INPUT" | jq -c \
|
|
38
|
+
--arg pid "$PPID" \
|
|
39
|
+
--arg tty "$HOOK_TTY" \
|
|
40
|
+
--arg sent_at "$SENT_AT" \
|
|
41
|
+
--arg term_program "${TERM_PROGRAM:-}" \
|
|
42
|
+
--arg term_program_version "${TERM_PROGRAM_VERSION:-}" \
|
|
43
|
+
--arg vscode_pid "${VSCODE_PID:-}" \
|
|
44
|
+
--arg term "${TERM:-}" \
|
|
45
|
+
--arg iterm_session "${ITERM_SESSION_ID:-}" \
|
|
46
|
+
--arg term_session "${TERM_SESSION_ID:-}" \
|
|
47
|
+
--arg kitty_window "${KITTY_WINDOW_ID:-}" \
|
|
48
|
+
--arg kitty_pid "${KITTY_PID:-}" \
|
|
49
|
+
--arg warp_session "${WARP_SESSION_ID:-}" \
|
|
50
|
+
--arg windowid "${WINDOWID:-}" \
|
|
51
|
+
--arg ghostty_resources "${GHOSTTY_RESOURCES_DIR:-}" \
|
|
52
|
+
--arg wezterm_pane "${WEZTERM_PANE:-}" \
|
|
53
|
+
--arg tmux "${TMUX:-}" \
|
|
54
|
+
--arg tmux_pane "${TMUX_PANE:-}" \
|
|
55
|
+
--arg agent_terminal_id "${AGENT_MANAGER_TERMINAL_ID:-}" \
|
|
56
|
+
--arg claude_project_dir "${CLAUDE_PROJECT_DIR:-}" \
|
|
57
|
+
'
|
|
58
|
+
(. + {
|
|
59
|
+
claude_pid: ($pid | tonumber),
|
|
60
|
+
hook_sent_at: (($sent_at | tonumber) * 1000),
|
|
61
|
+
tty_path: (if $tty != "" then $tty else null end),
|
|
62
|
+
term_program: (if $term_program != "" then $term_program else null end),
|
|
63
|
+
term_program_version: (if $term_program_version != "" then $term_program_version else null end),
|
|
64
|
+
vscode_pid: (if $vscode_pid != "" then ($vscode_pid | tonumber) else null end),
|
|
65
|
+
term: (if $term != "" then $term else null end),
|
|
66
|
+
tab_id: (
|
|
67
|
+
if $iterm_session != "" then $iterm_session
|
|
68
|
+
elif $kitty_window != "" then ("kitty:" + $kitty_window)
|
|
69
|
+
elif $warp_session != "" then ("warp:" + $warp_session)
|
|
70
|
+
elif $wezterm_pane != "" then ("wezterm:" + $wezterm_pane)
|
|
71
|
+
elif $term_session != "" then $term_session
|
|
72
|
+
else null end
|
|
73
|
+
),
|
|
74
|
+
window_id: (if $windowid != "" then ($windowid | tonumber) else null end),
|
|
75
|
+
tmux: (if $tmux != "" then {session: $tmux, pane: $tmux_pane} else null end),
|
|
76
|
+
is_ghostty: (if $ghostty_resources != "" then true else null end),
|
|
77
|
+
kitty_pid: (if $kitty_pid != "" then ($kitty_pid | tonumber) else null end),
|
|
78
|
+
agent_terminal_id: (if $agent_terminal_id != "" then $agent_terminal_id else null end),
|
|
79
|
+
claude_project_dir: (if $claude_project_dir != "" then $claude_project_dir else null end)
|
|
80
|
+
}),
|
|
81
|
+
"\(.hook_event_name // "")",
|
|
82
|
+
"\(.session_id // "")",
|
|
83
|
+
"\(.cwd // "")"
|
|
84
|
+
' 2>/dev/null)
|
|
85
|
+
|
|
86
|
+
ENRICHED=$(echo "$JQ_OUT" | head -1)
|
|
87
|
+
EVENT=$(echo "$JQ_OUT" | sed -n '2p' | tr -d '"')
|
|
88
|
+
SESSION_ID=$(echo "$JQ_OUT" | sed -n '3p' | tr -d '"')
|
|
89
|
+
CWD=$(echo "$JQ_OUT" | sed -n '4p' | tr -d '"')
|
|
90
|
+
|
|
91
|
+
[ -z "$ENRICHED" ] && ENRICHED="$INPUT"
|
|
92
|
+
|
|
93
|
+
# ── Tab title management ──
|
|
94
|
+
# Only refresh on state-changing events — skip rapid PreToolUse/PostToolUse
|
|
95
|
+
CACHE_DIR="/tmp/claude-tab-titles"
|
|
96
|
+
|
|
97
|
+
if [ -n "$HOOK_TTY" ] && [ -n "$SESSION_ID" ]; then
|
|
98
|
+
CACHE_FILE="$CACHE_DIR/$SESSION_ID"
|
|
99
|
+
|
|
100
|
+
case "$EVENT" in
|
|
101
|
+
SessionStart)
|
|
102
|
+
PROJECT=""
|
|
103
|
+
[ -n "$CWD" ] && PROJECT=$(basename "$CWD" 2>/dev/null)
|
|
104
|
+
if [ -z "$PROJECT" ] && [ -n "$PPID" ]; then
|
|
105
|
+
PROJECT=$(lsof -a -d cwd -p "$PPID" -Fn 2>/dev/null | grep '^n/' | head -1 | sed 's|^n||' | xargs basename 2>/dev/null)
|
|
106
|
+
fi
|
|
107
|
+
if [ -n "$PROJECT" ]; then
|
|
108
|
+
mkdir -p "$CACHE_DIR" 2>/dev/null
|
|
109
|
+
echo "$PROJECT" > "$CACHE_FILE" 2>/dev/null
|
|
110
|
+
printf '\033]0;Claude: %s\007' "$PROJECT" > "$HOOK_TTY" 2>/dev/null
|
|
111
|
+
fi
|
|
112
|
+
;;
|
|
113
|
+
SessionEnd)
|
|
114
|
+
rm -f "$CACHE_FILE" 2>/dev/null
|
|
115
|
+
;;
|
|
116
|
+
UserPromptSubmit|PermissionRequest|Stop|Notification)
|
|
117
|
+
PROJECT=""
|
|
118
|
+
[ -f "$CACHE_FILE" ] && PROJECT=$(cat "$CACHE_FILE" 2>/dev/null)
|
|
119
|
+
[ -n "$PROJECT" ] && printf '\033]0;Claude: %s\007' "$PROJECT" > "$HOOK_TTY" 2>/dev/null
|
|
120
|
+
;;
|
|
121
|
+
esac
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
# ── Deliver to dashboard via file-based MQ (primary) or HTTP (fallback) ──
|
|
125
|
+
MQ_DIR="/tmp/claude-session-center"
|
|
126
|
+
MQ_FILE="$MQ_DIR/queue.jsonl"
|
|
127
|
+
|
|
128
|
+
if [ -d "$MQ_DIR" ]; then
|
|
129
|
+
# Atomic append: POSIX guarantees atomicity for writes <= PIPE_BUF (4096 bytes).
|
|
130
|
+
# Our enriched JSON is typically 300-800 bytes — no process spawn, ~0.1ms.
|
|
131
|
+
echo "$ENRICHED" >> "$MQ_FILE" 2>/dev/null
|
|
132
|
+
else
|
|
133
|
+
# Fallback: HTTP POST when MQ dir doesn't exist (server not started yet)
|
|
134
|
+
echo "$ENRICHED" | curl -s --connect-timeout 1 -m 3 -X POST \
|
|
135
|
+
-H "Content-Type: application/json" \
|
|
136
|
+
--data-binary @- \
|
|
137
|
+
http://localhost:3333/api/hooks &>/dev/null
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
} &>/dev/null &
|
|
141
|
+
disown
|
|
142
|
+
exit 0
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"UserPromptSubmit": [
|
|
4
|
+
{
|
|
5
|
+
"hooks": [
|
|
6
|
+
{
|
|
7
|
+
"type": "command",
|
|
8
|
+
"command": "~/.claude/hooks/dashboard-hook.sh",
|
|
9
|
+
"async": true
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"Stop": [
|
|
15
|
+
{
|
|
16
|
+
"hooks": [
|
|
17
|
+
{
|
|
18
|
+
"type": "command",
|
|
19
|
+
"command": "~/.claude/hooks/dashboard-hook.sh",
|
|
20
|
+
"async": true
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"SessionStart": [
|
|
26
|
+
{
|
|
27
|
+
"hooks": [
|
|
28
|
+
{
|
|
29
|
+
"type": "command",
|
|
30
|
+
"command": "~/.claude/hooks/dashboard-hook.sh",
|
|
31
|
+
"async": true
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"PreToolUse": [
|
|
37
|
+
{
|
|
38
|
+
"hooks": [
|
|
39
|
+
{
|
|
40
|
+
"type": "command",
|
|
41
|
+
"command": "~/.claude/hooks/dashboard-hook.sh",
|
|
42
|
+
"async": true
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"PostToolUse": [
|
|
48
|
+
{
|
|
49
|
+
"hooks": [
|
|
50
|
+
{
|
|
51
|
+
"type": "command",
|
|
52
|
+
"command": "~/.claude/hooks/dashboard-hook.sh",
|
|
53
|
+
"async": true
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
],
|
|
58
|
+
"Notification": [
|
|
59
|
+
{
|
|
60
|
+
"hooks": [
|
|
61
|
+
{
|
|
62
|
+
"type": "command",
|
|
63
|
+
"command": "~/.claude/hooks/dashboard-hook.sh",
|
|
64
|
+
"async": true
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
"SubagentStart": [
|
|
70
|
+
{
|
|
71
|
+
"hooks": [
|
|
72
|
+
{
|
|
73
|
+
"type": "command",
|
|
74
|
+
"command": "~/.claude/hooks/dashboard-hook.sh",
|
|
75
|
+
"async": true
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
],
|
|
80
|
+
"SubagentStop": [
|
|
81
|
+
{
|
|
82
|
+
"hooks": [
|
|
83
|
+
{
|
|
84
|
+
"type": "command",
|
|
85
|
+
"command": "~/.claude/hooks/dashboard-hook.sh",
|
|
86
|
+
"async": true
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
],
|
|
91
|
+
"SessionEnd": [
|
|
92
|
+
{
|
|
93
|
+
"hooks": [
|
|
94
|
+
{
|
|
95
|
+
"type": "command",
|
|
96
|
+
"command": "~/.claude/hooks/dashboard-hook.sh",
|
|
97
|
+
"async": true
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
}
|