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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@
"plugins": [
{
"name": "headroom",
"source": {
"source": "github",
"repo": "riicodespretty/headroom-claude-plugin"
}
"source": "./"
}
]
}
63 changes: 56 additions & 7 deletions dev-install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
set -euo pipefail

REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CACHE_DIR="$HOME/.claude/plugins/cache/local/headroom/1.0.0"
MARKETPLACE_NAME="riicodespretty"
PLUGIN_NAME="headroom"
PLUGIN_VERSION="1.0.0"
CACHE_DIR="$HOME/.claude/plugins/cache/$MARKETPLACE_NAME/$PLUGIN_NAME/$PLUGIN_VERSION"
MARKETPLACE_DIR="$HOME/.claude/plugins/marketplaces/$MARKETPLACE_NAME"
KNOWN_MARKETPLACES="$HOME/.claude/plugins/known_marketplaces.json"

echo "Installing headroom plugin (dev mode)..."

Expand All @@ -15,7 +20,7 @@ if [ ! -x "$HOME/.venv/bin/headroom" ]; then
fi
echo " ✓ headroom binary found at ~/.venv/bin/headroom"

# 1. Create symlink into plugin cache
# 1. Symlink repo into plugin cache
mkdir -p "$(dirname "$CACHE_DIR")"
if [ -L "$CACHE_DIR" ]; then
rm "$CACHE_DIR"
Expand All @@ -26,24 +31,68 @@ fi
ln -s "$REPO_DIR" "$CACHE_DIR"
echo " ✓ Symlinked $REPO_DIR → $CACHE_DIR"

# 2. Register in enabledPlugins using Python for safe JSON editing
python3 - <<'PYEOF'
# 2. Symlink repo as the marketplace directory so Claude Code can resolve it
mkdir -p "$(dirname "$MARKETPLACE_DIR")"
if [ -L "$MARKETPLACE_DIR" ]; then
rm "$MARKETPLACE_DIR"
elif [ -e "$MARKETPLACE_DIR" ]; then
echo " ERROR: $MARKETPLACE_DIR exists and is not a symlink — remove it manually first." >&2
exit 1
fi
ln -s "$REPO_DIR" "$MARKETPLACE_DIR"
echo " ✓ Symlinked $REPO_DIR → $MARKETPLACE_DIR"

# 3. Register in known_marketplaces.json so Claude Code recognises the marketplace
python3 - "$REPO_DIR" "$MARKETPLACE_NAME" "$MARKETPLACE_DIR" <<'PYEOF'
import json, os, sys
from pathlib import Path
from datetime import datetime, timezone

repo_dir, marketplace_name, marketplace_dir = sys.argv[1], sys.argv[2], sys.argv[3]

known_path = Path.home() / ".claude" / "plugins" / "known_marketplaces.json"
known_path.parent.mkdir(parents=True, exist_ok=True)

data = {}
if known_path.exists():
try:
data = json.loads(known_path.read_text())
except (json.JSONDecodeError, OSError):
pass

data[marketplace_name] = {
"source": {"source": "directory", "path": repo_dir},
"installLocation": marketplace_dir,
"lastUpdated": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z"),
"autoUpdate": False,
}

tmp = known_path.with_suffix(".json.tmp")
tmp.write_text(json.dumps(data, indent=2))
os.replace(tmp, known_path)
print(f" ✓ Registered {marketplace_name} in known_marketplaces.json")
PYEOF

# 4. Enable the plugin and clear any stale ANTHROPIC_BASE_URL in settings.json
python3 - "$MARKETPLACE_NAME" "$PLUGIN_NAME" <<'PYEOF'
import json, os, sys
from pathlib import Path

marketplace_name, plugin_name = sys.argv[1], sys.argv[2]
plugin_key = f"{plugin_name}@{marketplace_name}"

settings_path = Path.home() / ".claude" / "settings.json"
if not settings_path.exists():
print(f" ERROR: {settings_path} not found. Start Claude Code at least once first.", file=sys.stderr)
sys.exit(1)

settings = json.loads(settings_path.read_text())
settings.setdefault("enabledPlugins", {})
settings["enabledPlugins"]["headroom@local"] = True
settings.setdefault("enabledPlugins", {})[plugin_key] = True

tmp = settings_path.with_suffix(".json.tmp")
tmp.write_text(json.dumps(settings, indent=2))
os.replace(tmp, settings_path)
print(" ✓ Registered headroom@local in enabledPlugins")
print(f" ✓ Enabled {plugin_key} in enabledPlugins")
PYEOF

echo ""
Expand Down
88 changes: 73 additions & 15 deletions dev-uninstall.sh
Original file line number Diff line number Diff line change
@@ -1,42 +1,74 @@
#!/usr/bin/env bash
set -euo pipefail

CACHE_DIR="$HOME/.claude/plugins/cache/local/headroom/1.0.0"
MARKETPLACE_NAME="riicodespretty"
PLUGIN_NAME="headroom"
PLUGIN_VERSION="1.0.0"
CACHE_DIR="$HOME/.claude/plugins/cache/$MARKETPLACE_NAME/$PLUGIN_NAME/$PLUGIN_VERSION"
MARKETPLACE_DIR="$HOME/.claude/plugins/marketplaces/$MARKETPLACE_NAME"

echo "Uninstalling headroom plugin (dev mode)..."

# 1. Remove symlink from plugin cache
if [ -L "$CACHE_DIR" ]; then
rm "$CACHE_DIR"
echo " ✓ Removed symlink $CACHE_DIR"
echo " ✓ Removed cache symlink $CACHE_DIR"
elif [ -e "$CACHE_DIR" ]; then
echo " ⚠ $CACHE_DIR exists but is not a symlink — skipping (manual removal needed)"
else
echo " ✓ Symlink already absent"
echo " ✓ Cache symlink already absent"
fi

# 2. Remove from enabledPlugins and clear ANTHROPIC_BASE_URL using Python
python3 - <<'PYEOF'
# 2. Remove marketplace symlink
if [ -L "$MARKETPLACE_DIR" ]; then
rm "$MARKETPLACE_DIR"
echo " ✓ Removed marketplace symlink $MARKETPLACE_DIR"
elif [ -e "$MARKETPLACE_DIR" ]; then
echo " ⚠ $MARKETPLACE_DIR exists but is not a symlink — skipping (manual removal needed)"
else
echo " ✓ Marketplace symlink already absent"
fi

# 3. Remove from known_marketplaces.json, enabledPlugins, and clear ANTHROPIC_BASE_URL
python3 - "$MARKETPLACE_NAME" "$PLUGIN_NAME" <<'PYEOF'
import json, os, sys
from pathlib import Path

marketplace_name, plugin_name = sys.argv[1], sys.argv[2]
plugin_key = f"{plugin_name}@{marketplace_name}"

# known_marketplaces.json
known_path = Path.home() / ".claude" / "plugins" / "known_marketplaces.json"
if known_path.exists():
try:
data = json.loads(known_path.read_text())
if marketplace_name in data:
del data[marketplace_name]
tmp = known_path.with_suffix(".json.tmp")
tmp.write_text(json.dumps(data, indent=2))
os.replace(tmp, known_path)
print(f" ✓ Removed {marketplace_name} from known_marketplaces.json")
else:
print(f" ✓ {marketplace_name} not in known_marketplaces.json — nothing to remove")
except (json.JSONDecodeError, OSError) as e:
print(f" ⚠ Could not update known_marketplaces.json: {e}", file=sys.stderr)

# settings.json — enabledPlugins and ANTHROPIC_BASE_URL
settings_path = Path.home() / ".claude" / "settings.json"
if not settings_path.exists():
print(f" ⚠ {settings_path} not found — nothing to update", file=sys.stderr)
print(f" ⚠ {settings_path} not found — skipping", file=sys.stderr)
sys.exit(0)

settings = json.loads(settings_path.read_text())

# Remove from enabledPlugins
plugins = settings.get("enabledPlugins", {})
if "headroom@local" in plugins:
del plugins["headroom@local"]
if plugin_key in plugins:
del plugins[plugin_key]
settings["enabledPlugins"] = plugins
print(" ✓ Removed headroom@local from enabledPlugins")
print(f" ✓ Removed {plugin_key} from enabledPlugins")
else:
print(" ✓ headroom@local was not registered — nothing to remove")
print(f" ✓ {plugin_key} was not registered — nothing to remove")

# Clear ANTHROPIC_BASE_URL from env
env = settings.get("env", {})
if "ANTHROPIC_BASE_URL" in env:
del env["ANTHROPIC_BASE_URL"]
Expand All @@ -45,14 +77,40 @@ if "ANTHROPIC_BASE_URL" in env:
else:
print(" ✓ ANTHROPIC_BASE_URL was not set — nothing to clear")

# Write back atomically
tmp = settings_path.with_suffix(".json.tmp")
tmp.write_text(json.dumps(settings, indent=2))
os.replace(tmp, settings_path)
PYEOF

# 4. Remove mcpServers.headroom from ~/.claude.json (reverting the command patch)
python3 - <<'PYEOF'
import json, os, sys
from pathlib import Path

claude_json = Path.home() / ".claude.json"
if not claude_json.exists():
print(" ✓ ~/.claude.json not found — nothing to revert")
sys.exit(0)

try:
data = json.loads(claude_json.read_text())
except (json.JSONDecodeError, OSError) as e:
print(f" ⚠ Could not read ~/.claude.json: {e} — skipping", file=sys.stderr)
sys.exit(0)

mcp_servers = data.get("mcpServers", {})
if "headroom" in mcp_servers:
del mcp_servers["headroom"]
data["mcpServers"] = mcp_servers
tmp = claude_json.with_suffix(".json.tmp")
tmp.write_text(json.dumps(data, indent=2))
os.replace(tmp, claude_json)
print(" ✓ Removed mcpServers.headroom from ~/.claude.json")
else:
print(" ✓ mcpServers.headroom not present in ~/.claude.json — nothing to remove")
PYEOF

# 3. Kill running proxy (if any) before removing state directory
# 5. Kill running proxy (if any) before removing state directory
PORT_FILE="$HOME/.headroom/proxy.port"
if [ -f "$PORT_FILE" ]; then
PROXY_PORT="$(cat "$PORT_FILE" 2>/dev/null || true)"
Expand All @@ -69,7 +127,7 @@ else
echo " ✓ No running proxy (port file absent)"
fi

# 4. Clean up runtime state directory
# 6. Clean up runtime state directory
HEADROOM_DIR="$HOME/.headroom"
if [ -d "$HEADROOM_DIR" ]; then
rm -rf "$HEADROOM_DIR"
Expand Down
13 changes: 1 addition & 12 deletions hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,13 @@
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/manager.py stop $PPID",
"timeout": 10
}
]
}
],
"SessionEnd": [
{
"hooks": [
{
"type": "command",
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/manager.py stop $PPID",
"timeout": 10
"timeout": 30
}
]
}
Expand Down
2 changes: 1 addition & 1 deletion scripts/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def check_proxy_health(port: int) -> bool:
"""Return True if the proxy at the given port reports healthy."""
try:
url = f"http://127.0.0.1:{port}/health"
with urllib.request.urlopen(url, timeout=2) as resp:
with urllib.request.urlopen(url, timeout=10) as resp:
data = json.loads(resp.read())
return data.get("status") == "healthy"
except Exception:
Expand Down