@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.
- package/.env.example +28 -0
- package/README.md +117 -0
- package/bin/codevibe-gemini +238 -0
- package/dist/appsync-client.d.ts +66 -0
- package/dist/appsync-client.d.ts.map +1 -0
- package/dist/appsync-client.js +819 -0
- package/dist/appsync-client.js.map +1 -0
- package/dist/auth-cli.d.ts +18 -0
- package/dist/auth-cli.d.ts.map +1 -0
- package/dist/auth-cli.js +472 -0
- package/dist/auth-cli.js.map +1 -0
- package/dist/command-executor.d.ts +20 -0
- package/dist/command-executor.d.ts.map +1 -0
- package/dist/command-executor.js +127 -0
- package/dist/command-executor.js.map +1 -0
- package/dist/config.d.ts +25 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +106 -0
- package/dist/config.js.map +1 -0
- package/dist/crypto-service.d.ts +115 -0
- package/dist/crypto-service.d.ts.map +1 -0
- package/dist/crypto-service.js +278 -0
- package/dist/crypto-service.js.map +1 -0
- package/dist/http-api.d.ts +63 -0
- package/dist/http-api.d.ts.map +1 -0
- package/dist/http-api.js +582 -0
- package/dist/http-api.js.map +1 -0
- package/dist/key-manager.d.ts +87 -0
- package/dist/key-manager.d.ts.map +1 -0
- package/dist/key-manager.js +287 -0
- package/dist/key-manager.js.map +1 -0
- package/dist/logger.d.ts +2 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +18 -0
- package/dist/logger.js.map +1 -0
- package/dist/prompt-responder.d.ts +22 -0
- package/dist/prompt-responder.d.ts.map +1 -0
- package/dist/prompt-responder.js +132 -0
- package/dist/prompt-responder.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +1422 -0
- package/dist/server.js.map +1 -0
- package/dist/token-storage.d.ts +39 -0
- package/dist/token-storage.d.ts.map +1 -0
- package/dist/token-storage.js +169 -0
- package/dist/token-storage.js.map +1 -0
- package/dist/transcript-watcher.d.ts +111 -0
- package/dist/transcript-watcher.d.ts.map +1 -0
- package/dist/transcript-watcher.js +324 -0
- package/dist/transcript-watcher.js.map +1 -0
- package/dist/types.d.ts +119 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/gemini-extension.json +84 -0
- package/hooks/after-agent.sh +122 -0
- package/hooks/after-tool.sh +71 -0
- package/hooks/before-agent.sh +46 -0
- package/hooks/before-tool.sh +17 -0
- package/hooks/common.sh +220 -0
- package/hooks/hooks.json +81 -0
- package/hooks/notification.sh +32 -0
- package/hooks/session-end.sh +70 -0
- package/hooks/session-start.sh +72 -0
- 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
|
package/hooks/common.sh
ADDED
|
@@ -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
|
package/hooks/hooks.json
ADDED
|
@@ -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
|