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
27 changes: 25 additions & 2 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,15 @@ Discord, Telegram 같은 외부 서비스 없이 HTTP POST 하나로 대화에

## 왜 Pulse인가?

Claude Code의 Channels는 세션에 메시지를 보내려면 Discord나 Slack 같은 외부 메신저를 거쳐야 해요. 로컬 스크립트에서 간단한 알림 하나 보내려고 봇 토큰, 계정, 외부 네트워크 접근이 필요한 건 불필요한 마찰이에요.
CI가 실패할 때마다 메일을 확인하고, 에이전트에게 "CI 결과 확인해봐"라고 말하고 있진 않나요?

Pulse는 이 의존성을 제거하는 **이벤트 추상화 레이어**예요. HTTP 호출이 가능한 모든 로컬 프로세스가 외부 메신저를 거치지 않고 Claude Code 세션과 직접 소통할 수 있어요.
빌드 에러가 나면 에러 로그를 직접 복사해서 붙여넣고 있진 않나요?

배포 스크립트를 돌려놓고, 끝났는지 직접 터미널을 열어보고 있진 않나요?

Pulse가 있으면 그럴 필요 없어요. 백그라운드 프로세스가 끝나면 세션에 직접 메시지를 보내요.

Pulse는 Claude Code의 [Channels](https://docs.anthropic.com/en/docs/claude-code/channels) 프로토콜을 활용하는 **이벤트 추상화 레이어**예요. 외부 메신저 없이, HTTP 호출이 가능한 모든 로컬 프로세스가 Claude Code 세션과 직접 소통할 수 있어요.

## 개념

Expand Down Expand Up @@ -103,6 +109,23 @@ curl localhost:3400/health

`gh`, `jq`, `curl`이 필요해요.

### 빌드 에러 자동 전달

빌드 스크립트에 추가하면, 실패 시 에러 로그를 세션으로 바로 보내요:

```bash
#!/bin/bash
BUILD_OUTPUT=$(npm run build 2>&1)
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
# 마지막 30줄만 전달 (너무 길면 잘림)
ERROR=$(echo "$BUILD_OUTPUT" | tail -30 | jq -Rsa .)
curl -s -X POST localhost:3400/notify \
-H "Content-Type: application/json" \
-d "{\"text\":$ERROR,\"source\":\"build\",\"level\":\"error\"}"
fi
```

### 배포 완료 알림

배포 스크립트 끝에 한 줄 추가:
Expand Down
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,15 @@ No Discord, no Telegram — just a single HTTP POST to push messages into your c

## Why Pulse?

Claude Code's Channels require external messaging services like Discord or Slack to push messages into a session. This creates unnecessary friction — you need a bot token, an account, network access to a third-party service, all just to send a simple notification from a local script.
Checking your email every time CI fails, then telling the agent "go check the CI result"?

Pulse is an **event abstraction layer** that removes this dependency. Any local process that can make an HTTP call can now communicate with your Claude Code session directly, without routing through an external messenger.
Copying and pasting build error logs into the session manually?

Running a deploy script and opening another terminal to see if it finished?

With Pulse, you don't have to. Background processes send messages directly to your session.

Pulse is an **event abstraction layer** built on Claude Code's [Channels](https://docs.anthropic.com/en/docs/claude-code/channels) protocol. Any local process that can make an HTTP call can communicate with your Claude Code session directly — no external messengers, no bot tokens, no accounts.

## Concept

Expand Down Expand Up @@ -103,6 +109,23 @@ curl localhost:3400/health

Requires `gh`, `jq`, `curl`.

### Build Error Auto-Report

Add to your build script — on failure, error logs are sent straight to your session:

```bash
#!/bin/bash
BUILD_OUTPUT=$(npm run build 2>&1)
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
# Send last 30 lines (trim if too long)
ERROR=$(echo "$BUILD_OUTPUT" | tail -30 | jq -Rsa .)
curl -s -X POST localhost:3400/notify \
-H "Content-Type: application/json" \
-d "{\"text\":$ERROR,\"source\":\"build\",\"level\":\"error\"}"
fi
```

### Deploy Notification

Add one line at the end of your deploy script:
Expand Down
83 changes: 55 additions & 28 deletions examples/ci-watcher.sh
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
#!/bin/bash
# CI Watcher — tracks GitHub Actions runs and reports results via Pulse
#
# Automatically triggered by Claude Code PostToolUse hook on `gh pr create`
# or `git push`. Watches all CI runs for the current commit and sends
# pass/fail notifications to the active Pulse channel.
# Automatically triggered by Claude Code PostToolUse hook on `gh pr create`,
# `git push`, or `gh pr merge`. Watches all CI/CD runs for the relevant
# commit and sends pass/fail notifications to the active Pulse channel.
#
# Features:
# - Finds Pulse port via ancestor Claude Code PID (session-aware)
# - Deduplicates notifications (lock file per commit + session)
# - Supports .ci-watch-ignore for skipping specific workflows
# - Tracks multiple concurrent runs, reports each as it completes
# - Tracks post-merge CI/CD runs (e.g. deploy pipelines on main)
# - 10 minute timeout with warning notification
#
# Requirements: gh, jq, curl
Expand Down Expand Up @@ -46,17 +47,20 @@ set -euo pipefail
# Parse hook input
# ---------------------------------------------------------------------------

CMD=$(jq -r '.tool_input.command // empty' 2>/dev/null)
STDIN_JSON=$(cat)
CMD=$(echo "$STDIN_JSON" | jq -r '.tool_input.command // empty' 2>/dev/null)
if [ -z "$CMD" ]; then exit 0; fi

# Only trigger on gh pr create or git push (not --delete)
if echo "$CMD" | grep -qE "gh pr create"; then
:
# Determine trigger type
TRIGGER=""
if echo "$CMD" | grep -qE "gh pr merge"; then
TRIGGER="merge"
elif echo "$CMD" | grep -qE "gh pr create"; then
TRIGGER="pr"
elif echo "$CMD" | grep -qE "git push" && ! echo "$CMD" | grep -qE "--delete|-d"; then
:
else
exit 0
TRIGGER="push"
fi
if [ -z "$TRIGGER" ]; then exit 0; fi

# ---------------------------------------------------------------------------
# Resolve Pulse port via ancestor Claude Code PID
Expand All @@ -82,21 +86,6 @@ CLAUDE_PID=$(find_claude_pid) || { exit 0; }
PULSE_PORT=$(cat ~/.pulse/"$CLAUDE_PID".port 2>/dev/null || echo "")
if [ -z "$PULSE_PORT" ]; then exit 0; fi

# ---------------------------------------------------------------------------
# Dedup: skip if another ci-watcher is already tracking this commit
# ---------------------------------------------------------------------------

SHA=$(git rev-parse HEAD 2>/dev/null || echo "")
if [ -z "$SHA" ]; then exit 0; fi

LOCK_DIR=~/.pulse/locks
mkdir -p "$LOCK_DIR"
LOCK_FILE="$LOCK_DIR/${CLAUDE_PID}-${SHA}.lock"
if ! mkdir "$LOCK_FILE" 2>/dev/null; then
exit 0
fi
trap 'rmdir "$LOCK_FILE" 2>/dev/null' EXIT

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
Expand All @@ -109,12 +98,50 @@ notify() {
-d "{\"text\":\"$text\",\"source\":\"ci\",\"level\":\"$level\"}" 2>/dev/null || true
}

# Get repo
REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null || echo "")
if [ -z "$REPO" ]; then exit 0; fi

# ---------------------------------------------------------------------------
# Load .ci-watch-ignore
# Resolve commit SHA based on trigger type
# ---------------------------------------------------------------------------

REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null || echo "")
if [ -z "$REPO" ]; then exit 0; fi
if [ "$TRIGGER" = "merge" ]; then
# Extract PR number from command (gh pr merge 123 or gh pr merge URL)
PR_NUM=$(echo "$CMD" | grep -oE 'gh pr merge [^ ]+' | awk '{print $NF}' | grep -oE '[0-9]+' | head -1)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Parse gh pr merge target without assuming numeric arg

The new merge path only derives PR_NUM by extracting digits from the first token after gh pr merge, so it exits for valid invocations like gh pr merge (no argument), branch-name arguments, or flag-first forms. The GitHub CLI synopsis allows [<number> | <url> | <branch>] and also supports no argument (current branch PR), so this parser causes the watcher to silently skip post-merge CI tracking for common merge flows.

Useful? React with 👍 / 👎.

if [ -z "$PR_NUM" ]; then exit 0; fi

# Get the merge commit SHA from the merged PR
PR_JSON=$(gh pr view "$PR_NUM" --repo "$REPO" --json mergeCommit,baseRefName 2>/dev/null || echo "{}")
SHA=$(echo "$PR_JSON" | jq -r '.mergeCommit.oid // empty' 2>/dev/null)

if [ -z "$SHA" ]; then
# Merge might not be complete yet, wait and retry
sleep 5
PR_JSON=$(gh pr view "$PR_NUM" --repo "$REPO" --json mergeCommit,baseRefName 2>/dev/null || echo "{}")
SHA=$(echo "$PR_JSON" | jq -r '.mergeCommit.oid // empty' 2>/dev/null)
Comment on lines +118 to +122
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 Wait for delayed merge completion before bailing out

This logic retries mergeCommit only once after 5 seconds, then aborts tracking when it is still empty. gh pr merge --auto and merge-queue paths can intentionally delay the actual merge until checks pass, so in those cases this script exits before the merge commit exists and never watches the CI/CD runs it was added to track.

Useful? React with 👍 / 👎.

fi
if [ -z "$SHA" ]; then exit 0; fi
else
SHA=$(git rev-parse HEAD 2>/dev/null || echo "")
if [ -z "$SHA" ]; then exit 0; fi
fi

# ---------------------------------------------------------------------------
# Dedup: skip if another ci-watcher is already tracking this commit
# ---------------------------------------------------------------------------

LOCK_DIR=~/.pulse/locks
mkdir -p "$LOCK_DIR"
LOCK_FILE="$LOCK_DIR/${CLAUDE_PID}-${SHA}.lock"
if ! mkdir "$LOCK_FILE" 2>/dev/null; then
exit 0
fi
trap 'rmdir "$LOCK_FILE" 2>/dev/null' EXIT

# ---------------------------------------------------------------------------
# Load .ci-watch-ignore
# ---------------------------------------------------------------------------

IGNORE_FILE="$(git rev-parse --show-toplevel 2>/dev/null)/.ci-watch-ignore"
IGNORE_PATTERNS=()
Expand Down
Loading