forked from claude-did-this/claude-hub
* feat: Implement Claude orchestration provider for parallel session management - Add ClaudeWebhookProvider implementing the webhook provider interface - Create orchestration system for running multiple Claude containers in parallel - Implement smart task decomposition to break complex projects into workstreams - Add session management with dependency tracking between sessions - Support multiple execution strategies (parallel, sequential, wait_for_core) - Create comprehensive test suite for all components - Add documentation for Claude orchestration API and usage This enables super-charged Claude capabilities for the MCP hackathon by allowing multiple Claude instances to work on different aspects of a project simultaneously, with intelligent coordination and result aggregation. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: Add session management endpoints for MCP integration - Add SessionHandler for individual session CRUD operations - Create endpoints: session.create, session.get, session.list, session.start, session.output - Fix Claude invocation in Docker containers using proper claude chat command - Add volume mounts for persistent storage across session lifecycle - Simplify OrchestrationHandler to create single coordination sessions - Update documentation with comprehensive MCP integration examples - Add comprehensive unit and integration tests for new endpoints - Support dependencies and automatic session queuing/starting This enables Claude Desktop to orchestrate multiple Claude Code sessions via MCP Server tools. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: Update ClaudeWebhookProvider validation for session endpoints - Make project fields optional for session management operations - Add validation for session.create requiring session field - Update tests to match new validation rules - Fix failing CI tests 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: Use Promise.reject for validation errors in parsePayload - Convert synchronous throws to Promise.reject for async consistency - Fixes failing unit tests expecting rejected promises 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: Mock SessionManager in integration tests to avoid Docker calls in CI - Add SessionManager mock to prevent Docker operations during tests - Fix claude-webhook.test.ts to use proper test setup and payload structure - Ensure all integration tests can run without Docker dependency - Fix payload structure to include 'data' wrapper 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: Mock child_process to prevent Docker calls in CI tests - Mock execSync and spawn at child_process level to prevent any Docker commands - This ensures tests work in CI environment without Docker - Tests now pass both locally and in CI Docker build 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: Address PR review comments and fix linter warnings - Move @types/uuid to devDependencies - Replace timestamp+Math.random with crypto.randomUUID() for better uniqueness - Extract magic number into EXTRA_SESSIONS_COUNT constant - Update determineStrategy return type to use literal union - Fix unnecessary optional chaining warnings - Handle undefined labels in GitHub transformers - Make TaskDecomposer.decompose synchronous - Add proper eslint-disable comments for intentional sync methods - Fix all TypeScript and formatting issues * fix: Mock SessionManager in integration tests to prevent Docker calls in CI - Add SessionManager mocks to claude-session.test.ts - Add SessionManager mocks to claude-webhook.test.ts - Prevents 500 errors when running tests in CI without Docker - All integration tests now pass without requiring Docker runtime * fix: Run only unit tests in Docker builds to avoid Docker-in-Docker issues - Change test stage to run 'npm run test:unit' instead of 'npm test' - Skips integration tests that require Docker runtime - Prevents CI failures in Docker container builds - Integration tests still run in regular CI workflow * fix: Use Dockerfile CMD for tests in Docker build CI - Remove explicit 'npm test' command from docker run - Let Docker use the CMD defined in Dockerfile (npm run test:unit) - This ensures consistency and runs only unit tests in Docker builds --------- Co-authored-by: Claude <noreply@anthropic.com>
210 lines
5.6 KiB
TypeScript
210 lines
5.6 KiB
TypeScript
import crypto from 'crypto';
|
|
import { createLogger } from '../../utils/logger';
|
|
import type { WebhookRequest } from '../../types/express';
|
|
import type {
|
|
WebhookProvider,
|
|
BaseWebhookPayload,
|
|
RepositoryInfo,
|
|
UserInfo,
|
|
IssueInfo,
|
|
PullRequestInfo
|
|
} from '../../types/webhook';
|
|
import type {
|
|
GitHubRepository,
|
|
GitHubUser,
|
|
GitHubIssue,
|
|
GitHubPullRequest
|
|
} from '../../types/github';
|
|
|
|
const logger = createLogger('GitHubWebhookProvider');
|
|
|
|
/**
|
|
* GitHub-specific webhook payload
|
|
*/
|
|
export interface GitHubWebhookEvent extends BaseWebhookPayload {
|
|
githubEvent: string;
|
|
githubDelivery: string;
|
|
action?: string;
|
|
repository?: GitHubRepository;
|
|
sender?: GitHubUser;
|
|
installation?: {
|
|
id: number;
|
|
account: GitHubUser;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* GitHub webhook provider implementation
|
|
*/
|
|
export class GitHubWebhookProvider implements WebhookProvider<GitHubWebhookEvent> {
|
|
readonly name = 'github';
|
|
|
|
/**
|
|
* Verify GitHub webhook signature
|
|
*/
|
|
verifySignature(req: WebhookRequest, secret: string): Promise<boolean> {
|
|
// eslint-disable-next-line no-sync
|
|
return Promise.resolve(this.verifySignatureSync(req, secret));
|
|
}
|
|
|
|
private verifySignatureSync(req: WebhookRequest, secret: string): boolean {
|
|
const signature = req.headers['x-hub-signature-256'] as string;
|
|
if (!signature) {
|
|
logger.warn('No signature found in GitHub webhook request');
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
const payload = req.rawBody ?? JSON.stringify(req.body);
|
|
const hmac = crypto.createHmac('sha256', secret);
|
|
const calculatedSignature = 'sha256=' + hmac.update(payload).digest('hex');
|
|
|
|
// Use timing-safe comparison
|
|
if (
|
|
signature.length === calculatedSignature.length &&
|
|
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(calculatedSignature))
|
|
) {
|
|
logger.debug('GitHub webhook signature verified successfully');
|
|
return true;
|
|
}
|
|
|
|
logger.warn('GitHub webhook signature verification failed');
|
|
return false;
|
|
} catch (error) {
|
|
logger.error({ err: error }, 'Error verifying GitHub webhook signature');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse GitHub webhook payload
|
|
*/
|
|
parsePayload(req: WebhookRequest): Promise<GitHubWebhookEvent> {
|
|
// eslint-disable-next-line no-sync
|
|
return Promise.resolve(this.parsePayloadSync(req));
|
|
}
|
|
|
|
private parsePayloadSync(req: WebhookRequest): GitHubWebhookEvent {
|
|
const githubEvent = req.headers['x-github-event'] as string;
|
|
const githubDelivery = req.headers['x-github-delivery'] as string;
|
|
const payload = req.body;
|
|
|
|
return {
|
|
id: githubDelivery || crypto.randomUUID(),
|
|
timestamp: new Date().toISOString(),
|
|
event: this.normalizeEventType(githubEvent, payload.action),
|
|
source: 'github',
|
|
githubEvent,
|
|
githubDelivery,
|
|
action: payload.action,
|
|
repository: payload.repository,
|
|
sender: payload.sender,
|
|
installation: payload.installation,
|
|
data: payload
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get normalized event type
|
|
*/
|
|
getEventType(payload: GitHubWebhookEvent): string {
|
|
return payload.event;
|
|
}
|
|
|
|
/**
|
|
* Get human-readable event description
|
|
*/
|
|
getEventDescription(payload: GitHubWebhookEvent): string {
|
|
const parts = [payload.githubEvent];
|
|
if (payload.action) {
|
|
parts.push(payload.action);
|
|
}
|
|
if (payload.repository) {
|
|
parts.push(`in ${payload.repository.full_name}`);
|
|
}
|
|
if (payload.sender) {
|
|
parts.push(`by ${payload.sender.login}`);
|
|
}
|
|
return parts.join(' ');
|
|
}
|
|
|
|
/**
|
|
* Normalize GitHub event type to a consistent format
|
|
*/
|
|
private normalizeEventType(event: string, action?: string): string {
|
|
if (!action) {
|
|
return event;
|
|
}
|
|
return `${event}.${action}`;
|
|
}
|
|
|
|
/**
|
|
* Transform GitHub repository to generic format
|
|
*/
|
|
static transformRepository(repo: GitHubRepository): RepositoryInfo {
|
|
return {
|
|
id: repo.id.toString(),
|
|
name: repo.name,
|
|
fullName: repo.full_name,
|
|
owner: repo.owner.login,
|
|
isPrivate: repo.private,
|
|
defaultBranch: repo.default_branch
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Transform GitHub user to generic format
|
|
*/
|
|
static transformUser(user: GitHubUser): UserInfo {
|
|
return {
|
|
id: user.id.toString(),
|
|
username: user.login,
|
|
email: user.email,
|
|
displayName: user.name ?? user.login
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Transform GitHub issue to generic format
|
|
*/
|
|
static transformIssue(issue: GitHubIssue): IssueInfo {
|
|
return {
|
|
id: issue.id,
|
|
number: issue.number,
|
|
title: issue.title,
|
|
body: issue.body ?? '',
|
|
state: issue.state,
|
|
author: GitHubWebhookProvider.transformUser(issue.user),
|
|
labels: issue.labels
|
|
? issue.labels.map(label => (typeof label === 'string' ? label : label.name))
|
|
: [],
|
|
createdAt: new Date(issue.created_at),
|
|
updatedAt: new Date(issue.updated_at)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Transform GitHub pull request to generic format
|
|
*/
|
|
static transformPullRequest(pr: GitHubPullRequest): PullRequestInfo {
|
|
return {
|
|
id: pr.id,
|
|
number: pr.number,
|
|
title: pr.title,
|
|
body: pr.body ?? '',
|
|
state: pr.state as 'open' | 'closed',
|
|
author: GitHubWebhookProvider.transformUser(pr.user),
|
|
labels: pr.labels
|
|
? pr.labels.map(label => (typeof label === 'string' ? label : label.name))
|
|
: [],
|
|
createdAt: new Date(pr.created_at),
|
|
updatedAt: new Date(pr.updated_at),
|
|
sourceBranch: pr.head.ref,
|
|
targetBranch: pr.base.ref,
|
|
isDraft: pr.draft || false,
|
|
isMerged: pr.merged || false,
|
|
mergedAt: pr.merged_at ? new Date(pr.merged_at) : undefined
|
|
};
|
|
}
|
|
}
|