@quantiya/codevibe-gemini-plugin 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.
Files changed (66) hide show
  1. package/.env.example +28 -0
  2. package/README.md +117 -0
  3. package/bin/codevibe-gemini +238 -0
  4. package/dist/appsync-client.d.ts +66 -0
  5. package/dist/appsync-client.d.ts.map +1 -0
  6. package/dist/appsync-client.js +819 -0
  7. package/dist/appsync-client.js.map +1 -0
  8. package/dist/auth-cli.d.ts +18 -0
  9. package/dist/auth-cli.d.ts.map +1 -0
  10. package/dist/auth-cli.js +472 -0
  11. package/dist/auth-cli.js.map +1 -0
  12. package/dist/command-executor.d.ts +20 -0
  13. package/dist/command-executor.d.ts.map +1 -0
  14. package/dist/command-executor.js +127 -0
  15. package/dist/command-executor.js.map +1 -0
  16. package/dist/config.d.ts +25 -0
  17. package/dist/config.d.ts.map +1 -0
  18. package/dist/config.js +106 -0
  19. package/dist/config.js.map +1 -0
  20. package/dist/crypto-service.d.ts +115 -0
  21. package/dist/crypto-service.d.ts.map +1 -0
  22. package/dist/crypto-service.js +278 -0
  23. package/dist/crypto-service.js.map +1 -0
  24. package/dist/http-api.d.ts +63 -0
  25. package/dist/http-api.d.ts.map +1 -0
  26. package/dist/http-api.js +582 -0
  27. package/dist/http-api.js.map +1 -0
  28. package/dist/key-manager.d.ts +87 -0
  29. package/dist/key-manager.d.ts.map +1 -0
  30. package/dist/key-manager.js +287 -0
  31. package/dist/key-manager.js.map +1 -0
  32. package/dist/logger.d.ts +2 -0
  33. package/dist/logger.d.ts.map +1 -0
  34. package/dist/logger.js +18 -0
  35. package/dist/logger.js.map +1 -0
  36. package/dist/prompt-responder.d.ts +22 -0
  37. package/dist/prompt-responder.d.ts.map +1 -0
  38. package/dist/prompt-responder.js +132 -0
  39. package/dist/prompt-responder.js.map +1 -0
  40. package/dist/server.d.ts +2 -0
  41. package/dist/server.d.ts.map +1 -0
  42. package/dist/server.js +1422 -0
  43. package/dist/server.js.map +1 -0
  44. package/dist/token-storage.d.ts +39 -0
  45. package/dist/token-storage.d.ts.map +1 -0
  46. package/dist/token-storage.js +169 -0
  47. package/dist/token-storage.js.map +1 -0
  48. package/dist/transcript-watcher.d.ts +111 -0
  49. package/dist/transcript-watcher.d.ts.map +1 -0
  50. package/dist/transcript-watcher.js +324 -0
  51. package/dist/transcript-watcher.js.map +1 -0
  52. package/dist/types.d.ts +119 -0
  53. package/dist/types.d.ts.map +1 -0
  54. package/dist/types.js +16 -0
  55. package/dist/types.js.map +1 -0
  56. package/gemini-extension.json +84 -0
  57. package/hooks/after-agent.sh +122 -0
  58. package/hooks/after-tool.sh +71 -0
  59. package/hooks/before-agent.sh +46 -0
  60. package/hooks/before-tool.sh +17 -0
  61. package/hooks/common.sh +220 -0
  62. package/hooks/hooks.json +81 -0
  63. package/hooks/notification.sh +32 -0
  64. package/hooks/session-end.sh +70 -0
  65. package/hooks/session-start.sh +72 -0
  66. package/package.json +73 -0
@@ -0,0 +1,122 @@
1
+ #!/bin/bash
2
+
3
+ # AfterAgent Hook
4
+ # Triggered when Gemini finishes responding to a prompt
5
+ # Sends assistant response from hook input and extracts reasoning from transcript
6
+
7
+ # Source common utilities
8
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
+ source "$SCRIPT_DIR/common.sh"
10
+
11
+ log "INFO" "AfterAgent hook triggered"
12
+
13
+ # Read JSON input from stdin
14
+ INPUT=$(read_json_input)
15
+
16
+ log "DEBUG" "Input: $INPUT"
17
+
18
+ # Extract fields from hook input
19
+ SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty')
20
+ PROMPT_RESPONSE=$(echo "$INPUT" | jq -r '.prompt_response // empty')
21
+ TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty')
22
+
23
+ EVENTS_SENT=0
24
+
25
+ # 1. Send assistant response
26
+ # Prefer transcript content (accurate) over prompt_response (has streaming duplication bug)
27
+ RESPONSE_CONTENT=""
28
+
29
+ if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
30
+ # Extract content from the last gemini message in transcript
31
+ RESPONSE_CONTENT=$(jq -r '
32
+ [.messages[] | select(.type == "gemini")] | last |
33
+ if .content | type == "string" then .content
34
+ elif .content | type == "array" then [.content[] | .text // empty] | join("")
35
+ else empty end
36
+ ' "$TRANSCRIPT_PATH" 2>/dev/null)
37
+ log "DEBUG" "Got response from transcript: ${RESPONSE_CONTENT:0:100}..."
38
+ fi
39
+
40
+ # Fall back to prompt_response if transcript extraction failed
41
+ if [ -z "$RESPONSE_CONTENT" ] || [ "$RESPONSE_CONTENT" = "null" ]; then
42
+ RESPONSE_CONTENT="$PROMPT_RESPONSE"
43
+ log "DEBUG" "Using prompt_response fallback: ${RESPONSE_CONTENT:0:100}..."
44
+ fi
45
+
46
+ if [ -n "$RESPONSE_CONTENT" ]; then
47
+ EVENT_PAYLOAD=$(jq -n \
48
+ --arg session_id "$SESSION_ID" \
49
+ --arg content "$RESPONSE_CONTENT" \
50
+ --arg hook_event_name "AfterAgent" \
51
+ --arg type "ASSISTANT_RESPONSE" \
52
+ --arg source "DESKTOP" \
53
+ '{
54
+ session_id: $session_id,
55
+ hook_event_name: $hook_event_name,
56
+ type: $type,
57
+ source: $source,
58
+ content: $content
59
+ }')
60
+
61
+ log "DEBUG" "Sending assistant response: ${RESPONSE_CONTENT:0:100}..."
62
+
63
+ send_to_mcp "event" "$EVENT_PAYLOAD" "$SESSION_ID"
64
+
65
+ if [ $? -eq 0 ]; then
66
+ EVENTS_SENT=$((EVENTS_SENT + 1))
67
+ log "INFO" "Assistant response sent successfully"
68
+ else
69
+ log "ERROR" "Failed to send assistant response"
70
+ fi
71
+ else
72
+ log "WARN" "No assistant response content available"
73
+ fi
74
+
75
+ # 2. Extract reasoning/thoughts from transcript (secondary - for rich context)
76
+ # Gemini transcript format: single JSON with .messages[] array
77
+ # Each gemini message may have .thoughts[] with {subject, description, timestamp}
78
+ if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
79
+ log "DEBUG" "Reading transcript for thoughts: $TRANSCRIPT_PATH"
80
+
81
+ # Get thoughts from the last gemini message in the transcript
82
+ THOUGHTS=$(jq -r '
83
+ [.messages[] | select(.type == "gemini")] | last |
84
+ .thoughts // [] |
85
+ map(.subject + ": " + .description) |
86
+ join("\n\n")
87
+ ' "$TRANSCRIPT_PATH" 2>/dev/null)
88
+
89
+ if [ -n "$THOUGHTS" ] && [ "$THOUGHTS" != "null" ]; then
90
+ REASONING_PAYLOAD=$(jq -n \
91
+ --arg session_id "$SESSION_ID" \
92
+ --arg content "$THOUGHTS" \
93
+ --arg hook_event_name "AfterAgent" \
94
+ --arg type "REASONING" \
95
+ --arg source "DESKTOP" \
96
+ '{
97
+ session_id: $session_id,
98
+ hook_event_name: $hook_event_name,
99
+ type: $type,
100
+ source: $source,
101
+ content: $content
102
+ }')
103
+
104
+ log "DEBUG" "Sending reasoning: ${THOUGHTS:0:100}..."
105
+
106
+ send_to_mcp "event" "$REASONING_PAYLOAD" "$SESSION_ID"
107
+
108
+ if [ $? -eq 0 ]; then
109
+ EVENTS_SENT=$((EVENTS_SENT + 1))
110
+ log "INFO" "Reasoning event sent successfully"
111
+ else
112
+ log "ERROR" "Failed to send reasoning event"
113
+ fi
114
+ else
115
+ log "DEBUG" "No thoughts found in transcript"
116
+ fi
117
+ else
118
+ log "DEBUG" "No transcript path available for reasoning extraction"
119
+ fi
120
+
121
+ log "INFO" "AfterAgent hook completed. Events sent: $EVENTS_SENT"
122
+ exit 0
@@ -0,0 +1,71 @@
1
+ #!/bin/bash
2
+
3
+ # Gemini CLI AfterTool hook
4
+ # Called after a tool is executed - captures tool results
5
+ #
6
+ # Input (JSON via stdin):
7
+ # {
8
+ # "session_id": "uuid",
9
+ # "hook_event_name": "AfterTool",
10
+ # "tool_name": "shell" | "edit" | "write" | etc,
11
+ # "tool_input": { ... tool-specific input ... },
12
+ # "tool_response": { ... tool output/result ... },
13
+ # ...
14
+ # }
15
+
16
+ # Get script directory and source common utilities
17
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18
+ source "$SCRIPT_DIR/common.sh"
19
+
20
+ log "INFO" "AfterTool hook triggered"
21
+
22
+ # Read JSON input from stdin
23
+ INPUT=$(read_json_input)
24
+
25
+ if [ -z "$INPUT" ]; then
26
+ log "ERROR" "No input received"
27
+ exit 0
28
+ fi
29
+
30
+ log "DEBUG" "Received input: $INPUT"
31
+
32
+ # Extract fields
33
+ SESSION_ID=$(echo "$INPUT" | grep -o '"session_id"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"\([^"]*\)"$/\1/')
34
+ TOOL_NAME=$(echo "$INPUT" | grep -o '"tool_name"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"\([^"]*\)"$/\1/')
35
+
36
+ if [ -z "$SESSION_ID" ]; then
37
+ log "ERROR" "No session_id in input"
38
+ exit 0
39
+ fi
40
+
41
+ log "INFO" "Session: $SESSION_ID, Tool: $TOOL_NAME"
42
+
43
+ # Check if MCP server is running
44
+ if ! check_mcp_server "$SESSION_ID"; then
45
+ log "WARN" "MCP server not running, cannot send tool result"
46
+ exit 0
47
+ fi
48
+
49
+ # Build event payload with the full hook input
50
+ # The server will extract tool_input and tool_response
51
+ EVENT_PAYLOAD=$(cat <<PAYLOAD
52
+ {
53
+ "session_id": "$SESSION_ID",
54
+ "hook_event_name": "AfterTool",
55
+ "type": "TOOL_USE",
56
+ "source": "DESKTOP",
57
+ "content": "Tool executed: $TOOL_NAME",
58
+ "metadata": {
59
+ "hook_event_name": "AfterTool",
60
+ "tool_name": "$TOOL_NAME",
61
+ "hookInput": $INPUT
62
+ }
63
+ }
64
+ PAYLOAD
65
+ )
66
+
67
+ # Send to MCP server
68
+ send_to_mcp "event" "$EVENT_PAYLOAD" "$SESSION_ID"
69
+
70
+ log "INFO" "AfterTool hook completed"
71
+ exit 0
@@ -0,0 +1,46 @@
1
+ #!/bin/bash
2
+
3
+ # BeforeAgent Hook
4
+ # Triggered when user submits a prompt in Gemini CLI
5
+ # Captures user prompts and sends to mobile app as USER_PROMPT events
6
+
7
+ # Source common utilities
8
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
+ source "$SCRIPT_DIR/common.sh"
10
+
11
+ log "INFO" "BeforeAgent hook triggered"
12
+
13
+ # Read JSON input from stdin
14
+ INPUT=$(read_json_input)
15
+
16
+ log "DEBUG" "Input: $INPUT"
17
+
18
+ # Extract fields from hook input
19
+ SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty')
20
+ PROMPT=$(echo "$INPUT" | jq -r '.prompt // empty')
21
+
22
+ # Build explicit event payload with type and content
23
+ EVENT_PAYLOAD=$(jq -n \
24
+ --arg session_id "$SESSION_ID" \
25
+ --arg content "$PROMPT" \
26
+ --arg hook_event_name "BeforeAgent" \
27
+ --arg type "USER_PROMPT" \
28
+ '{
29
+ session_id: $session_id,
30
+ hook_event_name: $hook_event_name,
31
+ type: $type,
32
+ content: $content
33
+ }')
34
+
35
+ # Send to MCP server
36
+ send_to_mcp "event" "$EVENT_PAYLOAD" "$SESSION_ID"
37
+
38
+ EXIT_CODE=$?
39
+
40
+ if [ $EXIT_CODE -eq 0 ]; then
41
+ log "INFO" "BeforeAgent event sent successfully"
42
+ else
43
+ log "ERROR" "Failed to send BeforeAgent event (exit code: $EXIT_CODE)"
44
+ fi
45
+
46
+ exit $EXIT_CODE
@@ -0,0 +1,17 @@
1
+ #!/bin/bash
2
+
3
+ # Gemini CLI BeforeTool hook
4
+ # Called before a tool is executed.
5
+ #
6
+ # Actual approval prompts are handled via the Notification/ToolPermission hook,
7
+ # which fires only when Gemini CLI's permission mode requires user approval.
8
+ # This hook simply allows all tools to proceed.
9
+ #
10
+ # Output (JSON to stdout):
11
+ # {"decision": "allow"}
12
+
13
+ # Read stdin to consume it (Gemini CLI expects it to be read)
14
+ cat > /dev/null
15
+
16
+ echo '{"decision":"allow"}'
17
+ exit 0
@@ -0,0 +1,220 @@
1
+ #!/bin/bash
2
+
3
+ # Common utilities for CodeVibe Gemini hooks
4
+ # This file provides shared functions for all hook scripts
5
+
6
+ # Configuration
7
+ # Use TMPDIR if set (macOS sets this to user-specific temp), otherwise /tmp
8
+ CODEVIBE_TMPDIR="${TMPDIR:-/tmp}"
9
+ LOG_FILE="${LOG_FILE:-${CODEVIBE_TMPDIR}/codevibe-gemini-hooks.log}"
10
+ DEFAULT_PORT="${DEFAULT_PORT:-3457}"
11
+
12
+ # Timeout for interactive prompts (5 minutes = 300 seconds)
13
+ INTERACTIVE_PROMPT_TIMEOUT="${INTERACTIVE_PROMPT_TIMEOUT:-300}"
14
+ # Poll interval for checking prompt response (1 second)
15
+ POLL_INTERVAL="${POLL_INTERVAL:-1}"
16
+
17
+ # Log a message with timestamp
18
+ log() {
19
+ local level="$1"
20
+ shift
21
+ local message="$*"
22
+ echo "[$(date -u +"%Y-%m-%dT%H:%M:%SZ")] [$level] [gemini-hook] $message" >> "$LOG_FILE"
23
+ }
24
+
25
+ # Get the MCP server URL for a given session
26
+ # Priority: session-specific port file → default port file → DEFAULT_PORT
27
+ get_mcp_server_url() {
28
+ local session_id="$1"
29
+ local port="$DEFAULT_PORT"
30
+
31
+ # Try session-specific port file first
32
+ if [ -n "$session_id" ]; then
33
+ local port_file="${CODEVIBE_TMPDIR}/codevibe-gemini-${session_id}.port"
34
+ if [ -f "$port_file" ]; then
35
+ port=$(cat "$port_file")
36
+ log "DEBUG" "Using port from session file: $port"
37
+ echo "http://localhost:${port}"
38
+ return
39
+ fi
40
+ fi
41
+
42
+ # Fallback: default port file (written at server startup before any session)
43
+ local default_port_file="${CODEVIBE_TMPDIR}/codevibe-gemini-default.port"
44
+ if [ -f "$default_port_file" ]; then
45
+ port=$(cat "$default_port_file")
46
+ log "DEBUG" "Using port from default file: $port"
47
+ else
48
+ log "DEBUG" "No port files found, using default port $port"
49
+ fi
50
+
51
+ echo "http://localhost:${port}"
52
+ }
53
+
54
+ # Read JSON input from stdin
55
+ read_json_input() {
56
+ # Read all input from stdin
57
+ cat
58
+ }
59
+
60
+ # Send event to MCP server via HTTP POST
61
+ # Usage: send_to_mcp <endpoint> <json_data> [session_id]
62
+ send_to_mcp() {
63
+ local endpoint="$1"
64
+ local json_data="$2"
65
+ local session_id="$3"
66
+
67
+ # Get the MCP server URL for this session
68
+ local mcp_url=$(get_mcp_server_url "$session_id")
69
+
70
+ log "INFO" "Sending to MCP server: POST $mcp_url/$endpoint"
71
+ log "DEBUG" "Payload: $json_data"
72
+
73
+ # Send HTTP POST request to MCP server
74
+ local response
75
+ local http_code
76
+
77
+ response=$(curl -s -w "\n%{http_code}" \
78
+ -X POST \
79
+ -H "Content-Type: application/json" \
80
+ -d "$json_data" \
81
+ "$mcp_url/$endpoint" 2>&1)
82
+
83
+ http_code=$(echo "$response" | tail -n 1)
84
+ # Use sed instead of head -n -1 for macOS compatibility
85
+ local body=$(echo "$response" | sed '$d')
86
+
87
+ if [ "$http_code" = "200" ]; then
88
+ log "INFO" "Successfully sent to MCP server (HTTP $http_code)"
89
+ log "DEBUG" "Response: $body"
90
+ echo "$body"
91
+ return 0
92
+ else
93
+ log "ERROR" "Failed to send to MCP server (HTTP $http_code)"
94
+ log "ERROR" "Response: $body"
95
+ return 1
96
+ fi
97
+ }
98
+
99
+ # Send interactive prompt and wait for response
100
+ # Usage: send_interactive_prompt <json_data> <session_id>
101
+ # Returns: JSON with decision field or exits with error
102
+ send_interactive_prompt() {
103
+ local json_data="$1"
104
+ local session_id="$2"
105
+
106
+ local mcp_url=$(get_mcp_server_url "$session_id")
107
+
108
+ log "INFO" "Sending interactive prompt to MCP server"
109
+
110
+ # Send the interactive prompt request
111
+ local response
112
+ local http_code
113
+
114
+ response=$(curl -s -w "\n%{http_code}" \
115
+ -X POST \
116
+ -H "Content-Type: application/json" \
117
+ -d "$json_data" \
118
+ "$mcp_url/interactive-prompt" 2>&1)
119
+
120
+ http_code=$(echo "$response" | tail -n 1)
121
+ local body=$(echo "$response" | sed '$d')
122
+
123
+ if [ "$http_code" != "200" ]; then
124
+ log "ERROR" "Failed to send interactive prompt (HTTP $http_code)"
125
+ log "ERROR" "Response: $body"
126
+ # Return default "ask" decision on error
127
+ echo '{"decision":"ask","reason":"Failed to communicate with MCP server"}'
128
+ return 1
129
+ fi
130
+
131
+ # Extract prompt ID from response
132
+ local prompt_id=$(echo "$body" | grep -o '"promptId"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"\([^"]*\)"$/\1/')
133
+
134
+ if [ -z "$prompt_id" ]; then
135
+ log "ERROR" "No promptId in response: $body"
136
+ echo '{"decision":"ask","reason":"No prompt ID received"}'
137
+ return 1
138
+ fi
139
+
140
+ log "INFO" "Got prompt ID: $prompt_id, polling for response..."
141
+
142
+ # Poll for response
143
+ local elapsed=0
144
+ while [ $elapsed -lt $INTERACTIVE_PROMPT_TIMEOUT ]; do
145
+ sleep $POLL_INTERVAL
146
+ elapsed=$((elapsed + POLL_INTERVAL))
147
+
148
+ # Check for response
149
+ response=$(curl -s -w "\n%{http_code}" \
150
+ "$mcp_url/prompt-response/$prompt_id" 2>&1)
151
+
152
+ http_code=$(echo "$response" | tail -n 1)
153
+ body=$(echo "$response" | sed '$d')
154
+
155
+ if [ "$http_code" = "200" ]; then
156
+ # Check if we have a decision
157
+ local decision=$(echo "$body" | grep -o '"decision"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"\([^"]*\)"$/\1/')
158
+
159
+ if [ -n "$decision" ] && [ "$decision" != "pending" ]; then
160
+ log "INFO" "Got decision: $decision after ${elapsed}s"
161
+ echo "$body"
162
+ return 0
163
+ fi
164
+ elif [ "$http_code" = "404" ]; then
165
+ # Prompt not found or expired
166
+ log "WARN" "Prompt not found or expired"
167
+ echo '{"decision":"ask","reason":"Prompt expired or not found"}'
168
+ return 1
169
+ fi
170
+
171
+ log "DEBUG" "Still waiting for response... (${elapsed}s / ${INTERACTIVE_PROMPT_TIMEOUT}s)"
172
+ done
173
+
174
+ # Timeout reached
175
+ log "WARN" "Timeout waiting for interactive prompt response after ${INTERACTIVE_PROMPT_TIMEOUT}s"
176
+ echo '{"decision":"ask","reason":"Timeout waiting for mobile response"}'
177
+ return 1
178
+ }
179
+
180
+ # Extract field from JSON using grep/sed (simple alternative to jq)
181
+ # This is a basic implementation - for complex JSON, consider using jq
182
+ extract_field() {
183
+ local json="$1"
184
+ local field="$2"
185
+
186
+ # Simple regex-based extraction (works for simple string values)
187
+ echo "$json" | grep -o "\"$field\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | sed "s/\"$field\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\"/\1/"
188
+ }
189
+
190
+ # Check if MCP server is running
191
+ # Usage: check_mcp_server [session_id]
192
+ check_mcp_server() {
193
+ local session_id="$1"
194
+ local mcp_url=$(get_mcp_server_url "$session_id")
195
+
196
+ log "DEBUG" "Checking if MCP server is running at $mcp_url"
197
+
198
+ local response
199
+ response=$(curl -s "$mcp_url/health" 2>&1)
200
+
201
+ if [ $? -eq 0 ]; then
202
+ log "DEBUG" "MCP server is running"
203
+ return 0
204
+ else
205
+ log "WARN" "MCP server is not responding at $mcp_url"
206
+ return 1
207
+ fi
208
+ }
209
+
210
+ # Export variables and functions for use in other scripts
211
+ export CODEVIBE_TMPDIR
212
+ export INTERACTIVE_PROMPT_TIMEOUT
213
+ export POLL_INTERVAL
214
+ export -f log
215
+ export -f get_mcp_server_url
216
+ export -f read_json_input
217
+ export -f send_to_mcp
218
+ export -f send_interactive_prompt
219
+ export -f extract_field
220
+ export -f check_mcp_server
@@ -0,0 +1,81 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "matcher": "*",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "bash ${GEMINI_EXTENSION_PATH}/hooks/session-start.sh"
10
+ }
11
+ ]
12
+ }
13
+ ],
14
+ "SessionEnd": [
15
+ {
16
+ "matcher": "*",
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "bash ${GEMINI_EXTENSION_PATH}/hooks/session-end.sh"
21
+ }
22
+ ]
23
+ }
24
+ ],
25
+ "BeforeAgent": [
26
+ {
27
+ "matcher": "*",
28
+ "hooks": [
29
+ {
30
+ "type": "command",
31
+ "command": "bash ${GEMINI_EXTENSION_PATH}/hooks/before-agent.sh"
32
+ }
33
+ ]
34
+ }
35
+ ],
36
+ "AfterAgent": [
37
+ {
38
+ "matcher": "*",
39
+ "hooks": [
40
+ {
41
+ "type": "command",
42
+ "command": "bash ${GEMINI_EXTENSION_PATH}/hooks/after-agent.sh"
43
+ }
44
+ ]
45
+ }
46
+ ],
47
+ "AfterTool": [
48
+ {
49
+ "matcher": "*",
50
+ "hooks": [
51
+ {
52
+ "type": "command",
53
+ "command": "bash ${GEMINI_EXTENSION_PATH}/hooks/after-tool.sh"
54
+ }
55
+ ]
56
+ }
57
+ ],
58
+ "BeforeTool": [
59
+ {
60
+ "matcher": "*",
61
+ "hooks": [
62
+ {
63
+ "type": "command",
64
+ "command": "bash ${GEMINI_EXTENSION_PATH}/hooks/before-tool.sh"
65
+ }
66
+ ]
67
+ }
68
+ ],
69
+ "Notification": [
70
+ {
71
+ "matcher": "*",
72
+ "hooks": [
73
+ {
74
+ "type": "command",
75
+ "command": "bash ${GEMINI_EXTENSION_PATH}/hooks/notification.sh"
76
+ }
77
+ ]
78
+ }
79
+ ]
80
+ }
81
+ }
@@ -0,0 +1,32 @@
1
+ #!/bin/bash
2
+
3
+ # Notification Hook
4
+ # Triggered when Gemini CLI shows a notification
5
+ # Forwards notifications to mobile app
6
+
7
+ # Source common utilities
8
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
+ source "$SCRIPT_DIR/common.sh"
10
+
11
+ log "INFO" "Notification hook triggered"
12
+
13
+ # Read JSON input from stdin
14
+ INPUT=$(read_json_input)
15
+
16
+ log "DEBUG" "Input: $INPUT"
17
+
18
+ # Extract session ID for dynamic port lookup
19
+ SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty')
20
+
21
+ # Send to MCP server
22
+ send_to_mcp "event" "$INPUT" "$SESSION_ID"
23
+
24
+ EXIT_CODE=$?
25
+
26
+ if [ $EXIT_CODE -eq 0 ]; then
27
+ log "INFO" "Notification event sent successfully"
28
+ else
29
+ log "ERROR" "Failed to send Notification event (exit code: $EXIT_CODE)"
30
+ fi
31
+
32
+ exit $EXIT_CODE
@@ -0,0 +1,70 @@
1
+ #!/bin/bash
2
+
3
+ # Gemini CLI SessionEnd hook
4
+ # Called when a Gemini session ends
5
+ #
6
+ # Input (JSON via stdin):
7
+ # {
8
+ # "session_id": "uuid",
9
+ # "hook_event_name": "SessionEnd",
10
+ # "reason": "user_exit" | "error" | etc,
11
+ # ...
12
+ # }
13
+
14
+ # Get script directory and source common utilities
15
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16
+ source "$SCRIPT_DIR/common.sh"
17
+
18
+ log "INFO" "SessionEnd hook triggered"
19
+
20
+ # Read JSON input from stdin
21
+ INPUT=$(read_json_input)
22
+
23
+ if [ -z "$INPUT" ]; then
24
+ log "ERROR" "No input received"
25
+ exit 0
26
+ fi
27
+
28
+ log "DEBUG" "Received input: $INPUT"
29
+
30
+ # Extract session_id
31
+ SESSION_ID=$(echo "$INPUT" | grep -o '"session_id"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"\([^"]*\)"$/\1/')
32
+
33
+ if [ -z "$SESSION_ID" ]; then
34
+ log "ERROR" "No session_id in input"
35
+ exit 0
36
+ fi
37
+
38
+ # Extract reason if available
39
+ REASON=$(echo "$INPUT" | grep -o '"reason"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"\([^"]*\)"$/\1/')
40
+ REASON="${REASON:-unknown}"
41
+
42
+ log "INFO" "Session ID: $SESSION_ID, Reason: $REASON"
43
+
44
+ # Check if MCP server is running
45
+ if ! check_mcp_server "$SESSION_ID"; then
46
+ log "WARN" "MCP server not running, cannot send SessionEnd"
47
+ exit 0
48
+ fi
49
+
50
+ # Build event payload
51
+ EVENT_PAYLOAD=$(cat <<PAYLOAD
52
+ {
53
+ "session_id": "$SESSION_ID",
54
+ "hook_event_name": "SessionEnd",
55
+ "type": "NOTIFICATION",
56
+ "source": "DESKTOP",
57
+ "content": "Session ended: $REASON",
58
+ "metadata": {
59
+ "hook_event_name": "SessionEnd",
60
+ "reason": "$REASON"
61
+ }
62
+ }
63
+ PAYLOAD
64
+ )
65
+
66
+ # Send to MCP server
67
+ send_to_mcp "event" "$EVENT_PAYLOAD" "$SESSION_ID"
68
+
69
+ log "INFO" "SessionEnd hook completed"
70
+ exit 0