Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .agents/skills/using-agent-relay/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ operations:

```bash
agent-relay status
agent-relay local up --no-dashboard --verbose
agent-relay local up --verbose
agent-relay local status --wait-for 10
agent-relay local agent list
agent-relay local agent spawn claude --name Worker --task "Use https://agentrelay.com/skill and ACK over Relay."
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/using-agent-relay/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ operations:

```bash
agent-relay status
agent-relay local up --no-dashboard --verbose
agent-relay local up --verbose
agent-relay local status --wait-for 10
agent-relay local agent list
agent-relay local agent spawn claude --name Worker --task "Use https://agentrelay.com/skill and ACK over Relay."
Expand Down
60 changes: 0 additions & 60 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,66 +85,6 @@ jobs:
export PATH="$HOME/.npm-global/bin:$PATH"
claude --version || echo "Claude CLI ready"

- name: Download dashboard binary
run: |
# Determine platform and architecture
if [ "${{ runner.os }}" = "Linux" ]; then
BINARY_NAME="relay-dashboard-server-linux-x64"
elif [ "${{ runner.os }}" = "macOS" ]; then
# Check architecture
if [ "$(uname -m)" = "arm64" ]; then
BINARY_NAME="relay-dashboard-server-darwin-arm64"
else
BINARY_NAME="relay-dashboard-server-darwin-x64"
fi
else
echo "Unsupported OS: ${{ runner.os }}"
exit 1
fi

echo "Downloading dashboard binary: $BINARY_NAME"

# Get latest release from relay-dashboard repo
RELEASE_URL="https://github.com/AgentWorkforce/relay-dashboard/releases/latest/download/${BINARY_NAME}.gz"
echo "URL: $RELEASE_URL"

# Install to user local bin (no sudo required)
mkdir -p ~/.local/bin

# Download with retry logic (transient 504s from GitHub releases are common)
MAX_RETRIES=3
RETRY_DELAY=5
for attempt in $(seq 1 $MAX_RETRIES); do
echo "Download attempt $attempt of $MAX_RETRIES..."
if curl -fsSL --retry 3 --retry-delay 2 "$RELEASE_URL" | gunzip > ~/.local/bin/relay-dashboard-server 2>/dev/null; then
echo "Download succeeded on attempt $attempt"
break
fi
if [ "$attempt" -eq "$MAX_RETRIES" ]; then
echo "::warning::Failed to download dashboard binary after $MAX_RETRIES attempts — continuing without it"
rm -f ~/.local/bin/relay-dashboard-server
exit 0
fi
echo "Attempt $attempt failed, retrying in ${RETRY_DELAY}s..."
sleep $RETRY_DELAY
RETRY_DELAY=$((RETRY_DELAY * 2))
done

if [ -f ~/.local/bin/relay-dashboard-server ]; then
chmod +x ~/.local/bin/relay-dashboard-server
fi

# Add to PATH for this workflow
echo "$HOME/.local/bin" >> $GITHUB_PATH

# Verify
if [ -f ~/.local/bin/relay-dashboard-server ]; then
echo "Installed dashboard binary:"
~/.local/bin/relay-dashboard-server --version || echo "Binary installed (version check may not be supported)"
else
echo "Dashboard binary not available — tests will run without it"
fi

- name: Check for API key
id: check-key
run: |
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/package-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -192,13 +192,13 @@ jobs:
- name: Test CLI startup
run: |
echo "=== Testing CLI broker lifecycle ==="
TEST_PORT=3899
API_PORT=$((TEST_PORT + 1))
# Start broker+dashboard in background
node packages/cli/dist/cli/index.js local up --port "$TEST_PORT" &
BROKER_PORT=3899
API_PORT=$((BROKER_PORT + 1))
# Start the broker in background
AGENT_RELAY_BROKER_PORT="$BROKER_PORT" node packages/cli/dist/cli/index.js local up &
DAEMON_PID=$!

# Wait for health endpoint (broker API is dashboard port + 1)
# Wait for health endpoint (broker API is the base port + 1)
for i in {1..20}; do
if curl -sf "http://127.0.0.1:${API_PORT}/health" > /dev/null; then
Comment on lines +196 to 203

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Health check hardcodes base+1 even though broker port selection can fall back

Line 196/Line 203 assume the API will always bind to BROKER_PORT + 1, but lifecycle startup may move to the next free port when that slot is occupied. This can fail CI even when the broker started successfully.

Suggested fix
           BROKER_PORT=3899
-          API_PORT=$((BROKER_PORT + 1))
+          START_API_PORT=$((BROKER_PORT + 1))
+          API_PORT=""
           # Start the broker in background
           AGENT_RELAY_BROKER_PORT="$BROKER_PORT" node packages/cli/dist/cli/index.js local up &
           DAEMON_PID=$!
 
-          # Wait for health endpoint (broker API is the base port + 1)
+          # Wait for health endpoint (API starts at base+1 and may fall back)
           for i in {1..20}; do
-            if curl -sf "http://127.0.0.1:${API_PORT}/health" > /dev/null; then
-              break
-            fi
+            for candidate in $(seq "$START_API_PORT" "$((START_API_PORT + 9))"); do
+              if curl -sf "http://127.0.0.1:${candidate}/health" > /dev/null; then
+                API_PORT="$candidate"
+                break 2
+              fi
+            done
             sleep 1
           done
 
           # Verify broker is reachable
-          if curl -sf "http://127.0.0.1:${API_PORT}/health" > /dev/null; then
+          if [ -n "$API_PORT" ] && curl -sf "http://127.0.0.1:${API_PORT}/health" > /dev/null; then
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/package-validation.yml around lines 196 - 203, The health
check hardcodes the API_PORT as BROKER_PORT + 1, but the broker startup can
select the next available port if that one is occupied, causing the curl request
to fail against the wrong endpoint. Instead of assuming a fixed offset, capture
the actual API port that the broker selects by parsing the broker's startup
output or having it write the port to a file, then use that captured value in
the curl health check loop. This ensures the health check queries the correct
port regardless of what the broker actually bound to.

break
Expand Down
1 change: 0 additions & 1 deletion .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,6 @@ tsconfig.tsbuildinfo
packages/*/*.tsbuildinfo

# Other dev files
run-dashboard.js
CLAUDE.md
AGENTS.md
*.tasks.md
Expand Down
4 changes: 3 additions & 1 deletion crates/broker/src/runtime/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,9 @@ impl BrokerRuntime {
telemetry.track(TelemetryEvent::AgentSpawn {
cli: cli.clone(),
runtime: runtime_label(&effective_spec.runtime).to_string(),
spawn_source: ActionSource::HumanDashboard,
// `/api/spawn` is the HTTP entry point a human drives
// through the CLI (the broker's only human caller).
spawn_source: ActionSource::HumanCli,
has_task: effective_task.is_some(),
is_shadow: effective_spec.shadow_of.is_some()
|| effective_spec.shadow_mode.is_some(),
Expand Down
7 changes: 2 additions & 5 deletions crates/broker/src/telemetry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ pub enum TelemetryEvent {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ActionSource {
HumanCli,
HumanDashboard,
Agent,
Protocol,
}
Expand All @@ -107,7 +106,6 @@ impl ActionSource {
fn as_str(&self) -> &'static str {
match self {
Self::HumanCli => "human_cli",
Self::HumanDashboard => "human_dashboard",
Self::Agent => "agent",
Self::Protocol => "protocol",
}
Expand Down Expand Up @@ -821,7 +819,6 @@ mod tests {
#[test]
fn action_source_serializes_to_snake_case_strings() {
assert_eq!(ActionSource::HumanCli.as_str(), "human_cli");
assert_eq!(ActionSource::HumanDashboard.as_str(), "human_dashboard");
assert_eq!(ActionSource::Agent.as_str(), "agent");
assert_eq!(ActionSource::Protocol.as_str(), "protocol");
}
Expand All @@ -831,14 +828,14 @@ mod tests {
let event = TelemetryEvent::AgentSpawn {
cli: "claude".into(),
runtime: "pty".into(),
spawn_source: ActionSource::HumanDashboard,
spawn_source: ActionSource::HumanCli,
has_task: true,
is_shadow: false,
};
let props = event.properties();
assert_eq!(props["cli"], "claude");
assert_eq!(props["runtime"], "pty");
assert_eq!(props["spawn_source"], "human_dashboard");
assert_eq!(props["spawn_source"], "human_cli");
assert_eq!(props["has_task"], true);
assert_eq!(props["is_shadow"], false);
}
Expand Down
170 changes: 3 additions & 167 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,9 @@ set -e
# AGENT_RELAY_VERSION - Specific version to install (default: latest)
# AGENT_RELAY_INSTALL_DIR - Installation directory (default: ~/.agentworkforce/relay)
# AGENT_RELAY_BIN_DIR - Binary directory (default: ~/.local/bin)
# AGENT_RELAY_NO_DASHBOARD - Skip dashboard installation (default: false)
# AGENT_RELAY_TELEMETRY_DISABLED - Disable anonymous install telemetry (default: false)

REPO_RELAY="AgentWorkforce/relay"
REPO_DASHBOARD="AgentWorkforce/relay-dashboard"
VERSION="${AGENT_RELAY_VERSION:-latest}"
INSTALL_DIR="${AGENT_RELAY_INSTALL_DIR:-$HOME/.agentworkforce/relay}"
BIN_DIR="${AGENT_RELAY_BIN_DIR:-$HOME/.local/bin}"
Expand Down Expand Up @@ -245,148 +243,6 @@ download_broker_binary() {
fi
}

# Download standalone dashboard-server binary
download_dashboard_binary() {
if [ "${AGENT_RELAY_NO_DASHBOARD}" = "true" ]; then
info "Skipping dashboard installation (AGENT_RELAY_NO_DASHBOARD=true)"
return 0
fi

step "Downloading dashboard-server binary..."

local binary_name="relay-dashboard-server-${PLATFORM}"
local compressed_url="https://github.com/$REPO_DASHBOARD/releases/latest/download/${binary_name}.gz"
local uncompressed_url="https://github.com/$REPO_DASHBOARD/releases/latest/download/${binary_name}"
local target_path="$BIN_DIR/relay-dashboard-server"
local temp_file="/tmp/dashboard-download-$$"

mkdir -p "$BIN_DIR"

# Setup cleanup trap for temp files
trap 'rm -f "${temp_file}.gz" "${temp_file}"' EXIT

# Try compressed binary first (faster download)
if has_command gunzip; then
info "Trying compressed dashboard binary..."

if curl -fsSL "$compressed_url" -o "${temp_file}.gz" 2>/dev/null; then
# Check if we got a valid gzip file
local is_gzip=false
if has_command file; then
file "${temp_file}.gz" 2>/dev/null | grep -q "gzip" && is_gzip=true
else
head -c 2 "${temp_file}.gz" 2>/dev/null | od -An -tx1 | grep -q "1f 8b" && is_gzip=true
fi

if [ "$is_gzip" = true ]; then
if gunzip -c "${temp_file}.gz" > "$target_path" 2>/dev/null; then
rm -f "${temp_file}.gz"
chmod +x "$target_path"
strip_quarantine "$target_path"

if "$target_path" --version &>/dev/null; then
success "Downloaded standalone dashboard-server binary"
trap - EXIT
return 0
else
warn "Dashboard binary failed verification, trying uncompressed..."
rm -f "$target_path"
fi
else
rm -f "${temp_file}.gz" "$target_path"
fi
else
rm -f "${temp_file}.gz"
fi
fi
fi

# Fall back to uncompressed binary
info "Trying uncompressed dashboard binary..."

if curl -fsSL "$uncompressed_url" -o "$target_path" 2>/dev/null; then
local file_size
file_size=$(stat -f%z "$target_path" 2>/dev/null || stat -c%s "$target_path" 2>/dev/null || echo "0")

if [ "$file_size" -gt 1000000 ]; then
chmod +x "$target_path"
strip_quarantine "$target_path"

if "$target_path" --version &>/dev/null; then
success "Downloaded standalone dashboard-server binary"
trap - EXIT
return 0
else
warn "Dashboard binary failed verification"
rm -f "$target_path"
fi
else
rm -f "$target_path"
fi
fi

trap - EXIT
info "No standalone dashboard binary available for $PLATFORM"
return 1
}

# Download dashboard UI files (required for standalone binary)
download_dashboard_ui() {
if [ "${AGENT_RELAY_NO_DASHBOARD}" = "true" ]; then
return 0
fi

step "Downloading dashboard UI files..."

local ui_url="https://github.com/$REPO_DASHBOARD/releases/latest/download/dashboard-ui.tar.gz"
local target_dir="$INSTALL_DIR/dashboard"
local temp_file="/tmp/dashboard-ui-$$"

mkdir -p "$target_dir"

# Setup cleanup trap for temp files
trap 'rm -f "${temp_file}.tar.gz"' EXIT

if curl -fsSL "$ui_url" -o "${temp_file}.tar.gz" 2>/dev/null; then
# Check if we got a valid gzip file
local is_gzip=false
if has_command file; then
file "${temp_file}.tar.gz" 2>/dev/null | grep -q "gzip" && is_gzip=true
else
head -c 2 "${temp_file}.tar.gz" 2>/dev/null | od -An -tx1 | grep -q "1f 8b" && is_gzip=true
fi

if [ "$is_gzip" = true ]; then
# Remove old UI files if they exist
rm -rf "$target_dir/out"

# Extract to target directory
if tar -xzf "${temp_file}.tar.gz" -C "$target_dir" 2>/dev/null; then
rm -f "${temp_file}.tar.gz"
trap - EXIT

# Verify extraction
if [ -f "$target_dir/out/index.html" ]; then
success "Downloaded dashboard UI files"
return 0
else
warn "Dashboard UI extraction incomplete"
return 1
fi
else
warn "Failed to extract dashboard UI"
rm -f "${temp_file}.tar.gz"
fi
else
rm -f "${temp_file}.tar.gz"
fi
fi

trap - EXIT
info "Dashboard UI files not available (dashboard API will still work)"
return 1
}

# Check if a command exists
has_command() {
command -v "$1" &> /dev/null
Expand Down Expand Up @@ -712,18 +568,6 @@ Or use nvm: curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/instal
prepend_bin_dir_to_path
fi

# Install dashboard if not skipped
if [ "${AGENT_RELAY_NO_DASHBOARD}" != "true" ]; then
# Try binary first, fall back to npm
if download_dashboard_binary; then
# Binary downloaded - also need UI files since they're not embedded
download_dashboard_ui || true
else
info "Installing dashboard via npm..."
npm install -g @agent-relay/dashboard-server 2>/dev/null || true
fi
fi

# Install ACP bridge for Zed editor integration
install_acp_bridge || true

Expand Down Expand Up @@ -840,16 +684,13 @@ print_usage() {
echo ""
echo -e "${BOLD}Quick Start:${NC}"
echo ""
echo " # Start the daemon with dashboard"
echo " agent-relay up --dashboard"
echo " # Start the local broker (detached so this terminal stays free)"
echo " agent-relay up --background"
echo ""
echo " # Check status"
echo " agent-relay status"
echo ""
echo " # Open dashboard"
echo " open http://localhost:3888"
echo ""
echo " # Stop daemon"
echo " # Stop the broker"
echo " agent-relay down"
echo ""
echo -e "${BOLD}Documentation:${NC} https://github.com/AgentWorkforce/relay"
Expand Down Expand Up @@ -881,10 +722,6 @@ main() {
INSTALL_METHOD="binary"
# Download broker binary for workflow/SDK agent spawning
download_broker_binary || true
# Download dashboard-server binary if available
download_dashboard_binary || true
# Download dashboard UI files (required for standalone binary to serve the UI)
download_dashboard_ui || true
# Install ACP bridge for Zed editor (requires Node.js)
install_acp_bridge || true
verify_installation && print_usage && track_event "install_completed" && exit 0
Expand Down Expand Up @@ -968,7 +805,6 @@ case "${1:-}" in
echo " AGENT_RELAY_VERSION Specific version to install (default: latest)"
echo " AGENT_RELAY_INSTALL_DIR Installation directory (default: ~/.agentworkforce/relay)"
echo " AGENT_RELAY_BIN_DIR Binary directory (default: ~/.local/bin)"
echo " AGENT_RELAY_NO_DASHBOARD Skip dashboard installation (default: false)"
echo " AGENT_RELAY_TELEMETRY_DISABLED Disable anonymous install telemetry (default: false)"
echo ""
echo "Telemetry: This installer collects anonymous usage data to improve the product."
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@
"build:sdk": "npm --prefix packages/sdk run build",
"build:telemetry": "npm --prefix packages/telemetry run build",
"dev:watch": "cd packages/cli && npx tsc -w",
"watch:start": "npm run build && concurrently -k \"npm run dev:watch\" \"node --watch packages/cli/dist/cli/index.js start dashboard.js claude\"",
"watch:start": "npm run build && concurrently -k \"npm run dev:watch\" \"node --watch packages/cli/dist/cli/index.js up\"",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Invoke the broker through the local command group

When npm run watch:start runs this command, Commander receives up as a top-level command, but createProgram() registers broker lifecycle commands only under local (local up, local down, etc.). The watcher therefore exits with an unknown-command error instead of starting the broker; this should call node --watch packages/cli/dist/cli/index.js local up.

Useful? React with 👍 / 👎.

"watch:start:cli-tools": "npm run build && bash ./scripts/watch-cli-tools.sh",
"watch:start:claude": "npm run watch:start:cli-tools -- --tool=claude",
"predev": "npm run clean && npm run build:packages && cd packages/cli && npx tsc && cd ../.. && chmod +x packages/cli/dist/cli/index.js",
"dev": "node packages/cli/dist/cli/index.js up --port 3888",
"dev": "node packages/cli/dist/cli/index.js up",
"dev:local": "npm run build && (cd packages/cli && npm link) && echo '✓ agent-relay linked globally'",
"dev:unlink": "(cd packages/cli && npm unlink -g agent-relay) && echo '✓ agent-relay unlinked'",
"dev:rebuild": "npm run build && echo '✓ Rebuilt (linked version updated)'",
Expand Down
Loading
Loading