feat: initial commit with orchestrator scripts and dev instructions
Import all 18 production orchestrator scripts from ~/. These are direct copies with hardcoded paths — generalization is the next step. Includes CLAUDE.local.md with project overview, architecture notes, and roadmap. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
commit
0273e8f3d0
|
|
@ -0,0 +1,110 @@
|
|||
# CLAUDE.local.md - Agent Orchestrator Development
|
||||
|
||||
You are working on **agent-orchestrator**, an open-source multi-agent orchestrator for Claude Code. This tool manages parallel coding agents across projects from a single control plane.
|
||||
|
||||
> **Dog-fooding**: This orchestrator is being built using a copy of itself. The `~/` home directory orchestrator manages sessions that work on this very repo.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Agent Orchestrator lets you:
|
||||
- Spawn and manage dozens of Claude Code agents working in parallel
|
||||
- Track progress via a live HTML dashboard with PR status, CI checks, and merge actions
|
||||
- Send commands to agents, kill/cleanup sessions, and view terminals in-browser
|
||||
- Organize agents by project with worktree-based isolation
|
||||
|
||||
## Repo Structure
|
||||
|
||||
```
|
||||
agent-orchestrator/
|
||||
├── scripts/ # Core orchestrator scripts
|
||||
│ ├── claude-status # Unified dashboard (CLI)
|
||||
│ ├── claude-batch-spawn # Spawn multiple sessions at once
|
||||
│ ├── claude-spawn # Spawn single session in new terminal tab
|
||||
│ ├── claude-dashboard # HTML dashboard with live PR status (web UI)
|
||||
│ ├── claude-open-all # Open terminal tabs for all sessions
|
||||
│ ├── claude-review-check # Trigger agents to address PR review comments
|
||||
│ ├── claude-bugbot-fix # Fix bugbot comments across sessions
|
||||
│ ├── claude-session-status # Health monitor (working/idle/blocked/waiting)
|
||||
│ ├── claude-spawn-with-context # Spawn with custom prompt file
|
||||
│ ├── claude-spawn-on-branch # Spawn on existing branch
|
||||
│ ├── claude-spawn-with-prompt # Spawn and deliver prompt after ready
|
||||
│ ├── get-claude-session-info # Extract session metadata from tmux
|
||||
│ ├── open-tmux-session # Switch to terminal tab for tmux session
|
||||
│ ├── open-iterm-tab # iTerm2 tab management
|
||||
│ ├── notify-session # iTerm2 notifications from sessions
|
||||
│ ├── send-to-session # Smart message delivery to tmux sessions
|
||||
│ ├── claude-integrator-session # Example: project session manager (Linear/integrator)
|
||||
│ └── claude-splitly-session # Example: project session manager (GitHub Issues/SafeSplit)
|
||||
├── CLAUDE.local.md # This file (dev instructions)
|
||||
├── CLAUDE.md # Repo-level instructions for contributors
|
||||
└── README.md # Project README
|
||||
```
|
||||
|
||||
## Current State
|
||||
|
||||
The scripts in `scripts/` are direct copies from the production orchestrator (`~/`). They currently have hardcoded paths and project-specific references (integrator, splitly). The work ahead is to:
|
||||
|
||||
1. **Generalize** — Remove hardcoded project names, make scripts project-agnostic
|
||||
2. **Configuration** — Add a config file (e.g., `orchestrator.yaml`) that defines projects, repos, branches, issue trackers
|
||||
3. **Installation** — Create an install script that symlinks scripts to `~/` or adds them to PATH
|
||||
4. **Documentation** — Write a comprehensive README with setup guide and examples
|
||||
5. **Self-hosting** — Make this repo use its own orchestrator for development
|
||||
|
||||
## Project Info
|
||||
|
||||
- **Repo**: ComposioHQ/agent-orchestrator (GitHub)
|
||||
- **Issue Tracker**: Linear
|
||||
- **Visibility**: Internal (ComposioHQ org)
|
||||
- **Everything runs locally** — no Datadog, no cloud infra
|
||||
|
||||
## Development Workflow
|
||||
|
||||
```bash
|
||||
# This repo is managed by the home directory orchestrator
|
||||
# Sessions are spawned via:
|
||||
~/claude-spawn-with-context i TICKET /tmp/prompt.txt --open
|
||||
|
||||
# Linear tickets for this project
|
||||
# (use Rube MCP LINEAR_CREATE_LINEAR_ISSUE to create tickets)
|
||||
|
||||
# GitHub issues (secondary):
|
||||
gh issue list --repo ComposioHQ/agent-orchestrator --state open
|
||||
```
|
||||
|
||||
## Key Design Principles
|
||||
|
||||
1. **tmux-based** — All agent sessions run in tmux. This gives persistence, detach/attach, and scriptability.
|
||||
2. **Metadata files** — Each session has a flat metadata file (`key=value` format) tracking branch, PR, issue, status, notes.
|
||||
3. **Live dashboard** — HTML dashboard served locally with real-time activity detection via Claude's JSONL session files.
|
||||
4. **Project-agnostic shared scripts** — Core scripts (spawn, status, dashboard) take project as an argument.
|
||||
5. **Project-specific session managers** — Each project gets a session manager script that handles worktrees, naming, and cleanup.
|
||||
6. **iTerm2 integration** — AppleScript-based tab management for macOS (should be made terminal-agnostic).
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
### Session Lifecycle
|
||||
```
|
||||
spawn → tmux session created → Claude started → working on issue
|
||||
↓
|
||||
metadata file written (branch, issue, status)
|
||||
↓
|
||||
agent creates PR → metadata updated (pr=URL)
|
||||
↓
|
||||
dashboard shows PR status, CI, review state
|
||||
↓
|
||||
PR merged → cleanup kills session, archives metadata
|
||||
```
|
||||
|
||||
### Activity Detection
|
||||
The dashboard detects if agents are working/idle/exited by:
|
||||
1. Checking Claude's JSONL session file modification time and last message type
|
||||
2. Walking the process tree from tmux pane PID to find `claude` processes
|
||||
3. Polling every 5 seconds via `/api/sessions` endpoint
|
||||
|
||||
### Dashboard Server
|
||||
- Python HTTP server serving static HTML + JSON API
|
||||
- `/api/sessions` — live activity status
|
||||
- `/api/terminal/:name` — spawn ttyd web terminal for session
|
||||
- `/api/kill/:name` — kill session + close iTerm tab + archive metadata
|
||||
- `/api/merge/:repo/:num` — merge PR via `gh`
|
||||
- `/api/send/:name` — send message to agent via `send-to-session`
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Claude Batch Session Spawner (v2 - worktree-based)
|
||||
# Delegates to session managers for worktree creation, symlinks, and MCP setup.
|
||||
# Adds duplicate detection: skips issues that already have active sessions.
|
||||
# Usage: ~/claude-batch-spawn <project> <issue1> [issue2] [issue3] ...
|
||||
|
||||
PROJECT="${1:-}"
|
||||
shift
|
||||
ISSUES=("$@")
|
||||
|
||||
if [ -z "$PROJECT" ] || [ ${#ISSUES[@]} -eq 0 ]; then
|
||||
echo "Usage: ~/claude-batch-spawn <project> <issue1> [issue2] [issue3] ..."
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " ~/claude-batch-spawn splitly 298 299 300 301"
|
||||
echo " ~/claude-batch-spawn integrator INT-1020 INT-1021"
|
||||
echo ""
|
||||
echo "This creates worktree-based sessions for each issue."
|
||||
echo "After creation, attach with: ~/claude-*-session attach <session-name>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Determine project settings
|
||||
case "$PROJECT" in
|
||||
integrator|i)
|
||||
SESSION_PREFIX="integrator"
|
||||
SESSION_MANAGER="$HOME/claude-integrator-session"
|
||||
SESSION_DATA_DIR="$HOME/.integrator-sessions"
|
||||
GITHUB_REPO=""
|
||||
;;
|
||||
splitly|s|safesplit)
|
||||
SESSION_PREFIX="splitly"
|
||||
SESSION_MANAGER="$HOME/claude-splitly-session"
|
||||
SESSION_DATA_DIR="$HOME/.splitly-sessions"
|
||||
GITHUB_REPO="UniverseOfThings/SafeSplit"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown project: $PROJECT"
|
||||
echo "Use 'integrator' (or 'i') or 'splitly' (or 's')"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
mkdir -p "$SESSION_DATA_DIR"
|
||||
|
||||
# Duplicate detection: check if an issue already has an active session
|
||||
issue_has_session() {
|
||||
local issue="$1"
|
||||
local issue_lower=$(echo "$issue" | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
for meta_file in "$SESSION_DATA_DIR"/*; do
|
||||
[ -f "$meta_file" ] || continue
|
||||
local session_name=$(basename "$meta_file")
|
||||
|
||||
# Check if tmux session is still active
|
||||
tmux has-session -t "$session_name" 2>/dev/null || continue
|
||||
|
||||
# Check issue field in metadata
|
||||
local meta_issue=$(grep "^issue=" "$meta_file" 2>/dev/null | cut -d= -f2-)
|
||||
local meta_issue_lower=$(echo "$meta_issue" | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
# Match by issue ID in the URL or direct match
|
||||
if echo "$meta_issue_lower" | grep -qi "$issue_lower"; then
|
||||
echo "$session_name"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
echo "╔══════════════════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ BATCH SESSION SPAWNER (v2) ║"
|
||||
echo "╚══════════════════════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo "Project: $PROJECT"
|
||||
echo "Issues to spawn: ${ISSUES[*]}"
|
||||
echo ""
|
||||
|
||||
CREATED=()
|
||||
SKIPPED=()
|
||||
FAILED=()
|
||||
|
||||
for ISSUE in "${ISSUES[@]}"; do
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
# Duplicate detection
|
||||
existing=$(issue_has_session "$ISSUE")
|
||||
if [ $? -eq 0 ]; then
|
||||
echo " ⏭️ Skipping $ISSUE — already has session: $existing"
|
||||
SKIPPED+=("$ISSUE:$existing")
|
||||
continue
|
||||
fi
|
||||
|
||||
echo " Creating session for: $ISSUE"
|
||||
|
||||
# Delegate to the session manager
|
||||
output=$("$SESSION_MANAGER" new "$ISSUE" 2>&1)
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo " ❌ Error creating session!"
|
||||
echo "$output" | sed 's/^/ /'
|
||||
FAILED+=("$ISSUE")
|
||||
continue
|
||||
fi
|
||||
|
||||
# Extract session name from session manager output
|
||||
SESSION=$(echo "$output" | grep "^SESSION=" | cut -d= -f2)
|
||||
|
||||
if [ -z "$SESSION" ]; then
|
||||
echo " ❌ Error: Could not determine session name!"
|
||||
echo "$output" | sed 's/^/ /'
|
||||
FAILED+=("$ISSUE")
|
||||
continue
|
||||
fi
|
||||
|
||||
# Update metadata with issue info
|
||||
if [ "$SESSION_PREFIX" = "splitly" ]; then
|
||||
ISSUE_NUM=$(echo "$ISSUE" | sed 's/#//' | grep -o '[0-9]*')
|
||||
ISSUE_URL="https://github.com/$GITHUB_REPO/issues/$ISSUE_NUM"
|
||||
else
|
||||
ISSUE_URL="$ISSUE"
|
||||
fi
|
||||
# Append issue to metadata (session manager may have already set it)
|
||||
grep -q "^issue=" "$SESSION_DATA_DIR/$SESSION" 2>/dev/null || \
|
||||
echo "issue=$ISSUE_URL" >> "$SESSION_DATA_DIR/$SESSION"
|
||||
|
||||
# Wait for Claude to start, then send the work prompt
|
||||
sleep 1
|
||||
|
||||
# Build initial prompt
|
||||
if [ "$SESSION_PREFIX" = "splitly" ]; then
|
||||
INITIAL_PROMPT="Please start working on GitHub issue #$ISSUE_NUM. First, fetch the issue details using: gh issue view $ISSUE_NUM --repo $GITHUB_REPO. Then create an appropriate feature branch (e.g., fix/issue-$ISSUE_NUM-short-description or feat/issue-$ISSUE_NUM-short-description), and start implementing the solution."
|
||||
else
|
||||
INITIAL_PROMPT="Please start working on $ISSUE, fetch ticket info, create the appropriate branch so that github auto links to linear, and start working on the task"
|
||||
fi
|
||||
|
||||
# Send prompt to the running Claude session
|
||||
tmux send-keys -t "$SESSION" "$INITIAL_PROMPT" Enter
|
||||
|
||||
echo " ✅ Session $SESSION created and prompted"
|
||||
CREATED+=("$SESSION:$ISSUE")
|
||||
|
||||
# Small delay between spawns
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
echo "📊 SUMMARY"
|
||||
echo " Created: ${#CREATED[@]} sessions"
|
||||
echo " Skipped: ${#SKIPPED[@]} (duplicate)"
|
||||
echo " Failed: ${#FAILED[@]}"
|
||||
echo ""
|
||||
|
||||
if [ ${#CREATED[@]} -gt 0 ]; then
|
||||
echo "Created sessions:"
|
||||
for item in "${CREATED[@]}"; do
|
||||
SESSION=$(echo "$item" | cut -d: -f1)
|
||||
ISSUE=$(echo "$item" | cut -d: -f2-)
|
||||
echo " • $SESSION → $ISSUE"
|
||||
done
|
||||
fi
|
||||
|
||||
if [ ${#SKIPPED[@]} -gt 0 ]; then
|
||||
echo ""
|
||||
echo "Skipped (already has session):"
|
||||
for item in "${SKIPPED[@]}"; do
|
||||
ISSUE=$(echo "$item" | cut -d: -f1)
|
||||
EXISTING=$(echo "$item" | cut -d: -f2-)
|
||||
echo " • $ISSUE → existing session: $EXISTING"
|
||||
done
|
||||
fi
|
||||
|
||||
if [ ${#FAILED[@]} -gt 0 ]; then
|
||||
echo ""
|
||||
echo "Failed issues: ${FAILED[*]}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "To see all sessions: ~/claude-status"
|
||||
echo "To open all tabs: ~/claude-open-all $SESSION_PREFIX"
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
#!/bin/bash
|
||||
# Check open PRs for unaddressed bugbot (cursor[bot]) comments
|
||||
# and send fix instructions to the relevant sessions.
|
||||
#
|
||||
# Usage: ~/claude-bugbot-fix [project] [--dry-run]
|
||||
#
|
||||
# Examples:
|
||||
# ~/claude-bugbot-fix integrator # Check and send fix instructions
|
||||
# ~/claude-bugbot-fix integrator --dry-run # Just report, don't send
|
||||
# ~/claude-bugbot-fix splitly
|
||||
|
||||
PROJECT="${1:-integrator}"
|
||||
DRY_RUN=false
|
||||
[ "$2" = "--dry-run" ] && DRY_RUN=true
|
||||
|
||||
case "$PROJECT" in
|
||||
integrator|i)
|
||||
REPO="ComposioHQ/integrator"
|
||||
SESSIONS_DIR="$HOME/.integrator-sessions"
|
||||
SESSION_PREFIX="integrator"
|
||||
;;
|
||||
splitly|s|safesplit)
|
||||
REPO="UniverseOfThings/SafeSplit"
|
||||
SESSIONS_DIR="$HOME/.splitly-sessions"
|
||||
SESSION_PREFIX="splitly"
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 [integrator|splitly] [--dry-run]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Checking $REPO for bugbot comments..."
|
||||
echo ""
|
||||
|
||||
# Build PR → session map from metadata
|
||||
declare -A PR_SESSION_MAP
|
||||
for s in $(ls "$SESSIONS_DIR" 2>/dev/null | grep "^${SESSION_PREFIX}-"); do
|
||||
pr_num=$(grep "^pr=" "$SESSIONS_DIR/$s" 2>/dev/null | grep -oE "[0-9]+" | tail -1)
|
||||
[ -n "$pr_num" ] && PR_SESSION_MAP[$pr_num]="$s"
|
||||
done
|
||||
|
||||
# Get all open PRs
|
||||
OPEN_PRS=$(gh pr list --repo "$REPO" --state open --json number -q '.[].number' --limit 50)
|
||||
|
||||
FOUND=0
|
||||
for pr in $OPEN_PRS; do
|
||||
# Check if latest cursor[bot] review reports issues
|
||||
latest_review=$(gh api "repos/$REPO/pulls/$pr/reviews" --jq '[.[] | select(.user.login == "cursor[bot]")] | last | .body // ""' 2>/dev/null)
|
||||
|
||||
if echo "$latest_review" | grep -q "potential issue"; then
|
||||
issue_count=$(echo "$latest_review" | grep -oE "found [0-9]+" | grep -oE "[0-9]+")
|
||||
session="${PR_SESSION_MAP[$pr]:-none}"
|
||||
|
||||
# Get the actual inline comments
|
||||
review_id=$(gh api "repos/$REPO/pulls/$pr/reviews" --jq '[.[] | select(.user.login == "cursor[bot]")] | last | .id' 2>/dev/null)
|
||||
comments=""
|
||||
if [ -n "$review_id" ]; then
|
||||
comments=$(gh api "repos/$REPO/pulls/$pr/reviews/$review_id/comments" --jq '.[] | "- [\(.path)] \(.body | split("\n")[0] | gsub("### "; ""))"' 2>/dev/null)
|
||||
fi
|
||||
|
||||
echo "PR #$pr: $issue_count bugbot issue(s) → session: $session"
|
||||
echo "$comments" | head -5
|
||||
echo ""
|
||||
|
||||
FOUND=$((FOUND + 1))
|
||||
|
||||
# Send fix instruction if session exists and not dry-run
|
||||
if [ "$session" != "none" ] && [ "$DRY_RUN" = "false" ]; then
|
||||
if tmux has-session -t "$session" 2>/dev/null; then
|
||||
# Build a concise fix message
|
||||
comment_summary=$(gh api "repos/$REPO/pulls/$pr/reviews/$review_id/comments" --jq '.[] | "(\(.path | split("/") | last)): \(.body | split("\n")[0] | gsub("### "; ""))"' 2>/dev/null | tr '\n' '; ')
|
||||
|
||||
tmux send-keys -t "$session" "You have $issue_count unaddressed bugbot comment(s) on PR #$pr. Please run: gh api repos/$REPO/pulls/$pr/comments --jq '.[] | select(.user.login == \"cursor[bot]\") | .body' to see full details. Issues: $comment_summary Fix and push." Enter
|
||||
echo " → Sent fix instruction to $session"
|
||||
else
|
||||
echo " → Session $session not running"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$FOUND" -eq 0 ]; then
|
||||
echo "No unaddressed bugbot comments found."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Done. $FOUND PR(s) with bugbot issues."
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,242 @@
|
|||
#!/bin/bash
|
||||
# claude-integrator-session-v2 - Worktree-based session manager for integrator
|
||||
|
||||
set -e
|
||||
|
||||
MAIN_REPO=~/integrator
|
||||
WORKTREE_DIR=~/.worktrees/integrator
|
||||
SESSION_DIR=~/.integrator-sessions
|
||||
ENV_TEMPLATE=~/.env-templates/integrator
|
||||
DEFAULT_BRANCH="next"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
usage() {
|
||||
echo "Usage: ~/claude-integrator-session-v2 <command> [args]"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " new [LINEAR-ID] [--open] Create new session (optionally with Linear ticket)"
|
||||
echo " ls List all sessions"
|
||||
echo " attach <session> Attach to session"
|
||||
echo " kill <session> Kill session and remove worktree"
|
||||
echo " cleanup Kill sessions where PR is merged or ticket is done"
|
||||
echo " update Update current session metadata"
|
||||
exit 1
|
||||
}
|
||||
|
||||
get_next_session_number() {
|
||||
local max=0
|
||||
for session in $(tmux list-sessions -F "#{session_name}" 2>/dev/null | grep "^integrator-[0-9]*$" | sed 's/integrator-//'); do
|
||||
if [ "$session" -gt "$max" ] 2>/dev/null; then
|
||||
max=$session
|
||||
fi
|
||||
done
|
||||
echo $((max + 1))
|
||||
}
|
||||
|
||||
cmd_new() {
|
||||
local linear_id=""
|
||||
local open_warp=false
|
||||
|
||||
# Parse args
|
||||
for arg in "$@"; do
|
||||
if [ "$arg" = "--open" ]; then
|
||||
open_warp=true
|
||||
elif [ -z "$linear_id" ] && [ "$arg" != "--open" ]; then
|
||||
linear_id="$arg"
|
||||
fi
|
||||
done
|
||||
|
||||
local session_num=$(get_next_session_number)
|
||||
local session_name="integrator-$session_num"
|
||||
local worktree_path="$WORKTREE_DIR/$session_name"
|
||||
|
||||
echo -e "${BLUE}Creating session: $session_name${NC}"
|
||||
|
||||
cd "$MAIN_REPO"
|
||||
git fetch origin --quiet
|
||||
|
||||
if [ -n "$linear_id" ]; then
|
||||
local branch_name="feat/$linear_id"
|
||||
git worktree add -b "$branch_name" "$worktree_path" "origin/$DEFAULT_BRANCH" 2>/dev/null || \
|
||||
git worktree add "$worktree_path" "origin/$DEFAULT_BRANCH"
|
||||
else
|
||||
git worktree add "$worktree_path" "origin/$DEFAULT_BRANCH" --detach
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✓ Created worktree at $worktree_path${NC}"
|
||||
|
||||
# Symlink shared resources from main repo
|
||||
[ -f "$ENV_TEMPLATE/.env" ] && ln -sf "$ENV_TEMPLATE/.env" "$worktree_path/.env"
|
||||
[ -f "$ENV_TEMPLATE/.envrc" ] && ln -sf "$ENV_TEMPLATE/.envrc" "$worktree_path/.envrc" && \
|
||||
(cd "$worktree_path" && direnv allow 2>/dev/null || true)
|
||||
[ -d "$MAIN_REPO/.venv" ] && ln -sf "$MAIN_REPO/.venv" "$worktree_path/.venv"
|
||||
[ -f "$MAIN_REPO/CLAUDE.local.md" ] && ln -sf "$MAIN_REPO/CLAUDE.local.md" "$worktree_path/CLAUDE.local.md"
|
||||
[ -d "$MAIN_REPO/.claude" ] && rm -rf "$worktree_path/.claude" && ln -s "$MAIN_REPO/.claude" "$worktree_path/.claude"
|
||||
|
||||
# Add Rube MCP before starting session
|
||||
(cd "$worktree_path" && claude mcp add rube --transport http https://rube.app/mcp &>/dev/null) || true
|
||||
|
||||
# Create tmux session and start Claude (unset CLAUDECODE to avoid nested session detection)
|
||||
tmux new-session -d -s "$session_name" -c "$worktree_path" -e "INTEGRATOR_SESSION=$session_name" -e "DIRENV_LOG_FORMAT="
|
||||
tmux send-keys -t "$session_name" "unset CLAUDECODE && claude --dangerously-skip-permissions" Enter
|
||||
|
||||
# Create metadata file
|
||||
mkdir -p "$SESSION_DIR"
|
||||
cat > "$SESSION_DIR/$session_name" << EOF
|
||||
worktree=$worktree_path
|
||||
branch=$(cd "$worktree_path" && git branch --show-current || echo "detached")
|
||||
status=starting
|
||||
EOF
|
||||
[ -n "$linear_id" ] && echo "issue=https://linear.app/composio/issue/$linear_id" >> "$SESSION_DIR/$session_name"
|
||||
|
||||
echo -e "${GREEN}✓ Created tmux session: $session_name${NC}"
|
||||
echo -e "${GREEN}✓ Started Claude in session${NC}"
|
||||
echo ""
|
||||
echo "Attach with: tmux attach -t $session_name"
|
||||
echo "Or open in iTerm2: ~/open-iterm-tab $session_name"
|
||||
|
||||
# Clean output for scripting (no ANSI codes)
|
||||
echo "SESSION=$session_name"
|
||||
|
||||
# Open in iTerm2 if requested
|
||||
if [ "$open_warp" = true ]; then
|
||||
~/open-iterm-tab "$session_name"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_ls() {
|
||||
echo -e "${BLUE}Integrator Sessions:${NC}"
|
||||
echo ""
|
||||
|
||||
for session in $(tmux list-sessions -F "#{session_name}" 2>/dev/null | grep "^integrator-" | sort -V); do
|
||||
local meta_file="$SESSION_DIR/$session"
|
||||
local worktree="" branch="" status="" pr=""
|
||||
|
||||
if [ -f "$meta_file" ]; then
|
||||
worktree=$(grep "^worktree=" "$meta_file" 2>/dev/null | cut -d= -f2-)
|
||||
branch=$(grep "^branch=" "$meta_file" 2>/dev/null | cut -d= -f2-)
|
||||
status=$(grep "^status=" "$meta_file" 2>/dev/null | cut -d= -f2-)
|
||||
pr=$(grep "^pr=" "$meta_file" 2>/dev/null | cut -d= -f2-)
|
||||
fi
|
||||
|
||||
[ -d "$worktree" ] && branch=$(cd "$worktree" && git branch --show-current 2>/dev/null || echo "$branch")
|
||||
|
||||
echo -e "${GREEN}$session${NC}"
|
||||
[ -n "$branch" ] && echo " Branch: $branch"
|
||||
[ -n "$status" ] && echo " Status: $status"
|
||||
[ -n "$pr" ] && echo " PR: $pr"
|
||||
echo ""
|
||||
done
|
||||
}
|
||||
|
||||
cmd_attach() {
|
||||
[ -z "$1" ] && echo "Usage: ~/claude-integrator-session-v2 attach <session>" && exit 1
|
||||
tmux attach -t "$1"
|
||||
}
|
||||
|
||||
cmd_kill() {
|
||||
local session="$1"
|
||||
[ -z "$session" ] && echo "Usage: ~/claude-integrator-session-v2 kill <session>" && exit 1
|
||||
|
||||
local meta_file="$SESSION_DIR/$session"
|
||||
local worktree=""
|
||||
[ -f "$meta_file" ] && worktree=$(grep "^worktree=" "$meta_file" 2>/dev/null | cut -d= -f2-)
|
||||
|
||||
# Close iTerm tab BEFORE killing tmux (tab name changes after tmux dies)
|
||||
osascript -e "
|
||||
tell application \"iTerm2\"
|
||||
repeat with w in windows
|
||||
repeat with t in tabs of w
|
||||
repeat with s in sessions of t
|
||||
if name of s contains \"$session\" then
|
||||
close t
|
||||
return
|
||||
end if
|
||||
end repeat
|
||||
end repeat
|
||||
end repeat
|
||||
end tell
|
||||
" 2>/dev/null && echo -e "${GREEN}✓ Closed iTerm tab: $session${NC}"
|
||||
|
||||
tmux kill-session -t "$session" 2>/dev/null && echo -e "${GREEN}✓ Killed tmux session: $session${NC}"
|
||||
|
||||
if [ -n "$worktree" ] && [ -d "$worktree" ]; then
|
||||
cd "$MAIN_REPO"
|
||||
git worktree remove --force "$worktree" 2>/dev/null && echo -e "${GREEN}✓ Removed worktree: $worktree${NC}"
|
||||
fi
|
||||
|
||||
# Archive metadata instead of deleting
|
||||
if [ -f "$meta_file" ]; then
|
||||
mkdir -p "$SESSION_DIR/archive"
|
||||
mv "$meta_file" "$SESSION_DIR/archive/${session}_$(date +%Y%m%d-%H%M%S)"
|
||||
echo -e "${GREEN}✓ Archived metadata${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_cleanup() {
|
||||
echo "Checking for completed sessions..."
|
||||
|
||||
for session in $(tmux list-sessions -F "#{session_name}" 2>/dev/null | grep "^integrator-"); do
|
||||
local meta_file="$SESSION_DIR/$session"
|
||||
local pr="" issue=""
|
||||
|
||||
[ -f "$meta_file" ] && {
|
||||
pr=$(grep "^pr=" "$meta_file" 2>/dev/null | cut -d= -f2-)
|
||||
issue=$(grep "^issue=" "$meta_file" 2>/dev/null | cut -d= -f2-)
|
||||
}
|
||||
|
||||
if [ -n "$pr" ]; then
|
||||
local pr_num=$(echo "$pr" | grep -o '[0-9]*$')
|
||||
if [ -n "$pr_num" ]; then
|
||||
local state=$(gh pr view "$pr_num" --repo ComposioHQ/integrator --json state -q '.state' 2>/dev/null)
|
||||
[ "$state" = "MERGED" ] && echo -e "${YELLOW}PR #$pr_num merged - killing $session${NC}" && cmd_kill "$session" && continue
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check Linear ticket status if we have an issue URL
|
||||
if [ -n "$issue" ] && [ -n "${LINEAR_API_KEY:-}" ]; then
|
||||
local ticket_id=$(echo "$issue" | grep -o 'INT-[0-9]*')
|
||||
if [ -n "$ticket_id" ]; then
|
||||
local ticket_state=$(curl -s -X POST https://api.linear.app/graphql \
|
||||
-H "Authorization: $LINEAR_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"query\": \"{ issueSearch(filter: { identifier: { eq: \\\"$ticket_id\\\" } }) { nodes { state { type } } } }\"}" \
|
||||
2>/dev/null | jq -r '.data.issueSearch.nodes[0].state.type // empty' 2>/dev/null)
|
||||
if [ "$ticket_state" = "completed" ] || [ "$ticket_state" = "canceled" ]; then
|
||||
echo -e "${YELLOW}$ticket_id $ticket_state - killing $session${NC}"
|
||||
cmd_kill "$session" && continue
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
echo "Cleanup complete."
|
||||
}
|
||||
|
||||
cmd_update() {
|
||||
local session="${INTEGRATOR_SESSION:-}"
|
||||
[ -z "$session" ] && echo "Error: Not in an integrator session" && exit 1
|
||||
|
||||
local meta_file="$SESSION_DIR/$session"
|
||||
local worktree=$(grep "^worktree=" "$meta_file" 2>/dev/null | cut -d= -f2-)
|
||||
[ -z "$worktree" ] || [ ! -d "$worktree" ] && worktree="$PWD"
|
||||
|
||||
local branch=$(cd "$worktree" && git branch --show-current 2>/dev/null)
|
||||
[ -n "$branch" ] && sed -i '' "s|^branch=.*|branch=$branch|" "$meta_file" 2>/dev/null
|
||||
|
||||
echo "Updated metadata for $session"
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
new) shift; cmd_new "$@" ;;
|
||||
ls) cmd_ls ;;
|
||||
attach) cmd_attach "$2" ;;
|
||||
kill) cmd_kill "$2" ;;
|
||||
cleanup) cmd_cleanup ;;
|
||||
update) cmd_update ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Open iTerm2 tabs for all active sessions (or a specific project)
|
||||
# Usage: ~/claude-open-all [--new-window] [integrator|splitly|all]
|
||||
|
||||
NEW_WINDOW=false
|
||||
PROJECT=""
|
||||
|
||||
# Parse arguments
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--new-window|-w)
|
||||
NEW_WINDOW=true
|
||||
;;
|
||||
integrator|i|splitly|s|safesplit|all)
|
||||
PROJECT="$arg"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $arg"
|
||||
echo "Usage: ~/claude-open-all [--new-window] [integrator|splitly|all]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
PROJECT="${PROJECT:-all}"
|
||||
|
||||
get_sessions() {
|
||||
local prefix="$1"
|
||||
tmux list-sessions -F '#{session_name}' 2>/dev/null | grep "^${prefix}-" | sort
|
||||
}
|
||||
|
||||
open_session_new_window() {
|
||||
local session="$1"
|
||||
osascript -e "
|
||||
tell application \"iTerm2\"
|
||||
activate
|
||||
set newWindow to (create window with default profile)
|
||||
tell current session of newWindow
|
||||
set name to \"$session\"
|
||||
write text \"printf '\\\\033]0;$session\\\\007' && tmux attach -t $session\"
|
||||
end tell
|
||||
end tell
|
||||
"
|
||||
}
|
||||
|
||||
open_session_new_tab() {
|
||||
local session="$1"
|
||||
osascript -e "
|
||||
tell application \"iTerm2\"
|
||||
activate
|
||||
tell current window
|
||||
create tab with default profile
|
||||
tell current session
|
||||
set name to \"$session\"
|
||||
write text \"printf '\\\\033]0;$session\\\\007' && tmux attach -t $session\"
|
||||
end tell
|
||||
end tell
|
||||
end tell
|
||||
"
|
||||
}
|
||||
|
||||
switch_to_existing_tab() {
|
||||
local session="$1"
|
||||
osascript -e "
|
||||
tell application \"iTerm2\"
|
||||
activate
|
||||
repeat with aWindow in windows
|
||||
repeat with aTab in tabs of aWindow
|
||||
repeat with aSession in sessions of aTab
|
||||
try
|
||||
if profile name of aSession is equal to \"$session\" then
|
||||
select aWindow
|
||||
select aTab
|
||||
return \"FOUND\"
|
||||
end if
|
||||
end try
|
||||
end repeat
|
||||
end repeat
|
||||
end repeat
|
||||
return \"NOT_FOUND\"
|
||||
end tell
|
||||
"
|
||||
}
|
||||
|
||||
SESSIONS=()
|
||||
|
||||
case "$PROJECT" in
|
||||
integrator|i)
|
||||
SESSIONS=($(get_sessions "integrator"))
|
||||
echo "Opening ${#SESSIONS[@]} integrator sessions..."
|
||||
;;
|
||||
splitly|s|safesplit)
|
||||
SESSIONS=($(get_sessions "splitly"))
|
||||
echo "Opening ${#SESSIONS[@]} splitly sessions..."
|
||||
;;
|
||||
all|"")
|
||||
SESSIONS=($(get_sessions "integrator") $(get_sessions "splitly"))
|
||||
echo "Opening ${#SESSIONS[@]} total sessions..."
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ ${#SESSIONS[@]} -eq 0 ]; then
|
||||
echo "No sessions found."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$NEW_WINDOW" = true ]; then
|
||||
echo "(in new window)"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
FIRST=true
|
||||
for session in "${SESSIONS[@]}"; do
|
||||
# Check if tab already exists
|
||||
existing=$(switch_to_existing_tab "$session" 2>/dev/null)
|
||||
|
||||
if [ "$existing" = "FOUND" ]; then
|
||||
echo " Switched to existing: $session"
|
||||
elif [ "$NEW_WINDOW" = true ] && [ "$FIRST" = true ]; then
|
||||
echo " Opening (new window): $session"
|
||||
open_session_new_window "$session"
|
||||
FIRST=false
|
||||
else
|
||||
echo " Opening (new tab): $session"
|
||||
open_session_new_tab "$session"
|
||||
fi
|
||||
sleep 0.3
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Done! Opened ${#SESSIONS[@]} iTerm2 tabs."
|
||||
echo "Use Cmd+Shift+[ or ] to switch between tabs."
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Check all sessions with PRs for review comments and trigger agents to address them
|
||||
# Usage: ~/claude-review-check [integrator|splitly]
|
||||
|
||||
PROJECT="${1:-splitly}"
|
||||
|
||||
case "$PROJECT" in
|
||||
integrator|i)
|
||||
SESSION_PREFIX="integrator"
|
||||
SESSION_DATA_DIR="$HOME/.integrator-sessions"
|
||||
;;
|
||||
splitly|s|safesplit)
|
||||
SESSION_PREFIX="splitly"
|
||||
SESSION_DATA_DIR="$HOME/.splitly-sessions"
|
||||
;;
|
||||
*)
|
||||
echo "Usage: ~/claude-review-check [integrator|splitly]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "╔══════════════════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ PR REVIEW COMMENT CHECKER ║"
|
||||
echo "╚══════════════════════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo "Project: $PROJECT"
|
||||
echo ""
|
||||
|
||||
# Get all sessions
|
||||
sessions=$(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep "^${SESSION_PREFIX}-" | sort)
|
||||
|
||||
if [ -z "$sessions" ]; then
|
||||
echo "No active sessions found."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
TRIGGERED=()
|
||||
SKIPPED_NO_PR=()
|
||||
SKIPPED_NO_COMMENTS=()
|
||||
ERRORS=()
|
||||
|
||||
for session in $sessions; do
|
||||
data_file="$SESSION_DATA_DIR/$session"
|
||||
|
||||
if [ ! -f "$data_file" ]; then
|
||||
SKIPPED_NO_PR+=("$session (no metadata)")
|
||||
continue
|
||||
fi
|
||||
|
||||
pr_url=$(grep "^pr=" "$data_file" 2>/dev/null | cut -d'=' -f2-)
|
||||
branch=$(grep "^branch=" "$data_file" 2>/dev/null | cut -d'=' -f2-)
|
||||
|
||||
if [ -z "$pr_url" ]; then
|
||||
SKIPPED_NO_PR+=("$session")
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "📍 $session"
|
||||
echo " Branch: ${branch:--}"
|
||||
echo " PR: $pr_url"
|
||||
|
||||
# Extract PR number and repo from URL
|
||||
# Format: https://github.com/owner/repo/pull/123
|
||||
if [[ "$pr_url" =~ github\.com/([^/]+)/([^/]+)/pull/([0-9]+) ]]; then
|
||||
owner="${BASH_REMATCH[1]}"
|
||||
repo="${BASH_REMATCH[2]}"
|
||||
pr_number="${BASH_REMATCH[3]}"
|
||||
else
|
||||
echo " ⚠️ Could not parse PR URL"
|
||||
ERRORS+=("$session: invalid PR URL")
|
||||
continue
|
||||
fi
|
||||
|
||||
# Check for review comments using gh
|
||||
# Get pending review comments (not resolved)
|
||||
echo " Checking for review comments..."
|
||||
|
||||
# Get PR review comments
|
||||
review_data=$(gh pr view "$pr_number" --repo "$owner/$repo" --json reviewDecision,reviews,comments 2>/dev/null)
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo " ⚠️ Failed to fetch PR data"
|
||||
ERRORS+=("$session: gh command failed")
|
||||
continue
|
||||
fi
|
||||
|
||||
# Check review decision
|
||||
review_decision=$(echo "$review_data" | jq -r '.reviewDecision // "NONE"')
|
||||
|
||||
# Count reviews that requested changes
|
||||
changes_requested=$(echo "$review_data" | jq '[.reviews[] | select(.state == "CHANGES_REQUESTED")] | length')
|
||||
|
||||
# Get review comments (these are inline code comments)
|
||||
# We'll use the gh api to get pending review comments
|
||||
pending_comments=$(gh api "repos/$owner/$repo/pulls/$pr_number/comments" --jq '[.[] | select(.in_reply_to_id == null)] | length' 2>/dev/null || echo "0")
|
||||
|
||||
# Also check for regular PR comments that might be review feedback
|
||||
pr_comments=$(gh pr view "$pr_number" --repo "$owner/$repo" --json comments --jq '.comments | length' 2>/dev/null || echo "0")
|
||||
|
||||
echo " Review decision: $review_decision"
|
||||
echo " Review comments: $pending_comments"
|
||||
echo " PR comments: $pr_comments"
|
||||
|
||||
# Determine if we need to trigger the agent
|
||||
should_trigger=false
|
||||
trigger_reason=""
|
||||
|
||||
if [ "$review_decision" = "CHANGES_REQUESTED" ]; then
|
||||
should_trigger=true
|
||||
trigger_reason="Changes requested"
|
||||
elif [ "$changes_requested" -gt 0 ]; then
|
||||
should_trigger=true
|
||||
trigger_reason="$changes_requested review(s) requested changes"
|
||||
elif [ "$pending_comments" -gt 0 ]; then
|
||||
should_trigger=true
|
||||
trigger_reason="$pending_comments review comment(s) to address"
|
||||
fi
|
||||
|
||||
if [ "$should_trigger" = true ]; then
|
||||
echo " 🔔 $trigger_reason - triggering agent..."
|
||||
|
||||
# Send prompt to the tmux session
|
||||
prompt="There are review comments on your PR ($pr_url). Please check the PR review comments using 'gh pr view $pr_number --repo $owner/$repo --comments' and 'gh api repos/$owner/$repo/pulls/$pr_number/comments', address each comment, commit and push your fixes, then reply to the comments indicating what you fixed."
|
||||
|
||||
# Send to tmux - first send Ctrl+C to interrupt if busy, then the prompt
|
||||
tmux send-keys -t "$session" C-c 2>/dev/null
|
||||
sleep 0.5
|
||||
tmux send-keys -t "$session" "$prompt" Enter
|
||||
|
||||
TRIGGERED+=("$session: $trigger_reason")
|
||||
echo " ✅ Agent triggered"
|
||||
else
|
||||
echo " ✓ No pending review comments"
|
||||
SKIPPED_NO_COMMENTS+=("$session")
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
echo "📊 SUMMARY"
|
||||
echo " Triggered: ${#TRIGGERED[@]}"
|
||||
echo " No PR: ${#SKIPPED_NO_PR[@]}"
|
||||
echo " No comments: ${#SKIPPED_NO_COMMENTS[@]}"
|
||||
echo " Errors: ${#ERRORS[@]}"
|
||||
|
||||
if [ ${#TRIGGERED[@]} -gt 0 ]; then
|
||||
echo ""
|
||||
echo "Triggered sessions:"
|
||||
for item in "${TRIGGERED[@]}"; do
|
||||
echo " • $item"
|
||||
done
|
||||
fi
|
||||
|
||||
if [ ${#ERRORS[@]} -gt 0 ]; then
|
||||
echo ""
|
||||
echo "Errors:"
|
||||
for item in "${ERRORS[@]}"; do
|
||||
echo " • $item"
|
||||
done
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
#!/bin/bash
|
||||
# Usage: ~/claude-session-status [session-pattern]
|
||||
# Shows status of Claude sessions: working, idle, or blocked
|
||||
|
||||
PATTERN="${1:-integrator-}"
|
||||
|
||||
echo "Session Status Report - $(date '+%H:%M:%S')"
|
||||
echo "==========================================="
|
||||
echo ""
|
||||
|
||||
working=()
|
||||
idle=()
|
||||
blocked=()
|
||||
not_running=()
|
||||
|
||||
for session in $(tmux list-sessions -F "#{session_name}" 2>/dev/null | grep "^$PATTERN" | sort -V); do
|
||||
# Capture last 15 lines of the pane
|
||||
output=$(tmux capture-pane -t "$session" -p -S -15 2>/dev/null)
|
||||
|
||||
if [[ -z "$output" ]]; then
|
||||
not_running+=("$session")
|
||||
continue
|
||||
fi
|
||||
|
||||
# Check for active processing indicators
|
||||
if echo "$output" | grep -qE "Thinking|Roosting|Garnishing|Levitating|Baking|Whirring|⏺|esc to interrupt"; then
|
||||
# Get what it's doing (last status line)
|
||||
status=$(echo "$output" | grep -oE "(Thinking|Roosting|Garnishing|Levitating|Baking|Whirring|Reading|Writing|Editing|Running).*" | tail -1 | cut -c1-50)
|
||||
working+=("$session|${status:-processing...}")
|
||||
# Check for prompt (idle/waiting for input)
|
||||
elif echo "$output" | grep -qE "^❯\s*$|^❯$"; then
|
||||
# Get last assistant message snippet
|
||||
last_msg=$(echo "$output" | grep -v "^❯" | grep -v "^─" | grep -v "^$" | tail -1 | cut -c1-60)
|
||||
idle+=("$session|${last_msg:-ready}")
|
||||
# Check for error/blocked states
|
||||
elif echo "$output" | grep -qiE "error|failed|permission denied|blocked|quota|rate limit"; then
|
||||
error=$(echo "$output" | grep -iE "error|failed|permission denied|blocked|quota|rate limit" | tail -1 | cut -c1-60)
|
||||
blocked+=("$session|${error:-unknown error}")
|
||||
# Check for yes/no prompts or questions
|
||||
elif echo "$output" | grep -qE "\[y/N\]|\[Y/n\]|Continue\?|Proceed\?"; then
|
||||
blocked+=("$session|waiting for confirmation")
|
||||
else
|
||||
# Unknown state - probably idle
|
||||
last_line=$(echo "$output" | grep -v "^$" | tail -1 | cut -c1-60)
|
||||
idle+=("$session|${last_line:-unknown}")
|
||||
fi
|
||||
done
|
||||
|
||||
# Print results
|
||||
if [[ ${#working[@]} -gt 0 ]]; then
|
||||
echo "🔄 WORKING (${#working[@]})"
|
||||
for item in "${working[@]}"; do
|
||||
session="${item%%|*}"
|
||||
status="${item#*|}"
|
||||
printf " %-20s %s\n" "$session" "$status"
|
||||
done
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if [[ ${#idle[@]} -gt 0 ]]; then
|
||||
echo "✅ IDLE/DONE (${#idle[@]})"
|
||||
for item in "${idle[@]}"; do
|
||||
session="${item%%|*}"
|
||||
status="${item#*|}"
|
||||
printf " %-20s %s\n" "$session" "$status"
|
||||
done
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if [[ ${#blocked[@]} -gt 0 ]]; then
|
||||
echo "⚠️ BLOCKED/ERROR (${#blocked[@]})"
|
||||
for item in "${blocked[@]}"; do
|
||||
session="${item%%|*}"
|
||||
status="${item#*|}"
|
||||
printf " %-20s %s\n" "$session" "$status"
|
||||
done
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if [[ ${#not_running[@]} -gt 0 ]]; then
|
||||
echo "💤 NOT RUNNING (${#not_running[@]})"
|
||||
printf " %s\n" "${not_running[@]}"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "==========================================="
|
||||
echo "Total: $((${#working[@]} + ${#idle[@]} + ${#blocked[@]})) active sessions"
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Claude Session Spawner
|
||||
# Opens a new iTerm2 window and starts a Claude session
|
||||
# Usage: ~/claude-spawn [integrator|splitly] [issue-url-or-number]
|
||||
|
||||
PROJECT="${1:-}"
|
||||
ISSUE="${2:-}"
|
||||
|
||||
show_help() {
|
||||
cat << 'EOF'
|
||||
CLAUDE-SPAWN(1) User Commands CLAUDE-SPAWN(1)
|
||||
|
||||
NAME
|
||||
claude-spawn - Spawn a new Claude session in a new iTerm2 window
|
||||
|
||||
SYNOPSIS
|
||||
~/claude-spawn <project> [issue]
|
||||
|
||||
DESCRIPTION
|
||||
Opens a new iTerm2 terminal window and starts a Claude Code session for the
|
||||
specified project. Useful when you want to spawn work without leaving
|
||||
your current terminal.
|
||||
|
||||
ARGUMENTS
|
||||
project Required. One of:
|
||||
integrator - Start an integrator session (Linear-based)
|
||||
splitly - Start a splitly/SafeSplit session (GitHub-based)
|
||||
i - Alias for integrator
|
||||
s - Alias for splitly
|
||||
|
||||
issue Optional. Issue to work on:
|
||||
- Linear URL for integrator (e.g., https://linear.app/team/issue/INT-123)
|
||||
- GitHub issue number or URL for splitly (e.g., 294)
|
||||
|
||||
EXAMPLES
|
||||
# Spawn a new integrator session (auto-picks checkout)
|
||||
~/claude-spawn integrator
|
||||
|
||||
# Spawn integrator session for a Linear ticket
|
||||
~/claude-spawn integrator https://linear.app/composio/issue/INT-1020
|
||||
|
||||
# Spawn splitly session for GitHub issue #294
|
||||
~/claude-spawn splitly 294
|
||||
|
||||
# Using aliases
|
||||
~/claude-spawn i INT-1020
|
||||
~/claude-spawn s 294
|
||||
|
||||
REQUIREMENTS
|
||||
- iTerm2 must be installed
|
||||
|
||||
SEE ALSO
|
||||
~/claude-integrator-session-v2
|
||||
~/claude-splitly-session-v2
|
||||
~/claude-status
|
||||
|
||||
2026-02-01 CLAUDE-SPAWN(1)
|
||||
EOF
|
||||
}
|
||||
|
||||
# Handle help
|
||||
if [[ "$PROJECT" == "help" || "$PROJECT" == "--help" || "$PROJECT" == "-h" ]]; then
|
||||
show_help
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Validate project
|
||||
case "$PROJECT" in
|
||||
integrator|i)
|
||||
SESSION_SCRIPT="$HOME/claude-integrator-session-v2"
|
||||
PROJECT_NAME="integrator"
|
||||
;;
|
||||
splitly|s|safesplit)
|
||||
SESSION_SCRIPT="$HOME/claude-splitly-session-v2"
|
||||
PROJECT_NAME="splitly"
|
||||
;;
|
||||
"")
|
||||
echo "Usage: ~/claude-spawn <integrator|splitly> [issue]"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " ~/claude-spawn integrator"
|
||||
echo " ~/claude-spawn integrator https://linear.app/composio/issue/INT-1020"
|
||||
echo " ~/claude-spawn splitly 294"
|
||||
echo ""
|
||||
echo "Run '~/claude-spawn help' for more information."
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
echo "Unknown project: $PROJECT"
|
||||
echo "Use 'integrator' (or 'i') or 'splitly' (or 's')"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Build the command to run in the new window
|
||||
if [ -n "$ISSUE" ]; then
|
||||
COMMAND="$SESSION_SCRIPT new '$ISSUE'"
|
||||
else
|
||||
COMMAND="$SESSION_SCRIPT new"
|
||||
fi
|
||||
|
||||
echo "Spawning $PROJECT_NAME session in new iTerm2 window..."
|
||||
if [ -n "$ISSUE" ]; then
|
||||
echo " Issue: $ISSUE"
|
||||
fi
|
||||
|
||||
# Open new iTerm2 window and run the command
|
||||
osascript -e "
|
||||
tell application \"iTerm2\"
|
||||
activate
|
||||
set newWindow to (create window with default profile)
|
||||
tell current session of newWindow
|
||||
write text \"$COMMAND\"
|
||||
end tell
|
||||
end tell
|
||||
"
|
||||
|
||||
echo "Done! Check your iTerm2 windows."
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
#!/bin/bash
|
||||
# Usage: ~/claude-spawn-on-branch <project> <branch> <prompt> [--open]
|
||||
# Creates session on branch - reuses existing worktree if one exists for the branch
|
||||
|
||||
set -e
|
||||
|
||||
PROJECT="$1"
|
||||
BRANCH="$2"
|
||||
PROMPT="$3"
|
||||
OPEN_TAB=false
|
||||
[ "$4" = "--open" ] && OPEN_TAB=true
|
||||
|
||||
# Normalize project name
|
||||
case "$PROJECT" in
|
||||
i|integrator)
|
||||
PROJECT="integrator"
|
||||
WORKTREE_BASE=~/.worktrees/integrator
|
||||
MAIN_REPO=~/secondary_checkouts/integrator-1
|
||||
;;
|
||||
s|splitly)
|
||||
PROJECT="splitly"
|
||||
WORKTREE_BASE=~/.worktrees/splitly
|
||||
MAIN_REPO=~/projects/SafeSplit
|
||||
;;
|
||||
*) echo "Usage: ~/claude-spawn-on-branch <project> <branch> <prompt> [--open]"; exit 1 ;;
|
||||
esac
|
||||
|
||||
[ -z "$BRANCH" ] || [ -z "$PROMPT" ] && { echo "Usage: ~/claude-spawn-on-branch <project> <branch> <prompt> [--open]"; exit 1; }
|
||||
|
||||
# Check if worktree already exists for this branch
|
||||
cd "$MAIN_REPO"
|
||||
git fetch origin -q
|
||||
|
||||
EXISTING_WORKTREE=$(git worktree list | grep "\[$BRANCH\]" | awk '{print $1}' | head -1)
|
||||
|
||||
if [ -n "$EXISTING_WORKTREE" ]; then
|
||||
# Reuse existing worktree - name session as <owner>-N
|
||||
OWNER_SESSION=$(basename "$EXISTING_WORKTREE")
|
||||
|
||||
# Find next sub-session number for this worktree
|
||||
MAX_SUB=1
|
||||
for s in $(tmux list-sessions -F "#{session_name}" 2>/dev/null | grep "^${OWNER_SESSION}-[0-9]*$" | sed "s/${OWNER_SESSION}-//"); do
|
||||
[ "$s" -gt "$MAX_SUB" ] 2>/dev/null && MAX_SUB=$s
|
||||
done
|
||||
SESSION="${OWNER_SESSION}-$((MAX_SUB + 1))"
|
||||
|
||||
tmux new-session -d -s "$SESSION" -c "$EXISTING_WORKTREE"
|
||||
tmux send-keys -t "$SESSION" "claude --dangerously-skip-permissions" Enter
|
||||
|
||||
echo "Reusing worktree: $EXISTING_WORKTREE"
|
||||
else
|
||||
# Create new session (which creates new worktree)
|
||||
output=$(~/claude-${PROJECT}-session new 2>&1)
|
||||
SESSION=$(echo "$output" | grep "^SESSION=" | cut -d= -f2)
|
||||
|
||||
[ -z "$SESSION" ] && { echo "Failed to create session"; echo "$output"; exit 1; }
|
||||
|
||||
cd "$WORKTREE_BASE/$SESSION" && git checkout "$BRANCH" -q && git pull origin "$BRANCH" -q 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Open tab if requested
|
||||
[ "$OPEN_TAB" = true ] && ~/open-iterm-tab "$SESSION"
|
||||
|
||||
# Wait for Claude prompt
|
||||
for i in {1..60}; do
|
||||
if tmux capture-pane -t "$SESSION" -p 2>/dev/null | grep -q "^❯"; then
|
||||
break
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
sleep 0.2
|
||||
tmux send-keys -t "$SESSION" "$PROMPT"
|
||||
sleep 0.1
|
||||
tmux send-keys -t "$SESSION" Enter
|
||||
|
||||
echo "✓ $SESSION on $BRANCH - prompt sent"
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
#!/bin/bash
|
||||
# Spawn a Claude session with custom context/prompt (v2 - worktree-based)
|
||||
# Delegates to session managers for worktree creation, symlinks, and MCP setup.
|
||||
# Usage: ~/claude-spawn-with-context <project> <ticket-id> <prompt-file> [--open]
|
||||
#
|
||||
# Examples:
|
||||
# ~/claude-spawn-with-context integrator INT-1020 /tmp/prompt.txt --open
|
||||
# ~/claude-spawn-with-context i INT-1020 /tmp/prompt.txt --open
|
||||
# ~/claude-spawn-with-context splitly 295 /tmp/prompt.txt
|
||||
# ~/claude-spawn-with-context s 295 /tmp/prompt.txt
|
||||
|
||||
set -e
|
||||
|
||||
# Parse args - support --open anywhere
|
||||
PROJECT=""
|
||||
TICKET=""
|
||||
PROMPT_FILE=""
|
||||
OPEN_TAB=false
|
||||
|
||||
for arg in "$@"; do
|
||||
if [ "$arg" = "--open" ]; then
|
||||
OPEN_TAB=true
|
||||
elif [ -z "$PROJECT" ]; then
|
||||
PROJECT="$arg"
|
||||
elif [ -z "$TICKET" ]; then
|
||||
TICKET="$arg"
|
||||
elif [ -z "$PROMPT_FILE" ]; then
|
||||
PROMPT_FILE="$arg"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$PROJECT" ] || [ -z "$TICKET" ] || [ -z "$PROMPT_FILE" ]; then
|
||||
echo "Usage: ~/claude-spawn-with-context <project> <ticket-id> <prompt-file> [--open]"
|
||||
echo ""
|
||||
echo " project: integrator (or i) / splitly (or s)"
|
||||
echo " ticket-id: Linear ticket (INT-XXXX) or GitHub issue number"
|
||||
echo " prompt-file: path to file containing the prompt"
|
||||
echo " --open: open in new iTerm2 tab after spawning"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " ~/claude-spawn-with-context i INT-1020 /tmp/prompt.txt --open"
|
||||
echo " ~/claude-spawn-with-context s 295 /tmp/prompt.txt"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$PROMPT_FILE" ]; then
|
||||
echo "Error: prompt file not found: $PROMPT_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Determine session manager
|
||||
case "$PROJECT" in
|
||||
integrator|i)
|
||||
PROJECT="integrator"
|
||||
SESSION_MANAGER="$HOME/claude-integrator-session"
|
||||
SESSIONS_DIR="$HOME/.integrator-sessions"
|
||||
;;
|
||||
splitly|s|safesplit)
|
||||
PROJECT="splitly"
|
||||
SESSION_MANAGER="$HOME/claude-splitly-session"
|
||||
SESSIONS_DIR="$HOME/.splitly-sessions"
|
||||
;;
|
||||
*)
|
||||
echo "Error: unknown project '$PROJECT'. Use 'integrator' (i) or 'splitly' (s)."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Creating session via $PROJECT session manager..."
|
||||
|
||||
# Delegate session creation to the session manager (handles worktree, symlinks, MCP, tmux, Claude)
|
||||
OPEN_FLAG=""
|
||||
[ "$OPEN_TAB" = true ] && OPEN_FLAG="--open"
|
||||
output=$("$SESSION_MANAGER" new "$TICKET" $OPEN_FLAG 2>&1)
|
||||
echo "$output"
|
||||
|
||||
# Extract session name from manager output
|
||||
SESSION=$(echo "$output" | grep "^SESSION=" | cut -d= -f2)
|
||||
|
||||
if [ -z "$SESSION" ]; then
|
||||
echo "Error: Could not determine session name from session manager output!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Update metadata with ticket info
|
||||
if [ "$PROJECT" = "integrator" ]; then
|
||||
grep -q "^issue=" "$SESSIONS_DIR/$SESSION" 2>/dev/null || \
|
||||
echo "issue=https://linear.app/composio/issue/$TICKET" >> "$SESSIONS_DIR/$SESSION"
|
||||
grep -q "^summary=" "$SESSIONS_DIR/$SESSION" 2>/dev/null && \
|
||||
sed -i '' "s|^summary=.*|summary=Starting work on $TICKET (custom context)|" "$SESSIONS_DIR/$SESSION" || \
|
||||
echo "summary=Starting work on $TICKET (custom context)" >> "$SESSIONS_DIR/$SESSION"
|
||||
else
|
||||
grep -q "^issue=" "$SESSIONS_DIR/$SESSION" 2>/dev/null || \
|
||||
echo "issue=https://github.com/UniverseOfThings/SafeSplit/issues/$TICKET" >> "$SESSIONS_DIR/$SESSION"
|
||||
fi
|
||||
|
||||
# Wait for Claude to start, then send the custom prompt
|
||||
sleep 2
|
||||
|
||||
# Use load-buffer + paste-buffer for multi-line prompts (handles special chars safely)
|
||||
tmux load-buffer "$PROMPT_FILE"
|
||||
tmux paste-buffer -t "$SESSION"
|
||||
sleep 0.3
|
||||
tmux send-keys -t "$SESSION" Enter
|
||||
|
||||
echo ""
|
||||
echo "Custom prompt sent to $SESSION"
|
||||
echo "Attach with: tmux attach -t $SESSION"
|
||||
echo "Done!"
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
#!/bin/bash
|
||||
# Usage: ~/claude-spawn-with-prompt <project> <prompt> [--open]
|
||||
set -e
|
||||
|
||||
PROJECT="$1"; PROMPT="$2"; OPEN_TAB=false
|
||||
[[ "$3" == "--open" ]] && OPEN_TAB=true
|
||||
|
||||
case "$PROJECT" in
|
||||
i|integrator) SCRIPT=~/claude-integrator-session ;;
|
||||
s|splitly) SCRIPT=~/claude-splitly-session ;;
|
||||
*) echo "Usage: ~/claude-spawn-with-prompt <i|s> \"prompt\" [--open]"; exit 1 ;;
|
||||
esac
|
||||
|
||||
[[ -z "$PROMPT" ]] && { echo "Usage: ~/claude-spawn-with-prompt <i|s> \"prompt\" [--open]"; exit 1; }
|
||||
|
||||
# Create session WITHOUT --open to avoid race condition
|
||||
# We'll open the tab AFTER sending the message
|
||||
SESSION=$($SCRIPT new 2>&1 | grep "^SESSION=" | cut -d= -f2)
|
||||
[[ -z "$SESSION" ]] && { echo "Failed to create session"; exit 1; }
|
||||
|
||||
# Wait for Claude prompt to appear (up to 6 seconds)
|
||||
for _ in {1..30}; do tmux capture-pane -t "$SESSION" -p 2>/dev/null | grep -q "^❯" && break; sleep 0.2; done
|
||||
|
||||
# Send the message
|
||||
~/send-to-session "$SESSION" "$PROMPT"
|
||||
|
||||
# NOW open the iTerm tab if requested (after message is sent)
|
||||
if [[ "$OPEN_TAB" == true ]]; then
|
||||
~/open-iterm-tab "$SESSION"
|
||||
fi
|
||||
|
||||
echo "✓ $SESSION"
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
#!/bin/bash
|
||||
# claude-splitly-session-v2 - Worktree-based session manager for splitly
|
||||
|
||||
set -e
|
||||
|
||||
MAIN_REPO=~/projects/SafeSplit
|
||||
WORKTREE_DIR=~/.worktrees/splitly
|
||||
SESSION_DIR=~/.splitly-sessions
|
||||
ENV_TEMPLATE=~/.env-templates/splitly
|
||||
DEFAULT_BRANCH="main"
|
||||
GITHUB_REPO="UniverseOfThings/SafeSplit"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
usage() {
|
||||
echo "Usage: ~/claude-splitly-session-v2 <command> [args]"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " new [ISSUE-NUM] [--open] Create new session (optionally with GitHub issue)"
|
||||
echo " ls List all sessions"
|
||||
echo " attach <session> Attach to session"
|
||||
echo " kill <session> Kill session and remove worktree"
|
||||
echo " cleanup Kill sessions where PR is merged or issue closed"
|
||||
echo " update Update current session metadata"
|
||||
exit 1
|
||||
}
|
||||
|
||||
get_next_session_number() {
|
||||
local max=0
|
||||
for session in $(tmux list-sessions -F "#{session_name}" 2>/dev/null | grep "^splitly-[0-9]*$" | sed 's/splitly-//'); do
|
||||
if [ "$session" -gt "$max" ] 2>/dev/null; then
|
||||
max=$session
|
||||
fi
|
||||
done
|
||||
echo $((max + 1))
|
||||
}
|
||||
|
||||
cmd_new() {
|
||||
local issue_num=""
|
||||
local open_warp=false
|
||||
|
||||
for arg in "$@"; do
|
||||
if [ "$arg" = "--open" ]; then
|
||||
open_warp=true
|
||||
elif [ -z "$issue_num" ] && [ "$arg" != "--open" ]; then
|
||||
issue_num="$arg"
|
||||
fi
|
||||
done
|
||||
|
||||
local session_num=$(get_next_session_number)
|
||||
local session_name="splitly-$session_num"
|
||||
local worktree_path="$WORKTREE_DIR/$session_name"
|
||||
|
||||
echo -e "${BLUE}Creating session: $session_name${NC}"
|
||||
|
||||
cd "$MAIN_REPO"
|
||||
git fetch origin --quiet
|
||||
|
||||
if [ -n "$issue_num" ]; then
|
||||
local branch_name="feat/issue-$issue_num"
|
||||
git worktree add -b "$branch_name" "$worktree_path" "origin/$DEFAULT_BRANCH" 2>/dev/null || \
|
||||
git worktree add "$worktree_path" "origin/$DEFAULT_BRANCH"
|
||||
else
|
||||
git worktree add "$worktree_path" "origin/$DEFAULT_BRANCH" --detach
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✓ Created worktree at $worktree_path${NC}"
|
||||
|
||||
# Symlink env files
|
||||
[ -f "$ENV_TEMPLATE/.env" ] && ln -sf "$ENV_TEMPLATE/.env" "$worktree_path/.env"
|
||||
[ -f "$ENV_TEMPLATE/.envrc" ] && ln -sf "$ENV_TEMPLATE/.envrc" "$worktree_path/.envrc" && \
|
||||
(cd "$worktree_path" && direnv allow 2>/dev/null || true)
|
||||
|
||||
# Symlink .claude directory for MCP access (remove if exists as dir)
|
||||
[ -d "$MAIN_REPO/.claude" ] && rm -rf "$worktree_path/.claude" && ln -s "$MAIN_REPO/.claude" "$worktree_path/.claude"
|
||||
|
||||
# Add Rube MCP before starting session
|
||||
(cd "$worktree_path" && claude mcp add rube --transport http https://rube.app/mcp &>/dev/null) || true
|
||||
|
||||
# Create tmux session and start Claude (unset CLAUDECODE to avoid nested session detection)
|
||||
tmux new-session -d -s "$session_name" -c "$worktree_path" -e "SPLITLY_SESSION=$session_name" -e "DIRENV_LOG_FORMAT="
|
||||
tmux send-keys -t "$session_name" "unset CLAUDECODE && claude --dangerously-skip-permissions" Enter
|
||||
|
||||
# Create metadata file
|
||||
mkdir -p "$SESSION_DIR"
|
||||
cat > "$SESSION_DIR/$session_name" << EOF
|
||||
worktree=$worktree_path
|
||||
branch=$(cd "$worktree_path" && git branch --show-current || echo "detached")
|
||||
status=starting
|
||||
EOF
|
||||
[ -n "$issue_num" ] && echo "issue=$issue_num" >> "$SESSION_DIR/$session_name"
|
||||
|
||||
echo -e "${GREEN}✓ Created tmux session: $session_name${NC}"
|
||||
echo -e "${GREEN}✓ Started Claude in session${NC}"
|
||||
echo ""
|
||||
echo "Attach with: tmux attach -t $session_name"
|
||||
echo "Or open in iTerm2: ~/open-iterm-tab $session_name"
|
||||
|
||||
# Clean output for scripting
|
||||
echo "SESSION=$session_name"
|
||||
|
||||
if [ "$open_warp" = true ]; then
|
||||
~/open-iterm-tab "$session_name"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_ls() {
|
||||
echo -e "${BLUE}Splitly Sessions:${NC}"
|
||||
echo ""
|
||||
|
||||
for session in $(tmux list-sessions -F "#{session_name}" 2>/dev/null | grep "^splitly-" | sort -V); do
|
||||
local meta_file="$SESSION_DIR/$session"
|
||||
local worktree="" branch="" status="" pr="" issue=""
|
||||
|
||||
if [ -f "$meta_file" ]; then
|
||||
worktree=$(grep "^worktree=" "$meta_file" 2>/dev/null | cut -d= -f2-)
|
||||
branch=$(grep "^branch=" "$meta_file" 2>/dev/null | cut -d= -f2-)
|
||||
status=$(grep "^status=" "$meta_file" 2>/dev/null | cut -d= -f2-)
|
||||
pr=$(grep "^pr=" "$meta_file" 2>/dev/null | cut -d= -f2-)
|
||||
issue=$(grep "^issue=" "$meta_file" 2>/dev/null | cut -d= -f2-)
|
||||
fi
|
||||
|
||||
[ -d "$worktree" ] && branch=$(cd "$worktree" && git branch --show-current 2>/dev/null || echo "$branch")
|
||||
|
||||
echo -e "${GREEN}$session${NC}"
|
||||
[ -n "$branch" ] && echo " Branch: $branch"
|
||||
[ -n "$issue" ] && echo " Issue: #$issue"
|
||||
[ -n "$status" ] && echo " Status: $status"
|
||||
[ -n "$pr" ] && echo " PR: $pr"
|
||||
echo ""
|
||||
done
|
||||
}
|
||||
|
||||
cmd_attach() {
|
||||
[ -z "$1" ] && echo "Usage: ~/claude-splitly-session-v2 attach <session>" && exit 1
|
||||
tmux attach -t "$1"
|
||||
}
|
||||
|
||||
cmd_kill() {
|
||||
local session="$1"
|
||||
[ -z "$session" ] && echo "Usage: ~/claude-splitly-session-v2 kill <session>" && exit 1
|
||||
|
||||
local meta_file="$SESSION_DIR/$session"
|
||||
local worktree=""
|
||||
[ -f "$meta_file" ] && worktree=$(grep "^worktree=" "$meta_file" 2>/dev/null | cut -d= -f2-)
|
||||
|
||||
tmux kill-session -t "$session" 2>/dev/null && echo -e "${GREEN}✓ Killed tmux session: $session${NC}"
|
||||
|
||||
if [ -n "$worktree" ] && [ -d "$worktree" ]; then
|
||||
cd "$MAIN_REPO"
|
||||
git worktree remove --force "$worktree" 2>/dev/null && echo -e "${GREEN}✓ Removed worktree: $worktree${NC}"
|
||||
fi
|
||||
|
||||
# Archive metadata instead of deleting
|
||||
if [ -f "$meta_file" ]; then
|
||||
mkdir -p "$SESSION_DIR/archive"
|
||||
mv "$meta_file" "$SESSION_DIR/archive/${session}_$(date +%Y%m%d-%H%M%S)"
|
||||
echo -e "${GREEN}✓ Archived metadata${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_cleanup() {
|
||||
echo "Checking for completed sessions..."
|
||||
|
||||
for session in $(tmux list-sessions -F "#{session_name}" 2>/dev/null | grep "^splitly-"); do
|
||||
local meta_file="$SESSION_DIR/$session"
|
||||
local pr="" issue=""
|
||||
|
||||
[ -f "$meta_file" ] && {
|
||||
pr=$(grep "^pr=" "$meta_file" 2>/dev/null | cut -d= -f2-)
|
||||
issue=$(grep "^issue=" "$meta_file" 2>/dev/null | cut -d= -f2-)
|
||||
}
|
||||
|
||||
if [ -n "$pr" ]; then
|
||||
local pr_num=$(echo "$pr" | grep -o '[0-9]*$')
|
||||
if [ -n "$pr_num" ]; then
|
||||
local state=$(gh pr view "$pr_num" --repo "$GITHUB_REPO" --json state -q '.state' 2>/dev/null)
|
||||
[ "$state" = "MERGED" ] && echo -e "${YELLOW}PR #$pr_num merged - killing $session${NC}" && cmd_kill "$session" && continue
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$issue" ]; then
|
||||
local issue_state=$(gh issue view "$issue" --repo "$GITHUB_REPO" --json state -q '.state' 2>/dev/null)
|
||||
[ "$issue_state" = "CLOSED" ] && echo -e "${YELLOW}Issue #$issue closed - killing $session${NC}" && cmd_kill "$session"
|
||||
fi
|
||||
done
|
||||
echo "Cleanup complete."
|
||||
}
|
||||
|
||||
cmd_update() {
|
||||
local session="${SPLITLY_SESSION:-}"
|
||||
[ -z "$session" ] && echo "Error: Not in a splitly session" && exit 1
|
||||
|
||||
local meta_file="$SESSION_DIR/$session"
|
||||
local worktree=$(grep "^worktree=" "$meta_file" 2>/dev/null | cut -d= -f2-)
|
||||
[ -z "$worktree" ] || [ ! -d "$worktree" ] && worktree="$PWD"
|
||||
|
||||
local branch=$(cd "$worktree" && git branch --show-current 2>/dev/null)
|
||||
[ -n "$branch" ] && sed -i '' "s|^branch=.*|branch=$branch|" "$meta_file" 2>/dev/null
|
||||
|
||||
echo "Updated metadata for $session"
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
new) shift; cmd_new "$@" ;;
|
||||
ls) cmd_ls ;;
|
||||
attach) cmd_attach "$2" ;;
|
||||
kill) cmd_kill "$2" ;;
|
||||
cleanup) cmd_cleanup ;;
|
||||
update) cmd_update ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
|
|
@ -0,0 +1,212 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Claude Unified Status
|
||||
# Shows all sessions across all projects with summary stats
|
||||
|
||||
# Helper function to get Claude session info directly from Claude's data
|
||||
get_claude_info() {
|
||||
local TMUX_SESSION="$1"
|
||||
|
||||
# Get the TTY for this tmux session
|
||||
local TTY=$(tmux list-panes -t "$TMUX_SESSION" -F '#{pane_tty}' 2>/dev/null | head -1)
|
||||
[ -z "$TTY" ] && return 1
|
||||
|
||||
# Find the Claude PID running on that TTY
|
||||
local TTY_SHORT="${TTY#/dev/}"
|
||||
local CLAUDE_PID=$(ps -eo pid,tty,comm 2>/dev/null | awk -v tty="$TTY_SHORT" '$2 == tty && $3 == "claude" {print $1; exit}')
|
||||
[ -z "$CLAUDE_PID" ] && return 1
|
||||
|
||||
# Get the working directory of that process
|
||||
local CWD=$(lsof -p "$CLAUDE_PID" 2>/dev/null | awk '$4 == "cwd" {print $NF}')
|
||||
[ -z "$CWD" ] && return 1
|
||||
|
||||
# Get current git branch from that directory
|
||||
local BRANCH=$(git -C "$CWD" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
|
||||
|
||||
# Convert to Claude's project path format and find active session
|
||||
local PROJECT_PATH=$(echo "$CWD" | tr '/_' '--')
|
||||
local PROJECT_DIR="$HOME/.claude/projects/$PROJECT_PATH"
|
||||
|
||||
local SESSION_ID=""
|
||||
local SUMMARY=""
|
||||
|
||||
if [ -d "$PROJECT_DIR" ]; then
|
||||
# Find the most recently modified session file (exclude agent- files)
|
||||
local ACTIVE_SESSION_FILE=$(ls -t "$PROJECT_DIR"/*.jsonl 2>/dev/null | grep -v "agent-" | head -1)
|
||||
if [ -n "$ACTIVE_SESSION_FILE" ]; then
|
||||
SESSION_ID=$(basename "$ACTIVE_SESSION_FILE" .jsonl)
|
||||
# Extract the most recent summary from the session file
|
||||
SUMMARY=$(grep '"type":"summary"' "$ACTIVE_SESSION_FILE" 2>/dev/null | tail -1 | jq -r '.summary // empty' 2>/dev/null)
|
||||
fi
|
||||
fi
|
||||
|
||||
# Output as key=value pairs (properly quoted for eval)
|
||||
printf 'claude_branch=%q\n' "$BRANCH"
|
||||
printf 'claude_session_id=%q\n' "$SESSION_ID"
|
||||
printf 'claude_summary=%q\n' "$SUMMARY"
|
||||
}
|
||||
|
||||
echo "╔══════════════════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ CLAUDE ORCHESTRATOR STATUS ║"
|
||||
echo "╚══════════════════════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# Count sessions
|
||||
INTEGRATOR_COUNT=$(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep -c "^integrator-" || echo 0)
|
||||
SPLITLY_COUNT=$(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep -c "^splitly-" || echo 0)
|
||||
TOTAL=$((INTEGRATOR_COUNT + SPLITLY_COUNT))
|
||||
|
||||
# Count empty checkouts
|
||||
INTEGRATOR_EMPTY=0
|
||||
for i in {0..9}; do
|
||||
count=$(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep -c "^integrator-${i}-" 2>/dev/null || true)
|
||||
count=${count:-0}
|
||||
count=$(echo "$count" | tr -d '[:space:]')
|
||||
[ "$count" -eq 0 ] 2>/dev/null && ((INTEGRATOR_EMPTY++))
|
||||
done
|
||||
|
||||
SPLITLY_EMPTY=0
|
||||
for i in {0..12}; do
|
||||
count=$(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep -c "^splitly-${i}-" 2>/dev/null || true)
|
||||
count=${count:-0}
|
||||
count=$(echo "$count" | tr -d '[:space:]')
|
||||
[ "$count" -eq 0 ] 2>/dev/null && ((SPLITLY_EMPTY++))
|
||||
done
|
||||
|
||||
echo "📊 SUMMARY: $TOTAL active sessions"
|
||||
echo " Integrator: $INTEGRATOR_COUNT sessions, $INTEGRATOR_EMPTY empty checkouts"
|
||||
echo " Splitly: $SPLITLY_COUNT sessions, $SPLITLY_EMPTY empty checkouts"
|
||||
echo ""
|
||||
|
||||
# Show integrator sessions
|
||||
echo "┌──────────────────────────────────────────────────────────────────────────────┐"
|
||||
echo "│ INTEGRATOR │"
|
||||
echo "└──────────────────────────────────────────────────────────────────────────────┘"
|
||||
|
||||
if [ "$INTEGRATOR_COUNT" -eq 0 ]; then
|
||||
echo " (no active sessions)"
|
||||
else
|
||||
# Get session details from the session data files
|
||||
for session in $(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep "^integrator-" | sort); do
|
||||
activity=$(tmux display-message -t "$session" -p '#{session_activity}' 2>/dev/null)
|
||||
now=$(date +%s)
|
||||
if [ -n "$activity" ]; then
|
||||
diff=$((now - activity))
|
||||
if [ $diff -lt 60 ]; then
|
||||
last_active="${diff}s ago"
|
||||
elif [ $diff -lt 3600 ]; then
|
||||
last_active="$((diff / 60))m ago"
|
||||
elif [ $diff -lt 86400 ]; then
|
||||
last_active="$((diff / 3600))h ago"
|
||||
else
|
||||
last_active="$((diff / 86400))d ago"
|
||||
fi
|
||||
else
|
||||
last_active="-"
|
||||
fi
|
||||
|
||||
data_file="$HOME/.integrator-sessions/$session"
|
||||
meta_branch=$(grep "^branch=" "$data_file" 2>/dev/null | cut -d'=' -f2-)
|
||||
meta_status=$(grep "^status=" "$data_file" 2>/dev/null | cut -d'=' -f2-)
|
||||
meta_summary=$(grep "^summary=" "$data_file" 2>/dev/null | cut -d'=' -f2- | cut -c1-60)
|
||||
|
||||
# Get live Claude session info
|
||||
eval "$(get_claude_info "$session" 2>/dev/null)"
|
||||
|
||||
echo " 📍 $session ($last_active)"
|
||||
|
||||
# Show branch - prefer live, show if different from metadata
|
||||
if [ -n "$claude_branch" ]; then
|
||||
if [ -n "$meta_branch" ] && [ "$claude_branch" != "$meta_branch" ]; then
|
||||
echo " Branch: $claude_branch (meta: $meta_branch)"
|
||||
else
|
||||
echo " Branch: $claude_branch"
|
||||
fi
|
||||
elif [ -n "$meta_branch" ]; then
|
||||
echo " Branch: $meta_branch (stale)"
|
||||
fi
|
||||
|
||||
# Show summary - prefer Claude's auto-generated, fall back to metadata
|
||||
if [ -n "$claude_summary" ]; then
|
||||
echo " Claude: ${claude_summary:0:65}"
|
||||
fi
|
||||
if [ -n "$meta_summary" ]; then
|
||||
echo " Status: ${meta_summary}..."
|
||||
fi
|
||||
|
||||
# Show session ID if available (useful for resuming)
|
||||
[ -n "$claude_session_id" ] && echo " Session: ${claude_session_id:0:8}..."
|
||||
done
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Show splitly sessions
|
||||
echo "┌──────────────────────────────────────────────────────────────────────────────┐"
|
||||
echo "│ SPLITLY / SAFESPLIT │"
|
||||
echo "└──────────────────────────────────────────────────────────────────────────────┘"
|
||||
|
||||
if [ "$SPLITLY_COUNT" -eq 0 ]; then
|
||||
echo " (no active sessions)"
|
||||
else
|
||||
for session in $(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep "^splitly-" | sort); do
|
||||
activity=$(tmux display-message -t "$session" -p '#{session_activity}' 2>/dev/null)
|
||||
now=$(date +%s)
|
||||
if [ -n "$activity" ]; then
|
||||
diff=$((now - activity))
|
||||
if [ $diff -lt 60 ]; then
|
||||
last_active="${diff}s ago"
|
||||
elif [ $diff -lt 3600 ]; then
|
||||
last_active="$((diff / 60))m ago"
|
||||
elif [ $diff -lt 86400 ]; then
|
||||
last_active="$((diff / 3600))h ago"
|
||||
else
|
||||
last_active="$((diff / 86400))d ago"
|
||||
fi
|
||||
else
|
||||
last_active="-"
|
||||
fi
|
||||
|
||||
data_file="$HOME/.splitly-sessions/$session"
|
||||
meta_branch=$(grep "^branch=" "$data_file" 2>/dev/null | cut -d'=' -f2-)
|
||||
meta_status=$(grep "^status=" "$data_file" 2>/dev/null | cut -d'=' -f2-)
|
||||
meta_summary=$(grep "^summary=" "$data_file" 2>/dev/null | cut -d'=' -f2- | cut -c1-60)
|
||||
|
||||
# Get live Claude session info
|
||||
eval "$(get_claude_info "$session" 2>/dev/null)"
|
||||
|
||||
echo " 📍 $session ($last_active)"
|
||||
|
||||
# Show branch - prefer live, show if different from metadata
|
||||
if [ -n "$claude_branch" ]; then
|
||||
if [ -n "$meta_branch" ] && [ "$claude_branch" != "$meta_branch" ]; then
|
||||
echo " Branch: $claude_branch (meta: $meta_branch)"
|
||||
else
|
||||
echo " Branch: $claude_branch"
|
||||
fi
|
||||
elif [ -n "$meta_branch" ]; then
|
||||
echo " Branch: $meta_branch (stale)"
|
||||
fi
|
||||
|
||||
# Show summary - prefer Claude's auto-generated, fall back to metadata
|
||||
if [ -n "$claude_summary" ]; then
|
||||
echo " Claude: ${claude_summary:0:65}"
|
||||
fi
|
||||
if [ -n "$meta_summary" ]; then
|
||||
echo " Status: ${meta_summary}..."
|
||||
fi
|
||||
|
||||
# Show session ID if available (useful for resuming)
|
||||
[ -n "$claude_session_id" ] && echo " Session: ${claude_session_id:0:8}..."
|
||||
done
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "┌──────────────────────────────────────────────────────────────────────────────┐"
|
||||
echo "│ QUICK ACTIONS │"
|
||||
echo "└──────────────────────────────────────────────────────────────────────────────┘"
|
||||
echo " ~/claude-spawn i [linear-url] - New integrator session in new iTerm2 tab"
|
||||
echo " ~/claude-spawn s [issue-num] - New splitly session in new iTerm2 tab"
|
||||
echo " ~/claude-integrator-session cleanup"
|
||||
echo " ~/claude-splitly-session cleanup"
|
||||
echo ""
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
#!/bin/bash
|
||||
# Get Claude session info for a tmux session
|
||||
# Extracts session ID, branch, and summary directly from Claude's data
|
||||
|
||||
TMUX_SESSION="$1"
|
||||
if [ -z "$TMUX_SESSION" ]; then
|
||||
echo "Usage: $0 <tmux-session-name>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 1. Get the TTY for this tmux session
|
||||
TTY=$(tmux list-panes -t "$TMUX_SESSION" -F '#{pane_tty}' 2>/dev/null | head -1)
|
||||
if [ -z "$TTY" ]; then
|
||||
echo "error: tmux session '$TMUX_SESSION' not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2. Find the Claude PID running on that TTY
|
||||
TTY_SHORT="${TTY#/dev/}"
|
||||
CLAUDE_PID=$(ps -eo pid,tty,comm 2>/dev/null | awk -v tty="$TTY_SHORT" '$2 == tty && $3 == "claude" {print $1; exit}')
|
||||
if [ -z "$CLAUDE_PID" ]; then
|
||||
echo "error: no claude process on $TTY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 3. Get the working directory of that process
|
||||
CWD=$(lsof -p "$CLAUDE_PID" 2>/dev/null | awk '$4 == "cwd" {print $NF}')
|
||||
|
||||
# 4. Get current git branch from that directory
|
||||
BRANCH=$(git -C "$CWD" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
|
||||
|
||||
# 5. Convert to Claude's project path format and find active session
|
||||
PROJECT_PATH=$(echo "$CWD" | tr '/.' '--')
|
||||
PROJECT_DIR="$HOME/.claude/projects/$PROJECT_PATH"
|
||||
|
||||
if [ ! -d "$PROJECT_DIR" ]; then
|
||||
echo "session_id: unknown"
|
||||
echo "branch: $BRANCH"
|
||||
echo "summary: (no claude project found)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 6. Find the most recently modified session file
|
||||
ACTIVE_SESSION_FILE=$(ls -t "$PROJECT_DIR"/*.jsonl 2>/dev/null | grep -v "agent-" | head -1)
|
||||
if [ -z "$ACTIVE_SESSION_FILE" ]; then
|
||||
echo "session_id: unknown"
|
||||
echo "branch: $BRANCH"
|
||||
echo "summary: (no session file)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
SESSION_ID=$(basename "$ACTIVE_SESSION_FILE" .jsonl)
|
||||
|
||||
# 7. Extract the most recent summary from the session file
|
||||
SUMMARY=$(grep '"type":"summary"' "$ACTIVE_SESSION_FILE" 2>/dev/null | tail -1 | jq -r '.summary // empty')
|
||||
if [ -z "$SUMMARY" ]; then
|
||||
# Fall back to first user message
|
||||
SUMMARY=$(grep '"type":"user"' "$ACTIVE_SESSION_FILE" 2>/dev/null | head -1 | jq -r '.message.content' 2>/dev/null | head -c 80)
|
||||
fi
|
||||
|
||||
echo "session_id: $SESSION_ID"
|
||||
echo "branch: $BRANCH"
|
||||
echo "summary: ${SUMMARY:-"(no summary)"}"
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
#!/bin/bash
|
||||
# Usage: ~/notify-session [message]
|
||||
#
|
||||
# Sends an iTerm2 notification that navigates to THIS tab when clicked.
|
||||
# Works when called from Claude Code's Bash tool (which captures stdout).
|
||||
#
|
||||
# Examples:
|
||||
# ~/notify-session "PR comments addressed, ready for review"
|
||||
# ~/notify-session "Build complete"
|
||||
# ~/notify-session # defaults to "Task complete"
|
||||
#
|
||||
# Writes directly to the tmux pane's TTY so the escape sequence reaches
|
||||
# iTerm2 even when stdout is captured by Claude's Bash tool.
|
||||
|
||||
MESSAGE="${1:-Task complete}"
|
||||
SESSION_NAME=$(tmux display-message -p '#S' 2>/dev/null || echo "Claude")
|
||||
|
||||
# Get the TTY of the current tmux pane
|
||||
PANE_TTY=$(tmux display-message -p '#{pane_tty}' 2>/dev/null)
|
||||
|
||||
if [ -n "$PANE_TTY" ]; then
|
||||
# Write directly to the pane's TTY with tmux passthrough
|
||||
printf '\033Ptmux;\033\033]9;%s\007\033\\' "$MESSAGE" > "$PANE_TTY"
|
||||
elif [ -n "$TMUX" ]; then
|
||||
# Fallback: we're in tmux but can't find pane TTY
|
||||
printf '\033Ptmux;\033\033]9;%s\007\033\\' "$MESSAGE"
|
||||
else
|
||||
# Outside tmux: send OSC 9 directly
|
||||
printf '\033]9;%s\007' "$MESSAGE"
|
||||
fi
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
#!/bin/bash
|
||||
# Usage: ~/open-iterm-tab <session-name>
|
||||
# Opens a tmux session in iTerm2 - reuses existing tab if found, otherwise opens new
|
||||
# Sets both the profile name (for detection) and tab title (for visual display)
|
||||
|
||||
SESSION="$1"
|
||||
if [ -z "$SESSION" ]; then
|
||||
echo "Usage: ~/open-iterm-tab <session-name>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify tmux session exists
|
||||
if ! tmux has-session -t "$SESSION" 2>/dev/null; then
|
||||
echo "Error: tmux session '$SESSION' does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if there's already an iTerm2 tab with this session's profile name
|
||||
result=$(osascript -e "
|
||||
tell application \"iTerm2\"
|
||||
activate
|
||||
repeat with aWindow in windows
|
||||
repeat with aTab in tabs of aWindow
|
||||
repeat with aSession in sessions of aTab
|
||||
try
|
||||
if profile name of aSession is equal to \"$SESSION\" then
|
||||
select aWindow
|
||||
select aTab
|
||||
return \"EXISTING\"
|
||||
end if
|
||||
end try
|
||||
end repeat
|
||||
end repeat
|
||||
end repeat
|
||||
return \"NOT_FOUND\"
|
||||
end tell
|
||||
" 2>&1)
|
||||
|
||||
if [ "$result" = "EXISTING" ]; then
|
||||
echo "✓ Switched to existing tab for $SESSION"
|
||||
else
|
||||
echo "Opening new tab for $SESSION..."
|
||||
osascript -e "
|
||||
tell application \"iTerm2\"
|
||||
activate
|
||||
tell current window
|
||||
create tab with default profile
|
||||
tell current session
|
||||
-- Set profile name for programmatic detection
|
||||
set name to \"$SESSION\"
|
||||
-- Set visible tab title via escape code, then attach to tmux
|
||||
write text \"printf '\\\\033]0;$SESSION\\\\007' && tmux attach -t $SESSION\"
|
||||
end tell
|
||||
end tell
|
||||
end tell
|
||||
" 2>&1
|
||||
echo "✓ Opened $SESSION in new iTerm2 tab"
|
||||
fi
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
#!/bin/bash
|
||||
# Switch to the iTerm tab running a given tmux session
|
||||
# Usage: ~/open-tmux-session <session-name>
|
||||
|
||||
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:$PATH"
|
||||
|
||||
SESSION="$1"
|
||||
[ -z "$SESSION" ] && echo "Usage: open-tmux-session <session-name>" && exit 1
|
||||
|
||||
# Find the tty attached to this tmux session
|
||||
TARGET_TTY=$(tmux list-clients -t "$SESSION" -F "#{client_tty}" 2>/dev/null | head -1)
|
||||
|
||||
if [ -n "$TARGET_TTY" ]; then
|
||||
# Find and switch to the iTerm tab with this tty
|
||||
osascript << APPLESCRIPT
|
||||
tell application "iTerm2"
|
||||
activate
|
||||
set foundIt to false
|
||||
repeat with w in windows
|
||||
tell w
|
||||
set tabCount to count of tabs
|
||||
repeat with i from 1 to tabCount
|
||||
set theTab to item i of tabs
|
||||
repeat with theSession in sessions of theTab
|
||||
set sessionTTY to tty of theSession
|
||||
if sessionTTY is "$TARGET_TTY" then
|
||||
set index of w to 1
|
||||
select theTab
|
||||
set foundIt to true
|
||||
exit repeat
|
||||
end if
|
||||
end repeat
|
||||
if foundIt then exit repeat
|
||||
end repeat
|
||||
end tell
|
||||
if foundIt then exit repeat
|
||||
end repeat
|
||||
end tell
|
||||
APPLESCRIPT
|
||||
else
|
||||
# No client attached — create a new iTerm tab and attach
|
||||
osascript << APPLESCRIPT
|
||||
tell application "iTerm2"
|
||||
activate
|
||||
tell current window
|
||||
set newTab to (create tab with default profile)
|
||||
tell current session of newTab
|
||||
write text "tmux attach -t $SESSION"
|
||||
end tell
|
||||
end tell
|
||||
end tell
|
||||
APPLESCRIPT
|
||||
fi
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
#!/bin/bash
|
||||
# Usage: ~/send-to-session <session-name> <message>
|
||||
# or: ~/send-to-session <session-name> -f <file>
|
||||
# Sends a message to a Claude agent in a tmux session and submits it.
|
||||
#
|
||||
# If the session is busy (Claude is thinking), waits for it to become idle
|
||||
# before sending. Retries Enter if the message wasn't picked up.
|
||||
|
||||
SESSION="$1"
|
||||
shift
|
||||
|
||||
if [ -z "$SESSION" ]; then
|
||||
echo "Usage: ~/send-to-session <session-name> <message>"
|
||||
echo " or: ~/send-to-session <session-name> -f <file>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify session exists
|
||||
if ! tmux has-session -t "$SESSION" 2>/dev/null; then
|
||||
echo "Error: Session '$SESSION' does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
is_busy() {
|
||||
# Deterministic: check if the last non-empty line has a prompt (❯) or shell ($) — means idle
|
||||
local last_line=$(tmux capture-pane -t "$SESSION" -p -S -5 2>/dev/null | grep -v '^$' | tail -1)
|
||||
# Idle indicators: prompt char or permission mode line
|
||||
if echo "$last_line" | grep -qE '❯|^\$|⏵⏵|bypass permissions'; then
|
||||
return 1 # not busy
|
||||
fi
|
||||
# Active indicators: "esc to interrupt" only appears during live processing
|
||||
local output=$(tmux capture-pane -t "$SESSION" -p -S -3 2>/dev/null)
|
||||
echo "$output" | grep -qF "esc to interrupt"
|
||||
}
|
||||
|
||||
is_processing() {
|
||||
local output=$(tmux capture-pane -t "$SESSION" -p -S -10 2>/dev/null)
|
||||
echo "$output" | grep -qE "Thinking|Roosting|Garnishing|Levitating|Baking|Whirring|Musing|Cooked|Churned|Brewed|Worked|Cogitated|Frolicking|Prestidigitating|Whisking|Mustering|Percolating|Running…|esc to interrupt|⏺"
|
||||
}
|
||||
|
||||
has_queued_message() {
|
||||
local output=$(tmux capture-pane -t "$SESSION" -p -S -5 2>/dev/null)
|
||||
echo "$output" | grep -qE "Press up to edit queued messages"
|
||||
}
|
||||
|
||||
# Wait for session to be idle (up to 10 minutes)
|
||||
WAIT_COUNT=0
|
||||
MAX_WAIT=120 # 120 * 5s = 10 minutes
|
||||
while is_busy; do
|
||||
if [ $WAIT_COUNT -eq 0 ]; then
|
||||
echo "⏳ Waiting for $SESSION to finish current task..."
|
||||
fi
|
||||
WAIT_COUNT=$((WAIT_COUNT + 1))
|
||||
if [ $WAIT_COUNT -ge $MAX_WAIT ]; then
|
||||
echo "⚠ Timeout waiting for $SESSION to become idle. Sending anyway."
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
|
||||
# Clear any partial input
|
||||
tmux send-keys -t "$SESSION" C-u
|
||||
sleep 0.2
|
||||
|
||||
# Send the message
|
||||
send_message() {
|
||||
if [ "$1" = "-f" ]; then
|
||||
local file="$2"
|
||||
if [ ! -f "$file" ]; then
|
||||
echo "Error: File not found: $file"
|
||||
exit 1
|
||||
fi
|
||||
tmux load-buffer "$file"
|
||||
tmux paste-buffer -t "$SESSION"
|
||||
else
|
||||
local msg="$*"
|
||||
if echo "$msg" | grep -q $'\n' || [ ${#msg} -gt 200 ]; then
|
||||
local tmpfile=$(mktemp /tmp/send-to-session-XXXXXX.txt)
|
||||
printf '%s' "$msg" > "$tmpfile"
|
||||
tmux load-buffer "$tmpfile"
|
||||
tmux paste-buffer -t "$SESSION"
|
||||
rm -f "$tmpfile"
|
||||
else
|
||||
tmux send-keys -t "$SESSION" "$msg"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
send_message "$@"
|
||||
sleep 0.3
|
||||
tmux send-keys -t "$SESSION" Enter
|
||||
|
||||
# Verify it's processing, retry Enter if needed
|
||||
for attempt in 1 2 3; do
|
||||
sleep 2
|
||||
if is_processing; then
|
||||
echo "✓ Message sent and processing"
|
||||
exit 0
|
||||
fi
|
||||
# Check if message is sitting in queue (Claude was still wrapping up)
|
||||
if has_queued_message; then
|
||||
echo "✓ Message queued (session is finishing previous task)"
|
||||
exit 0
|
||||
fi
|
||||
# Message might be in input buffer — retry Enter
|
||||
if [ $attempt -lt 3 ]; then
|
||||
tmux send-keys -t "$SESSION" Enter
|
||||
sleep 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "⚠ Message sent - could not confirm it was received"
|
||||
Loading…
Reference in New Issue