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
4 changes: 4 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
* @jpelaez-23blocks
scripts/ @jpelaez-23blocks
tests/ @jpelaez-23blocks
.github/ @jpelaez-23blocks
60 changes: 60 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: CI
on:
pull_request:
branches: [main]
paths:
- 'scripts/**'
- 'tests/**'
- 'run_tests.sh'
- 'package.json'
- 'package-lock.json'
- '.github/workflows/tests.yml'
- '.shellcheckrc'
push:
branches: [main]
paths:
- 'scripts/**'
- 'tests/**'
- 'run_tests.sh'
- 'package.json'
- 'package-lock.json'
- '.github/workflows/tests.yml'
- '.shellcheckrc'

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
lint:
name: ShellCheck
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- uses: actions/checkout@v4
- uses: ludeeus/action-shellcheck@2.0.0
with:
scandir: scripts
severity: warning
- run: shellcheck --severity=warning run_tests.sh
- run: shellcheck --severity=warning -x tests/test_helper.bash

test:
name: Tests
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- name: Install system dependencies
run: sudo apt-get update && sudo apt-get install -y jq xxd
- name: Verify Ed25519 support
run: openssl genpkey -algorithm Ed25519 -out /tmp/test-ed25519.pem 2>/dev/null && echo "Ed25519 OK" && rm /tmp/test-ed25519.pem
- name: Run unit tests
run: npx bats --formatter tap tests/unit/
- name: Run integration tests
run: npx bats --formatter tap tests/integration/
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules/
15 changes: 15 additions & 0 deletions .shellcheckrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
shell=bash

# SC2155: Declare and assign separately. ~38 instances of `local var=$(...)`
# in amp-helper.sh/amp-security.sh. Fixing is a code change, not CI change.
disable=SC2155

# SC1090/SC1091: Can't follow non-constant/missing source.
# Every script does `source "${SCRIPT_DIR}/amp-helper.sh"` with computed path.
disable=SC1091
disable=SC1090

# SC2034: Variable appears unused. Many variables are set by helper functions
# (parse_address, etc.) for use by sourcing scripts — shellcheck can't track
# cross-file usage since each script sources amp-helper.sh dynamically.
disable=SC2034
48 changes: 48 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "amp-claude-plugin",
"version": "0.1.2",
"private": true,
"description": "Agent Messaging Protocol - Claude Code Plugin",
"scripts": {
"test": "./run_tests.sh all",
"test:unit": "./run_tests.sh unit",
"test:integration": "./run_tests.sh integration",
"lint": "shellcheck scripts/*.sh run_tests.sh"
},
"devDependencies": {
"bats": "^1.11.0",
"bats-assert": "^2.1.0",
"bats-support": "^0.3.0"
}
}
53 changes: 53 additions & 0 deletions run_tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/bin/bash
# =============================================================================
# AMP Plugin Test Runner
# =============================================================================
#
# Usage:
# ./run_tests.sh # Run all tests
# ./run_tests.sh unit # Run unit tests only
# ./run_tests.sh integration # Run integration tests only
# ./run_tests.sh <file> # Run a specific test file
#
# =============================================================================

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"

# Ensure bats is installed
if [ ! -d "node_modules/.bin" ] || [ ! -x "node_modules/.bin/bats" ]; then
echo "Installing test dependencies..."
npm install
fi

BATS="node_modules/.bin/bats"
BATS_OPTS="--formatter tap"

case "${1:-all}" in
unit)
echo "Running unit tests..."
$BATS $BATS_OPTS tests/unit/
;;
integration)
echo "Running integration tests..."
$BATS $BATS_OPTS tests/integration/
;;
all)
echo "Running all tests..."
$BATS $BATS_OPTS tests/unit/ tests/integration/
;;
*)
# Run specific file
if [ -f "$1" ]; then
echo "Running $1..."
$BATS $BATS_OPTS "$1"
else
echo "Error: File not found: $1"
echo ""
echo "Usage: ./run_tests.sh [unit|integration|all|<file>]"
exit 1
fi
;;
esac
2 changes: 1 addition & 1 deletion scripts/amp-delete.sh
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ rm "$MSG_FILE"

# Clean up downloaded attachments for this message
if [ -d "${AMP_ATTACHMENTS_DIR}/${MESSAGE_ID}" ]; then
rm -rf "${AMP_ATTACHMENTS_DIR}/${MESSAGE_ID}"
rm -rf "${AMP_ATTACHMENTS_DIR:?}/${MESSAGE_ID:?}"
fi

# Clean up empty sender/recipient directory
Expand Down
44 changes: 44 additions & 0 deletions scripts/amp-helper.sh
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ require_openssl() {
# (symlink resolves to UUID dir if migrated)
# 4. tmux session name → ~/.agent-messaging/agents/<name>/
# If the directory doesn't exist, it is auto-created.
# 3.5 Working directory (DETACHED sessions): a .claude/settings.local.json
# env.CLAUDE_AGENT_NAME hint walked up from $PWD, else the AI Maestro
# registry's unique owner of $PWD. Only resolves when unambiguous.
#
AMP_AGENTS_BASE="${HOME}/.agent-messaging/agents"

Expand Down Expand Up @@ -158,6 +161,47 @@ if [ -z "${AMP_DIR:-}" ]; then
unset _amp_agent_name _amp_uuid
fi

# Priority 3.5: Working directory → per-project hint or AI Maestro registry.
# For DETACHED sessions (no --id, no CLAUDE_AGENT_NAME env, no tmux) the cwd
# is the only signal to pick one of many agents. Without this, Priority 4
# below aborts with "Multiple AMP agents found" and AMP is unusable from a
# detached Claude Code session. Resolve ONLY when the cwd is owned by exactly
# one agent (most-specific match, unique); never guess an identity.
if [ "$_amp_resolved" = false ]; then
_amp_cwd="${PWD}"
_amp_hint=""
# (a) Walk up for a per-project settings.local.json env hint (same field
# Claude Code injects as CLAUDE_AGENT_NAME and the statusline reads).
_amp_d="$_amp_cwd"
while [ -n "$_amp_d" ] && [ "$_amp_d" != "/" ]; do
_amp_s="${_amp_d}/.claude/settings.local.json"
if [ -f "$_amp_s" ]; then
_amp_hint=$(jq -r '(.env.CLAUDE_AGENT_NAME // .env.AIM_AGENT_NAME // empty)' "$_amp_s" 2>/dev/null)
[ -n "$_amp_hint" ] && break
fi
_amp_d=$(dirname "$_amp_d")
done
# (b) Else ask AI Maestro which agent UNIQUELY owns this cwd.
if [ -z "$_amp_hint" ]; then
_amp_hint=$(curl -s --connect-timeout 1 "${AMP_MAESTRO_URL:-http://localhost:23000}/api/agents" 2>/dev/null | \
jq -r --arg cwd "$_amp_cwd" '
[ .agents[]
| (.workingDirectory // .session.workingDirectory // "") as $wd
| select($wd != "" and ($cwd == $wd or ($cwd | startswith($wd + "/"))))
| {name: .name, len: ($wd | length)} ]
| (map(.len) | max) as $mx
| map(select(.len == $mx))
| if length == 1 then .[0].name else empty end
' 2>/dev/null)
fi
if [ -n "$_amp_hint" ]; then
_amp_uuid=$(_index_lookup "$_amp_hint" 2>/dev/null) || true
AMP_DIR="${AMP_AGENTS_BASE}/${_amp_uuid:-$_amp_hint}"
_amp_resolved=true
fi
unset _amp_cwd _amp_hint _amp_d _amp_s _amp_uuid
fi

# Priority 4: Single agent auto-select (convenience for solo setups)
if [ "$_amp_resolved" = false ]; then
_amp_index_file="${AMP_AGENTS_BASE}/.index.json"
Expand Down
23 changes: 18 additions & 5 deletions scripts/amp-statusline.sh
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,22 @@ elif [ -n "${TMUX:-}" ]; then

# Priority 4: Working directory → AI Maestro API
elif [ -n "$CWD" ]; then
# Match by working directory, but ONLY when it uniquely identifies one agent.
# Many agents can share a dir, or claim a broad one ($HOME), so taking the first
# match silently showed the WRONG identity (every session under $HOME becoming
# "piano-instructor", or a dev dir picking a random one of the agents there).
# Pick the MOST SPECIFIC match (longest workingDirectory); if several tie, the cwd
# is ambiguous, so show nothing rather than a wrong name.
MAESTRO_AGENT=$(curl -s --connect-timeout 1 "${AMP_MAESTRO_URL:-http://localhost:23000}/api/agents" 2>/dev/null | \
jq -r --arg cwd "$CWD" '
.agents[] |
select((.workingDirectory // .session.workingDirectory // "") as $wd |
$wd != "" and ($cwd == $wd or ($cwd | startswith($wd + "/"))))
| .name' 2>/dev/null | head -1)
[ .agents[]
| (.workingDirectory // .session.workingDirectory // "") as $wd
| select($wd != "" and ($cwd == $wd or ($cwd | startswith($wd + "/"))))
| {name: .name, len: ($wd | length)} ]
| (map(.len) | max) as $mx
| map(select(.len == $mx))
| if length == 1 then .[0].name else empty end
' 2>/dev/null)
[ -n "$MAESTRO_AGENT" ] && AGENT_NAME="$MAESTRO_AGENT"
fi

Expand All @@ -140,7 +150,10 @@ if [ -z "$AGENT_UUID" ] && [ -z "$AGENT_NAME" ] && [ -n "$CWD" ]; then
while [ "$_dir" != "/" ] && [ "$_dir" != "$HOME" ]; do
_settings="${_dir}/.claude/settings.local.json"
if [ -f "$_settings" ]; then
_hint=$(grep -o 'CLAUDE_AGENT_NAME=[a-zA-Z0-9_-]*' "$_settings" 2>/dev/null | head -1 | cut -d= -f2)
# Read the REAL env hint, not any occurrence of the string in the file.
# grep-ing the raw file matched permission allow-list entries like
# "Bash(CLAUDE_AGENT_NAME=foo amp-send.sh:*)" and showed a bogus identity.
_hint=$(jq -r '(.env.CLAUDE_AGENT_NAME // .env.AIM_AGENT_NAME // empty)' "$_settings" 2>/dev/null)
if [ -n "$_hint" ]; then
AGENT_NAME="$_hint"
break
Expand Down
Loading
Loading