diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..1298f41 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,27 @@ +{ + "name": "ctrlb-hq", + "owner": { + "name": "CtrlB", + "email": "engineering@ctrlb.ai" + }, + "metadata": { + "description": "Official CtrlB plugins for Claude Code" + }, + "plugins": [ + { + "name": "ctrlb-decompose", + "source": { + "source": "git-subdir", + "url": "https://github.com/hvsk004/ctrlb-decompose.git", + "path": "plugin" + }, + "description": "Analyze log files with ctrlb-decompose — pattern clustering, anomaly detection, and severity scoring", + "version": "1.0.0", + "author": { "name": "CtrlB", "email": "engineering@ctrlb.ai" }, + "homepage": "https://github.com/hvsk004/ctrlb-decompose", + "repository": "https://github.com/hvsk004/ctrlb-decompose", + "license": "MIT", + "keywords": ["logs", "log-analysis", "observability", "patterns", "anomaly-detection"] + } + ] +} diff --git a/README.md b/README.md index 8b7d4a0..3044602 100644 --- a/README.md +++ b/README.md @@ -206,6 +206,31 @@ Options: --- +## Claude Code Plugin + +Use ctrlb-decompose directly from [Claude Code](https://claude.ai/code) — no CLI knowledge needed. The plugin installs ctrlb-decompose automatically and lets you analyze logs just by asking. + +### Install + +``` +/plugin marketplace add ctrlb-hq/ctrlb-decompose +/plugin install ctrlb-decompose@ctrlb-hq +``` + +### Usage + +Just describe what you want in plain language: + +- "Analyze the errors in `/var/log/app.log`" +- "What are the most common patterns in this log file?" +- "Summarize these logs and highlight anomalies" + +Claude will check if ctrlb-decompose is installed (and walk you through installation if not), run the analysis, and explain the results — surfacing errors first, calling out anomalies, and suggesting what to investigate next. + +See [`plugin/README.md`](plugin/README.md) for full details. + +--- + ## License [MIT](LICENSE) diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json new file mode 100644 index 0000000..bf84c21 --- /dev/null +++ b/plugin/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "ctrlb-decompose", + "description": "Analyze log files with ctrlb-decompose — pattern clustering, anomaly detection, and severity scoring", + "version": "1.0.0", + "author": { + "name": "CtrlB", + "email": "engineering@ctrlb.ai" + }, + "homepage": "https://github.com/ctrlb-hq/ctrlb-decompose", + "repository": "https://github.com/ctrlb-hq/ctrlb-decompose", + "license": "MIT" +} diff --git a/plugin/README.md b/plugin/README.md new file mode 100644 index 0000000..ba3b681 --- /dev/null +++ b/plugin/README.md @@ -0,0 +1,49 @@ +# ctrlb-decompose Claude Code Plugin + +Adds log analysis to Claude Code via [ctrlb-decompose](https://github.com/ctrlb-hq/ctrlb-decompose) — compress millions of log lines into a handful of actionable patterns with typed variable statistics, anomaly detection, and severity scoring. + +## Install + +In Claude Code: + +``` +/plugin marketplace add ctrlb-hq/ctrlb-decompose +/plugin install ctrlb-decompose@ctrlb-hq +``` + +## Usage + +Just ask Claude to analyze logs — the skill triggers automatically: + +- "Analyze the errors in `/var/log/app.log`" +- "What are the most common patterns in this log file?" +- "Summarize these logs:" followed by pasted log lines + +If `ctrlb-decompose` is not installed on your system, Claude will detect your OS +and walk you through installation (Homebrew, apt/rpm package, or binary download). +If those fail, it can build from source using the Rust toolchain. + +## What the skill does + +1. Checks if `ctrlb-decompose` is installed; installs it if not +2. Runs `ctrlb-decompose --llm` on the log file or pasted text +3. Interprets the output — surfaces errors first, explains anomalies, summarizes variable patterns + +## Manual install of ctrlb-decompose + +**macOS:** +```bash +brew tap ctrlb-hq/tap && brew install ctrlb-decompose +``` + +**Debian / Ubuntu:** +```bash +curl -LO https://github.com/ctrlb-hq/ctrlb-decompose/releases/latest/download/ctrlb-decompose_amd64.deb +sudo dpkg -i ctrlb-decompose_amd64.deb +``` + +**Build from source:** +```bash +git clone https://github.com/ctrlb-hq/ctrlb-decompose.git +cd ctrlb-decompose && cargo build --release +``` diff --git a/plugin/bin/check-log-paste b/plugin/bin/check-log-paste new file mode 100755 index 0000000..20ae2ac --- /dev/null +++ b/plugin/bin/check-log-paste @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +""" +UserPromptSubmit hook for ctrlb-decompose plugin. + +When the user pastes a large volume of log-like content: + 1. Extracts the log block (first..last log-matching line, preserving continuations) + 2. Runs ctrlb-decompose --llm on the log block + 3. Blocks the original prompt (raw lines never enter Claude's context) + 4. Injects only the compressed ctrlb-decompose output as additionalContext + +Falls back to a warning-only inject (no block) if ctrlb-decompose is not +installed or fails, so the hook never breaks the conversation. + +Input (stdin): JSON {"prompt": "...", ...} +Output (stdout): JSON with decision/hookSpecificOutput +Exit code: always 0 +""" + +import json +import os +import re +import shutil +import subprocess +import sys +import tempfile + +# Trigger threshold: minimum log-matching lines to activate the hook +THRESHOLD = 50 + +# Set to False to disable blocking (inject-only fallback mode) +BLOCK_ENABLED = True + +LOG_PATTERN = re.compile( + r"(" + r"\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}" # ISO 8601 / datetime + r"|\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}" # syslog: Jan 5 12:00:00 + r"|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}.*\[.*\]" # Apache: IP [date] + r"|\b(ERROR|WARN(?:ING)?|INFO|DEBUG|FATAL|CRITICAL|TRACE|NOTICE)\b" + r")", + re.IGNORECASE, +) + + +def block_with_context(reason_msg, context): + """Block the prompt and inject context for Claude.""" + print(json.dumps({ + "decision": "block", + "reason": reason_msg, + "hookSpecificOutput": { + "hookEventName": "UserPromptSubmit", + "additionalContext": context, + }, + })) + sys.exit(0) + + +def not_installed_fallback(log_line_count): + """ctrlb-decompose is not installed — block and ask Claude to install it first.""" + block_with_context( + reason_msg=( + f"Your message contained {log_line_count} log lines but ctrlb-decompose " + f"is not installed yet. Let me install it first — then paste your logs again." + ), + context=( + f"[ctrlb-decompose hook] The user pasted ~{log_line_count} log lines but " + f"ctrlb-decompose is not installed on this machine.\n\n" + f"Install ctrlb-decompose now using the analyze-logs skill installation flow " + f"(Step 1), then tell the user: " + f"'ctrlb-decompose is now installed — please paste your logs again and I'll " + f"analyze them automatically.'" + ), + ) + + +def process_failed_fallback(log_line_count): + """ctrlb-decompose ran but produced no output — block and redirect to file.""" + block_with_context( + reason_msg=( + f"Your message contained {log_line_count} log lines but ctrlb-decompose " + f"could not process the pasted content. Please save the logs to a file and " + f"share the path instead." + ), + context=( + f"[ctrlb-decompose hook] The user pasted ~{log_line_count} log lines but " + f"ctrlb-decompose failed to produce output (possible format issue or timeout).\n\n" + f"Ask the user to save their logs to a file and share the path. " + f"Then run: ctrlb-decompose --llm --top 20 /path/to/their.log" + ), + ) + + +def main(): + try: + data = json.load(sys.stdin) + except (json.JSONDecodeError, ValueError): + sys.exit(0) + + prompt = data.get("prompt", "") + if not prompt: + sys.exit(0) + + lines = prompt.splitlines() + total_lines = len(lines) + + # Find which lines match the log pattern + matches = [bool(LOG_PATTERN.search(line)) for line in lines] + log_line_count = sum(matches) + + if log_line_count < THRESHOLD: + sys.exit(0) + + # --- Separate log block from user question --- + # Log block: everything from the first to last matching line (inclusive). + # This preserves continuation lines (stack traces, JSON payloads) that sit + # between log-matching lines but don't match the pattern themselves. + first_log_idx = next(i for i, m in enumerate(matches) if m) + last_log_idx = len(matches) - 1 - next(i for i, m in enumerate(reversed(matches)) if m) + + log_block_lines = lines[first_log_idx:last_log_idx + 1] + + # User's question: non-empty lines before the first log line and after the last + question_lines = ( + [l.strip() for l in lines[:first_log_idx] if l.strip()] + + [l.strip() for l in lines[last_log_idx + 1:] if l.strip()] + ) + user_question = " ".join(question_lines) if question_lines else None + + # --- Check ctrlb-decompose is available --- + if not shutil.which("ctrlb-decompose"): + not_installed_fallback(log_line_count) + + # --- Run ctrlb-decompose on the extracted log block --- + tmp_path = None + try: + with tempfile.NamedTemporaryFile( + mode="w", suffix=".log", prefix="ctrlb_hook_", delete=False + ) as tmp: + tmp.write("\n".join(log_block_lines)) + tmp_path = tmp.name + + result = subprocess.run( + ["ctrlb-decompose", "--llm", "--top", "20", tmp_path], + capture_output=True, + text=True, + timeout=30, + ) + ctrlb_output = result.stdout.strip() + except Exception: + ctrlb_output = "" + finally: + if tmp_path and os.path.exists(tmp_path): + os.unlink(tmp_path) + + if not ctrlb_output: + process_failed_fallback(log_line_count) + + # --- Build context to inject --- + question_note = ( + f"User's question: {user_question}\n\n" if user_question + else "" + ) + additional_context = ( + f"{question_note}" + f"ctrlb-decompose compressed {log_line_count} log lines " + f"({len(log_block_lines)} in log block) into patterns:\n\n" + f"{ctrlb_output}\n\n" + f"Answer the user's question based solely on this output. " + f"Do not reference or quote any raw log lines." + ) + + if BLOCK_ENABLED: + question_ack = f'Your question ("{user_question}") was noted.' if user_question else "" + reason = ( + f"Your message contained {log_line_count} log lines. " + f"They were compressed to patterns by ctrlb-decompose. " + f"{question_ack} " + f"Continue the conversation to get the analysis." + ).strip() + + print(json.dumps({ + "decision": "block", + "reason": reason, + "hookSpecificOutput": { + "hookEventName": "UserPromptSubmit", + "additionalContext": additional_context, + }, + })) + else: + # Inject-only fallback: raw lines still in context but Claude is + # told to use only the compressed output + print(json.dumps({ + "hookSpecificOutput": { + "hookEventName": "UserPromptSubmit", + "additionalContext": additional_context, + }, + })) + + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/plugin/hooks/hooks.json b/plugin/hooks/hooks.json new file mode 100644 index 0000000..4de030c --- /dev/null +++ b/plugin/hooks/hooks.json @@ -0,0 +1,14 @@ +{ + "hooks": { + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/bin/check-log-paste" + } + ] + } + ] + } +} diff --git a/plugin/skills/analyze-logs/SKILL.md b/plugin/skills/analyze-logs/SKILL.md new file mode 100644 index 0000000..5f67878 --- /dev/null +++ b/plugin/skills/analyze-logs/SKILL.md @@ -0,0 +1,141 @@ +--- +name: analyze-logs +description: > + Use this skill when the user asks to: "analyze logs", "analyze this log file", + "what's in my logs", "investigate errors in logs", "find patterns in log file", + "summarize logs", "detect anomalies in logs", "parse log file", "understand log + output", "what are the most common errors", or shares a log file path or pastes + log content for review. +version: 1.0.0 +--- + +# ctrlb-decompose Log Analysis + +## Overview + +Analyze log files or raw log text using [ctrlb-decompose](https://github.com/ctrlb-hq/ctrlb-decompose) — a CLI that compresses millions of log lines into a handful of typed patterns with variable statistics, anomaly detection, and severity scoring. Output is optimized for LLM consumption via `--llm`. + +## Workflow + +1. Gate large pastes — redirect to file path if volume is high +2. Check if `ctrlb-decompose` is installed; install if not +3. Run analysis on the file or pasted content +4. Interpret results — lead with problems, surface anomalies, suggest next steps + +--- + +## Large Paste Policy + +If the user signals they are about to paste log content (e.g. "here are my logs", "let me paste some logs"), ask first: + +> About how many lines are you sharing? For more than ~100 lines, save to a file and share the path — it keeps context clean and ctrlb-decompose handles files directly: +> ```bash +> ctrlb-decompose --llm --top 20 /path/to/your.log +> ``` + +Proceed with pasted content only if the user insists or the volume is clearly small (a handful of lines). + +**After processing pasted content:** do not quote, repeat, or reference individual raw log lines. Work only from ctrlb-decompose output — the raw lines are consumed. + +--- + +## Step 1 — Verify Installation + +```bash +which ctrlb-decompose +``` + +If found → skip to Step 2. + +If not found → detect the OS: + +```bash +uname -s +``` + +### macOS (Darwin) + +```bash +which brew +``` + +**Homebrew found** — install ctrlb-decompose: +```bash +brew tap ctrlb-hq/tap && brew install ctrlb-decompose +``` + +**Homebrew not found** — install Homebrew first: +```bash +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" +``` +Then install ctrlb-decompose: +```bash +brew tap ctrlb-hq/tap && brew install ctrlb-decompose +``` + +### Linux + +```bash +cat /etc/os-release +``` + +| Distro | Install command | +|--------|----------------| +| Debian / Ubuntu (`ID=debian`, `ID=ubuntu`, or `ID_LIKE` contains `debian`) | `curl -LO https://github.com/ctrlb-hq/ctrlb-decompose/releases/latest/download/ctrlb-decompose_amd64.deb && sudo dpkg -i ctrlb-decompose_amd64.deb` | +| RHEL / Fedora / CentOS (`ID` is `rhel`, `fedora`, or `centos`) | `curl -LO https://github.com/ctrlb-hq/ctrlb-decompose/releases/latest/download/ctrlb-decompose_amd64.rpm && sudo rpm -i ctrlb-decompose_amd64.rpm` | +| Other Linux | `curl -LO https://github.com/ctrlb-hq/ctrlb-decompose/releases/latest/download/ctrlb-decompose-linux-x86_64 && chmod +x ctrlb-decompose-linux-x86_64 && sudo mv ctrlb-decompose-linux-x86_64 /usr/local/bin/ctrlb-decompose` | + +### Fallback — Build from Source + +If any installation method fails (wrong architecture, missing binary, package manager error): + +```bash +which git && which cargo +``` + +If `cargo` is missing, install Rust: +```bash +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && source "$HOME/.cargo/env" +``` + +Clone and build: +```bash +git clone https://github.com/ctrlb-hq/ctrlb-decompose.git /tmp/ctrlb-decompose-build \ + && cd /tmp/ctrlb-decompose-build \ + && cargo build --release \ + && sudo cp target/release/ctrlb-decompose /usr/local/bin/ctrlb-decompose \ + && cd / && rm -rf /tmp/ctrlb-decompose-build +``` + +Confirm with `which ctrlb-decompose`, then continue to Step 2. + +--- + +## Step 2 — Run Analysis + +**File path provided:** +```bash +ctrlb-decompose --llm --top 20 "$FILE_PATH" +``` + +**Pasted log text:** Use the Write tool to save content to `/tmp/ctrlb_input.log` (handles multi-line text, special characters, and quotes correctly), then: +```bash +ctrlb-decompose --llm --top 20 /tmp/ctrlb_input.log && rm /tmp/ctrlb_input.log +``` + +Use `--top 20` by default. Increase to `--top 50` if the user wants more patterns. For all available flags and options, run `ctrlb-decompose --help`. + +--- + +## Step 3 — Interpret Results + +Present findings in this priority order: + +| Priority | Focus | What to cover | +|----------|-------|---------------| +| 1st | **Problems** | ERROR, FATAL, WARN patterns — frequency, rate, associated variables | +| 2nd | **Anomalies** | Frequency spikes, high error rates, unexpectedly low-cardinality variables | +| 3rd | **Variable summaries** | Most common IPs, slowest durations (p99), notable enum distributions | +| 4th | **Next steps** | Which patterns to drill into, what filters or time ranges to investigate | + +Keep the response grounded in ctrlb-decompose output. Do not speculate about root causes beyond what the pattern data supports.