Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 17 additions & 21 deletions plugins/session-bridge/commands/bridge.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,21 @@ Register this session as a bridge peer.

### `connect <session-id>`

Connect to a peer session. Auto-starts this session's bridge if not already active.
Connect to a peer session. Always ensures this session has its own bridge first.

1. Extract the session ID from the argument.
2. Check if this session has a bridge:
```bash
bash "${CLAUDE_PLUGIN_ROOT}/scripts/get-session-id.sh"
```
If it exits with code 1 (not found), auto-start the bridge first:
1. Extract the target session ID from the argument.
2. **Always register first** to guarantee this session has its own unique bridge identity.
`register.sh` is idempotent — if `BRIDGE_SESSION_ID` env var is already set (from a
prior `/bridge start` in this process), it reuses that session. Otherwise it creates a new one:
```bash
bash "${CLAUDE_PLUGIN_ROOT}/scripts/register.sh"
```
Capture the session ID from stdout and note it for the user.
3. Then connect to the peer:
Capture the returned session ID — this is YOUR session's ID.
3. Then connect to the peer using YOUR session ID from step 2:
```bash
BRIDGE_SESSION_ID=$(bash "${CLAUDE_PLUGIN_ROOT}/scripts/get-session-id.sh") bash "${CLAUDE_PLUGIN_ROOT}/scripts/connect-peer.sh" "<session-id>"
BRIDGE_SESSION_ID=<your-session-id-from-step-2> bash "${CLAUDE_PLUGIN_ROOT}/scripts/connect-peer.sh" "<target-session-id>"
```
4. If successful, display the peer's project name and path. If you auto-started in step 2, also show this session's ID.
4. If successful, display the peer's project name and path, and show this session's ID.
5. If it fails (peer not found), suggest `/bridge peers` to see available sessions.
6. Tell the user: "Connected! Use `/bridge listen` to start answering peer queries, or `/bridge ask <question>` to ask them something."

Expand All @@ -67,15 +65,13 @@ Enter listening mode — continuously wait for peer messages and respond to them

**This is a loop. You MUST keep listening until the user interrupts (Ctrl+C).**

**Auto-start:** First, check if this session has a bridge:
```bash
bash "${CLAUDE_PLUGIN_ROOT}/scripts/get-session-id.sh"
```
If it exits with code 1, auto-start:
**Auto-start:** Always register first to ensure this session has its own bridge:
```bash
bash "${CLAUDE_PLUGIN_ROOT}/scripts/register.sh"
```
Display: "Bridge auto-started! Session ID: <id>". Then proceed to the loop.
Capture the returned session ID. If this session was already registered (via `BRIDGE_SESSION_ID`
env var), `register.sh` reuses it. Otherwise it creates a new one.
Display: "Bridge active! Session ID: <id>". Then proceed to the loop.

Store the session ID in a variable (e.g., `MY_SESSION`) for use throughout the loop.

Expand Down Expand Up @@ -114,11 +110,11 @@ The loop:

Send a query to a connected peer and wait for the response.

1. Get session ID:
1. Get session ID — register first to ensure this session has its own bridge:
```bash
MY_SESSION=$(bash "${CLAUDE_PLUGIN_ROOT}/scripts/get-session-id.sh")
MY_SESSION=$(bash "${CLAUDE_PLUGIN_ROOT}/scripts/register.sh")
```
If it fails, tell the user to run `/bridge start` first.
This is idempotent — reuses the existing session if `BRIDGE_SESSION_ID` env var is set.
2. Find connected peers:
```bash
find ~/.claude/session-bridge/sessions/$MY_SESSION/inbox -name "*.json" -exec jq -r 'select(.type == "ping") | .from' {} \; 2>/dev/null | sort -u
Expand Down Expand Up @@ -158,7 +154,7 @@ Show current bridge state.
```bash
MY_SESSION=$(bash "${CLAUDE_PLUGIN_ROOT}/scripts/get-session-id.sh")
```
If it fails, say "Bridge is not active. Run `/bridge start` to begin."
If it exits with code 1, say "Bridge is not active. Run `/bridge start` to begin."
2. Display the session ID.
3. List connected peers (from ping messages in inbox).
4. Count pending (unread) messages in inbox.
Expand Down
14 changes: 9 additions & 5 deletions plugins/session-bridge/scripts/bridge-listen.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,15 @@ if [ -n "${1:-}" ] && [ "${1:-}" != "0" ] && ! echo "$1" | grep -qE '^[0-9]+$';
SESSION_ID="$1"
TIMEOUT="${2:-0}"
else
# No session ID given, try to find it
SESSION_ID=$(bash "$SCRIPT_DIR/get-session-id.sh" 2>/dev/null) || {
echo "Error: No bridge session found. Run /bridge start first." >&2
exit 1
}
# No session ID given — prefer BRIDGE_SESSION_ID env var, then fall back to get-session-id.sh
if [ -n "${BRIDGE_SESSION_ID:-}" ]; then
SESSION_ID="$BRIDGE_SESSION_ID"
else
SESSION_ID=$(bash "$SCRIPT_DIR/get-session-id.sh" 2>/dev/null) || {
echo "Error: No bridge session found. Run /bridge start first." >&2
exit 1
}
fi
TIMEOUT="${1:-0}"
fi

Expand Down
38 changes: 23 additions & 15 deletions plugins/session-bridge/scripts/cleanup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,21 @@ PROJECT_DIR="${PROJECT_DIR:-$(pwd)}"
BRIDGE_SESSION_FILE="$PROJECT_DIR/.claude/bridge-session"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

# Find session ID
SESSION_ID=""
if [ -f "$BRIDGE_SESSION_FILE" ]; then
SESSION_ID=$(cat "$BRIDGE_SESSION_FILE")
else
for MANIFEST_FILE in "$BRIDGE_DIR"/sessions/*/manifest.json; do
[ -f "$MANIFEST_FILE" ] || continue
MANIFEST_PATH=$(jq -r '.projectPath // ""' "$MANIFEST_FILE" 2>/dev/null)
if [ "$MANIFEST_PATH" = "$PROJECT_DIR" ]; then
SESSION_ID=$(jq -r '.sessionId' "$MANIFEST_FILE")
break
fi
done
# Find session ID — prefer env var (per-process), then file, then scan
SESSION_ID="${BRIDGE_SESSION_ID:-}"
if [ -z "$SESSION_ID" ]; then
if [ -f "$BRIDGE_SESSION_FILE" ]; then
SESSION_ID=$(cat "$BRIDGE_SESSION_FILE")
else
for MANIFEST_FILE in "$BRIDGE_DIR"/sessions/*/manifest.json; do
[ -f "$MANIFEST_FILE" ] || continue
MANIFEST_PATH=$(jq -r '.projectPath // ""' "$MANIFEST_FILE" 2>/dev/null)
if [ "$MANIFEST_PATH" = "$PROJECT_DIR" ]; then
SESSION_ID=$(jq -r '.sessionId' "$MANIFEST_FILE")
break
fi
done
fi
fi

if [ -z "$SESSION_ID" ]; then
Expand Down Expand Up @@ -52,8 +54,14 @@ done
# Remove session directory
rm -rf "$SESSION_DIR"

# Remove bridge-session pointer
rm -f "$BRIDGE_SESSION_FILE"
# Only remove bridge-session pointer if it still points to THIS session.
# Another session in the same repo may have overwritten it with their own ID.
if [ -f "$BRIDGE_SESSION_FILE" ]; then
FILE_ID=$(cat "$BRIDGE_SESSION_FILE")
if [ "$FILE_ID" = "$SESSION_ID" ]; then
rm -f "$BRIDGE_SESSION_FILE"
fi
fi

# Clean up stale sessions (heartbeat older than 30 minutes)
STALE_CUTOFF=$(date -u -v-30M +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -d "30 minutes ago" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "")
Expand Down
31 changes: 21 additions & 10 deletions plugins/session-bridge/scripts/get-session-id.sh
Original file line number Diff line number Diff line change
@@ -1,32 +1,43 @@
#!/usr/bin/env bash
# scripts/get-session-id.sh — Reliably find this project's bridge session ID.
# scripts/get-session-id.sh — Reliably find this session's bridge session ID.
# Works even if the agent cd'd into a subdirectory.
#
# Strategy:
# 1. Try .claude/bridge-session in current directory (fast path)
# 2. Scan all session manifests for one whose projectPath is a parent of $(pwd)
# 1. Check BRIDGE_SESSION_ID env var (per-process, always correct)
# 2. Try .claude/bridge-session file (convenience pointer, may belong to another session)
# 3. Scan all session manifests for one whose projectPath is a parent of $(pwd)
#
# Outputs: session ID to stdout, or exits 1 if not found.
set -euo pipefail

BRIDGE_DIR="${BRIDGE_DIR:-$HOME/.claude/session-bridge}"
CURRENT_DIR="${PROJECT_DIR:-$(pwd)}"

# Fast path: .claude/bridge-session in current directory
# Fast path: env var set by register.sh via CLAUDE_ENV_FILE (per-process, no collisions)
if [ -n "${BRIDGE_SESSION_ID:-}" ]; then
SESSION_DIR="$BRIDGE_DIR/sessions/$BRIDGE_SESSION_ID"
if [ -d "$SESSION_DIR" ] && [ -f "$SESSION_DIR/manifest.json" ]; then
echo -n "$BRIDGE_SESSION_ID"
exit 0
fi
fi

# Fallback: .claude/bridge-session file (may point to a different session in the same repo)
if [ -f "$CURRENT_DIR/.claude/bridge-session" ]; then
cat "$CURRENT_DIR/.claude/bridge-session"
exit 0
FILE_ID=$(cat "$CURRENT_DIR/.claude/bridge-session")
SESSION_DIR="$BRIDGE_DIR/sessions/$FILE_ID"
if [ -d "$SESSION_DIR" ] && [ -f "$SESSION_DIR/manifest.json" ]; then
echo -n "$FILE_ID"
exit 0
fi
fi

# Fallback: scan all session manifests for one whose projectPath is a parent of current dir.
# e.g., if we're at /projects/my-lib/src/main/kotlin and a session has
# projectPath=/projects/my-lib, that's a match.
# Last resort: scan all session manifests for one whose projectPath is a parent of current dir.
for MANIFEST in "$BRIDGE_DIR"/sessions/*/manifest.json; do
[ -f "$MANIFEST" ] || continue
PROJ_PATH=$(jq -r '.projectPath // ""' "$MANIFEST" 2>/dev/null)
[ -n "$PROJ_PATH" ] || continue

# Check: is our current directory inside (or equal to) this project's path?
case "$CURRENT_DIR" in
"$PROJ_PATH"|"$PROJ_PATH"/*)
jq -r '.sessionId' "$MANIFEST"
Expand Down
17 changes: 10 additions & 7 deletions plugins/session-bridge/scripts/register.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
#!/usr/bin/env bash
# scripts/register.sh — Register this session as a bridge peer.
# Env: BRIDGE_DIR (default: ~/.claude/session-bridge), PROJECT_DIR (default: pwd)
# Env: BRIDGE_DIR (default: ~/.claude/session-bridge), PROJECT_DIR (default: pwd),
# BRIDGE_SESSION_ID (if set, reuse this session instead of creating a new one)
# Outputs: session ID to stdout
# If a bridge session already exists for this project, reuses it.
# Each Claude session gets its own bridge — multiple sessions in the same repo are independent.
set -euo pipefail

command -v jq >/dev/null 2>&1 || { echo "Error: jq is required. Install with: brew install jq (macOS) or apt install jq (Linux)" >&2; exit 1; }
Expand All @@ -12,18 +13,18 @@ PROJECT_DIR="${PROJECT_DIR:-$(pwd)}"
PROJECT_NAME=$(basename "$PROJECT_DIR")
BRIDGE_SESSION_FILE="$PROJECT_DIR/.claude/bridge-session"

# Reuse existing session if the bridge-session file exists and points to a valid session
if [ -f "$BRIDGE_SESSION_FILE" ]; then
EXISTING_ID=$(cat "$BRIDGE_SESSION_FILE")
EXISTING_DIR="$BRIDGE_DIR/sessions/$EXISTING_ID"
# Reuse existing session only if BRIDGE_SESSION_ID env var is set and points to a valid session.
# This env var is per-process, so two Claude sessions in the same repo won't collide.
if [ -n "${BRIDGE_SESSION_ID:-}" ]; then
EXISTING_DIR="$BRIDGE_DIR/sessions/$BRIDGE_SESSION_ID"

if [ -d "$EXISTING_DIR" ] && [ -f "$EXISTING_DIR/manifest.json" ]; then
# Update heartbeat and reuse
NOW=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
TMP=$(mktemp "$EXISTING_DIR/manifest.XXXXXX")
jq --arg hb "$NOW" '.lastHeartbeat = $hb' "$EXISTING_DIR/manifest.json" > "$TMP"
mv "$TMP" "$EXISTING_DIR/manifest.json"
echo -n "$EXISTING_ID"
echo -n "$BRIDGE_SESSION_ID"
exit 0
fi
fi
Expand All @@ -49,6 +50,8 @@ cat > "$MANIFEST_TMP" <<MANIFEST
MANIFEST
mv "$MANIFEST_TMP" "$SESSION_DIR/manifest.json"

# Write the session file as a convenience pointer (last-registered session for this project).
# Other scripts should prefer BRIDGE_SESSION_ID env var over this file.
mkdir -p "$PROJECT_DIR/.claude"
echo -n "$SESSION_ID" > "$BRIDGE_SESSION_FILE"

Expand Down
10 changes: 7 additions & 3 deletions plugins/session-bridge/skills/bridge-awareness/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,19 @@ description: Activates when a bridge session is active. Teaches the agent to com

You are connected to other Claude Code sessions via the Claude Bridge plugin. This skill defines how you communicate with peer agents.

**Getting your session ID:** Always use `bash "${CLAUDE_PLUGIN_ROOT}/scripts/get-session-id.sh"` to get your session ID. This works even if you've cd'd into a subdirectory. In listen mode, use `TO_ID` from the message output instead (even more reliable).
**Getting your session ID:** Always register first to ensure you have your own bridge identity:
```bash
MY_SESSION=$(bash "${CLAUDE_PLUGIN_ROOT}/scripts/register.sh")
```
This is idempotent — if `BRIDGE_SESSION_ID` env var is set (from a prior registration), it reuses that session. In listen mode, use `TO_ID` from the message output instead (even more reliable).

## Querying Peers

When the user asks you to communicate with a peer — whether via `/bridge ask`, or in natural language like "ask the library about...", "check with the peer", "what did the other session change", etc. — do this:

1. Get your session ID. If no bridge exists yet, auto-register first:
```bash
MY_SESSION=$(bash "${CLAUDE_PLUGIN_ROOT}/scripts/get-session-id.sh" 2>/dev/null) || MY_SESSION=$(bash "${CLAUDE_PLUGIN_ROOT}/scripts/register.sh")
MY_SESSION=$(bash "${CLAUDE_PLUGIN_ROOT}/scripts/register.sh")
```
Then find connected peers:
```bash
Expand Down Expand Up @@ -100,7 +104,7 @@ BRIDGE_SESSION_ID=<TO_ID> bash "${CLAUDE_PLUGIN_ROOT}/scripts/send-message.sh" <
- **In listen mode, NEVER break the loop.** Respond, then immediately listen again.
- **Always use `send-message.sh`** — never write message JSON files directly.
- **In listen mode, use `TO_ID`** from the message output as BRIDGE_SESSION_ID.
- **Outside listen mode, use `get-session-id.sh`** to get your session ID reliably.
- **Outside listen mode, use `register.sh`** to get your session ID (idempotent — reuses existing via env var).
- **Never use `$(cat .claude/bridge-session)` directly** — it's a relative path that breaks when the working directory changes.
- **Include real code in responses** — read actual files and paste relevant sections. Don't just describe changes in prose.
- **Route to the right peer** when connected to multiple. Use project names to decide relevance.
Expand Down
30 changes: 25 additions & 5 deletions plugins/session-bridge/tests/test-cleanup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,16 @@ echo ""
echo "Test 1: Cleanup removes session directory"
SESSION_A_DIR="$BRIDGE_DIR/sessions/$SESSION_A"
assert_dir_exists "session dir exists before cleanup" "$SESSION_A_DIR"
BRIDGE_DIR="$BRIDGE_DIR" PROJECT_DIR="$PROJECT_A" bash "$CLEANUP"
BRIDGE_DIR="$BRIDGE_DIR" PROJECT_DIR="$PROJECT_A" BRIDGE_SESSION_ID="$SESSION_A" bash "$CLEANUP"
if [ ! -d "$SESSION_A_DIR" ]; then
echo " PASS: session dir removed"; PASS=$((PASS + 1))
else
echo " FAIL: session dir still exists"; FAIL=$((FAIL + 1))
fi

# --- Test 2: bridge-session pointer removed ---
# --- Test 2: bridge-session pointer removed (file belongs to this session) ---
echo ""
echo "Test 2: bridge-session file removed"
echo "Test 2: bridge-session file removed when it belongs to the exiting session"
if [ ! -f "$PROJECT_A/.claude/bridge-session" ]; then
echo " PASS: bridge-session file removed"; PASS=$((PASS + 1))
else
Expand Down Expand Up @@ -93,7 +93,7 @@ assert_dir_exists "stale session exists before cleanup" "$STALE_DIR"
PROJECT_D="$TEST_TMPDIR/project-d"
mkdir -p "$PROJECT_D"
SESSION_D=$(BRIDGE_DIR="$BRIDGE_DIR" PROJECT_DIR="$PROJECT_D" bash "$REGISTER")
BRIDGE_DIR="$BRIDGE_DIR" PROJECT_DIR="$PROJECT_D" bash "$CLEANUP"
BRIDGE_DIR="$BRIDGE_DIR" PROJECT_DIR="$PROJECT_D" BRIDGE_SESSION_ID="$SESSION_D" bash "$CLEANUP"
if [ ! -d "$STALE_DIR" ]; then
echo " PASS: stale session cleaned up"; PASS=$((PASS + 1))
else
Expand Down Expand Up @@ -123,8 +123,28 @@ assert_dir_exists "session B still exists" "$SESSION_B_DIR"
PROJECT_F="$TEST_TMPDIR/project-f"
mkdir -p "$PROJECT_F"
SESSION_F=$(BRIDGE_DIR="$BRIDGE_DIR" PROJECT_DIR="$PROJECT_F" bash "$REGISTER")
BRIDGE_DIR="$BRIDGE_DIR" PROJECT_DIR="$PROJECT_F" bash "$CLEANUP"
BRIDGE_DIR="$BRIDGE_DIR" PROJECT_DIR="$PROJECT_F" BRIDGE_SESSION_ID="$SESSION_F" bash "$CLEANUP"

assert_dir_exists "active session B not cleaned" "$SESSION_B_DIR"

# --- Test 7: Cleanup of one session doesn't delete bridge-session file owned by another ---
echo ""
echo "Test 7: Cleanup preserves bridge-session file owned by another session"
PROJECT_SHARED="$TEST_TMPDIR/project-shared"
mkdir -p "$PROJECT_SHARED"
SID_1=$(BRIDGE_DIR="$BRIDGE_DIR" PROJECT_DIR="$PROJECT_SHARED" bash "$REGISTER")
SID_2=$(BRIDGE_DIR="$BRIDGE_DIR" PROJECT_DIR="$PROJECT_SHARED" bash "$REGISTER")
# File now points to SID_2 (last registered)
assert_eq "file points to SID_2" "$SID_2" "$(cat "$PROJECT_SHARED/.claude/bridge-session")"
# Clean up SID_1 — should NOT delete the file since it belongs to SID_2
BRIDGE_DIR="$BRIDGE_DIR" PROJECT_DIR="$PROJECT_SHARED" BRIDGE_SESSION_ID="$SID_1" bash "$CLEANUP"
if [ ! -d "$BRIDGE_DIR/sessions/$SID_1" ]; then
echo " PASS: session 1 dir removed"; PASS=$((PASS + 1))
else
echo " FAIL: session 1 dir still exists"; FAIL=$((FAIL + 1))
fi
assert_file_exists "bridge-session file preserved for session 2" "$PROJECT_SHARED/.claude/bridge-session"
assert_eq "file still points to SID_2" "$SID_2" "$(cat "$PROJECT_SHARED/.claude/bridge-session")"
assert_dir_exists "session 2 dir untouched" "$BRIDGE_DIR/sessions/$SID_2"

print_results
28 changes: 27 additions & 1 deletion plugins/session-bridge/tests/test-get-session-id.sh
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,32 @@ fi
echo ""
echo "Test 7: Fast path — uses bridge-session file directly when available"
FOUND=$(BRIDGE_DIR="$BRIDGE_DIR" PROJECT_DIR="$PROJECT_DIR" bash "$GET_ID")
assert_eq "fast path works" "$SESSION_ID" "$FOUND"
# File may now point to SESSION_B (last registered), but should still return a valid session
if [ -n "$FOUND" ]; then
echo " PASS: fast path returns a session"; PASS=$((PASS + 1))
else
echo " FAIL: fast path returned nothing"; FAIL=$((FAIL + 1))
fi

# --- Test 8: BRIDGE_SESSION_ID env var takes priority over file ---
echo ""
echo "Test 8: Env var takes priority over bridge-session file"
# Register a second session in the same project (simulates second Claude session)
SESSION_ID_2=$(BRIDGE_DIR="$BRIDGE_DIR" PROJECT_DIR="$PROJECT_DIR" bash "$REGISTER")
# The file now points to SESSION_ID_2, but env var should win
FOUND=$(BRIDGE_DIR="$BRIDGE_DIR" PROJECT_DIR="$PROJECT_DIR" BRIDGE_SESSION_ID="$SESSION_ID" bash "$GET_ID")
assert_eq "env var wins over file" "$SESSION_ID" "$FOUND"

# --- Test 9: Without env var, falls back to file ---
echo ""
echo "Test 9: Without env var, falls back to bridge-session file"
FOUND=$(BRIDGE_DIR="$BRIDGE_DIR" PROJECT_DIR="$PROJECT_DIR" bash "$GET_ID")
assert_eq "falls back to file" "$SESSION_ID_2" "$FOUND"

# --- Test 10: Stale env var (deleted session) falls back to file ---
echo ""
echo "Test 10: Stale env var (session dir gone) falls back to file"
FOUND=$(BRIDGE_DIR="$BRIDGE_DIR" PROJECT_DIR="$PROJECT_DIR" BRIDGE_SESSION_ID="nonexistent" bash "$GET_ID")
assert_eq "stale env var falls back to file" "$SESSION_ID_2" "$FOUND"

print_results
Loading