mirror of
https://github.com/anthropics/claude-code.git
synced 2026-02-19 04:27:33 -08:00
Compare commits
1 Commits
1b50583382
...
claude/sla
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69585d4ffa |
125
.github/workflows/claude-issue-triage.yml
vendored
125
.github/workflows/claude-issue-triage.yml
vendored
@@ -1,20 +1,13 @@
|
||||
name: Claude Issue Triage
|
||||
description: Automatically triage GitHub issues using Claude Code
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
triage-issue:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
if: >-
|
||||
github.event_name == 'issues' ||
|
||||
(github.event_name == 'issue_comment' && !github.event.issue.pull_request && github.event.comment.user.type != 'Bot')
|
||||
concurrency:
|
||||
group: issue-triage-${{ github.event.issue.number }}
|
||||
cancel-in-progress: true
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
@@ -24,6 +17,30 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup GitHub MCP Server
|
||||
run: |
|
||||
mkdir -p /tmp/mcp-config
|
||||
cat > /tmp/mcp-config/mcp-servers.json << 'EOF'
|
||||
{
|
||||
"mcpServers": {
|
||||
"github": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"-e",
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
||||
"ghcr.io/github/github-mcp-server:sha-7aced2b"
|
||||
],
|
||||
"env": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
- name: Run Claude Code for Issue Triage
|
||||
timeout-minutes: 5
|
||||
uses: anthropics/claude-code-action@v1
|
||||
@@ -33,72 +50,56 @@ jobs:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
allowed_non_write_users: "*"
|
||||
prompt: |
|
||||
You're an issue triage assistant. Analyze the issue and manage labels.
|
||||
You're an issue triage assistant for GitHub issues. Your task is to analyze the issue and select appropriate labels from the provided list.
|
||||
|
||||
IMPORTANT: Don't post any comments or messages to the issue. Your only actions are adding or removing labels.
|
||||
IMPORTANT: Don't post any comments or messages to the issue. Your only action should be to apply labels.
|
||||
|
||||
Context:
|
||||
Issue Information:
|
||||
- REPO: ${{ github.repository }}
|
||||
- ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
- EVENT: ${{ github.event_name }}
|
||||
|
||||
ALLOWED LABELS — you may ONLY use labels from this list. Never invent new labels.
|
||||
TASK OVERVIEW:
|
||||
|
||||
Type: bug, enhancement, question, documentation, duplicate, invalid
|
||||
Lifecycle: needs-repro, needs-info
|
||||
Platform: platform:linux, platform:macos, platform:windows, platform:wsl, platform:ios, platform:android, platform:vscode, platform:intellij, platform:web, platform:aws-bedrock
|
||||
API: api:bedrock, api:vertex
|
||||
1. First, fetch the list of labels available in this repository by running: `gh label list`. Run exactly this command with nothing else.
|
||||
|
||||
TOOLS:
|
||||
- `gh issue view NUMBER`: Read the issue title, body, and labels
|
||||
- `gh issue view NUMBER --comments`: Read the conversation
|
||||
- `gh search issues QUERY`: Find similar or duplicate issues
|
||||
- `gh issue edit NUMBER --add-label` / `--remove-label`: Add or remove labels
|
||||
2. Next, use the GitHub tools to get context about the issue:
|
||||
- You have access to these tools:
|
||||
- mcp__github__get_issue: Use this to retrieve the current issue's details including title, description, and existing labels
|
||||
- mcp__github__get_issue_comments: Use this to read any discussion or additional context provided in the comments
|
||||
- mcp__github__update_issue: Use this to apply labels to the issue (do not use this for commenting)
|
||||
- mcp__github__search_issues: Use this to find similar issues that might provide context for proper categorization and to identify potential duplicate issues
|
||||
- mcp__github__list_issues: Use this to understand patterns in how other issues are labeled
|
||||
- Start by using mcp__github__get_issue to get the issue details
|
||||
|
||||
TASK:
|
||||
3. Analyze the issue content, considering:
|
||||
- The issue title and description
|
||||
- The type of issue (bug report, feature request, question, etc.)
|
||||
- Technical areas mentioned
|
||||
- Severity or priority indicators
|
||||
- User impact
|
||||
- Components affected
|
||||
|
||||
1. Run `gh issue view ${{ github.event.issue.number }}` to read the issue details.
|
||||
2. Run `gh issue view ${{ github.event.issue.number }} --comments` to read the conversation.
|
||||
4. Select appropriate labels from the available labels list provided above:
|
||||
- Choose labels that accurately reflect the issue's nature
|
||||
- Be specific but comprehensive
|
||||
- Select priority labels if you can determine urgency (high-priority, med-priority, or low-priority)
|
||||
- Consider platform labels (android, ios) if applicable
|
||||
- If you find similar issues using mcp__github__search_issues, consider using a "duplicate" label if appropriate. Only do so if the issue is a duplicate of another OPEN issue.
|
||||
|
||||
**If EVENT is "issues" (new issue):**
|
||||
5. Apply the selected labels:
|
||||
- Use mcp__github__update_issue to apply your selected labels
|
||||
- DO NOT post any comments explaining your decision
|
||||
- DO NOT communicate directly with users
|
||||
- If no labels are clearly applicable, do not apply any labels
|
||||
|
||||
3. First, check if this issue is actually about Claude Code (the CLI/IDE tool). Issues about the Claude API, claude.ai, the Claude app, Anthropic billing, or other Anthropic products should be labeled `invalid`. If invalid, apply only that label and stop.
|
||||
|
||||
4. Analyze and apply category labels:
|
||||
- Type (bug, enhancement, question, etc.)
|
||||
- Technical areas and platform
|
||||
- Check for duplicates with `gh search issues`. Only mark as duplicate of OPEN issues.
|
||||
|
||||
5. Evaluate lifecycle labels:
|
||||
- `needs-repro` (bugs only, 7 days): Bug reports without clear steps to reproduce. A good repro has specific, followable steps that someone else could use to see the same issue.
|
||||
Do NOT apply if the user already provided error messages, logs, file paths, or a description of what they did. Don't require a specific format — narrative descriptions count.
|
||||
For model behavior issues (e.g. "Claude does X when it should do Y"), don't require traditional repro steps — examples and patterns are sufficient.
|
||||
- `needs-info` (bugs only, 7 days): The issue needs something from the community before it can progress — e.g. error messages, versions, environment details, or answers to follow-up questions. Don't apply to questions or enhancements.
|
||||
Do NOT apply if the user already provided version, environment, and error details. If the issue just needs engineering investigation, that's not `needs-info`.
|
||||
|
||||
Issues with these labels are automatically closed after the timeout if there's no response.
|
||||
The goal is to avoid issues lingering without a clear next step.
|
||||
|
||||
6. Apply all selected labels:
|
||||
`gh issue edit ${{ github.event.issue.number }} --add-label "label1" --add-label "label2"`
|
||||
|
||||
**If EVENT is "issue_comment" (comment on existing issue):**
|
||||
|
||||
3. Evaluate lifecycle labels based on the full conversation:
|
||||
- If the issue has `needs-repro` or `needs-info` and the missing information has now been provided, remove the label:
|
||||
`gh issue edit ${{ github.event.issue.number }} --remove-label "needs-repro"`
|
||||
- If the issue doesn't have lifecycle labels but clearly needs them (e.g., a maintainer asked for repro steps or more details), add the appropriate label.
|
||||
- Comments like "+1", "me too", "same here", or emoji reactions are NOT the missing information. Only remove labels when substantive details are actually provided.
|
||||
- Do NOT add or remove category labels (bug, enhancement, etc.) on comment events.
|
||||
|
||||
GUIDELINES:
|
||||
- ONLY use labels from the ALLOWED LABELS list above — never create or guess label names
|
||||
IMPORTANT GUIDELINES:
|
||||
- Be thorough in your analysis
|
||||
- Only select labels from the provided list above
|
||||
- DO NOT post any comments to the issue
|
||||
- Be conservative with lifecycle labels — only apply when clearly warranted
|
||||
- Only apply lifecycle labels (`needs-repro`, `needs-info`) to bugs — never to questions or enhancements
|
||||
- When in doubt, don't apply a lifecycle label — false positives are worse than missing labels
|
||||
- Your ONLY action should be to apply labels using mcp__github__update_issue
|
||||
- It's okay to not add any labels if none are clearly applicable
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
claude_args: |
|
||||
--model claude-opus-4-6
|
||||
--allowedTools "Bash(gh issue view:*),Bash(gh issue edit:*),Bash(gh search issues:*)"
|
||||
--model claude-sonnet-4-5-20250929
|
||||
--mcp-config /tmp/mcp-config/mcp-servers.json
|
||||
--allowedTools "Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues"
|
||||
|
||||
157
.github/workflows/stale-issue-manager.yml
vendored
Normal file
157
.github/workflows/stale-issue-manager.yml
vendored
Normal file
@@ -0,0 +1,157 @@
|
||||
name: "Manage Stale Issues"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# 2am Pacific = 9am UTC (10am UTC during DST)
|
||||
- cron: "0 10 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
concurrency:
|
||||
group: stale-issue-manager
|
||||
|
||||
jobs:
|
||||
manage-stale-issues:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Manage stale issues
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const oneMonthAgo = new Date();
|
||||
oneMonthAgo.setDate(oneMonthAgo.getDate() - 30);
|
||||
|
||||
const twoMonthsAgo = new Date();
|
||||
twoMonthsAgo.setDate(twoMonthsAgo.getDate() - 60);
|
||||
|
||||
const warningComment = `This issue has been inactive for 30 days. If the issue is still occurring, please comment to let us know. Otherwise, this issue will be automatically closed in 30 days for housekeeping purposes.`;
|
||||
|
||||
const closingComment = `This issue has been automatically closed due to 60 days of inactivity. If you're still experiencing this issue, please open a new issue with updated information.`;
|
||||
|
||||
let page = 1;
|
||||
let hasMore = true;
|
||||
let totalWarned = 0;
|
||||
let totalClosed = 0;
|
||||
let totalLabeled = 0;
|
||||
|
||||
while (hasMore) {
|
||||
// Get open issues sorted by last updated (oldest first)
|
||||
const { data: issues } = await github.rest.issues.listForRepo({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
sort: 'updated',
|
||||
direction: 'asc',
|
||||
per_page: 100,
|
||||
page: page
|
||||
});
|
||||
|
||||
if (issues.length === 0) {
|
||||
hasMore = false;
|
||||
break;
|
||||
}
|
||||
|
||||
for (const issue of issues) {
|
||||
// Skip if already locked
|
||||
if (issue.locked) continue;
|
||||
|
||||
// Skip pull requests
|
||||
if (issue.pull_request) continue;
|
||||
|
||||
// Check if updated more recently than 30 days ago
|
||||
const updatedAt = new Date(issue.updated_at);
|
||||
if (updatedAt > oneMonthAgo) {
|
||||
// Since issues are sorted by updated_at ascending,
|
||||
// once we hit a recent issue, all remaining will be recent too
|
||||
hasMore = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if issue has autoclose label
|
||||
const hasAutocloseLabel = issue.labels.some(label =>
|
||||
typeof label === 'object' && label.name === 'autoclose'
|
||||
);
|
||||
|
||||
try {
|
||||
// Get comments to check for existing warning
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
per_page: 100
|
||||
});
|
||||
|
||||
// Find the last comment from github-actions bot
|
||||
const botComments = comments.filter(comment =>
|
||||
comment.user && comment.user.login === 'github-actions[bot]' &&
|
||||
comment.body && comment.body.includes('inactive for 30 days')
|
||||
);
|
||||
|
||||
const lastBotComment = botComments[botComments.length - 1];
|
||||
|
||||
if (lastBotComment) {
|
||||
// Check if the bot comment is older than 30 days (total 60 days of inactivity)
|
||||
const botCommentDate = new Date(lastBotComment.created_at);
|
||||
if (botCommentDate < oneMonthAgo) {
|
||||
// Close the issue - it's been stale for 60+ days
|
||||
console.log(`Closing issue #${issue.number} (stale for 60+ days): ${issue.title}`);
|
||||
|
||||
// Post closing comment
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: closingComment
|
||||
});
|
||||
|
||||
// Close the issue
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
state: 'closed',
|
||||
state_reason: 'not_planned'
|
||||
});
|
||||
|
||||
totalClosed++;
|
||||
}
|
||||
// If bot comment exists but is recent, issue already has warning
|
||||
} else if (updatedAt < oneMonthAgo) {
|
||||
// No bot warning yet, issue is 30+ days old
|
||||
console.log(`Warning issue #${issue.number} (stale for 30+ days): ${issue.title}`);
|
||||
|
||||
// Post warning comment
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: warningComment
|
||||
});
|
||||
|
||||
totalWarned++;
|
||||
|
||||
// Add autoclose label if not present
|
||||
if (!hasAutocloseLabel) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
labels: ['autoclose']
|
||||
});
|
||||
totalLabeled++;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to process issue #${issue.number}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
page++;
|
||||
}
|
||||
|
||||
console.log(`Summary:`);
|
||||
console.log(`- Issues warned (30 days stale): ${totalWarned}`);
|
||||
console.log(`- Issues labeled with autoclose: ${totalLabeled}`);
|
||||
console.log(`- Issues closed (60 days stale): ${totalClosed}`);
|
||||
31
.github/workflows/sweep.yml
vendored
31
.github/workflows/sweep.yml
vendored
@@ -1,31 +0,0 @@
|
||||
name: "Issue Sweep"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 10,22 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
concurrency:
|
||||
group: daily-issue-sweep
|
||||
|
||||
jobs:
|
||||
sweep:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Enforce lifecycle timeouts
|
||||
run: bun run scripts/sweep.ts
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }}
|
||||
GITHUB_REPOSITORY_NAME: ${{ github.event.repository.name }}
|
||||
39
CHANGELOG.md
39
CHANGELOG.md
@@ -1,44 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 2.1.41
|
||||
|
||||
- Fixed AWS auth refresh hanging indefinitely by adding a 3-minute timeout
|
||||
- Added `claude auth login`, `claude auth status`, and `claude auth logout` CLI subcommands
|
||||
- Added Windows ARM64 (win32-arm64) native binary support
|
||||
- Improved `/rename` to auto-generate session name from conversation context when called without arguments
|
||||
- Improved narrow terminal layout for prompt footer
|
||||
- Fixed file resolution failing for @-mentions with anchor fragments (e.g., `@README.md#installation`)
|
||||
- Fixed FileReadTool blocking the process on FIFOs, `/dev/stdin`, and large files
|
||||
- Fixed background task notifications not being delivered in streaming Agent SDK mode
|
||||
- Fixed cursor jumping to end on each keystroke in classifier rule input
|
||||
- Fixed markdown link display text being dropped for raw URL
|
||||
- Fixed auto-compact failure error notifications being shown to users
|
||||
- Fixed permission wait time being included in subagent elapsed time display
|
||||
- Fixed proactive ticks firing while in plan mode
|
||||
- Fixed clear stale permission rules when settings change on disk
|
||||
- Fixed hook blocking errors showing stderr content in UI
|
||||
|
||||
## 2.1.39
|
||||
|
||||
- Added guard against launching Claude Code inside another Claude Code session
|
||||
- Fixed Agent Teams using wrong model identifier for Bedrock, Vertex, and Foundry customers
|
||||
- Fixed a crash when MCP tools return image content during streaming
|
||||
- Fixed /resume session previews showing raw XML tags instead of readable command names
|
||||
- Improved model error messages for Bedrock/Vertex/Foundry users with fallback suggestions
|
||||
- Fixed plugin browse showing misleading "Space to Toggle" hint for already-installed plugins
|
||||
- Fixed hook blocking errors (exit code 2) not showing stderr to the user
|
||||
- Added `speed` attribute to OTel events and trace spans for fast mode visibility
|
||||
- Fixed /resume showing interrupt messages as session titles
|
||||
- Fixed Opus 4.6 launch announcement showing for Bedrock/Vertex/Foundry users
|
||||
- Improved error message for many-image dimension limit errors with /compact suggestion
|
||||
- Fixed structured-outputs beta header being sent unconditionally on Vertex/Bedrock
|
||||
- Fixed spurious warnings for non-agent markdown files in `.claude/agents/` directory
|
||||
- Improved terminal rendering performance
|
||||
- Fixed fatal errors being swallowed instead of displayed
|
||||
- Fixed process hanging after session close
|
||||
- Fixed character loss at terminal screen boundary
|
||||
- Fixed blank lines in verbose transcript view
|
||||
|
||||
## 2.1.38
|
||||
|
||||
- Fixed VS Code terminal scroll-to-top regression introduced in 2.1.37
|
||||
|
||||
@@ -25,6 +25,7 @@ Learn more in the [official plugins documentation](https://docs.claude.com/en/do
|
||||
| [pr-review-toolkit](./pr-review-toolkit/) | Comprehensive PR review agents specializing in comments, tests, error handling, type design, code quality, and code simplification | **Command:** `/pr-review-toolkit:review-pr` - Run with optional review aspects (comments, tests, errors, types, code, simplify, all)<br>**Agents:** `comment-analyzer`, `pr-test-analyzer`, `silent-failure-hunter`, `type-design-analyzer`, `code-reviewer`, `code-simplifier` |
|
||||
| [ralph-wiggum](./ralph-wiggum/) | Interactive self-referential AI loops for iterative development. Claude works on the same task repeatedly until completion | **Commands:** `/ralph-loop`, `/cancel-ralph` - Start/stop autonomous iteration loops<br>**Hook:** Stop - Intercepts exit attempts to continue iteration |
|
||||
| [security-guidance](./security-guidance/) | Security reminder hook that warns about potential security issues when editing files | **Hook:** PreToolUse - Monitors 9 security patterns including command injection, XSS, eval usage, dangerous HTML, pickle deserialization, and os.system calls |
|
||||
| [slack-quote-formatter](./slack-quote-formatter/) | Enhances visual display of Slack forwarded and quoted messages with distinctive Unicode box formatting | **Hook:** UserPromptSubmit - Transforms `[Forwarded Message]` blocks into visually distinctive formatted boxes with clear boundaries and indentation |
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
@@ -56,15 +56,10 @@ Note: Still review Claude generated PR's.
|
||||
|
||||
6. Filter out any issues that were not validated in step 5. This step will give us our list of high signal issues for our review.
|
||||
|
||||
7. Output a summary of the review findings to the terminal:
|
||||
- If issues were found, list each issue with a brief description.
|
||||
- If no issues were found, state: "No issues found. Checked for bugs and CLAUDE.md compliance."
|
||||
7. If issues were found, skip to step 8 to post inline comments directly.
|
||||
|
||||
If `--comment` argument was NOT provided, stop here. Do not post any GitHub comments.
|
||||
|
||||
If `--comment` argument IS provided and NO issues were found, post a summary comment using `gh pr comment` and stop.
|
||||
|
||||
If `--comment` argument IS provided and issues were found, continue to step 8.
|
||||
If NO issues were found, post a summary comment using `gh pr comment` (if `--comment` argument is provided):
|
||||
"No issues found. Checked for bugs and CLAUDE.md compliance."
|
||||
|
||||
8. Create a list of all comments that you plan on leaving. This is only for you to make sure you are comfortable with the comments. Do not post this list anywhere.
|
||||
|
||||
@@ -90,7 +85,7 @@ Notes:
|
||||
- Use gh CLI to interact with GitHub (e.g., fetch pull requests, create comments). Do not use web fetch.
|
||||
- Create a todo list before starting.
|
||||
- You must cite and link each issue in inline comments (e.g., if referring to a CLAUDE.md, include a link to it).
|
||||
- If no issues are found and `--comment` argument is provided, post a comment with the following format:
|
||||
- If no issues are found, post a comment with the following format:
|
||||
|
||||
---
|
||||
|
||||
|
||||
18
plugins/slack-quote-formatter/.claude-plugin/plugin.json
Normal file
18
plugins/slack-quote-formatter/.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "slack-quote-formatter",
|
||||
"version": "1.0.0",
|
||||
"description": "Enhances visual display of Slack forwarded and quoted messages with distinctive formatting",
|
||||
"author": {
|
||||
"name": "Anthropic",
|
||||
"email": "support@anthropic.com"
|
||||
},
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"slack",
|
||||
"formatting",
|
||||
"quotes",
|
||||
"messages",
|
||||
"visual"
|
||||
],
|
||||
"hooks": "./hooks/hooks.json"
|
||||
}
|
||||
87
plugins/slack-quote-formatter/README.md
Normal file
87
plugins/slack-quote-formatter/README.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# Slack Quote Formatter
|
||||
|
||||
Enhances the visual display of Slack forwarded and quoted messages with distinctive Unicode box formatting.
|
||||
|
||||
## Overview
|
||||
|
||||
When Claude Code receives messages from Slack that include forwarded messages, they typically appear as plain text:
|
||||
|
||||
```
|
||||
[Forwarded Message]
|
||||
From: Felix Klock
|
||||
Message: I assume it would be a matter of reviewing the entries...
|
||||
```
|
||||
|
||||
This plugin transforms these plain blocks into visually distinctive formatted quotes:
|
||||
|
||||
```
|
||||
╔════════════════════════════════════════════════════════════════════╗
|
||||
║ FORWARDED MESSAGE ║
|
||||
╠════════════════════════════════════════════════════════════════════╣
|
||||
║ From: Felix Klock ║
|
||||
╟────────────────────────────────────────────────────────────────────╢
|
||||
║ I assume it would be a matter of reviewing the entries... ║
|
||||
╚════════════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Unicode box drawing characters for clear visual boundaries
|
||||
- Distinct header section highlighting "FORWARDED MESSAGE"
|
||||
- Sender information prominently displayed
|
||||
- Message content indented for quote-like appearance
|
||||
- Automatic text wrapping for long messages
|
||||
- Handles multiple forwarded messages in a single prompt
|
||||
|
||||
## Installation
|
||||
|
||||
This plugin is included in the Claude Code plugins repository. To enable it:
|
||||
|
||||
1. Add the plugin to your project's `.claude/settings.json`:
|
||||
```json
|
||||
{
|
||||
"plugins": [
|
||||
"path/to/plugins/slack-quote-formatter"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
2. Or install via the Claude Code plugin command:
|
||||
```
|
||||
/plugin add slack-quote-formatter
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
The plugin registers a `UserPromptSubmit` hook that:
|
||||
|
||||
1. Intercepts incoming prompts
|
||||
2. Detects `[Forwarded Message]` blocks in the Slack context
|
||||
3. Transforms them into formatted boxes with visual distinction
|
||||
4. Passes the transformed prompt to Claude
|
||||
|
||||
## Configuration
|
||||
|
||||
No configuration required. The plugin automatically activates when Slack forwarded messages are detected.
|
||||
|
||||
## Plugin Structure
|
||||
|
||||
```
|
||||
slack-quote-formatter/
|
||||
├── .claude-plugin/
|
||||
│ └── plugin.json # Plugin metadata
|
||||
├── hooks/
|
||||
│ ├── hooks.json # Hook configuration
|
||||
│ └── format_slack_quotes.py # Transformation logic
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Why Visual Distinction Matters
|
||||
|
||||
When working with Slack context in Claude Code, quoted messages can easily blend into the surrounding text, making it harder to:
|
||||
|
||||
- Identify what content is quoted vs. original
|
||||
- Understand the source of different pieces of information
|
||||
- Parse complex threads with multiple forwarded messages
|
||||
|
||||
This plugin makes quoted content immediately recognizable, improving readability and reducing confusion.
|
||||
185
plugins/slack-quote-formatter/hooks/format_slack_quotes.py
Executable file
185
plugins/slack-quote-formatter/hooks/format_slack_quotes.py
Executable file
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Slack Quote Formatter - UserPromptSubmit Hook
|
||||
|
||||
Transforms plain Slack forwarded/quoted messages into visually distinctive
|
||||
formatted blocks with Unicode box characters and better layout.
|
||||
|
||||
Before:
|
||||
[Forwarded Message]
|
||||
From: John Doe
|
||||
Message: Here is some text...
|
||||
|
||||
After:
|
||||
╔═══════════════════════════════════════════════════════════╗
|
||||
║ FORWARDED MESSAGE ║
|
||||
╠═══════════════════════════════════════════════════════════╣
|
||||
║ From: John Doe ║
|
||||
╟───────────────────────────────────────────────────────────╢
|
||||
║ Here is some text... ║
|
||||
╚═══════════════════════════════════════════════════════════╝
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
def format_forwarded_message(from_line: str, message_content: str) -> str:
|
||||
"""Format a single forwarded message with visual box styling."""
|
||||
|
||||
# Box drawing characters
|
||||
TOP_LEFT = "╔"
|
||||
TOP_RIGHT = "╗"
|
||||
BOTTOM_LEFT = "╚"
|
||||
BOTTOM_RIGHT = "╝"
|
||||
HORIZONTAL = "═"
|
||||
VERTICAL = "║"
|
||||
LEFT_T = "╠"
|
||||
RIGHT_T = "╣"
|
||||
LEFT_LIGHT = "╟"
|
||||
RIGHT_LIGHT = "╢"
|
||||
LIGHT_HORIZONTAL = "─"
|
||||
|
||||
# Configuration
|
||||
BOX_WIDTH = 70
|
||||
INNER_WIDTH = BOX_WIDTH - 4 # Account for "║ " and " ║"
|
||||
|
||||
def pad_line(text: str, width: int = INNER_WIDTH) -> str:
|
||||
"""Pad a line to fill the box width."""
|
||||
if len(text) > width:
|
||||
return text[:width]
|
||||
return text + " " * (width - len(text))
|
||||
|
||||
def wrap_text(text: str, width: int) -> list:
|
||||
"""Wrap text to fit within the specified width."""
|
||||
words = text.split()
|
||||
lines = []
|
||||
current_line = []
|
||||
current_length = 0
|
||||
|
||||
for word in words:
|
||||
word_len = len(word)
|
||||
if current_length + word_len + (1 if current_line else 0) <= width:
|
||||
current_line.append(word)
|
||||
current_length += word_len + (1 if len(current_line) > 1 else 0)
|
||||
else:
|
||||
if current_line:
|
||||
lines.append(" ".join(current_line))
|
||||
current_line = [word]
|
||||
current_length = word_len
|
||||
|
||||
if current_line:
|
||||
lines.append(" ".join(current_line))
|
||||
|
||||
return lines if lines else [""]
|
||||
|
||||
lines = []
|
||||
|
||||
# Top border with header
|
||||
lines.append(f"{TOP_LEFT}{HORIZONTAL * (BOX_WIDTH - 2)}{TOP_RIGHT}")
|
||||
|
||||
# Header line
|
||||
header = "FORWARDED MESSAGE"
|
||||
lines.append(f"{VERTICAL} {pad_line(header)}{VERTICAL}")
|
||||
|
||||
# Separator after header
|
||||
lines.append(f"{LEFT_T}{HORIZONTAL * (BOX_WIDTH - 2)}{RIGHT_T}")
|
||||
|
||||
# From line
|
||||
from_display = from_line.strip()
|
||||
lines.append(f"{VERTICAL} {pad_line(from_display)}{VERTICAL}")
|
||||
|
||||
# Light separator before message
|
||||
lines.append(f"{LEFT_LIGHT}{LIGHT_HORIZONTAL * (BOX_WIDTH - 2)}{RIGHT_LIGHT}")
|
||||
|
||||
# Message content (indented and wrapped)
|
||||
message_lines = message_content.strip().split("\n")
|
||||
for msg_line in message_lines:
|
||||
# Handle each line, wrapping if necessary
|
||||
wrapped = wrap_text(msg_line, INNER_WIDTH - 2) # Extra indent
|
||||
for wrapped_line in wrapped:
|
||||
indented = " " + wrapped_line # Add indent for quote appearance
|
||||
lines.append(f"{VERTICAL} {pad_line(indented)}{VERTICAL}")
|
||||
|
||||
# Bottom border
|
||||
lines.append(f"{BOTTOM_LEFT}{HORIZONTAL * (BOX_WIDTH - 2)}{BOTTOM_RIGHT}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def transform_slack_context(text: str) -> str:
|
||||
"""
|
||||
Find and transform [Forwarded Message] blocks in the text.
|
||||
|
||||
Matches patterns like:
|
||||
[Forwarded Message]
|
||||
From: Name Here
|
||||
Message: Content here that may span
|
||||
multiple lines until we hit the next section
|
||||
"""
|
||||
|
||||
# Pattern to match forwarded message blocks
|
||||
# This handles the common format from Slack context
|
||||
pattern = r'\[Forwarded Message\]\s*\n\s*From:\s*([^\n]+)\s*\nMessage:\s*(.+?)(?=\n\s*\[|\n\[pnkfelix\]|\n<|\Z)'
|
||||
|
||||
def replace_match(match):
|
||||
from_name = match.group(1).strip()
|
||||
message = match.group(2).strip()
|
||||
|
||||
formatted = format_forwarded_message(f"From: {from_name}", message)
|
||||
return f"\n{formatted}\n"
|
||||
|
||||
# Apply transformation
|
||||
result = re.sub(pattern, replace_match, text, flags=re.DOTALL)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point for the UserPromptSubmit hook."""
|
||||
try:
|
||||
# Read input from stdin
|
||||
input_data = json.load(sys.stdin)
|
||||
|
||||
# Get the user prompt if available
|
||||
user_prompt = input_data.get("user_prompt", "")
|
||||
|
||||
if not user_prompt:
|
||||
# No prompt to transform, just pass through
|
||||
print(json.dumps({}))
|
||||
sys.exit(0)
|
||||
|
||||
# Check if there are forwarded messages to format
|
||||
if "[Forwarded Message]" not in user_prompt:
|
||||
# Nothing to transform
|
||||
print(json.dumps({}))
|
||||
sys.exit(0)
|
||||
|
||||
# Transform the prompt
|
||||
transformed_prompt = transform_slack_context(user_prompt)
|
||||
|
||||
# If we made changes, return the transformed prompt
|
||||
if transformed_prompt != user_prompt:
|
||||
result = {
|
||||
"transformedPrompt": transformed_prompt
|
||||
}
|
||||
print(json.dumps(result))
|
||||
else:
|
||||
print(json.dumps({}))
|
||||
|
||||
except json.JSONDecodeError:
|
||||
# No valid JSON input, pass through
|
||||
print(json.dumps({}))
|
||||
except Exception as e:
|
||||
# Log error but don't block
|
||||
error_output = {
|
||||
"systemMessage": f"[slack-quote-formatter] Warning: {str(e)}"
|
||||
}
|
||||
print(json.dumps(error_output))
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
14
plugins/slack-quote-formatter/hooks/hooks.json
Normal file
14
plugins/slack-quote-formatter/hooks/hooks.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"matcher": ".*",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/format_slack_quotes.py",
|
||||
"timeout": 5000
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
163
scripts/sweep.ts
163
scripts/sweep.ts
@@ -1,163 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
// --
|
||||
|
||||
const NEW_ISSUE = "https://github.com/anthropics/claude-code/issues/new/choose";
|
||||
const DRY_RUN = process.argv.includes("--dry-run");
|
||||
const STALE_DAYS = 14;
|
||||
const STALE_UPVOTE_THRESHOLD = 10;
|
||||
|
||||
const CLOSE_MESSAGE = (reason: string) =>
|
||||
`Closing for now — ${reason}. Please [open a new issue](${NEW_ISSUE}) if this is still relevant.`;
|
||||
|
||||
const lifecycle = [
|
||||
{ label: "invalid", days: 3, reason: "this doesn't appear to be about Claude Code" },
|
||||
{ label: "needs-repro", days: 7, reason: "we still need reproduction steps to investigate" },
|
||||
{ label: "needs-info", days: 7, reason: "we still need a bit more information to move forward" },
|
||||
{ label: "stale", days: 14, reason: "inactive for too long" },
|
||||
{ label: "autoclose", days: 14, reason: "inactive for too long" },
|
||||
];
|
||||
|
||||
// --
|
||||
|
||||
async function githubRequest<T>(
|
||||
endpoint: string,
|
||||
method = "GET",
|
||||
body?: unknown
|
||||
): Promise<T> {
|
||||
const token = process.env.GITHUB_TOKEN;
|
||||
if (!token) throw new Error("GITHUB_TOKEN required");
|
||||
|
||||
const response = await fetch(`https://api.github.com${endpoint}`, {
|
||||
method,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
"User-Agent": "sweep",
|
||||
...(body && { "Content-Type": "application/json" }),
|
||||
},
|
||||
...(body && { body: JSON.stringify(body) }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) return {} as T;
|
||||
const text = await response.text();
|
||||
throw new Error(`GitHub API ${response.status}: ${text}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// --
|
||||
|
||||
async function markStale(owner: string, repo: string) {
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - STALE_DAYS);
|
||||
|
||||
let labeled = 0;
|
||||
|
||||
console.log(`\n=== marking stale (${STALE_DAYS}d inactive) ===`);
|
||||
|
||||
for (let page = 1; page <= 10; page++) {
|
||||
const issues = await githubRequest<any[]>(
|
||||
`/repos/${owner}/${repo}/issues?state=open&sort=updated&direction=asc&per_page=100&page=${page}`
|
||||
);
|
||||
if (issues.length === 0) break;
|
||||
|
||||
for (const issue of issues) {
|
||||
if (issue.pull_request) continue;
|
||||
if (issue.locked) continue;
|
||||
if (issue.assignees?.length > 0) continue;
|
||||
|
||||
const updatedAt = new Date(issue.updated_at);
|
||||
if (updatedAt > cutoff) return labeled;
|
||||
|
||||
const alreadyStale = issue.labels?.some(
|
||||
(l: any) => l.name === "stale" || l.name === "autoclose"
|
||||
);
|
||||
if (alreadyStale) continue;
|
||||
|
||||
const isEnhancement = issue.labels?.some(
|
||||
(l: any) => l.name === "enhancement"
|
||||
);
|
||||
const thumbsUp = issue.reactions?.["+1"] ?? 0;
|
||||
if (isEnhancement && thumbsUp >= STALE_UPVOTE_THRESHOLD) continue;
|
||||
|
||||
const base = `/repos/${owner}/${repo}/issues/${issue.number}`;
|
||||
|
||||
if (DRY_RUN) {
|
||||
const age = Math.floor((Date.now() - updatedAt.getTime()) / 86400000);
|
||||
console.log(`#${issue.number}: would label stale (${age}d inactive) — ${issue.title}`);
|
||||
} else {
|
||||
await githubRequest(`${base}/labels`, "POST", { labels: ["stale"] });
|
||||
console.log(`#${issue.number}: labeled stale — ${issue.title}`);
|
||||
}
|
||||
labeled++;
|
||||
}
|
||||
}
|
||||
|
||||
return labeled;
|
||||
}
|
||||
|
||||
async function closeExpired(owner: string, repo: string) {
|
||||
let closed = 0;
|
||||
|
||||
for (const { label, days, reason } of lifecycle) {
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - days);
|
||||
console.log(`\n=== ${label} (${days}d timeout) ===`);
|
||||
|
||||
for (let page = 1; page <= 10; page++) {
|
||||
const issues = await githubRequest<any[]>(
|
||||
`/repos/${owner}/${repo}/issues?state=open&labels=${label}&sort=updated&direction=asc&per_page=100&page=${page}`
|
||||
);
|
||||
if (issues.length === 0) break;
|
||||
|
||||
for (const issue of issues) {
|
||||
if (issue.pull_request) continue;
|
||||
const base = `/repos/${owner}/${repo}/issues/${issue.number}`;
|
||||
|
||||
const events = await githubRequest<any[]>(`${base}/events?per_page=100`);
|
||||
|
||||
const labeledAt = events
|
||||
.filter((e) => e.event === "labeled" && e.label?.name === label)
|
||||
.map((e) => new Date(e.created_at))
|
||||
.pop();
|
||||
|
||||
if (!labeledAt || labeledAt > cutoff) continue;
|
||||
|
||||
if (DRY_RUN) {
|
||||
const age = Math.floor((Date.now() - labeledAt.getTime()) / 86400000);
|
||||
console.log(`#${issue.number}: would close (${label}, ${age}d old) — ${issue.title}`);
|
||||
} else {
|
||||
await githubRequest(`${base}/comments`, "POST", { body: CLOSE_MESSAGE(reason) });
|
||||
await githubRequest(base, "PATCH", { state: "closed", state_reason: "not_planned" });
|
||||
console.log(`#${issue.number}: closed (${label})`);
|
||||
}
|
||||
closed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return closed;
|
||||
}
|
||||
|
||||
// --
|
||||
|
||||
async function main() {
|
||||
const owner = process.env.GITHUB_REPOSITORY_OWNER;
|
||||
const repo = process.env.GITHUB_REPOSITORY_NAME;
|
||||
if (!owner || !repo)
|
||||
throw new Error("GITHUB_REPOSITORY_OWNER and GITHUB_REPOSITORY_NAME required");
|
||||
|
||||
if (DRY_RUN) console.log("DRY RUN — no changes will be made\n");
|
||||
|
||||
const labeled = await markStale(owner, repo);
|
||||
const closed = await closeExpired(owner, repo);
|
||||
|
||||
console.log(`\nDone: ${labeled} ${DRY_RUN ? "would be labeled" : "labeled"} stale, ${closed} ${DRY_RUN ? "would be closed" : "closed"}`);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
|
||||
export {};
|
||||
Reference in New Issue
Block a user