agent-relay-plugin 0.6.0 → 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.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "agent-relay",
3
3
  "description": "Client connector for Agent Relay — auto-registers Claude Code sessions as agents and enables inter-agent messaging via a lightweight HTTP message bus",
4
- "version": "0.6.0"
4
+ "version": "0.10.0"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-plugin",
3
- "version": "0.6.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.
@@ -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
- }
@@ -1,214 +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
- project=$(basename "${PWD:-unknown}")
30
- if [ -n "$CLAUDE_RIG_NAME" ]; then rig="$CLAUDE_RIG_NAME"
31
- elif [ -n "$CLAUDE_CONFIG_DIR" ]; then rig=$(basename "$CLAUDE_CONFIG_DIR")
32
- else rig="default"; fi
33
- ar_init_profile "claude" "$rig" "$project"
34
- cleanup() { rm -f "$pid_file"; }
35
- trap cleanup EXIT
36
- echo $$ > "$pid_file"
37
-
38
- fetch_cursor() {
39
- local cursor
40
- cursor=$(curl -s --fail "${auth_header_args[@]}" "${RELAY_URL}/api/messages/cursor" 2>/dev/null | jq -r '.latestId // empty' 2>/dev/null) || return 1
41
- case "$cursor" in
42
- ''|*[!0-9]*) return 1 ;;
43
- *) printf '%s\n' "$cursor" ;;
44
- esac
45
- }
46
-
47
- current_status() {
48
- local status
49
- status=$(head -1 "$status_file" 2>/dev/null || true)
50
- case "$status" in
51
- online|idle|busy|offline) printf '%s\n' "$status" ;;
52
- *) printf 'idle\n' ;;
53
- esac
54
- }
55
-
56
- deliver_message() {
57
- local line="$1"
58
- if [ "${AGENT_RELAY_CLAUDE_DELIVERY:-monitor}" = "tmux" ] && [ -n "${AGENT_RELAY_CLAUDE_TMUX_SESSION:-}" ] && command -v tmux >/dev/null 2>&1; then
59
- if tmux send-keys -t "$AGENT_RELAY_CLAUDE_TMUX_SESSION" -l "$line" 2>/dev/null &&
60
- tmux send-keys -t "$AGENT_RELAY_CLAUDE_TMUX_SESSION" C-m 2>/dev/null; then
61
- return 0
62
- fi
63
- echo "warn: tmux delivery failed for ${AGENT_RELAY_CLAUDE_TMUX_SESSION}" >&2
64
- return 1
65
- fi
66
-
67
- printf '%s\n' "$line"
68
- }
69
-
70
- mark_read_or_queue() {
71
- local mid="$1"
72
- curl -s -X PATCH "${RELAY_URL}/api/messages/${mid}" "${auth_header_args[@]}" \
73
- -H 'Content-Type: application/json' \
74
- -d "{\"readBy\":\"${AGENT_ID}\"}" > /dev/null 2>&1 ||
75
- printf '%s\n' "$mid" >> "$delivered_file"
76
- }
77
-
78
- re_register() {
79
- local machine rig project reg_code
80
- machine=$(hostname)
81
- project=$(basename "${PWD:-unknown}")
82
- if [ -n "$CLAUDE_RIG_NAME" ]; then rig="$CLAUDE_RIG_NAME"
83
- elif [ -n "$CLAUDE_CONFIG_DIR" ]; then rig=$(basename "$CLAUDE_CONFIG_DIR")
84
- else rig="default"; fi
85
- ar_init_profile "claude" "$rig" "$project"
86
-
87
- reg_code=$(curl -s -o /dev/null -w '%{http_code}' -X POST "${RELAY_URL}/api/agents" "${auth_header_args[@]}" \
88
- -H 'Content-Type: application/json' \
89
- -d "$(jq -n \
90
- --arg id "$AGENT_ID" \
91
- --arg name "$project ($rig @ $machine)" \
92
- --arg machine "$machine" \
93
- --arg rig "$rig" \
94
- --arg label "$AR_LABEL" \
95
- --argjson tags "$AR_TAGS_JSON" \
96
- --argjson caps "$AR_CAPS_JSON" \
97
- --argjson meta "$AR_META_JSON" \
98
- '{id: $id, name: $name, machine: $machine, rig: $rig, tags: $tags, capabilities: $caps, status: "online", meta: $meta} + (if $label != "" then {label: $label} else {} end)'
99
- )" 2>/dev/null)
100
-
101
- [ "$reg_code" = "200" ] || [ "$reg_code" = "201" ]
102
- }
103
-
104
- set_status() {
105
- printf '%s\n' "$1" > "$status_file"
106
- curl -s -o /dev/null -X PATCH "${RELAY_URL}/api/agents/${AGENT_ID}/status" "${auth_header_args[@]}" \
107
- -H 'Content-Type: application/json' -d "{\"status\":\"$1\"}" 2>/dev/null
108
- }
109
-
110
- # Bootstrap cursor at current max so we only see messages arriving after session start.
111
- since_id=$(fetch_cursor || echo 0)
112
-
113
- fail_streak=0
114
- marked_ready=false
115
- # Save parent PID (Claude Code wrapper) — if it dies, we're orphaned.
116
- _parent_pid="$PPID"
117
- while true; do
118
- if [ "${AGENT_RELAY_DISABLED:-}" = "1" ]; then
119
- set_status "offline"
120
- exit 0
121
- fi
122
-
123
- # Exit if parent process is gone — prevents orphaned monitors from heartbeating forever.
124
- if ! kill -0 "$_parent_pid" 2>/dev/null; then
125
- set_status "offline"
126
- exit 0
127
- fi
128
-
129
- hb_code=$(curl -s -o /dev/null -w '%{http_code}' -X POST "${auth_header_args[@]}" \
130
- "${RELAY_URL}/api/agents/${AGENT_ID}/heartbeat" 2>/dev/null)
131
-
132
- # Agent was pruned — re-register instead of exiting.
133
- if [ "$hb_code" = "404" ]; then
134
- if re_register; then
135
- if since_id=$(fetch_cursor); then
136
- marked_ready=false
137
- hb_code="200"
138
- else
139
- hb_code="000"
140
- fi
141
- else
142
- hb_code="000"
143
- fi
144
- fi
145
-
146
- msgs=$(curl -s --fail "${auth_header_args[@]}" "${RELAY_URL}/api/messages?for=${AGENT_ID}&sinceId=${since_id}&unread=true" 2>/dev/null)
147
- poll_rc=$?
148
-
149
- if [ "$hb_code" != "200" ] && [ "$hb_code" != "404" ] || [ $poll_rc -ne 0 ]; then
150
- fail_streak=$((fail_streak + 1))
151
- case "$fail_streak" in
152
- 1) delay=2 ;;
153
- 2) delay=10 ;;
154
- 3) delay=30 ;;
155
- *) delay=60 ;;
156
- esac
157
- if [ "$fail_streak" = "1" ]; then
158
- echo "warn: agent-relay unreachable (${RELAY_URL}) — backing off" >&2
159
- fi
160
- sleep "$delay"
161
- continue
162
- fi
163
-
164
- # Recovery from failure streak — re-assert status so dashboard shows correct state.
165
- if [ "$fail_streak" -gt 0 ]; then
166
- set_status "$(current_status)"
167
- fi
168
- fail_streak=0
169
-
170
- if [ "$marked_ready" = "false" ]; then
171
- curl -s -o /dev/null -X PATCH "${RELAY_URL}/api/agents/${AGENT_ID}/ready" "${auth_header_args[@]}" \
172
- -H 'Content-Type: application/json' -d '{"ready":true}' 2>/dev/null
173
- set_status "idle"
174
- marked_ready=true
175
- fi
176
-
177
- count=$(echo "$msgs" | jq 'length' 2>/dev/null || echo 0)
178
- if [ "$count" -gt 0 ] 2>/dev/null && [ "$count" != "0" ]; then
179
- delivery_failed=false
180
- while IFS= read -r msg; do
181
- mid=$(printf '%s' "$msg" | jq -r '.id')
182
- claimable=$(printf '%s' "$msg" | jq -r '.claimable // false')
183
- if [ "$(ar_message_matches_channels "$msg")" != "true" ]; then
184
- continue
185
- fi
186
-
187
- if [ "$claimable" = "true" ]; then
188
- claim_code=$(curl -s -o /dev/null -w '%{http_code}' -X POST \
189
- "${RELAY_URL}/api/messages/${mid}/claim" "${auth_header_args[@]}" \
190
- -H 'Content-Type: application/json' \
191
- -d "{\"agentId\":\"${AGENT_ID}\"}" 2>/dev/null)
192
- if [ "$claim_code" != "200" ]; then
193
- continue
194
- fi
195
- fi
196
-
197
- 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
198
- echo "warn: failed to format relay message ${mid}" >&2
199
- delivery_failed=true
200
- continue
201
- fi
202
- if deliver_message "$line"; then
203
- mark_read_or_queue "$mid"
204
- else
205
- delivery_failed=true
206
- fi
207
- done < <(echo "$msgs" | jq -c '.[]')
208
- if [ "$delivery_failed" = "false" ]; then
209
- since_id=$(echo "$msgs" | jq '[.[].id] | max')
210
- fi
211
- fi
212
-
213
- sleep "$INTERVAL"
214
- done
@@ -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
- }
@@ -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
@@ -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
@@ -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
@@ -1,7 +0,0 @@
1
- [
2
- {
3
- "name": "agent-relay-inbox",
4
- "command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/relay-monitor.sh\"",
5
- "description": "Agent Relay inbox"
6
- }
7
- ]