agent-relay-plugin 0.6.1 → 0.10.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/.claude-plugin/plugin.json +1 -1
- package/package.json +4 -3
- package/skills/reply/SKILL.md +25 -0
- package/hooks/approval-gate.sh +0 -143
- package/hooks/hooks.json +0 -61
- package/hooks/poll-inbox.sh +0 -227
- package/hooks/profile-lib.sh +0 -73
- package/hooks/relay-monitor.sh +0 -209
- package/hooks/session-end.sh +0 -48
- package/hooks/set-status.sh +0 -65
- package/monitors/monitors.json +0 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Claude Code plugin for Agent Relay — auto-registers sessions as agents and enables inter-agent messaging",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "AGPL-3.0-or-later",
|
|
@@ -14,10 +14,11 @@
|
|
|
14
14
|
"url": "https://github.com/edimuj/agent-relay/issues"
|
|
15
15
|
},
|
|
16
16
|
"homepage": "https://github.com/edimuj/agent-relay/tree/main/claude#readme",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"agent-relay-sdk": "0.2.0"
|
|
19
|
+
},
|
|
17
20
|
"files": [
|
|
18
21
|
".claude-plugin/",
|
|
19
|
-
"hooks/",
|
|
20
|
-
"monitors/",
|
|
21
22
|
"skills/"
|
|
22
23
|
],
|
|
23
24
|
"keywords": [
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: reply
|
|
3
|
+
description: Reply to an Agent Relay message by ID. Auto-routes to the sender and inherits channel context — no target needed. Use when the user invokes /reply or asks to reply to a specific relay message.
|
|
4
|
+
argument-hint: "<messageId> <message>"
|
|
5
|
+
allowed-tools: [Bash]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Agent Relay Reply
|
|
9
|
+
|
|
10
|
+
Run:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
agent-relay /reply $ARGUMENTS
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The server auto-routes the reply to the original sender and inherits the channel (Telegram, Slack, etc.) if applicable. No target or channel ID needed.
|
|
17
|
+
|
|
18
|
+
Examples:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
agent-relay /reply 206 "Sounds good, I'll take a look"
|
|
22
|
+
agent-relay /reply 42 "Done — the fix is in commit abc123"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Report the sent message id and resolved target briefly.
|
package/hooks/approval-gate.sh
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Agent Relay plugin — approval gate for PreToolUse and PermissionDenied hooks.
|
|
3
|
-
# Enforces AGENT_RELAY_APPROVAL/profile approval policy: open (default), guarded, read-only.
|
|
4
|
-
|
|
5
|
-
if [ "${AGENT_RELAY_DISABLED:-}" = "1" ]; then
|
|
6
|
-
exit 0
|
|
7
|
-
fi
|
|
8
|
-
|
|
9
|
-
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
10
|
-
source "${script_dir}/profile-lib.sh"
|
|
11
|
-
project=$(basename "${PWD:-unknown}")
|
|
12
|
-
if [ -n "$CLAUDE_RIG_NAME" ]; then rig="$CLAUDE_RIG_NAME"
|
|
13
|
-
elif [ -n "$CLAUDE_CONFIG_DIR" ]; then rig=$(basename "$CLAUDE_CONFIG_DIR")
|
|
14
|
-
else rig="default"; fi
|
|
15
|
-
ar_init_profile "claude" "$rig" "$project"
|
|
16
|
-
MODE="${AGENT_RELAY_APPROVAL:-$AR_APPROVAL}"
|
|
17
|
-
MODE="${MODE:-open}"
|
|
18
|
-
[ "$MODE" = "open" ] && exit 0
|
|
19
|
-
|
|
20
|
-
input=$(cat)
|
|
21
|
-
event=$(printf '%s' "$input" | jq -r '.hook_event_name')
|
|
22
|
-
|
|
23
|
-
# --- PermissionDenied: allow retry so the model adapts ---
|
|
24
|
-
if [ "$event" = "PermissionDenied" ]; then
|
|
25
|
-
jq -n '{hookSpecificOutput:{hookEventName:"PermissionDenied",retry:true}}'
|
|
26
|
-
exit 0
|
|
27
|
-
fi
|
|
28
|
-
|
|
29
|
-
tool=$(printf '%s' "$input" | jq -r '.tool_name')
|
|
30
|
-
|
|
31
|
-
# --- Always allowed: read-only and orchestration tools ---
|
|
32
|
-
case "$tool" in
|
|
33
|
-
Read|Grep|Glob|WebFetch|WebSearch|Agent|Monitor|ToolSearch|AskUserQuestion|Skill|ScheduleWakeup|CronList|EnterPlanMode|ExitPlanMode)
|
|
34
|
-
exit 0 ;;
|
|
35
|
-
Task*|*McpResource*)
|
|
36
|
-
exit 0 ;;
|
|
37
|
-
mcp__*)
|
|
38
|
-
exit 0 ;;
|
|
39
|
-
esac
|
|
40
|
-
|
|
41
|
-
deny() {
|
|
42
|
-
jq -n --arg r "$1" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}'
|
|
43
|
-
exit 0
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
# --- read-only: deny mutation tools ---
|
|
47
|
-
if [ "$MODE" = "read-only" ]; then
|
|
48
|
-
case "$tool" in
|
|
49
|
-
Write|Edit|NotebookEdit|CronCreate|CronDelete|PushNotification|RemoteTrigger|EnterWorktree|ExitWorktree)
|
|
50
|
-
deny "${tool} blocked by read-only policy" ;;
|
|
51
|
-
esac
|
|
52
|
-
fi
|
|
53
|
-
|
|
54
|
-
# --- Bash command analysis ---
|
|
55
|
-
if [ "$tool" = "Bash" ]; then
|
|
56
|
-
cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // ""')
|
|
57
|
-
|
|
58
|
-
if [ "$MODE" = "guarded" ]; then
|
|
59
|
-
# Denylist: block destructive patterns anywhere in the command
|
|
60
|
-
if printf '%s' "$cmd" | grep -qE '(^|[;&|]\s*)\s*(rm|rmdir)\b'; then
|
|
61
|
-
deny "rm/rmdir blocked by guarded policy"
|
|
62
|
-
fi
|
|
63
|
-
if printf '%s' "$cmd" | grep -qE '(^|[;&|]\s*)\s*mv\b'; then
|
|
64
|
-
deny "mv blocked by guarded policy (use cp instead)"
|
|
65
|
-
fi
|
|
66
|
-
if printf '%s' "$cmd" | grep -qE '(^|[;&|]\s*)\s*(chmod|chown|chattr)\b'; then
|
|
67
|
-
deny "Permission change blocked by guarded policy"
|
|
68
|
-
fi
|
|
69
|
-
if printf '%s' "$cmd" | grep -qE '(^|[;&|]\s*)\s*(kill|pkill|killall)\b'; then
|
|
70
|
-
deny "Process signal blocked by guarded policy"
|
|
71
|
-
fi
|
|
72
|
-
if printf '%s' "$cmd" | grep -qE '(^|[;&|]\s*)\s*(dd|mkfs|truncate|shred)\b'; then
|
|
73
|
-
deny "Destructive command blocked by guarded policy"
|
|
74
|
-
fi
|
|
75
|
-
if printf '%s' "$cmd" | grep -qE '(^|[;&|]\s*)\s*sudo\b'; then
|
|
76
|
-
deny "sudo blocked by guarded policy"
|
|
77
|
-
fi
|
|
78
|
-
if printf '%s' "$cmd" | grep -qE 'git\s+(reset\s+--hard|push\s+.*(-f\b|--force)|clean\s+-[a-z]*f)'; then
|
|
79
|
-
deny "Destructive git operation blocked by guarded policy"
|
|
80
|
-
fi
|
|
81
|
-
exit 0
|
|
82
|
-
fi
|
|
83
|
-
|
|
84
|
-
if [ "$MODE" = "read-only" ]; then
|
|
85
|
-
# Check for file-writing redirects (allow >/dev/null and fd redirects like 2>&1)
|
|
86
|
-
redirect_cleaned=$(printf '%s' "$cmd" | sed 's|[0-9]*>[[:space:]]*/dev/null||g; s|[0-9]*>&[0-9]*||g')
|
|
87
|
-
if printf '%s' "$redirect_cleaned" | grep -qE '>>|[^a-zA-Z0-9_]>|^>'; then
|
|
88
|
-
deny "Output redirection blocked by read-only policy"
|
|
89
|
-
fi
|
|
90
|
-
|
|
91
|
-
# Allowlist: check first command word
|
|
92
|
-
first_word=$(printf '%s' "$cmd" | sed 's/^[[:space:]]*//' | awk '{print $1}')
|
|
93
|
-
|
|
94
|
-
case "$first_word" in
|
|
95
|
-
# Filesystem reads
|
|
96
|
-
ls|cat|head|tail|less|more|file|stat|tree|du|df|readlink|realpath|basename|dirname)
|
|
97
|
-
exit 0 ;;
|
|
98
|
-
# Text processing
|
|
99
|
-
wc|sort|uniq|diff|comm|tr|cut|paste|column|fmt|fold|tac|rev|nl|seq|xargs)
|
|
100
|
-
exit 0 ;;
|
|
101
|
-
# Search
|
|
102
|
-
find|fd|grep|egrep|fgrep|rg|ag|ast-grep)
|
|
103
|
-
exit 0 ;;
|
|
104
|
-
# Shell builtins / info
|
|
105
|
-
echo|printf|pwd|which|whoami|date|env|printenv|uname|id|hostname|type|true|false|test|\[)
|
|
106
|
-
exit 0 ;;
|
|
107
|
-
# System info
|
|
108
|
-
ps|top|htop|uptime|free|lsof|netstat|ss|ip|ifconfig)
|
|
109
|
-
exit 0 ;;
|
|
110
|
-
# Data processing
|
|
111
|
-
jq|yq|bc|expr|base64|xxd|hexdump|od|strings|md5sum|sha256sum|sha1sum|shasum)
|
|
112
|
-
exit 0 ;;
|
|
113
|
-
# Network reads
|
|
114
|
-
curl|wget|dig|nslookup|ping|traceroute|ssh)
|
|
115
|
-
exit 0 ;;
|
|
116
|
-
# Git — subcommand check
|
|
117
|
-
git)
|
|
118
|
-
git_sub=$(printf '%s' "$cmd" | sed 's/^[[:space:]]*git[[:space:]]*//' | awk '{print $1}')
|
|
119
|
-
case "$git_sub" in
|
|
120
|
-
status|log|diff|show|branch|remote|tag|rev-parse|config|shortlog|reflog|blame|describe|ls-files|ls-tree|cat-file|rev-list|for-each-ref|name-rev|merge-base|version)
|
|
121
|
-
exit 0 ;;
|
|
122
|
-
stash)
|
|
123
|
-
stash_action=$(printf '%s' "$cmd" | sed 's/.*stash[[:space:]]*//' | awk '{print $1}')
|
|
124
|
-
case "$stash_action" in
|
|
125
|
-
list|show|"") exit 0 ;;
|
|
126
|
-
*) deny "git stash ${stash_action} blocked by read-only policy" ;;
|
|
127
|
-
esac ;;
|
|
128
|
-
*)
|
|
129
|
-
deny "git ${git_sub} blocked by read-only policy" ;;
|
|
130
|
-
esac ;;
|
|
131
|
-
# Everything else denied
|
|
132
|
-
*)
|
|
133
|
-
deny "Command '${first_word}' not in read-only allowlist" ;;
|
|
134
|
-
esac
|
|
135
|
-
fi
|
|
136
|
-
fi
|
|
137
|
-
|
|
138
|
-
# Unknown tools: guarded → allow, read-only → deny
|
|
139
|
-
if [ "$MODE" = "read-only" ]; then
|
|
140
|
-
deny "${tool} not in read-only allowlist"
|
|
141
|
-
fi
|
|
142
|
-
|
|
143
|
-
exit 0
|
package/hooks/hooks.json
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"hooks": {
|
|
3
|
-
"PreToolUse": [
|
|
4
|
-
{
|
|
5
|
-
"hooks": [
|
|
6
|
-
{
|
|
7
|
-
"type": "command",
|
|
8
|
-
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/approval-gate.sh\"",
|
|
9
|
-
"timeout": 5
|
|
10
|
-
}
|
|
11
|
-
]
|
|
12
|
-
}
|
|
13
|
-
],
|
|
14
|
-
"PermissionDenied": [
|
|
15
|
-
{
|
|
16
|
-
"hooks": [
|
|
17
|
-
{
|
|
18
|
-
"type": "command",
|
|
19
|
-
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/approval-gate.sh\"",
|
|
20
|
-
"timeout": 5
|
|
21
|
-
}
|
|
22
|
-
]
|
|
23
|
-
}
|
|
24
|
-
],
|
|
25
|
-
"UserPromptSubmit": [
|
|
26
|
-
{
|
|
27
|
-
"hooks": [
|
|
28
|
-
{
|
|
29
|
-
"type": "command",
|
|
30
|
-
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/set-status.sh\" busy",
|
|
31
|
-
"async": true,
|
|
32
|
-
"timeout": 5
|
|
33
|
-
}
|
|
34
|
-
]
|
|
35
|
-
}
|
|
36
|
-
],
|
|
37
|
-
"Stop": [
|
|
38
|
-
{
|
|
39
|
-
"hooks": [
|
|
40
|
-
{
|
|
41
|
-
"type": "command",
|
|
42
|
-
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/set-status.sh\" idle",
|
|
43
|
-
"async": true,
|
|
44
|
-
"timeout": 5
|
|
45
|
-
}
|
|
46
|
-
]
|
|
47
|
-
}
|
|
48
|
-
],
|
|
49
|
-
"SessionEnd": [
|
|
50
|
-
{
|
|
51
|
-
"hooks": [
|
|
52
|
-
{
|
|
53
|
-
"type": "command",
|
|
54
|
-
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/session-end.sh\"",
|
|
55
|
-
"timeout": 5
|
|
56
|
-
}
|
|
57
|
-
]
|
|
58
|
-
}
|
|
59
|
-
]
|
|
60
|
-
}
|
|
61
|
-
}
|
package/hooks/poll-inbox.sh
DELETED
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Agent Relay plugin — inbox poller
|
|
3
|
-
# Each new message prints one stdout line (consumed by Monitor as a notification).
|
|
4
|
-
# Usage: poll-inbox.sh <relay-url> <agent-id> [interval-seconds]
|
|
5
|
-
|
|
6
|
-
if [ "${AGENT_RELAY_DISABLED:-}" = "1" ]; then
|
|
7
|
-
exit 0
|
|
8
|
-
fi
|
|
9
|
-
|
|
10
|
-
RELAY_URL="$1"
|
|
11
|
-
AGENT_ID="$2"
|
|
12
|
-
INTERVAL="${3:-10}"
|
|
13
|
-
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
14
|
-
source "${script_dir}/profile-lib.sh"
|
|
15
|
-
auth_header_args=()
|
|
16
|
-
if [ -n "${AGENT_RELAY_TOKEN:-}" ]; then
|
|
17
|
-
auth_header_args=(-H "X-Agent-Relay-Token: ${AGENT_RELAY_TOKEN}")
|
|
18
|
-
fi
|
|
19
|
-
|
|
20
|
-
if [ -z "$RELAY_URL" ] || [ -z "$AGENT_ID" ]; then
|
|
21
|
-
echo "Usage: poll-inbox.sh <relay-url> <agent-id> [interval]" >&2
|
|
22
|
-
exit 1
|
|
23
|
-
fi
|
|
24
|
-
|
|
25
|
-
# PID file so session-start.sh can find and kill us on /clear or /compact.
|
|
26
|
-
pid_file="/tmp/agent-relay-poll-${AGENT_ID}.pid"
|
|
27
|
-
status_file="/tmp/agent-relay-status-${AGENT_ID}.state"
|
|
28
|
-
delivered_file="/tmp/agent-relay-delivered-${AGENT_ID}.queue"
|
|
29
|
-
cursor_file="/tmp/agent-relay-cursor-${AGENT_ID}.state"
|
|
30
|
-
project=$(basename "${PWD:-unknown}")
|
|
31
|
-
if [ -n "$CLAUDE_RIG_NAME" ]; then rig="$CLAUDE_RIG_NAME"
|
|
32
|
-
elif [ -n "$CLAUDE_CONFIG_DIR" ]; then rig=$(basename "$CLAUDE_CONFIG_DIR")
|
|
33
|
-
else rig="default"; fi
|
|
34
|
-
ar_init_profile "claude" "$rig" "$project"
|
|
35
|
-
cleanup() { rm -f "$pid_file"; }
|
|
36
|
-
trap cleanup EXIT
|
|
37
|
-
echo $$ > "$pid_file"
|
|
38
|
-
|
|
39
|
-
fetch_cursor() {
|
|
40
|
-
local cursor
|
|
41
|
-
cursor=$(curl -s --fail "${auth_header_args[@]}" "${RELAY_URL}/api/messages/cursor" 2>/dev/null | jq -r '.latestId // empty' 2>/dev/null) || return 1
|
|
42
|
-
case "$cursor" in
|
|
43
|
-
''|*[!0-9]*) return 1 ;;
|
|
44
|
-
*) printf '%s\n' "$cursor" ;;
|
|
45
|
-
esac
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
read_saved_cursor() {
|
|
49
|
-
local cursor
|
|
50
|
-
cursor=$(head -1 "$cursor_file" 2>/dev/null || true)
|
|
51
|
-
case "$cursor" in
|
|
52
|
-
''|*[!0-9]*) return 1 ;;
|
|
53
|
-
*) printf '%s\n' "$cursor" ;;
|
|
54
|
-
esac
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
save_cursor() {
|
|
58
|
-
printf '%s\n' "$1" > "$cursor_file"
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
current_status() {
|
|
62
|
-
local status
|
|
63
|
-
status=$(head -1 "$status_file" 2>/dev/null || true)
|
|
64
|
-
case "$status" in
|
|
65
|
-
online|idle|busy|offline) printf '%s\n' "$status" ;;
|
|
66
|
-
*) printf 'idle\n' ;;
|
|
67
|
-
esac
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
deliver_message() {
|
|
71
|
-
local line="$1"
|
|
72
|
-
if [ "${AGENT_RELAY_CLAUDE_DELIVERY:-monitor}" = "tmux" ] && [ -n "${AGENT_RELAY_CLAUDE_TMUX_SESSION:-}" ] && command -v tmux >/dev/null 2>&1; then
|
|
73
|
-
if tmux send-keys -t "$AGENT_RELAY_CLAUDE_TMUX_SESSION" -l "$line" 2>/dev/null &&
|
|
74
|
-
tmux send-keys -t "$AGENT_RELAY_CLAUDE_TMUX_SESSION" C-m 2>/dev/null; then
|
|
75
|
-
return 0
|
|
76
|
-
fi
|
|
77
|
-
echo "warn: tmux delivery failed for ${AGENT_RELAY_CLAUDE_TMUX_SESSION}" >&2
|
|
78
|
-
return 1
|
|
79
|
-
fi
|
|
80
|
-
|
|
81
|
-
printf '%s\n' "$line"
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
mark_read_or_queue() {
|
|
85
|
-
local mid="$1"
|
|
86
|
-
curl -s -X PATCH "${RELAY_URL}/api/messages/${mid}" "${auth_header_args[@]}" \
|
|
87
|
-
-H 'Content-Type: application/json' \
|
|
88
|
-
-d "{\"readBy\":\"${AGENT_ID}\"}" > /dev/null 2>&1 ||
|
|
89
|
-
printf '%s\n' "$mid" >> "$delivered_file"
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
re_register() {
|
|
93
|
-
local machine rig project reg_code
|
|
94
|
-
machine=$(hostname)
|
|
95
|
-
project=$(basename "${PWD:-unknown}")
|
|
96
|
-
if [ -n "$CLAUDE_RIG_NAME" ]; then rig="$CLAUDE_RIG_NAME"
|
|
97
|
-
elif [ -n "$CLAUDE_CONFIG_DIR" ]; then rig=$(basename "$CLAUDE_CONFIG_DIR")
|
|
98
|
-
else rig="default"; fi
|
|
99
|
-
ar_init_profile "claude" "$rig" "$project"
|
|
100
|
-
|
|
101
|
-
reg_code=$(curl -s -o /dev/null -w '%{http_code}' -X POST "${RELAY_URL}/api/agents" "${auth_header_args[@]}" \
|
|
102
|
-
-H 'Content-Type: application/json' \
|
|
103
|
-
-d "$(jq -n \
|
|
104
|
-
--arg id "$AGENT_ID" \
|
|
105
|
-
--arg name "$project ($rig @ $machine)" \
|
|
106
|
-
--arg machine "$machine" \
|
|
107
|
-
--arg rig "$rig" \
|
|
108
|
-
--arg label "$AR_LABEL" \
|
|
109
|
-
--argjson tags "$AR_TAGS_JSON" \
|
|
110
|
-
--argjson caps "$AR_CAPS_JSON" \
|
|
111
|
-
--argjson meta "$AR_META_JSON" \
|
|
112
|
-
'{id: $id, name: $name, machine: $machine, rig: $rig, tags: $tags, capabilities: $caps, status: "online", meta: $meta} + (if $label != "" then {label: $label} else {} end)'
|
|
113
|
-
)" 2>/dev/null)
|
|
114
|
-
|
|
115
|
-
[ "$reg_code" = "200" ] || [ "$reg_code" = "201" ]
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
set_status() {
|
|
119
|
-
printf '%s\n' "$1" > "$status_file"
|
|
120
|
-
curl -s -o /dev/null -X PATCH "${RELAY_URL}/api/agents/${AGENT_ID}/status" "${auth_header_args[@]}" \
|
|
121
|
-
-H 'Content-Type: application/json' -d "{\"status\":\"$1\"}" 2>/dev/null
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
# First start bootstraps at current max; restarted monitors resume their saved
|
|
125
|
-
# cursor so messages that arrived during downtime are still delivered.
|
|
126
|
-
since_id=$(read_saved_cursor || fetch_cursor || echo 0)
|
|
127
|
-
save_cursor "$since_id"
|
|
128
|
-
|
|
129
|
-
fail_streak=0
|
|
130
|
-
marked_ready=false
|
|
131
|
-
# Save parent PID (Claude Code wrapper) — if it dies, we're orphaned.
|
|
132
|
-
_parent_pid="$PPID"
|
|
133
|
-
while true; do
|
|
134
|
-
if [ "${AGENT_RELAY_DISABLED:-}" = "1" ]; then
|
|
135
|
-
set_status "offline"
|
|
136
|
-
exit 0
|
|
137
|
-
fi
|
|
138
|
-
|
|
139
|
-
# Exit if parent process is gone — prevents orphaned monitors from heartbeating forever.
|
|
140
|
-
if ! kill -0 "$_parent_pid" 2>/dev/null; then
|
|
141
|
-
set_status "offline"
|
|
142
|
-
exit 0
|
|
143
|
-
fi
|
|
144
|
-
|
|
145
|
-
hb_code=$(curl -s -o /dev/null -w '%{http_code}' -X POST "${auth_header_args[@]}" \
|
|
146
|
-
"${RELAY_URL}/api/agents/${AGENT_ID}/heartbeat" 2>/dev/null)
|
|
147
|
-
|
|
148
|
-
# Agent was pruned — re-register instead of exiting.
|
|
149
|
-
if [ "$hb_code" = "404" ]; then
|
|
150
|
-
if re_register; then
|
|
151
|
-
marked_ready=false
|
|
152
|
-
hb_code="200"
|
|
153
|
-
else
|
|
154
|
-
hb_code="000"
|
|
155
|
-
fi
|
|
156
|
-
fi
|
|
157
|
-
|
|
158
|
-
msgs=$(curl -s --fail "${auth_header_args[@]}" "${RELAY_URL}/api/messages?for=${AGENT_ID}&sinceId=${since_id}&unread=true" 2>/dev/null)
|
|
159
|
-
poll_rc=$?
|
|
160
|
-
|
|
161
|
-
if [ "$hb_code" != "200" ] && [ "$hb_code" != "404" ] || [ $poll_rc -ne 0 ]; then
|
|
162
|
-
fail_streak=$((fail_streak + 1))
|
|
163
|
-
case "$fail_streak" in
|
|
164
|
-
1) delay=2 ;;
|
|
165
|
-
2) delay=10 ;;
|
|
166
|
-
3) delay=30 ;;
|
|
167
|
-
*) delay=60 ;;
|
|
168
|
-
esac
|
|
169
|
-
if [ "$fail_streak" = "1" ]; then
|
|
170
|
-
echo "warn: agent-relay unreachable (${RELAY_URL}) — backing off" >&2
|
|
171
|
-
fi
|
|
172
|
-
sleep "$delay"
|
|
173
|
-
continue
|
|
174
|
-
fi
|
|
175
|
-
|
|
176
|
-
# Recovery from failure streak — re-assert status so dashboard shows correct state.
|
|
177
|
-
if [ "$fail_streak" -gt 0 ]; then
|
|
178
|
-
set_status "$(current_status)"
|
|
179
|
-
fi
|
|
180
|
-
fail_streak=0
|
|
181
|
-
|
|
182
|
-
if [ "$marked_ready" = "false" ]; then
|
|
183
|
-
curl -s -o /dev/null -X PATCH "${RELAY_URL}/api/agents/${AGENT_ID}/ready" "${auth_header_args[@]}" \
|
|
184
|
-
-H 'Content-Type: application/json' -d '{"ready":true}' 2>/dev/null
|
|
185
|
-
set_status "idle"
|
|
186
|
-
marked_ready=true
|
|
187
|
-
fi
|
|
188
|
-
|
|
189
|
-
count=$(echo "$msgs" | jq 'length' 2>/dev/null || echo 0)
|
|
190
|
-
if [ "$count" -gt 0 ] 2>/dev/null && [ "$count" != "0" ]; then
|
|
191
|
-
delivery_failed=false
|
|
192
|
-
while IFS= read -r msg; do
|
|
193
|
-
mid=$(printf '%s' "$msg" | jq -r '.id')
|
|
194
|
-
claimable=$(printf '%s' "$msg" | jq -r '.claimable // false')
|
|
195
|
-
if [ "$(ar_message_matches_channels "$msg")" != "true" ]; then
|
|
196
|
-
continue
|
|
197
|
-
fi
|
|
198
|
-
|
|
199
|
-
if [ "$claimable" = "true" ]; then
|
|
200
|
-
claim_code=$(curl -s -o /dev/null -w '%{http_code}' -X POST \
|
|
201
|
-
"${RELAY_URL}/api/messages/${mid}/claim" "${auth_header_args[@]}" \
|
|
202
|
-
-H 'Content-Type: application/json' \
|
|
203
|
-
-d "{\"agentId\":\"${AGENT_ID}\"}" 2>/dev/null)
|
|
204
|
-
if [ "$claim_code" != "200" ]; then
|
|
205
|
-
continue
|
|
206
|
-
fi
|
|
207
|
-
fi
|
|
208
|
-
|
|
209
|
-
if ! line=$(printf '%s' "$msg" | jq -r '(.payload.pairId // "") as $pair | (if $pair != "" then " [pair:\($pair)]" else "" end) as $pairText | (.body | gsub("\r"; "\\r") | gsub("\n"; "\\n")) as $body | if .kind == "system" or .kind == "control" then "⚠ SYSTEM [msg:\(.id)]\($pairText): \($body)" else "[msg:\(.id)]\($pairText) \(.from) → \(.to) | \(.subject // "(no subject)"): \($body)" end') || [ -z "$line" ]; then
|
|
210
|
-
echo "warn: failed to format relay message ${mid}" >&2
|
|
211
|
-
delivery_failed=true
|
|
212
|
-
continue
|
|
213
|
-
fi
|
|
214
|
-
if deliver_message "$line"; then
|
|
215
|
-
mark_read_or_queue "$mid"
|
|
216
|
-
else
|
|
217
|
-
delivery_failed=true
|
|
218
|
-
fi
|
|
219
|
-
done < <(echo "$msgs" | jq -c '.[]')
|
|
220
|
-
if [ "$delivery_failed" = "false" ]; then
|
|
221
|
-
since_id=$(echo "$msgs" | jq '[.[].id] | max')
|
|
222
|
-
save_cursor "$since_id"
|
|
223
|
-
fi
|
|
224
|
-
fi
|
|
225
|
-
|
|
226
|
-
sleep "$INTERVAL"
|
|
227
|
-
done
|
package/hooks/profile-lib.sh
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
|
|
3
|
-
ar_csv_json() {
|
|
4
|
-
printf '%s' "$1" | jq -R 'split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0)) | reduce .[] as $x ([]; if index($x) then . else . + [$x] end)'
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
ar_profile_value() {
|
|
8
|
-
local key="$1"
|
|
9
|
-
printf '%s' "${AR_PROFILE_JSON}" | jq -r --arg key "$key" '.[$key] // empty | if type == "string" then gsub("^\\s+|\\s+$"; "") else empty end'
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
ar_profile_array() {
|
|
13
|
-
local key="$1"
|
|
14
|
-
printf '%s' "${AR_PROFILE_JSON}" | jq -c --arg key "$key" 'if (.[$key] | type) == "array" then .[$key] | map(select(type == "string") | gsub("^\\s+|\\s+$"; "") | select(length > 0)) | reduce .[] as $x ([]; if index($x) then . else . + [$x] end) else [] end'
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
ar_init_profile() {
|
|
18
|
-
local provider="$1"
|
|
19
|
-
local rig="$2"
|
|
20
|
-
local project="$3"
|
|
21
|
-
local profiles_file profile_meta extra_tags
|
|
22
|
-
|
|
23
|
-
AR_PROFILE_NAME="${AGENT_RELAY_PROFILE:-}"
|
|
24
|
-
profiles_file="${AGENT_RELAY_PROFILES_FILE:-${HOME:-}/.config/agent-relay/profiles.json}"
|
|
25
|
-
AR_PROFILE_JSON="{}"
|
|
26
|
-
if [ -n "$AR_PROFILE_NAME" ] && [ -f "$profiles_file" ]; then
|
|
27
|
-
AR_PROFILE_JSON=$(jq -c --arg name "$AR_PROFILE_NAME" '.[$name] // {}' "$profiles_file" 2>/dev/null || printf '{}')
|
|
28
|
-
fi
|
|
29
|
-
|
|
30
|
-
if [ -n "${AGENT_RELAY_CAPS:-}" ]; then
|
|
31
|
-
AR_CAPS_JSON=$(ar_csv_json "$AGENT_RELAY_CAPS")
|
|
32
|
-
else
|
|
33
|
-
AR_CAPS_JSON=$(ar_profile_array capabilities)
|
|
34
|
-
if [ "$AR_CAPS_JSON" = "[]" ]; then AR_CAPS_JSON='["chat"]'; fi
|
|
35
|
-
fi
|
|
36
|
-
|
|
37
|
-
if [ -n "${AGENT_RELAY_TAGS:-}" ]; then
|
|
38
|
-
extra_tags=$(ar_csv_json "$AGENT_RELAY_TAGS")
|
|
39
|
-
else
|
|
40
|
-
extra_tags=$(ar_profile_array tags)
|
|
41
|
-
fi
|
|
42
|
-
AR_TAGS_JSON=$(jq -n --arg provider "$provider" --arg rig "$rig" --arg project "$project" --argjson extra "$extra_tags" \
|
|
43
|
-
'[$provider, $rig, $project] + $extra | map(select(length > 0)) | reduce .[] as $x ([]; if index($x) then . else . + [$x] end)')
|
|
44
|
-
|
|
45
|
-
if [ -n "${AGENT_RELAY_CHANNELS:-}" ]; then
|
|
46
|
-
AR_CHANNELS_JSON=$(ar_csv_json "$AGENT_RELAY_CHANNELS")
|
|
47
|
-
else
|
|
48
|
-
AR_CHANNELS_JSON=$(ar_profile_array channels)
|
|
49
|
-
fi
|
|
50
|
-
|
|
51
|
-
AR_LABEL="${AGENT_RELAY_LABEL:-$(ar_profile_value label)}"
|
|
52
|
-
AR_APPROVAL="${AGENT_RELAY_APPROVAL:-$(ar_profile_value approval)}"
|
|
53
|
-
AR_APPROVAL="${AR_APPROVAL:-open}"
|
|
54
|
-
case "$AR_APPROVAL" in
|
|
55
|
-
open|guarded|read-only) ;;
|
|
56
|
-
*) AR_APPROVAL="open" ;;
|
|
57
|
-
esac
|
|
58
|
-
profile_meta=$(printf '%s' "$AR_PROFILE_JSON" | jq -c 'if (.meta | type) == "object" then .meta else {} end' 2>/dev/null || printf '{}')
|
|
59
|
-
AR_META_JSON=$(jq -n \
|
|
60
|
-
--arg provider "$provider" \
|
|
61
|
-
--arg cwd "${PWD:-}" \
|
|
62
|
-
--arg approval "$AR_APPROVAL" \
|
|
63
|
-
--arg profile "$AR_PROFILE_NAME" \
|
|
64
|
-
--argjson channels "$AR_CHANNELS_JSON" \
|
|
65
|
-
--argjson profileMeta "$profile_meta" \
|
|
66
|
-
'$profileMeta + {provider: $provider, cwd: $cwd, approvalMode: $approval, channels: $channels} + (if $profile != "" then {profile: $profile} else {} end)')
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
ar_message_matches_channels() {
|
|
70
|
-
local msg="$1"
|
|
71
|
-
printf '%s' "$msg" | jq -r --argjson channels "${AR_CHANNELS_JSON:-[]}" \
|
|
72
|
-
'(.channel // "") as $ch | if (($channels | length) == 0) or ($ch == "") or (($channels | index($ch)) != null) then "true" else "false" end'
|
|
73
|
-
}
|
package/hooks/relay-monitor.sh
DELETED
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Agent Relay — self-contained native monitor
|
|
3
|
-
# Registers this session as an agent, outputs messaging context, then polls inbox.
|
|
4
|
-
# Runs automatically via monitors.json — no user interaction required.
|
|
5
|
-
# CLAUDE_CODE_SESSION_ID = stable session key for state file coordination between monitor and hooks.
|
|
6
|
-
|
|
7
|
-
if [ "${AGENT_RELAY_DISABLED:-}" = "1" ]; then
|
|
8
|
-
exit 0
|
|
9
|
-
fi
|
|
10
|
-
|
|
11
|
-
RELAY_URL="${AGENT_RELAY_URL:-http://localhost:4850}"
|
|
12
|
-
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
13
|
-
source "${script_dir}/profile-lib.sh"
|
|
14
|
-
plugin_root="${CLAUDE_PLUGIN_ROOT:-$(cd "${script_dir}/.." && pwd)}"
|
|
15
|
-
PLUGIN_VERSION=$(
|
|
16
|
-
jq -r '.version // empty' "${plugin_root}/.claude-plugin/plugin.json" 2>/dev/null ||
|
|
17
|
-
jq -r '.version // empty' "${plugin_root}/package.json" 2>/dev/null ||
|
|
18
|
-
true
|
|
19
|
-
)
|
|
20
|
-
PLUGIN_VERSION="${PLUGIN_VERSION:-unknown}"
|
|
21
|
-
auth_header_args=()
|
|
22
|
-
auth_header_example=' ${AGENT_RELAY_TOKEN:+-H "X-Agent-Relay-Token: ${AGENT_RELAY_TOKEN}"}'
|
|
23
|
-
if [ -n "${AGENT_RELAY_TOKEN:-}" ]; then
|
|
24
|
-
auth_header_args=(-H "X-Agent-Relay-Token: ${AGENT_RELAY_TOKEN}")
|
|
25
|
-
fi
|
|
26
|
-
|
|
27
|
-
# --- Compute agent identity ---
|
|
28
|
-
machine=$(hostname)
|
|
29
|
-
project=$(basename "${PWD:-unknown}")
|
|
30
|
-
|
|
31
|
-
if [ -n "$CLAUDE_RIG_NAME" ]; then
|
|
32
|
-
rig="$CLAUDE_RIG_NAME"
|
|
33
|
-
elif [ -n "$CLAUDE_CONFIG_DIR" ]; then
|
|
34
|
-
rig=$(basename "$CLAUDE_CONFIG_DIR")
|
|
35
|
-
else
|
|
36
|
-
rig="default"
|
|
37
|
-
fi
|
|
38
|
-
ar_init_profile "claude" "$rig" "$project"
|
|
39
|
-
|
|
40
|
-
session_key="${CLAUDE_CODE_SESSION_ID:-$PPID}"
|
|
41
|
-
safe_session_key=$(printf '%s' "$session_key" | sed 's/[^A-Za-z0-9_.:-]/_/g')
|
|
42
|
-
if command -v shasum >/dev/null 2>&1; then
|
|
43
|
-
short_pid=$(printf '%s' "$session_key" | shasum -a 1 | head -c 6)
|
|
44
|
-
elif command -v sha1sum >/dev/null 2>&1; then
|
|
45
|
-
short_pid=$(printf '%s' "$session_key" | sha1sum | head -c 6)
|
|
46
|
-
else
|
|
47
|
-
short_pid=$(printf '%s' "$session_key" | md5sum | head -c 6)
|
|
48
|
-
fi
|
|
49
|
-
agent_id="${machine}-${rig}-${project}-${short_pid}"
|
|
50
|
-
|
|
51
|
-
# --- Clean up previous agent from this instance ---
|
|
52
|
-
instance_state="/tmp/agent-relay-instance-${session_key}.state"
|
|
53
|
-
if [ -f "$instance_state" ]; then
|
|
54
|
-
old_agent_id=$(head -1 "$instance_state" 2>/dev/null)
|
|
55
|
-
if [ -n "$old_agent_id" ] && [ "$old_agent_id" != "$agent_id" ]; then
|
|
56
|
-
old_pid_file="/tmp/agent-relay-poll-${old_agent_id}.pid"
|
|
57
|
-
if [ -f "$old_pid_file" ]; then
|
|
58
|
-
old_pid=$(cat "$old_pid_file" 2>/dev/null)
|
|
59
|
-
if [ -n "$old_pid" ] && kill -0 "$old_pid" 2>/dev/null; then
|
|
60
|
-
kill "$old_pid" 2>/dev/null
|
|
61
|
-
fi
|
|
62
|
-
rm -f "$old_pid_file"
|
|
63
|
-
fi
|
|
64
|
-
curl -s -o /dev/null -X PATCH "${RELAY_URL}/api/agents/${old_agent_id}/status" "${auth_header_args[@]}" \
|
|
65
|
-
-H 'Content-Type: application/json' -d '{"status":"offline"}' 2>/dev/null
|
|
66
|
-
fi
|
|
67
|
-
fi
|
|
68
|
-
printf '%s\n' "$agent_id" > "$instance_state"
|
|
69
|
-
|
|
70
|
-
context_dir="${HOME:-/tmp}/.agent-relay/contexts"
|
|
71
|
-
context_path="${context_dir}/${safe_session_key}.json"
|
|
72
|
-
mkdir -p "$context_dir" 2>/dev/null || true
|
|
73
|
-
jq -n \
|
|
74
|
-
--arg agentId "$agent_id" \
|
|
75
|
-
--arg relayUrl "$RELAY_URL" \
|
|
76
|
-
--arg cwd "${PWD:-}" \
|
|
77
|
-
--arg session "$session_key" \
|
|
78
|
-
--arg updatedAt "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
79
|
-
'{
|
|
80
|
-
version: 1,
|
|
81
|
-
agentId: $agentId,
|
|
82
|
-
provider: "claude",
|
|
83
|
-
relayUrl: $relayUrl,
|
|
84
|
-
cwd: $cwd,
|
|
85
|
-
matchEnv: [{name: "CLAUDE_CODE_SESSION_ID", value: $session}],
|
|
86
|
-
updatedAt: $updatedAt
|
|
87
|
-
}' > "$context_path" 2>/dev/null || true
|
|
88
|
-
|
|
89
|
-
reg_code=$(curl -s -o /dev/null -w '%{http_code}' -X POST "${RELAY_URL}/api/agents" "${auth_header_args[@]}" \
|
|
90
|
-
-H 'Content-Type: application/json' \
|
|
91
|
-
-d "$(jq -n \
|
|
92
|
-
--arg id "$agent_id" \
|
|
93
|
-
--arg name "$project ($rig @ $machine)" \
|
|
94
|
-
--arg machine "$machine" \
|
|
95
|
-
--arg rig "$rig" \
|
|
96
|
-
--arg label "$AR_LABEL" \
|
|
97
|
-
--argjson tags "$AR_TAGS_JSON" \
|
|
98
|
-
--argjson caps "$AR_CAPS_JSON" \
|
|
99
|
-
--argjson meta "$AR_META_JSON" \
|
|
100
|
-
'{id: $id, name: $name, machine: $machine, rig: $rig, tags: $tags, capabilities: $caps, status: "online", meta: $meta} + (if $label != "" then {label: $label} else {} end)'
|
|
101
|
-
)" 2>/dev/null)
|
|
102
|
-
|
|
103
|
-
if [ "$reg_code" != "201" ] && [ "$reg_code" != "200" ]; then
|
|
104
|
-
echo "warn: agent-relay register failed (HTTP ${reg_code:-none}) — ${RELAY_URL}" >&2
|
|
105
|
-
fi
|
|
106
|
-
|
|
107
|
-
# --- Check server version ---
|
|
108
|
-
server_version=$(curl -s "${auth_header_args[@]}" "${RELAY_URL}/api/stats" 2>/dev/null | jq -r '.version // empty' 2>/dev/null)
|
|
109
|
-
version_warning=""
|
|
110
|
-
if [ -z "$server_version" ]; then
|
|
111
|
-
version_warning="⚠ Server does not report its version — consider updating: agent-relay upgrade"
|
|
112
|
-
elif [ "$PLUGIN_VERSION" = "unknown" ]; then
|
|
113
|
-
version_warning="⚠ Version mismatch — server ${server_version}, plugin unknown. Restart Claude Code or update the Agent Relay plugin."
|
|
114
|
-
elif [ "$server_version" != "$PLUGIN_VERSION" ]; then
|
|
115
|
-
version_warning="⚠ Version mismatch — server ${server_version}, plugin ${PLUGIN_VERSION}. Update/restart Agent Relay server and plugin so the versions match."
|
|
116
|
-
fi
|
|
117
|
-
|
|
118
|
-
# --- Output context (first stdout = notification injected into conversation) ---
|
|
119
|
-
approval_note=""
|
|
120
|
-
case "$AR_APPROVAL" in
|
|
121
|
-
guarded) approval_note=" — destructive ops (rm, mv, chmod, kill, force-push) are blocked" ;;
|
|
122
|
-
read-only) approval_note=" — observe/analyze/report only, no file writes or mutation commands" ;;
|
|
123
|
-
esac
|
|
124
|
-
|
|
125
|
-
cat <<CONTEXT
|
|
126
|
-
Agent Relay active. Your agent ID: ${agent_id}
|
|
127
|
-
Relay URL: ${RELAY_URL} | Server: ${server_version:-unknown} | Plugin: ${PLUGIN_VERSION}
|
|
128
|
-
Profile: ${AR_PROFILE_NAME:-none} | Label: ${AR_LABEL:-none}
|
|
129
|
-
Tags: $(printf '%s' "$AR_TAGS_JSON" | jq -r 'join(", ")') | Capabilities: $(printf '%s' "$AR_CAPS_JSON" | jq -r 'join(", ")')
|
|
130
|
-
Channels: $(printf '%s' "$AR_CHANNELS_JSON" | jq -r 'if length == 0 then "all" else join(", ") end')
|
|
131
|
-
Approval mode: ${AR_APPROVAL}${approval_note}
|
|
132
|
-
|
|
133
|
-
To send a message:
|
|
134
|
-
curl -s -X POST ${RELAY_URL}/api/messages${auth_header_example} -H 'Content-Type: application/json' -d '{"from":"${agent_id}","to":"TARGET","body":"MESSAGE"}'
|
|
135
|
-
TARGET: agent-id (direct), tag:NAME (by tag), cap:NAME (by capability), label:NAME (by human-set label, fan-out), broadcast (all)
|
|
136
|
-
|
|
137
|
-
To rename yourself (set a short, human-friendly label so the user can refer to you easily):
|
|
138
|
-
curl -s -X PATCH ${RELAY_URL}/api/agents/${agent_id}/label${auth_header_example} -H 'Content-Type: application/json' -d '{"label":"backend fixing"}'
|
|
139
|
-
When the user tells you to rename yourself ("call yourself X", "rename to X"), call this endpoint.
|
|
140
|
-
Labels are NOT unique — if multiple sessions share a label, \`to: "label:X"\` fans out to all of them.
|
|
141
|
-
Use \`to: "label:backend fixing"\` when the user says "tell 'backend fixing' to do Y".
|
|
142
|
-
|
|
143
|
-
To reply to a message (threading):
|
|
144
|
-
curl -s -X POST ${RELAY_URL}/api/messages${auth_header_example} -H 'Content-Type: application/json' -d '{"from":"${agent_id}","to":"TARGET","body":"MESSAGE","replyTo":MSG_ID}'
|
|
145
|
-
Always use replyTo when responding to a specific message — it creates a thread.
|
|
146
|
-
The inbox monitor prefixes each message with [msg:ID] — parse the ID from there.
|
|
147
|
-
Message etiquette: when another agent messages you, send a short acknowledgement or status reply unless the message is obvious spam/noise.
|
|
148
|
-
Anti-loop rule: do not auto-reply to pure acknowledgements, "thanks", or "received" messages; acknowledge once, then only send follow-ups when there is new work, a decision, or a deliverable.
|
|
149
|
-
|
|
150
|
-
To send a claimable task (only one agent can claim it):
|
|
151
|
-
curl -s -X POST ${RELAY_URL}/api/messages${auth_header_example} -H 'Content-Type: application/json' -d '{"from":"${agent_id}","to":"cap:review","kind":"task","body":"TASK","claimable":true,"payload":{"title":"TASK"}}'
|
|
152
|
-
|
|
153
|
-
To claim a task:
|
|
154
|
-
curl -s -X POST ${RELAY_URL}/api/messages/MSG_ID/claim${auth_header_example} -H 'Content-Type: application/json' -d '{"agentId":"${agent_id}"}'
|
|
155
|
-
When you see a claimable message, claim it BEFORE starting work. If claim fails, someone else got it — move on.
|
|
156
|
-
|
|
157
|
-
To view a thread:
|
|
158
|
-
curl -s${auth_header_example} '${RELAY_URL}/api/messages/MSG_ID/thread'
|
|
159
|
-
|
|
160
|
-
To check messages:
|
|
161
|
-
curl -s${auth_header_example} '${RELAY_URL}/api/messages?for=${agent_id}&unread=true'
|
|
162
|
-
|
|
163
|
-
To mark read:
|
|
164
|
-
curl -s -X PATCH ${RELAY_URL}/api/messages/ID${auth_header_example} -H 'Content-Type: application/json' -d '{"readBy":"${agent_id}"}'
|
|
165
|
-
The inbox monitor marks read after it successfully delivers a message to Claude. The Stop hook retries any failed read acknowledgements.
|
|
166
|
-
|
|
167
|
-
To find agents by capability:
|
|
168
|
-
curl -s${auth_header_example} '${RELAY_URL}/api/agents/find?capability=review'
|
|
169
|
-
|
|
170
|
-
To list online agents:
|
|
171
|
-
curl -s${auth_header_example} '${RELAY_URL}/api/agents?status=online'
|
|
172
|
-
|
|
173
|
-
To pair with one available agent for focused two-party live collaboration:
|
|
174
|
-
If the user types "/pair codex", "/pair status", "/pair send PAIR_ID MESSAGE", "/disconnect", "/status", "/label LABEL", or "/tags TAGS", run the matching agent-relay CLI command.
|
|
175
|
-
agent-relay /pair codex "WORK TO SOLVE TOGETHER"
|
|
176
|
-
agent-relay /pair status
|
|
177
|
-
agent-relay /pair send PAIR_ID "MESSAGE"
|
|
178
|
-
agent-relay /disconnect
|
|
179
|
-
agent-relay /status
|
|
180
|
-
agent-relay /label backend-fixer
|
|
181
|
-
agent-relay /tags backend tests urgent
|
|
182
|
-
If the CLI is unavailable, use the API directly:
|
|
183
|
-
curl -s -X POST ${RELAY_URL}/api/pairs${auth_header_example} -H 'Content-Type: application/json' -d '{"from":"${agent_id}","target":"codex","objective":"WORK TO SOLVE TOGETHER"}'
|
|
184
|
-
Accept an incoming pair invite: curl -s -X POST ${RELAY_URL}/api/pairs/PAIR_ID/accept${auth_header_example} -H 'Content-Type: application/json' -d '{"agentId":"${agent_id}"}'
|
|
185
|
-
Send pair chat: curl -s -X POST ${RELAY_URL}/api/pairs/PAIR_ID/messages${auth_header_example} -H 'Content-Type: application/json' -d '{"from":"${agent_id}","body":"MESSAGE"}'
|
|
186
|
-
Hang up: curl -s -X POST ${RELAY_URL}/api/pairs/PAIR_ID/hangup${auth_header_example} -H 'Content-Type: application/json' -d '{"agentId":"${agent_id}"}'
|
|
187
|
-
Pairing is exclusive: one agent can have only one pending or active pair.
|
|
188
|
-
|
|
189
|
-
System messages:
|
|
190
|
-
Messages prefixed with "⚠ SYSTEM" are relay-level instructions (e.g. plugin updates, restart requests).
|
|
191
|
-
When you see one, follow the instruction immediately — for example, restart the inbox monitor if asked.
|
|
192
|
-
CONTEXT
|
|
193
|
-
|
|
194
|
-
if [ -n "$version_warning" ]; then
|
|
195
|
-
echo ""
|
|
196
|
-
echo "$version_warning"
|
|
197
|
-
fi
|
|
198
|
-
|
|
199
|
-
# --- Check for unread messages ---
|
|
200
|
-
unread=$(curl -s --fail "${auth_header_args[@]}" "${RELAY_URL}/api/messages?for=${agent_id}&unread=true" 2>/dev/null)
|
|
201
|
-
unread_count=$(echo "$unread" | jq 'length' 2>/dev/null || echo "0")
|
|
202
|
-
if [ "$unread_count" -gt 0 ] 2>/dev/null && [ "$unread_count" != "0" ]; then
|
|
203
|
-
echo ""
|
|
204
|
-
echo "${unread_count} unread message(s):"
|
|
205
|
-
echo "$unread" | jq -r '.[] | " [\(.from)] \(.subject // "(no subject)"): \(.body | if length > 120 then .[:120] + "..." else . end)"'
|
|
206
|
-
fi
|
|
207
|
-
|
|
208
|
-
# --- Start poll loop ---
|
|
209
|
-
exec "$(dirname "$0")/poll-inbox.sh" "$RELAY_URL" "$agent_id" 10
|
package/hooks/session-end.sh
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Agent Relay plugin — SessionEnd hook
|
|
3
|
-
# Marks this session's agent as offline using state file written by relay-monitor.
|
|
4
|
-
|
|
5
|
-
if [ "${AGENT_RELAY_DISABLED:-}" = "1" ]; then
|
|
6
|
-
exit 0
|
|
7
|
-
fi
|
|
8
|
-
|
|
9
|
-
RELAY_URL="${AGENT_RELAY_URL:-http://localhost:4850}"
|
|
10
|
-
auth_header_args=()
|
|
11
|
-
if [ -n "${AGENT_RELAY_TOKEN:-}" ]; then
|
|
12
|
-
auth_header_args=(-H "X-Agent-Relay-Token: ${AGENT_RELAY_TOKEN}")
|
|
13
|
-
fi
|
|
14
|
-
input=$(cat)
|
|
15
|
-
session_key="${CLAUDE_CODE_SESSION_ID:-}"
|
|
16
|
-
if [ -z "$session_key" ] && [ -n "$input" ]; then
|
|
17
|
-
session_key=$(printf '%s' "$input" | jq -r '.session_id // empty' 2>/dev/null)
|
|
18
|
-
fi
|
|
19
|
-
if [ -z "$session_key" ]; then
|
|
20
|
-
exit 0
|
|
21
|
-
fi
|
|
22
|
-
instance_state="/tmp/agent-relay-instance-${session_key}.state"
|
|
23
|
-
safe_session_key=$(printf '%s' "$session_key" | sed 's/[^A-Za-z0-9_.:-]/_/g')
|
|
24
|
-
context_path="${HOME:-/tmp}/.agent-relay/contexts/${safe_session_key}.json"
|
|
25
|
-
|
|
26
|
-
if [ -f "$instance_state" ]; then
|
|
27
|
-
agent_id=$(head -1 "$instance_state" 2>/dev/null)
|
|
28
|
-
fi
|
|
29
|
-
|
|
30
|
-
if [ -n "${agent_id:-}" ]; then
|
|
31
|
-
poll_pid_file="/tmp/agent-relay-poll-${agent_id}.pid"
|
|
32
|
-
if [ -f "$poll_pid_file" ]; then
|
|
33
|
-
poll_pid=$(cat "$poll_pid_file" 2>/dev/null)
|
|
34
|
-
if [ -n "$poll_pid" ] && kill -0 "$poll_pid" 2>/dev/null; then
|
|
35
|
-
kill "$poll_pid" 2>/dev/null
|
|
36
|
-
fi
|
|
37
|
-
rm -f "$poll_pid_file"
|
|
38
|
-
fi
|
|
39
|
-
printf 'offline\n' > "/tmp/agent-relay-status-${agent_id}.state"
|
|
40
|
-
curl -s -X PATCH "${RELAY_URL}/api/agents/${agent_id}/status" "${auth_header_args[@]}" \
|
|
41
|
-
-H 'Content-Type: application/json' \
|
|
42
|
-
-d '{"status":"offline"}' \
|
|
43
|
-
> /dev/null 2>&1 &
|
|
44
|
-
[ -n "$instance_state" ] && [ -f "$instance_state" ] && rm -f "$instance_state"
|
|
45
|
-
[ -f "$context_path" ] && rm -f "$context_path"
|
|
46
|
-
fi
|
|
47
|
-
|
|
48
|
-
exit 0
|
package/hooks/set-status.sh
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Agent Relay plugin — status update helper for Claude turn hooks.
|
|
3
|
-
# Usage: set-status.sh <online|idle|busy|offline>
|
|
4
|
-
|
|
5
|
-
if [ "${AGENT_RELAY_DISABLED:-}" = "1" ]; then
|
|
6
|
-
exit 0
|
|
7
|
-
fi
|
|
8
|
-
|
|
9
|
-
status="${1:-}"
|
|
10
|
-
case "$status" in
|
|
11
|
-
online|idle|busy|offline) ;;
|
|
12
|
-
*) exit 0 ;;
|
|
13
|
-
esac
|
|
14
|
-
|
|
15
|
-
RELAY_URL="${AGENT_RELAY_URL:-http://localhost:4850}"
|
|
16
|
-
auth_header_args=()
|
|
17
|
-
if [ -n "${AGENT_RELAY_TOKEN:-}" ]; then
|
|
18
|
-
auth_header_args=(-H "X-Agent-Relay-Token: ${AGENT_RELAY_TOKEN}")
|
|
19
|
-
fi
|
|
20
|
-
input=$(cat)
|
|
21
|
-
session_key="${CLAUDE_CODE_SESSION_ID:-}"
|
|
22
|
-
if [ -z "$session_key" ] && [ -n "$input" ]; then
|
|
23
|
-
session_key=$(printf '%s' "$input" | jq -r '.session_id // empty' 2>/dev/null)
|
|
24
|
-
fi
|
|
25
|
-
if [ -z "$session_key" ]; then
|
|
26
|
-
exit 0
|
|
27
|
-
fi
|
|
28
|
-
instance_state="/tmp/agent-relay-instance-${session_key}.state"
|
|
29
|
-
|
|
30
|
-
if [ ! -f "$instance_state" ]; then
|
|
31
|
-
exit 0
|
|
32
|
-
fi
|
|
33
|
-
|
|
34
|
-
agent_id=$(head -1 "$instance_state" 2>/dev/null || true)
|
|
35
|
-
if [ -z "$agent_id" ]; then
|
|
36
|
-
exit 0
|
|
37
|
-
fi
|
|
38
|
-
|
|
39
|
-
printf '%s\n' "$status" > "/tmp/agent-relay-status-${agent_id}.state"
|
|
40
|
-
|
|
41
|
-
curl -s -o /dev/null -X PATCH "${RELAY_URL}/api/agents/${agent_id}/status" "${auth_header_args[@]}" \
|
|
42
|
-
-H 'Content-Type: application/json' \
|
|
43
|
-
-d "{\"status\":\"${status}\"}" \
|
|
44
|
-
2>/dev/null || true
|
|
45
|
-
|
|
46
|
-
if [ "$status" = "idle" ]; then
|
|
47
|
-
delivered_file="/tmp/agent-relay-delivered-${agent_id}.queue"
|
|
48
|
-
if [ -s "$delivered_file" ]; then
|
|
49
|
-
processing_file="${delivered_file}.$$"
|
|
50
|
-
if mv "$delivered_file" "$processing_file" 2>/dev/null; then
|
|
51
|
-
sort -n -u "$processing_file" | while IFS= read -r mid; do
|
|
52
|
-
case "$mid" in
|
|
53
|
-
''|*[!0-9]*) continue ;;
|
|
54
|
-
esac
|
|
55
|
-
curl -s -o /dev/null -X PATCH "${RELAY_URL}/api/messages/${mid}" "${auth_header_args[@]}" \
|
|
56
|
-
-H 'Content-Type: application/json' \
|
|
57
|
-
-d "{\"readBy\":\"${agent_id}\"}" \
|
|
58
|
-
2>/dev/null || printf '%s\n' "$mid" >> "$delivered_file"
|
|
59
|
-
done
|
|
60
|
-
rm -f "$processing_file"
|
|
61
|
-
fi
|
|
62
|
-
fi
|
|
63
|
-
fi
|
|
64
|
-
|
|
65
|
-
exit 0
|