forked from claude-did-this/claude-hub
Compare commits
12 Commits
fix-claude
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6005ced7ff | ||
|
|
3c8aebced8 | ||
|
|
c067efa13e | ||
|
|
65a590784c | ||
|
|
9a8187d72a | ||
|
|
42201732c1 | ||
|
|
be941b2149 | ||
|
|
a423786200 | ||
|
|
ea812f5b8f | ||
|
|
346199ebbd | ||
|
|
8da021bb00 | ||
|
|
8926d0026d |
@@ -1,5 +1,6 @@
|
||||
codecov:
|
||||
require_ci_to_pass: false
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
coverage:
|
||||
status:
|
||||
@@ -25,4 +26,4 @@ comment:
|
||||
|
||||
github_checks:
|
||||
# Disable check suites to prevent hanging on non-main branches
|
||||
annotations: false
|
||||
annotations: false
|
||||
|
||||
6
.github/workflows/deploy.yml
vendored
6
.github/workflows/deploy.yml
vendored
@@ -191,7 +191,7 @@ jobs:
|
||||
run: rm -f .env.staging
|
||||
|
||||
- name: Create deployment record
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
await github.rest.repos.createDeployment({
|
||||
@@ -268,7 +268,7 @@ jobs:
|
||||
run: rm -f .env
|
||||
|
||||
- name: Create deployment record
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const deployment = await github.rest.repos.createDeployment({
|
||||
@@ -291,7 +291,7 @@ jobs:
|
||||
});
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
await github.rest.repos.createRelease({
|
||||
|
||||
8
.github/workflows/main.yml
vendored
8
.github/workflows/main.yml
vendored
@@ -22,12 +22,18 @@ jobs:
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run lint:check
|
||||
- run: npm run test:ci
|
||||
- name: Install CLI dependencies
|
||||
working-directory: ./cli
|
||||
run: npm ci
|
||||
- name: Generate combined coverage
|
||||
run: ./scripts/combine-coverage.js
|
||||
env:
|
||||
NODE_ENV: test
|
||||
- uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
directory: ./coverage-combined
|
||||
fail_ci_if_error: true
|
||||
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
19
.github/workflows/pull-request.yml
vendored
19
.github/workflows/pull-request.yml
vendored
@@ -17,16 +17,33 @@ jobs:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run format:check
|
||||
- run: npm run lint:check
|
||||
- run: npm run test:unit
|
||||
- run: npm run typecheck
|
||||
- name: Install CLI dependencies
|
||||
working-directory: ./cli
|
||||
run: npm ci
|
||||
- name: Generate combined coverage
|
||||
run: ./scripts/combine-coverage.js
|
||||
env:
|
||||
NODE_ENV: test
|
||||
- uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
directory: ./coverage-combined
|
||||
fail_ci_if_error: true
|
||||
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: ./scripts/security/credential-audit.sh
|
||||
- uses: trufflesecurity/trufflehog@main
|
||||
with:
|
||||
path: ./
|
||||
base: ${{ github.event.pull_request.base.sha }}
|
||||
head: ${{ github.event.pull_request.head.sha }}
|
||||
extra_args: --debug --only-verified
|
||||
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
20
BREAKING_CHANGES.md
Normal file
20
BREAKING_CHANGES.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Breaking Changes
|
||||
|
||||
## PR #181 - Enhanced Session Validation and API Documentation
|
||||
|
||||
### Event Pattern Change
|
||||
- **Changed**: Session handler event pattern changed from `session` to `session*`
|
||||
- **Impact**: Any integrations listening for specific session events may need to update their event filtering logic
|
||||
- **Migration**: Update event listeners to use wildcard pattern matching or specific event names (e.g., `session.create`, `session.start`)
|
||||
|
||||
### Volume Naming Pattern
|
||||
- **Changed**: Volume naming pattern in SessionManager changed to use a more consistent format
|
||||
- **Previous**: Various inconsistent naming patterns
|
||||
- **New**: Standardized naming with session ID prefixes
|
||||
- **Impact**: Existing volumes created with old naming patterns may not be recognized
|
||||
- **Migration**: Existing sessions may need to be recreated or volumes renamed to match new pattern
|
||||
|
||||
### API Validation
|
||||
- **Added**: Strict UUID validation for session dependencies
|
||||
- **Impact**: Sessions with invalid dependency IDs will now be rejected
|
||||
- **Migration**: Ensure all dependency IDs are valid UUIDs before creating sessions
|
||||
@@ -11,8 +11,8 @@ Get Claude responding to your GitHub issues in minutes using Cloudflare Tunnel.
|
||||
## Step 1: Create a GitHub Bot Account
|
||||
|
||||
1. Sign out of GitHub and create a new account for your bot (e.g., `YourProjectBot`)
|
||||
2. In your main account, create a [Personal Access Token](https://github.com/settings/tokens) with `repo` and `write` permissions
|
||||
3. Add the bot account as a collaborator to your repositories
|
||||
2. Sign in to your bot account and create a [Personal Access Token](https://github.com/settings/tokens) with `repo` and `write` permissions
|
||||
3. Add the bot account as a collaborator to your repositories from your main account
|
||||
|
||||
## Step 2: Clone and Configure
|
||||
|
||||
@@ -29,7 +29,7 @@ nano .env
|
||||
```
|
||||
|
||||
Required values:
|
||||
- `GITHUB_TOKEN`: Your GitHub Personal Access Token
|
||||
- `GITHUB_TOKEN`: Your bot account's GitHub Personal Access Token
|
||||
- `GITHUB_WEBHOOK_SECRET`: Generate with `openssl rand -hex 32`
|
||||
- `BOT_USERNAME`: Your bot's GitHub username (e.g., `@YourProjectBot`)
|
||||
- `BOT_EMAIL`: Your bot's email
|
||||
|
||||
21
README.md
21
README.md
@@ -77,13 +77,6 @@ That's it! Your bot is ready to use. See the **[complete quickstart guide](./QUI
|
||||
- **Context-aware**: Claude understands your entire repository structure and development patterns
|
||||
- **Stateless execution**: Each request runs in isolated Docker containers
|
||||
|
||||
### Claude Orchestration (NEW) 🎭
|
||||
- **Parallel Claude Sessions**: Run multiple Claude containers concurrently for complex tasks
|
||||
- **Smart Task Decomposition**: Automatically breaks down projects into parallel workstreams
|
||||
- **Dependency Management**: Sessions wait for prerequisites before starting
|
||||
- **MCP Integration**: Built for the MCP hackathon to showcase super-charged Claude capabilities
|
||||
- **See [Claude Orchestration Documentation](./docs/claude-orchestration.md) for details**
|
||||
|
||||
### Performance Architecture ⚡
|
||||
- Parallel test execution with strategic runner distribution
|
||||
- Conditional Docker builds (only when code changes)
|
||||
@@ -103,7 +96,7 @@ That's it! Your bot is ready to use. See the **[complete quickstart guide](./QUI
|
||||
**Current Setup**: You need to create your own GitHub bot account:
|
||||
|
||||
1. **Create a dedicated GitHub account** for your bot (e.g., `MyProjectBot`)
|
||||
2. **Generate a Personal Access Token** with repository permissions
|
||||
2. **Generate a Personal Access Token** from the bot account with repository permissions
|
||||
3. **Configure the bot username** in your environment variables
|
||||
4. **Add the bot account** as a collaborator to your repositories
|
||||
|
||||
@@ -117,7 +110,7 @@ That's it! Your bot is ready to use. See the **[complete quickstart guide](./QUI
|
||||
# Core settings
|
||||
BOT_USERNAME=YourBotName # GitHub bot account username (create your own bot account)
|
||||
GITHUB_WEBHOOK_SECRET=<generated> # Webhook validation
|
||||
GITHUB_TOKEN=<fine-grained-pat> # Repository access (from your bot account)
|
||||
GITHUB_TOKEN=<fine-grained-pat> # Repository access (PAT from your bot account)
|
||||
|
||||
# Claude Authentication - Choose ONE method:
|
||||
|
||||
@@ -374,14 +367,6 @@ npm run dev
|
||||
- ESLint + Prettier for code formatting
|
||||
- Conventional commits for version management
|
||||
|
||||
### Security Checklist
|
||||
|
||||
- [ ] No hardcoded credentials
|
||||
- [ ] All inputs sanitized
|
||||
- [ ] Webhook signatures verified
|
||||
- [ ] Container permissions minimal
|
||||
- [ ] Logs redact sensitive data
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
@@ -408,4 +393,4 @@ npm run dev
|
||||
|
||||
## License
|
||||
|
||||
MIT - See the [LICENSE file](LICENSE) for details.
|
||||
MIT - See the [LICENSE file](LICENSE) for details.
|
||||
|
||||
83
analyze-combined-coverage.js
Executable file
83
analyze-combined-coverage.js
Executable file
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Read combined lcov.info
|
||||
const lcovPath = path.join(__dirname, 'coverage-combined', 'lcov.info');
|
||||
if (!fs.existsSync(lcovPath)) {
|
||||
console.error('No coverage-combined/lcov.info file found. Run npm run test:combined-coverage first.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const lcovContent = fs.readFileSync(lcovPath, 'utf8');
|
||||
const lines = lcovContent.split('\n');
|
||||
|
||||
let currentFile = null;
|
||||
const fileStats = {};
|
||||
let totalLines = 0;
|
||||
let coveredLines = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('SF:')) {
|
||||
currentFile = line.substring(3);
|
||||
if (!fileStats[currentFile]) {
|
||||
fileStats[currentFile] = { lines: 0, covered: 0, functions: 0, functionsHit: 0 };
|
||||
}
|
||||
} else if (line.startsWith('DA:')) {
|
||||
const [lineNum, hits] = line.substring(3).split(',').map(Number);
|
||||
if (currentFile) {
|
||||
fileStats[currentFile].lines++;
|
||||
totalLines++;
|
||||
if (hits > 0) {
|
||||
fileStats[currentFile].covered++;
|
||||
coveredLines++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const overallCoverage = (coveredLines / totalLines) * 100;
|
||||
|
||||
console.log('\n=== Combined Coverage Analysis ===\n');
|
||||
console.log(`Total Lines: ${totalLines}`);
|
||||
console.log(`Covered Lines: ${coveredLines}`);
|
||||
console.log(`Overall Coverage: ${overallCoverage.toFixed(2)}%`);
|
||||
console.log(`Target: 80%`);
|
||||
console.log(`Status: ${overallCoverage >= 80 ? '✅ PASSED' : '❌ FAILED'}\n`);
|
||||
|
||||
// Break down by directory
|
||||
const srcFiles = Object.entries(fileStats).filter(([file]) => file.startsWith('src/'));
|
||||
const cliFiles = Object.entries(fileStats).filter(([file]) => file.startsWith('cli/'));
|
||||
|
||||
const srcStats = srcFiles.reduce((acc, [, stats]) => ({
|
||||
lines: acc.lines + stats.lines,
|
||||
covered: acc.covered + stats.covered
|
||||
}), { lines: 0, covered: 0 });
|
||||
|
||||
const cliStats = cliFiles.reduce((acc, [, stats]) => ({
|
||||
lines: acc.lines + stats.lines,
|
||||
covered: acc.covered + stats.covered
|
||||
}), { lines: 0, covered: 0 });
|
||||
|
||||
console.log('=== Coverage by Component ===');
|
||||
console.log(`Main src/: ${((srcStats.covered / srcStats.lines) * 100).toFixed(2)}% (${srcStats.covered}/${srcStats.lines} lines)`);
|
||||
console.log(`CLI: ${((cliStats.covered / cliStats.lines) * 100).toFixed(2)}% (${cliStats.covered}/${cliStats.lines} lines)`);
|
||||
|
||||
// Show files with lowest coverage
|
||||
console.log('\n=== Files with Lowest Coverage ===');
|
||||
const sorted = Object.entries(fileStats)
|
||||
.map(([file, stats]) => ({
|
||||
file,
|
||||
coverage: (stats.covered / stats.lines) * 100,
|
||||
lines: stats.lines,
|
||||
covered: stats.covered
|
||||
}))
|
||||
.sort((a, b) => a.coverage - b.coverage)
|
||||
.slice(0, 10);
|
||||
|
||||
sorted.forEach(({ file, coverage, covered, lines }) => {
|
||||
console.log(`${file.padEnd(60)} ${coverage.toFixed(2).padStart(6)}% (${covered}/${lines})`);
|
||||
});
|
||||
|
||||
process.exit(overallCoverage >= 80 ? 0 : 1);
|
||||
83
analyze-coverage.js
Normal file
83
analyze-coverage.js
Normal file
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Read lcov.info
|
||||
const lcovPath = path.join(__dirname, 'coverage', 'lcov.info');
|
||||
if (!fs.existsSync(lcovPath)) {
|
||||
console.error('No coverage/lcov.info file found. Run npm test:coverage first.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const lcovContent = fs.readFileSync(lcovPath, 'utf8');
|
||||
const lines = lcovContent.split('\n');
|
||||
|
||||
let currentFile = null;
|
||||
const fileStats = {};
|
||||
let totalLines = 0;
|
||||
let coveredLines = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('SF:')) {
|
||||
currentFile = line.substring(3);
|
||||
if (!fileStats[currentFile]) {
|
||||
fileStats[currentFile] = { lines: 0, covered: 0, functions: 0, functionsHit: 0 };
|
||||
}
|
||||
} else if (line.startsWith('DA:')) {
|
||||
const [lineNum, hits] = line.substring(3).split(',').map(Number);
|
||||
if (currentFile) {
|
||||
fileStats[currentFile].lines++;
|
||||
totalLines++;
|
||||
if (hits > 0) {
|
||||
fileStats[currentFile].covered++;
|
||||
coveredLines++;
|
||||
}
|
||||
}
|
||||
} else if (line.startsWith('FNF:')) {
|
||||
if (currentFile) {
|
||||
fileStats[currentFile].functions = parseInt(line.substring(4));
|
||||
}
|
||||
} else if (line.startsWith('FNH:')) {
|
||||
if (currentFile) {
|
||||
fileStats[currentFile].functionsHit = parseInt(line.substring(4));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n=== Coverage Analysis ===\n');
|
||||
console.log(`Total Lines: ${totalLines}`);
|
||||
console.log(`Covered Lines: ${coveredLines}`);
|
||||
console.log(`Overall Coverage: ${((coveredLines / totalLines) * 100).toFixed(2)}%\n`);
|
||||
|
||||
console.log('=== File Breakdown ===\n');
|
||||
const sortedFiles = Object.entries(fileStats).sort((a, b) => {
|
||||
const coverageA = (a[1].covered / a[1].lines) * 100;
|
||||
const coverageB = (b[1].covered / b[1].lines) * 100;
|
||||
return coverageA - coverageB;
|
||||
});
|
||||
|
||||
for (const [file, stats] of sortedFiles) {
|
||||
const coverage = ((stats.covered / stats.lines) * 100).toFixed(2);
|
||||
console.log(`${file.padEnd(60)} ${coverage.padStart(6)}% (${stats.covered}/${stats.lines} lines)`);
|
||||
}
|
||||
|
||||
// Check if CLI coverage is included
|
||||
console.log('\n=== Coverage Scope Analysis ===\n');
|
||||
const cliFiles = sortedFiles.filter(([file]) => file.includes('cli/'));
|
||||
const srcFiles = sortedFiles.filter(([file]) => file.startsWith('src/'));
|
||||
|
||||
console.log(`Main src/ files: ${srcFiles.length}`);
|
||||
console.log(`CLI files: ${cliFiles.length}`);
|
||||
|
||||
if (cliFiles.length > 0) {
|
||||
console.log('\nCLI files found in coverage:');
|
||||
cliFiles.forEach(([file]) => console.log(` - ${file}`));
|
||||
}
|
||||
|
||||
// Check for any unexpected files
|
||||
const otherFiles = sortedFiles.filter(([file]) => !file.startsWith('src/') && !file.includes('cli/'));
|
||||
if (otherFiles.length > 0) {
|
||||
console.log('\nOther files in coverage:');
|
||||
otherFiles.forEach(([file]) => console.log(` - ${file}`));
|
||||
}
|
||||
99
calculate-codecov-match.js
Normal file
99
calculate-codecov-match.js
Normal file
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Coverage data from the test output
|
||||
const coverageData = {
|
||||
'src/index.ts': { statements: 92.64, branches: 78.94, functions: 85.71, lines: 92.64 },
|
||||
'src/controllers/githubController.ts': { statements: 69.65, branches: 64.47, functions: 84.61, lines: 69.2 },
|
||||
'src/core/webhook/WebhookProcessor.ts': { statements: 100, branches: 92.3, functions: 100, lines: 100 },
|
||||
'src/core/webhook/WebhookRegistry.ts': { statements: 97.77, branches: 100, functions: 100, lines: 97.67 },
|
||||
'src/core/webhook/constants.ts': { statements: 100, branches: 100, functions: 100, lines: 100 },
|
||||
'src/core/webhook/index.ts': { statements: 0, branches: 100, functions: 0, lines: 0 },
|
||||
'src/providers/claude/ClaudeWebhookProvider.ts': { statements: 77.41, branches: 46.66, functions: 100, lines: 77.41 },
|
||||
'src/providers/claude/index.ts': { statements: 100, branches: 100, functions: 0, lines: 100 },
|
||||
'src/providers/claude/handlers/OrchestrationHandler.ts': { statements: 95.65, branches: 75, functions: 100, lines: 95.65 },
|
||||
'src/providers/claude/handlers/SessionHandler.ts': { statements: 96.66, branches: 89.28, functions: 100, lines: 96.66 },
|
||||
'src/providers/claude/services/SessionManager.ts': { statements: 6.06, branches: 0, functions: 0, lines: 6.06 },
|
||||
'src/providers/claude/services/TaskDecomposer.ts': { statements: 96.87, branches: 93.75, functions: 100, lines: 96.66 },
|
||||
'src/providers/github/GitHubWebhookProvider.ts': { statements: 95.45, branches: 90.62, functions: 100, lines: 95.45 },
|
||||
'src/providers/github/index.ts': { statements: 100, branches: 100, functions: 100, lines: 100 },
|
||||
'src/providers/github/handlers/IssueHandler.ts': { statements: 30.43, branches: 0, functions: 0, lines: 30.43 },
|
||||
'src/routes/github.ts': { statements: 100, branches: 100, functions: 100, lines: 100 },
|
||||
'src/routes/webhooks.ts': { statements: 92.1, branches: 100, functions: 57.14, lines: 91.66 },
|
||||
'src/services/claudeService.ts': { statements: 85.62, branches: 66.17, functions: 100, lines: 86.66 },
|
||||
'src/services/githubService.ts': { statements: 72.22, branches: 78.57, functions: 75, lines: 71.93 },
|
||||
'src/types/claude.ts': { statements: 0, branches: 100, functions: 100, lines: 0 },
|
||||
'src/types/environment.ts': { statements: 0, branches: 0, functions: 0, lines: 0 },
|
||||
'src/types/index.ts': { statements: 0, branches: 0, functions: 0, lines: 0 },
|
||||
'src/utils/awsCredentialProvider.ts': { statements: 65.68, branches: 59.25, functions: 54.54, lines: 65.68 },
|
||||
'src/utils/logger.ts': { statements: 51.61, branches: 47.36, functions: 100, lines: 51.72 },
|
||||
'src/utils/sanitize.ts': { statements: 100, branches: 100, functions: 100, lines: 100 },
|
||||
'src/utils/secureCredentials.ts': { statements: 54.28, branches: 70.58, functions: 33.33, lines: 54.28 },
|
||||
'src/utils/startup-metrics.ts': { statements: 100, branches: 100, functions: 100, lines: 100 }
|
||||
};
|
||||
|
||||
// Calculate different scenarios
|
||||
console.log('\n=== Coverage Analysis - Matching Codecov ===\n');
|
||||
|
||||
// Scenario 1: Exclude type definition files
|
||||
const withoutTypes = Object.entries(coverageData)
|
||||
.filter(([file]) => !file.includes('/types/'))
|
||||
.reduce((acc, [file, data]) => {
|
||||
acc[file] = data;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const avgWithoutTypes = calculateAverage(withoutTypes);
|
||||
console.log(`1. Without type files: ${avgWithoutTypes.toFixed(2)}%`);
|
||||
|
||||
// Scenario 2: Exclude files with 0% coverage
|
||||
const withoutZeroCoverage = Object.entries(coverageData)
|
||||
.filter(([file, data]) => data.lines > 0)
|
||||
.reduce((acc, [file, data]) => {
|
||||
acc[file] = data;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const avgWithoutZero = calculateAverage(withoutZeroCoverage);
|
||||
console.log(`2. Without 0% coverage files: ${avgWithoutZero.toFixed(2)}%`);
|
||||
|
||||
// Scenario 3: Exclude specific low coverage files
|
||||
const excludeLowCoverage = Object.entries(coverageData)
|
||||
.filter(([file]) => {
|
||||
return !file.includes('/types/') &&
|
||||
!file.includes('SessionManager.ts') &&
|
||||
!file.includes('IssueHandler.ts');
|
||||
})
|
||||
.reduce((acc, [file, data]) => {
|
||||
acc[file] = data;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const avgExcludeLow = calculateAverage(excludeLowCoverage);
|
||||
console.log(`3. Without types, SessionManager, IssueHandler: ${avgExcludeLow.toFixed(2)}%`);
|
||||
|
||||
// Scenario 4: Statement coverage only (what codecov might be reporting)
|
||||
const statementOnly = calculateStatementAverage(coverageData);
|
||||
console.log(`4. Statement coverage only: ${statementOnly.toFixed(2)}%`);
|
||||
|
||||
// Show which files have the biggest impact
|
||||
console.log('\n=== Files with lowest coverage ===');
|
||||
const sorted = Object.entries(coverageData)
|
||||
.sort((a, b) => a[1].lines - b[1].lines)
|
||||
.slice(0, 10);
|
||||
|
||||
sorted.forEach(([file, data]) => {
|
||||
console.log(`${file.padEnd(60)} ${data.lines.toFixed(2)}%`);
|
||||
});
|
||||
|
||||
function calculateAverage(data) {
|
||||
const values = Object.values(data).map(d => d.lines);
|
||||
return values.reduce((sum, val) => sum + val, 0) / values.length;
|
||||
}
|
||||
|
||||
function calculateStatementAverage(data) {
|
||||
const values = Object.values(data).map(d => d.statements);
|
||||
return values.reduce((sum, val) => sum + val, 0) / values.length;
|
||||
}
|
||||
569
claude-api-swagger.yaml
Normal file
569
claude-api-swagger.yaml
Normal file
@@ -0,0 +1,569 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Claude Webhook API
|
||||
description: |
|
||||
API for creating and managing Claude Code sessions for automated code generation, analysis, and orchestration.
|
||||
This API enables parallel execution of multiple Claude instances for complex software engineering tasks.
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: Claude Hub Support
|
||||
url: https://github.com/claude-hub/claude-hub
|
||||
|
||||
servers:
|
||||
- url: https://your-domain.com
|
||||
description: Production server
|
||||
- url: http://localhost:3002
|
||||
description: Local development server
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
|
||||
paths:
|
||||
/health:
|
||||
get:
|
||||
summary: Health check
|
||||
description: Check the health status of the API and its dependencies
|
||||
tags:
|
||||
- System
|
||||
security: []
|
||||
responses:
|
||||
'200':
|
||||
description: Service is healthy
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HealthCheckResponse'
|
||||
|
||||
/api/webhooks/health:
|
||||
get:
|
||||
summary: Webhook health check
|
||||
description: Check the health status of webhook providers
|
||||
tags:
|
||||
- System
|
||||
security: []
|
||||
responses:
|
||||
'200':
|
||||
description: Webhook providers are healthy
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
example: healthy
|
||||
providers:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
handlerCount:
|
||||
type: integer
|
||||
|
||||
/api/webhooks/github:
|
||||
post:
|
||||
summary: GitHub webhook endpoint (legacy)
|
||||
description: Legacy endpoint for GitHub webhooks. Use /api/webhooks/github instead.
|
||||
deprecated: true
|
||||
tags:
|
||||
- Webhooks
|
||||
security: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
responses:
|
||||
'200':
|
||||
description: Webhook processed successfully
|
||||
'401':
|
||||
description: Invalid webhook signature
|
||||
'404':
|
||||
description: Webhook event not handled
|
||||
|
||||
/api/webhooks/{provider}:
|
||||
post:
|
||||
summary: Generic webhook endpoint
|
||||
description: Process webhooks from various providers (github, claude)
|
||||
tags:
|
||||
- Webhooks
|
||||
security: []
|
||||
parameters:
|
||||
- name: provider
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
enum: [github, claude]
|
||||
description: The webhook provider name
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/ClaudeWebhookRequest'
|
||||
- $ref: '#/components/schemas/GitHubWebhookPayload'
|
||||
examples:
|
||||
createSession:
|
||||
summary: Create a new Claude session
|
||||
value:
|
||||
type: session.create
|
||||
session:
|
||||
type: implementation
|
||||
project:
|
||||
repository: acme/webapp
|
||||
branch: feature/user-auth
|
||||
requirements: Implement JWT authentication middleware for Express.js with refresh token support
|
||||
context: Use existing User model, bcrypt for passwords, and jsonwebtoken library
|
||||
dependencies: []
|
||||
createSessionWithDependencies:
|
||||
summary: Create a session that depends on others
|
||||
value:
|
||||
type: session.create
|
||||
session:
|
||||
type: testing
|
||||
project:
|
||||
repository: acme/webapp
|
||||
branch: feature/user-auth
|
||||
requirements: Write comprehensive integration tests for the JWT authentication middleware
|
||||
context: Test all edge cases including token expiration, invalid tokens, and refresh flow
|
||||
dependencies:
|
||||
- 550e8400-e29b-41d4-a716-446655440000
|
||||
- 660e8400-e29b-41d4-a716-446655440001
|
||||
startSession:
|
||||
summary: Start an existing session
|
||||
value:
|
||||
type: session.start
|
||||
sessionId: 550e8400-e29b-41d4-a716-446655440000
|
||||
orchestrate:
|
||||
summary: Create an orchestration with multiple sessions
|
||||
value:
|
||||
type: orchestrate
|
||||
autoStart: true
|
||||
project:
|
||||
repository: acme/webapp
|
||||
branch: feature/complete-auth
|
||||
requirements: |
|
||||
Implement a complete authentication system:
|
||||
1. JWT middleware with refresh tokens
|
||||
2. User registration and login endpoints
|
||||
3. Password reset functionality
|
||||
4. Integration tests for all auth endpoints
|
||||
context: Use existing User model, PostgreSQL database, and follow REST API conventions
|
||||
responses:
|
||||
'200':
|
||||
description: Webhook processed successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/WebhookResponse'
|
||||
examples:
|
||||
sessionCreated:
|
||||
summary: Session created successfully
|
||||
value:
|
||||
success: true
|
||||
message: Session created successfully
|
||||
data:
|
||||
session:
|
||||
id: 550e8400-e29b-41d4-a716-446655440000
|
||||
type: implementation
|
||||
status: initializing
|
||||
containerId: claude-session-550e8400
|
||||
project:
|
||||
repository: acme/webapp
|
||||
branch: feature/user-auth
|
||||
requirements: Implement JWT authentication middleware for Express.js with refresh token support
|
||||
context: Use existing User model, bcrypt for passwords, and jsonwebtoken library
|
||||
dependencies: []
|
||||
sessionStarted:
|
||||
summary: Session started with dependencies
|
||||
value:
|
||||
success: true
|
||||
message: Session queued, waiting for dependencies
|
||||
data:
|
||||
session:
|
||||
id: 660e8400-e29b-41d4-a716-446655440001
|
||||
status: pending
|
||||
waitingFor:
|
||||
- 550e8400-e29b-41d4-a716-446655440000
|
||||
'400':
|
||||
description: Bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'401':
|
||||
description: Unauthorized - Invalid token or signature
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'404':
|
||||
description: Provider not found or session not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'409':
|
||||
description: Conflict - Session already started
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'429':
|
||||
description: Too many requests
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
example: Too many webhook requests
|
||||
message:
|
||||
type: string
|
||||
example: Too many webhook requests from this IP, please try again later.
|
||||
'500':
|
||||
description: Internal server error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
description: Use CLAUDE_WEBHOOK_SECRET as the bearer token
|
||||
|
||||
schemas:
|
||||
HealthCheckResponse:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
enum: [ok, degraded]
|
||||
timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
startup:
|
||||
type: object
|
||||
properties:
|
||||
totalStartupTime:
|
||||
type: integer
|
||||
milestones:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
docker:
|
||||
type: object
|
||||
properties:
|
||||
available:
|
||||
type: boolean
|
||||
error:
|
||||
type: string
|
||||
nullable: true
|
||||
checkTime:
|
||||
type: integer
|
||||
nullable: true
|
||||
claudeCodeImage:
|
||||
type: object
|
||||
properties:
|
||||
available:
|
||||
type: boolean
|
||||
error:
|
||||
type: string
|
||||
nullable: true
|
||||
checkTime:
|
||||
type: integer
|
||||
nullable: true
|
||||
healthCheckDuration:
|
||||
type: integer
|
||||
|
||||
ClaudeWebhookRequest:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/SessionCreateRequest'
|
||||
- $ref: '#/components/schemas/SessionStartRequest'
|
||||
- $ref: '#/components/schemas/SessionGetRequest'
|
||||
- $ref: '#/components/schemas/SessionOutputRequest'
|
||||
- $ref: '#/components/schemas/SessionListRequest'
|
||||
- $ref: '#/components/schemas/OrchestrateRequest'
|
||||
discriminator:
|
||||
propertyName: type
|
||||
mapping:
|
||||
session.create: '#/components/schemas/SessionCreateRequest'
|
||||
session.start: '#/components/schemas/SessionStartRequest'
|
||||
session.get: '#/components/schemas/SessionGetRequest'
|
||||
session.output: '#/components/schemas/SessionOutputRequest'
|
||||
session.list: '#/components/schemas/SessionListRequest'
|
||||
orchestrate: '#/components/schemas/OrchestrateRequest'
|
||||
|
||||
SessionCreateRequest:
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
- session
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
enum: [session.create]
|
||||
session:
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
- project
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
enum: [implementation, analysis, testing, review, coordination]
|
||||
description: Type of Claude session
|
||||
project:
|
||||
type: object
|
||||
required:
|
||||
- repository
|
||||
- requirements
|
||||
properties:
|
||||
repository:
|
||||
type: string
|
||||
pattern: '^[a-zA-Z0-9-]+/[a-zA-Z0-9-_.]+$'
|
||||
example: acme/webapp
|
||||
description: GitHub repository in owner/repo format
|
||||
branch:
|
||||
type: string
|
||||
example: feature/user-auth
|
||||
description: Target branch name
|
||||
requirements:
|
||||
type: string
|
||||
example: Implement JWT authentication middleware for Express.js
|
||||
description: Clear description of what Claude should do
|
||||
context:
|
||||
type: string
|
||||
example: Use existing User model and bcrypt for password hashing
|
||||
description: Additional context about the codebase or requirements
|
||||
dependencies:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Array of session IDs that must complete before this session starts
|
||||
|
||||
SessionStartRequest:
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
- sessionId
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
enum: [session.start]
|
||||
sessionId:
|
||||
type: string
|
||||
format: uuid
|
||||
example: 550e8400-e29b-41d4-a716-446655440000
|
||||
|
||||
SessionGetRequest:
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
- sessionId
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
enum: [session.get]
|
||||
sessionId:
|
||||
type: string
|
||||
format: uuid
|
||||
|
||||
SessionOutputRequest:
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
- sessionId
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
enum: [session.output]
|
||||
sessionId:
|
||||
type: string
|
||||
format: uuid
|
||||
|
||||
SessionListRequest:
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
enum: [session.list]
|
||||
orchestrationId:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Filter sessions by orchestration ID
|
||||
|
||||
OrchestrateRequest:
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
- project
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
enum: [orchestrate]
|
||||
sessionType:
|
||||
type: string
|
||||
enum: [coordination]
|
||||
default: coordination
|
||||
autoStart:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Whether to start the session immediately
|
||||
project:
|
||||
type: object
|
||||
required:
|
||||
- repository
|
||||
- requirements
|
||||
properties:
|
||||
repository:
|
||||
type: string
|
||||
pattern: '^[a-zA-Z0-9-]+/[a-zA-Z0-9-_.]+$'
|
||||
branch:
|
||||
type: string
|
||||
requirements:
|
||||
type: string
|
||||
context:
|
||||
type: string
|
||||
|
||||
WebhookResponse:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
message:
|
||||
type: string
|
||||
data:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
|
||||
ErrorResponse:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
example: false
|
||||
error:
|
||||
type: string
|
||||
example: Session not found
|
||||
|
||||
Session:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
type:
|
||||
type: string
|
||||
enum: [implementation, analysis, testing, review, coordination]
|
||||
status:
|
||||
type: string
|
||||
enum: [pending, initializing, running, completed, failed, cancelled]
|
||||
containerId:
|
||||
type: string
|
||||
nullable: true
|
||||
claudeSessionId:
|
||||
type: string
|
||||
nullable: true
|
||||
project:
|
||||
type: object
|
||||
properties:
|
||||
repository:
|
||||
type: string
|
||||
branch:
|
||||
type: string
|
||||
requirements:
|
||||
type: string
|
||||
context:
|
||||
type: string
|
||||
dependencies:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
startedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
completedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
output:
|
||||
type: object
|
||||
nullable: true
|
||||
error:
|
||||
type: string
|
||||
nullable: true
|
||||
|
||||
SessionOutput:
|
||||
type: object
|
||||
properties:
|
||||
logs:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
artifacts:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
enum: [file, commit, pr, issue, comment]
|
||||
path:
|
||||
type: string
|
||||
content:
|
||||
type: string
|
||||
sha:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
summary:
|
||||
type: string
|
||||
example: Implemented JWT authentication middleware with refresh token support
|
||||
nextSteps:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example: [Add rate limiting, Implement password reset flow]
|
||||
|
||||
GitHubWebhookPayload:
|
||||
type: object
|
||||
description: GitHub webhook payload (simplified schema)
|
||||
properties:
|
||||
action:
|
||||
type: string
|
||||
repository:
|
||||
type: object
|
||||
properties:
|
||||
full_name:
|
||||
type: string
|
||||
sender:
|
||||
type: object
|
||||
properties:
|
||||
login:
|
||||
type: string
|
||||
|
||||
tags:
|
||||
- name: System
|
||||
description: System health and status endpoints
|
||||
- name: Webhooks
|
||||
description: Webhook processing endpoints
|
||||
- name: Sessions
|
||||
description: Claude session management operations
|
||||
439
cli/package-lock.json
generated
439
cli/package-lock.json
generated
@@ -10,12 +10,9 @@
|
||||
"dependencies": {
|
||||
"axios": "^1.6.2",
|
||||
"chalk": "^4.1.2",
|
||||
"cli-table3": "^0.6.3",
|
||||
"commander": "^14.0.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"ora": "^5.4.1",
|
||||
"uuid": "^9.0.0",
|
||||
"yaml": "^2.3.4"
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"claude-hub": "claude-hub"
|
||||
@@ -24,12 +21,16 @@
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/mock-fs": "^4.13.4",
|
||||
"@types/node": "^20.10.0",
|
||||
"@types/ora": "^3.1.0",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"cli-table3": "^0.6.5",
|
||||
"jest": "^29.5.0",
|
||||
"mock-fs": "^5.5.0",
|
||||
"ora": "^8.2.0",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.3.2"
|
||||
"typescript": "^5.3.2",
|
||||
"yaml": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
@@ -501,6 +502,7 @@
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
|
||||
"integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.1.90"
|
||||
@@ -1016,6 +1018,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.4.tgz",
|
||||
"integrity": "sha512-mXmM0o6lULPI8z3XNnQCpL0BGxPwx1Ul1wXYEPBGl4efShyxW2Rln0JOPEWGyZaYZMM6OVXM/15zUuFMY52ljg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
@@ -1029,6 +1032,16 @@
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ora": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/ora/-/ora-3.1.0.tgz",
|
||||
"integrity": "sha512-4e15N42qhHRlxyP5SpX9fK3q4tXvEkdmGdof2DZ0mqPu7glrNT8cs9bbI73NhwEGApq1TSXhs2aFmn19VCTwCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/stack-utils": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
|
||||
@@ -1099,6 +1112,7 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -1282,35 +1296,6 @@
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/bl": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||
"dependencies": {
|
||||
"buffer": "^5.5.0",
|
||||
"inherits": "^2.0.4",
|
||||
"readable-stream": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
@@ -1386,29 +1371,6 @@
|
||||
"node-int64": "^0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
@@ -1511,20 +1473,26 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/cli-cursor": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
|
||||
"integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
|
||||
"integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"restore-cursor": "^3.1.0"
|
||||
"restore-cursor": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-spinners": {
|
||||
"version": "2.9.2",
|
||||
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
|
||||
"integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
@@ -1536,6 +1504,8 @@
|
||||
"version": "0.6.5",
|
||||
"resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz",
|
||||
"integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0"
|
||||
},
|
||||
@@ -1560,14 +1530,6 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/clone": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
|
||||
"integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/co": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||
@@ -1712,17 +1674,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/defaults": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
|
||||
"integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==",
|
||||
"dependencies": {
|
||||
"clone": "^1.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
@@ -1818,7 +1769,8 @@
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/error-ex": {
|
||||
"version": "1.3.2",
|
||||
@@ -2098,6 +2050,19 @@
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-east-asian-width": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz",
|
||||
"integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
@@ -2260,25 +2225,6 @@
|
||||
"node": ">=10.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/import-local": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz",
|
||||
@@ -2321,7 +2267,8 @@
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/is-arrayish": {
|
||||
"version": "0.2.1",
|
||||
@@ -2348,6 +2295,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -2362,11 +2310,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/is-interactive": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz",
|
||||
"integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
|
||||
"integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
@@ -2391,11 +2344,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/is-unicode-supported": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
|
||||
"integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
|
||||
"integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
@@ -3162,15 +3117,43 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/log-symbols": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
|
||||
"integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz",
|
||||
"integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.0",
|
||||
"is-unicode-supported": "^0.1.0"
|
||||
"chalk": "^5.3.0",
|
||||
"is-unicode-supported": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/log-symbols/node_modules/chalk": {
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
|
||||
"integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/log-symbols/node_modules/is-unicode-supported": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz",
|
||||
"integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
@@ -3277,10 +3260,24 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
|
||||
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/mimic-function": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
|
||||
"integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
@@ -3298,6 +3295,7 @@
|
||||
"resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.5.0.tgz",
|
||||
"integrity": "sha512-d/P1M/RacgM3dB0sJ8rjeRNXxtapkPCUnMGmIN0ixJ16F/E4GUZCvWcSGfWGz8eaXYvn1s9baUwNjI4LOPEjiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
@@ -3360,6 +3358,7 @@
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
|
||||
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"mimic-fn": "^2.1.0"
|
||||
},
|
||||
@@ -3371,27 +3370,96 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ora": {
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz",
|
||||
"integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==",
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz",
|
||||
"integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bl": "^4.1.0",
|
||||
"chalk": "^4.1.0",
|
||||
"cli-cursor": "^3.1.0",
|
||||
"cli-spinners": "^2.5.0",
|
||||
"is-interactive": "^1.0.0",
|
||||
"is-unicode-supported": "^0.1.0",
|
||||
"log-symbols": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wcwidth": "^1.0.1"
|
||||
"chalk": "^5.3.0",
|
||||
"cli-cursor": "^5.0.0",
|
||||
"cli-spinners": "^2.9.2",
|
||||
"is-interactive": "^2.0.0",
|
||||
"is-unicode-supported": "^2.0.0",
|
||||
"log-symbols": "^6.0.0",
|
||||
"stdin-discarder": "^0.2.2",
|
||||
"string-width": "^7.2.0",
|
||||
"strip-ansi": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/ora/node_modules/ansi-regex": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
||||
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ora/node_modules/chalk": {
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
|
||||
"integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ora/node_modules/emoji-regex": {
|
||||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
|
||||
"integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ora/node_modules/string-width": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
|
||||
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^10.3.0",
|
||||
"get-east-asian-width": "^1.0.0",
|
||||
"strip-ansi": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/ora/node_modules/strip-ansi": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||
@@ -3599,19 +3667,6 @@
|
||||
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
@@ -3672,35 +3727,50 @@
|
||||
}
|
||||
},
|
||||
"node_modules/restore-cursor": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
|
||||
"integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
|
||||
"integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"onetime": "^5.1.0",
|
||||
"signal-exit": "^3.0.2"
|
||||
"onetime": "^7.0.0",
|
||||
"signal-exit": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
]
|
||||
"node_modules/restore-cursor/node_modules/onetime": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
|
||||
"integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mimic-function": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/restore-cursor/node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
@@ -3735,7 +3805,8 @@
|
||||
"node_modules/signal-exit": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
|
||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/sisteransi": {
|
||||
"version": "1.0.5",
|
||||
@@ -3789,12 +3860,17 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.2.0"
|
||||
"node_modules/stdin-discarder": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz",
|
||||
"integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/string-length": {
|
||||
@@ -3814,6 +3890,7 @@
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
@@ -3827,6 +3904,7 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
@@ -4105,11 +4183,6 @@
|
||||
"browserslist": ">= 4.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
||||
@@ -4151,14 +4224,6 @@
|
||||
"makeerror": "1.0.12"
|
||||
}
|
||||
},
|
||||
"node_modules/wcwidth": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
|
||||
"integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==",
|
||||
"dependencies": {
|
||||
"defaults": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
@@ -4229,6 +4294,8 @@
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
|
||||
"integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
|
||||
@@ -20,22 +20,23 @@
|
||||
"dependencies": {
|
||||
"axios": "^1.6.2",
|
||||
"chalk": "^4.1.2",
|
||||
"cli-table3": "^0.6.3",
|
||||
"commander": "^14.0.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"ora": "^5.4.1",
|
||||
"uuid": "^9.0.0",
|
||||
"yaml": "^2.3.4"
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/mock-fs": "^4.13.4",
|
||||
"@types/node": "^20.10.0",
|
||||
"@types/ora": "^3.1.0",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"cli-table3": "^0.6.5",
|
||||
"jest": "^29.5.0",
|
||||
"mock-fs": "^5.5.0",
|
||||
"ora": "^8.2.0",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.3.2"
|
||||
"typescript": "^5.3.2",
|
||||
"yaml": "^2.8.0"
|
||||
}
|
||||
}
|
||||
|
||||
3819
coverage-combined/lcov.info
Normal file
3819
coverage-combined/lcov.info
Normal file
File diff suppressed because it is too large
Load Diff
@@ -34,6 +34,7 @@ services:
|
||||
- GITHUB_TOKEN=${GITHUB_TOKEN}
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||
- GITHUB_WEBHOOK_SECRET=${GITHUB_WEBHOOK_SECRET}
|
||||
- CLAUDE_WEBHOOK_SECRET=${CLAUDE_WEBHOOK_SECRET}
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:${PORT:-3002}/health"]
|
||||
|
||||
941
docs/claude-webhook-api.md
Normal file
941
docs/claude-webhook-api.md
Normal file
@@ -0,0 +1,941 @@
|
||||
# Claude Webhook API Documentation
|
||||
|
||||
## Overview
|
||||
The Claude Webhook API provides endpoints for creating and managing Claude Code sessions for automated code generation, analysis, and orchestration. This API is designed to enable parallel execution of multiple Claude instances for complex software engineering tasks.
|
||||
|
||||
## API Design Philosophy
|
||||
This API follows a simple, focused design:
|
||||
- **Single responsibility**: Each session handles one specific task
|
||||
- **Orchestration via MCP/LLM agents**: Complex workflows are managed by the calling agent, not the API
|
||||
- **Consistent response format**: All responses follow the same structure for predictable parsing
|
||||
|
||||
## Base Configuration
|
||||
|
||||
### Base URL
|
||||
```
|
||||
POST https://your-domain.com/api/webhooks/claude
|
||||
```
|
||||
|
||||
### Authentication
|
||||
All requests require Bearer token authentication:
|
||||
```http
|
||||
Authorization: Bearer <CLAUDE_WEBHOOK_SECRET>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
### Response Format
|
||||
All API responses follow this consistent structure:
|
||||
```json
|
||||
{
|
||||
"success": boolean,
|
||||
"message": "string", // Human-readable status message
|
||||
"data": object, // Response data (when success=true)
|
||||
"error": "string" // Error description (when success=false)
|
||||
}
|
||||
```
|
||||
|
||||
### Rate Limiting
|
||||
- Currently not implemented (planned for future release)
|
||||
- Recommended client-side rate limiting: 10 requests per minute
|
||||
|
||||
## Endpoints
|
||||
|
||||
### 1. Create Session
|
||||
Creates a new Claude Code session. Sessions can be configured with dependencies, metadata, and execution options.
|
||||
|
||||
**Endpoint:** `POST /api/webhooks/claude`
|
||||
**Type:** `session.create`
|
||||
|
||||
#### Request Body
|
||||
```json
|
||||
{
|
||||
"type": "session.create",
|
||||
"session": {
|
||||
"type": "implementation | analysis | testing | review | coordination",
|
||||
"project": {
|
||||
"repository": "string", // Required: "owner/repo" format
|
||||
"branch": "string", // Optional: target branch
|
||||
"requirements": "string", // Required: task description
|
||||
"context": "string" // Optional: additional context
|
||||
},
|
||||
"dependencies": ["string"], // Optional: array of session IDs to wait for
|
||||
"metadata": { // Optional: custom metadata
|
||||
"batchId": "string", // Group related sessions
|
||||
"tags": ["string"], // Categorization tags
|
||||
"priority": "string" // Priority level
|
||||
}
|
||||
},
|
||||
"options": { // Optional: execution options
|
||||
"autoStart": boolean, // Start when dependencies complete (default: false)
|
||||
"timeout": number, // Custom timeout in seconds (default: 1800)
|
||||
"notifyUrl": "string" // Webhook URL for completion notification
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `type` | string | Yes | Must be "session.create" |
|
||||
| `session` | object | Yes | Session configuration object |
|
||||
| `session.type` | string | Yes | Type of session: `implementation`, `analysis`, `testing`, `review`, or `coordination` |
|
||||
| `session.project` | object | Yes | Project information |
|
||||
| `session.project.repository` | string | Yes | GitHub repository in "owner/repo" format |
|
||||
| `session.project.branch` | string | No | Target branch name (defaults to main/master) |
|
||||
| `session.project.requirements` | string | Yes | Clear description of what Claude should do |
|
||||
| `session.project.context` | string | No | Additional context about the codebase or requirements |
|
||||
| `session.dependencies` | string[] | No | Array of valid UUID session IDs that must complete before this session starts (filters out "none", empty strings) |
|
||||
| `session.metadata` | object | No | Custom metadata for organizing sessions |
|
||||
| `session.metadata.batchId` | string | No | User-provided ID for grouping related sessions |
|
||||
| `session.metadata.tags` | string[] | No | Tags for categorization |
|
||||
| `session.metadata.priority` | string | No | Priority level (high, medium, low) |
|
||||
| `options` | object | No | Execution options |
|
||||
| `options.autoStart` | boolean | No | Automatically start when dependencies complete (default: false) |
|
||||
| `options.timeout` | number | No | Custom timeout in seconds (default: 1800 = 30 minutes) |
|
||||
| `options.notifyUrl` | string | No | Webhook URL to call on completion/failure |
|
||||
|
||||
#### Response
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Session created successfully",
|
||||
"data": {
|
||||
"session": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"type": "implementation",
|
||||
"status": "pending",
|
||||
"project": {
|
||||
"repository": "acme/webapp",
|
||||
"branch": "feature/user-auth",
|
||||
"requirements": "Implement JWT authentication middleware",
|
||||
"context": "Use existing User model"
|
||||
},
|
||||
"dependencies": [],
|
||||
"metadata": {
|
||||
"batchId": "auth-feature-batch",
|
||||
"tags": ["feature", "auth"],
|
||||
"priority": "high"
|
||||
},
|
||||
"options": {
|
||||
"autoStart": false,
|
||||
"timeout": 1800,
|
||||
"notifyUrl": null
|
||||
},
|
||||
"containerId": null,
|
||||
"claudeSessionId": null,
|
||||
"createdAt": "2024-01-06T10:00:00Z",
|
||||
"startedAt": null,
|
||||
"completedAt": null,
|
||||
"output": null,
|
||||
"error": null
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Example
|
||||
```bash
|
||||
curl -X POST https://your-domain.com/api/webhooks/claude \
|
||||
-H "Authorization: Bearer your-secret-token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"type": "session.create",
|
||||
"session": {
|
||||
"type": "implementation",
|
||||
"project": {
|
||||
"repository": "acme/webapp",
|
||||
"branch": "feature/user-auth",
|
||||
"requirements": "Implement JWT authentication middleware for Express.js",
|
||||
"context": "Use existing User model and bcrypt for password hashing"
|
||||
},
|
||||
"dependencies": []
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Start Session
|
||||
Starts a previously created session or queues it if dependencies aren't met.
|
||||
|
||||
**Endpoint:** `POST /api/webhooks/claude`
|
||||
**Type:** `session.start`
|
||||
|
||||
#### Request Body
|
||||
```json
|
||||
{
|
||||
"type": "session.start",
|
||||
"sessionId": "string"
|
||||
}
|
||||
```
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `type` | string | Yes | Must be "session.start" |
|
||||
| `sessionId` | string | Yes | UUID of the session to start |
|
||||
|
||||
#### Response
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Session started successfully",
|
||||
"data": {
|
||||
"session": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"status": "initializing", // or "running" if started immediately
|
||||
"containerId": "docker-container-id",
|
||||
"claudeSessionId": "claude-internal-session-id",
|
||||
// ... full session object
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For queued sessions (waiting on dependencies):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Session queued",
|
||||
"data": {
|
||||
"session": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"status": "pending",
|
||||
// ... full session object
|
||||
},
|
||||
"queueStatus": {
|
||||
"waitingFor": ["dependency-session-id-1", "dependency-session-id-2"],
|
||||
"estimatedStartTime": null
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Example
|
||||
```bash
|
||||
curl -X POST https://your-domain.com/api/webhooks/claude \
|
||||
-H "Authorization: Bearer your-secret-token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"type": "session.start",
|
||||
"sessionId": "550e8400-e29b-41d4-a716-446655440000"
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Get Session Status
|
||||
Retrieves the current status and details of a session.
|
||||
|
||||
**Endpoint:** `POST /api/webhooks/claude`
|
||||
**Type:** `session.get`
|
||||
|
||||
#### Request Body
|
||||
```json
|
||||
{
|
||||
"type": "session.get",
|
||||
"sessionId": "string"
|
||||
}
|
||||
```
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `type` | string | Yes | Must be "session.get" |
|
||||
| `sessionId` | string | Yes | UUID of the session to query |
|
||||
|
||||
#### Response
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Session found",
|
||||
"data": {
|
||||
"session": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"type": "implementation",
|
||||
"status": "running",
|
||||
"containerId": "docker-container-id",
|
||||
"claudeSessionId": "claude-internal-session-id",
|
||||
"project": {
|
||||
"repository": "acme/webapp",
|
||||
"branch": "feature/user-auth",
|
||||
"requirements": "Implement JWT authentication middleware",
|
||||
"context": "Use existing User model"
|
||||
},
|
||||
"dependencies": [],
|
||||
"metadata": {},
|
||||
"options": {},
|
||||
"createdAt": "2024-01-06T10:00:00Z",
|
||||
"startedAt": "2024-01-06T10:30:00Z",
|
||||
"completedAt": null,
|
||||
"output": null,
|
||||
"error": null
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Session Status Values
|
||||
- `pending` - Session created but not started
|
||||
- `initializing` - Container is being created
|
||||
- `running` - Session is actively executing
|
||||
- `completed` - Session finished successfully
|
||||
- `failed` - Session encountered an error
|
||||
- `cancelled` - Session was manually cancelled
|
||||
|
||||
---
|
||||
|
||||
### 4. Get Session Output
|
||||
Retrieves the output and artifacts from a completed session.
|
||||
|
||||
**Endpoint:** `POST /api/webhooks/claude`
|
||||
**Type:** `session.output`
|
||||
|
||||
#### Request Body
|
||||
```json
|
||||
{
|
||||
"type": "session.output",
|
||||
"sessionId": "string"
|
||||
}
|
||||
```
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `type` | string | Yes | Must be "session.output" |
|
||||
| `sessionId` | string | Yes | UUID of the session |
|
||||
|
||||
#### Response
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Session output retrieved",
|
||||
"data": {
|
||||
"output": {
|
||||
"logs": ["Container started", "Running Claude command...", "Task completed"],
|
||||
"artifacts": [
|
||||
{
|
||||
"type": "file",
|
||||
"path": "src/middleware/auth.js",
|
||||
"content": "// JWT authentication middleware\n...",
|
||||
"sha": "abc123...",
|
||||
"url": "https://github.com/acme/webapp/blob/feature/user-auth/src/middleware/auth.js",
|
||||
"metadata": {
|
||||
"linesAdded": 150,
|
||||
"linesRemoved": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"summary": "Implemented JWT authentication middleware with refresh token support",
|
||||
"nextSteps": ["Add rate limiting", "Implement password reset flow"],
|
||||
"executionTime": 180, // seconds
|
||||
"resourceUsage": {
|
||||
"cpuTime": 45.2,
|
||||
"memoryPeak": "512MB"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note: The current implementation returns a simplified structure. Full artifact details and metadata are planned for future releases.
|
||||
|
||||
---
|
||||
|
||||
### 5. List Sessions
|
||||
Lists all sessions, optionally filtered by orchestration ID.
|
||||
|
||||
**Endpoint:** `POST /api/webhooks/claude`
|
||||
**Type:** `session.list`
|
||||
|
||||
#### Request Body
|
||||
```json
|
||||
{
|
||||
"type": "session.list",
|
||||
"orchestrationId": "string" // Optional
|
||||
}
|
||||
```
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `type` | string | Yes | Must be "session.list" |
|
||||
| `orchestrationId` | string | No | Filter sessions by orchestration ID |
|
||||
|
||||
#### Response
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Sessions retrieved",
|
||||
"data": {
|
||||
"sessions": [
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"type": "implementation",
|
||||
"status": "completed",
|
||||
"project": {
|
||||
"repository": "acme/webapp",
|
||||
"branch": "feature/user-auth",
|
||||
"requirements": "Implement JWT authentication",
|
||||
"context": null
|
||||
},
|
||||
"dependencies": [],
|
||||
"metadata": {
|
||||
"batchId": "auth-feature-batch",
|
||||
"tags": ["feature", "auth"]
|
||||
},
|
||||
"createdAt": "2024-01-06T10:00:00Z",
|
||||
"startedAt": "2024-01-06T10:30:00Z",
|
||||
"completedAt": "2024-01-06T10:45:00Z",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"id": "660e8400-e29b-41d4-a716-446655440001",
|
||||
"type": "testing",
|
||||
"status": "running",
|
||||
"project": {
|
||||
"repository": "acme/webapp",
|
||||
"branch": "feature/user-auth",
|
||||
"requirements": "Write tests for JWT middleware"
|
||||
},
|
||||
"dependencies": ["550e8400-e29b-41d4-a716-446655440000"],
|
||||
"metadata": {
|
||||
"batchId": "auth-feature-batch",
|
||||
"tags": ["testing"]
|
||||
},
|
||||
"createdAt": "2024-01-06T10:46:00Z",
|
||||
"startedAt": "2024-01-06T10:47:00Z",
|
||||
"completedAt": null,
|
||||
"error": null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Session Types
|
||||
|
||||
### implementation
|
||||
For implementing new features or functionality. Claude will:
|
||||
- Analyze requirements
|
||||
- Write production-ready code
|
||||
- Follow existing patterns and conventions
|
||||
- Create or modify files as needed
|
||||
|
||||
### analysis
|
||||
For analyzing existing code. Claude will:
|
||||
- Review code structure and patterns
|
||||
- Identify potential issues
|
||||
- Suggest improvements
|
||||
- Document findings
|
||||
|
||||
### testing
|
||||
For creating and running tests. Claude will:
|
||||
- Write unit and integration tests
|
||||
- Ensure code coverage
|
||||
- Validate functionality
|
||||
- Fix failing tests
|
||||
|
||||
### review
|
||||
For code review tasks. Claude will:
|
||||
- Review pull requests
|
||||
- Check for security issues
|
||||
- Validate best practices
|
||||
- Provide feedback
|
||||
|
||||
### coordination
|
||||
For orchestrating multiple sessions. Claude will:
|
||||
- Break down complex tasks
|
||||
- Create dependent sessions
|
||||
- Monitor progress
|
||||
- Coordinate results
|
||||
|
||||
## Dependency Management
|
||||
|
||||
Sessions can depend on other sessions using the `dependencies` parameter:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "session.create",
|
||||
"session": {
|
||||
"type": "testing",
|
||||
"project": {
|
||||
"repository": "acme/webapp",
|
||||
"requirements": "Write tests for the JWT authentication middleware"
|
||||
},
|
||||
"dependencies": ["implementation-session-id"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Dependency Behavior
|
||||
- Sessions with dependencies won't start until all dependencies are `completed`
|
||||
- If any dependency fails, the dependent session will be marked as `failed`
|
||||
- Circular dependencies are detected and rejected
|
||||
- Maximum dependency depth is 10 levels
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Error Response Format
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Error description"
|
||||
}
|
||||
```
|
||||
|
||||
### Common Error Codes
|
||||
- `400` - Bad Request (invalid parameters)
|
||||
- `401` - Unauthorized (invalid token)
|
||||
- `404` - Not Found (session doesn't exist)
|
||||
- `409` - Conflict (session already started)
|
||||
- `429` - Too Many Requests (rate limit exceeded)
|
||||
- `500` - Internal Server Error
|
||||
|
||||
### Example Error Response
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Session not found: 550e8400-e29b-41d4-a716-446655440000"
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Clear Requirements
|
||||
Provide detailed, actionable requirements:
|
||||
```json
|
||||
{
|
||||
"requirements": "Implement JWT authentication middleware with:\n- Access token (15min expiry)\n- Refresh token (7 days expiry)\n- Token blacklisting for logout\n- Rate limiting per user"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Use Dependencies Wisely
|
||||
Chain related tasks:
|
||||
```
|
||||
analysis → implementation → testing → review
|
||||
```
|
||||
|
||||
### 3. Provide Context
|
||||
Include relevant context about your codebase:
|
||||
```json
|
||||
{
|
||||
"context": "We use Express.js with TypeScript, Prisma ORM, and follow REST API conventions. Authentication should integrate with existing User model."
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Monitor Session Status
|
||||
Poll session status every 5-10 seconds:
|
||||
```bash
|
||||
while [ "$status" != "completed" ]; do
|
||||
status=$(curl -s -X POST ... | jq -r '.data.status')
|
||||
sleep 5
|
||||
done
|
||||
```
|
||||
|
||||
### 5. Handle Failures Gracefully
|
||||
Check session status and error messages:
|
||||
```javascript
|
||||
if (response.data.status === 'failed') {
|
||||
console.error('Session failed:', response.data.error);
|
||||
// Implement retry logic or alternative approach
|
||||
}
|
||||
```
|
||||
|
||||
## Integration Examples
|
||||
|
||||
### Node.js/TypeScript
|
||||
```typescript
|
||||
import axios from 'axios';
|
||||
|
||||
const CLAUDE_API_URL = 'https://your-domain.com/api/webhooks/claude';
|
||||
const AUTH_TOKEN = process.env.CLAUDE_WEBHOOK_SECRET;
|
||||
|
||||
async function createAndRunSession() {
|
||||
// Create session
|
||||
const createResponse = await axios.post(
|
||||
CLAUDE_API_URL,
|
||||
{
|
||||
type: 'session.create',
|
||||
session: {
|
||||
type: 'implementation',
|
||||
project: {
|
||||
repository: 'acme/webapp',
|
||||
requirements: 'Implement user profile API endpoints',
|
||||
context: 'Use existing auth middleware'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${AUTH_TOKEN}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const sessionId = createResponse.data.data.sessionId;
|
||||
|
||||
// Start session
|
||||
await axios.post(
|
||||
CLAUDE_API_URL,
|
||||
{
|
||||
type: 'session.start',
|
||||
sessionId
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${AUTH_TOKEN}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Poll for completion
|
||||
let status = 'running';
|
||||
while (status === 'running' || status === 'initializing') {
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
|
||||
const statusResponse = await axios.post(
|
||||
CLAUDE_API_URL,
|
||||
{
|
||||
type: 'session.get',
|
||||
sessionId
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${AUTH_TOKEN}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
status = statusResponse.data.data.status;
|
||||
}
|
||||
|
||||
// Get output
|
||||
if (status === 'completed') {
|
||||
const outputResponse = await axios.post(
|
||||
CLAUDE_API_URL,
|
||||
{
|
||||
type: 'session.output',
|
||||
sessionId
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${AUTH_TOKEN}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
console.log('Session completed:', outputResponse.data.data.summary);
|
||||
console.log('Artifacts:', outputResponse.data.data.artifacts);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Python
|
||||
```python
|
||||
import requests
|
||||
import time
|
||||
import os
|
||||
|
||||
CLAUDE_API_URL = 'https://your-domain.com/api/webhooks/claude'
|
||||
AUTH_TOKEN = os.environ['CLAUDE_WEBHOOK_SECRET']
|
||||
|
||||
headers = {
|
||||
'Authorization': f'Bearer {AUTH_TOKEN}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
# Create session
|
||||
create_response = requests.post(
|
||||
CLAUDE_API_URL,
|
||||
json={
|
||||
'type': 'session.create',
|
||||
'session': {
|
||||
'type': 'implementation',
|
||||
'project': {
|
||||
'repository': 'acme/webapp',
|
||||
'requirements': 'Implement user profile API endpoints'
|
||||
}
|
||||
}
|
||||
},
|
||||
headers=headers
|
||||
)
|
||||
|
||||
session_id = create_response.json()['data']['sessionId']
|
||||
|
||||
# Start session
|
||||
requests.post(
|
||||
CLAUDE_API_URL,
|
||||
json={
|
||||
'type': 'session.start',
|
||||
'sessionId': session_id
|
||||
},
|
||||
headers=headers
|
||||
)
|
||||
|
||||
# Poll for completion
|
||||
status = 'running'
|
||||
while status in ['running', 'initializing']:
|
||||
time.sleep(5)
|
||||
status_response = requests.post(
|
||||
CLAUDE_API_URL,
|
||||
json={
|
||||
'type': 'session.get',
|
||||
'sessionId': session_id
|
||||
},
|
||||
headers=headers
|
||||
)
|
||||
status = status_response.json()['data']['status']
|
||||
|
||||
# Get output
|
||||
if status == 'completed':
|
||||
output_response = requests.post(
|
||||
CLAUDE_API_URL,
|
||||
json={
|
||||
'type': 'session.output',
|
||||
'sessionId': session_id
|
||||
},
|
||||
headers=headers
|
||||
)
|
||||
output = output_response.json()['data']
|
||||
print(f"Summary: {output['summary']}")
|
||||
print(f"Artifacts: {output['artifacts']}")
|
||||
```
|
||||
|
||||
## LLM Agent Integration Guide
|
||||
|
||||
This section provides specific guidance for LLM agents (via MCP servers or other integrations) consuming this API.
|
||||
|
||||
### Response Parsing
|
||||
All responses follow a consistent structure, making them easy to parse:
|
||||
```typescript
|
||||
interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
message: string;
|
||||
data?: T; // Present when success=true
|
||||
error?: string; // Present when success=false
|
||||
}
|
||||
```
|
||||
|
||||
### Session Orchestration Pattern
|
||||
Since this API focuses on single-session creation, orchestration should be handled by the LLM agent:
|
||||
|
||||
```python
|
||||
# Example: LLM agent orchestrating a feature implementation
|
||||
async def implement_feature(repo: str, feature_desc: str):
|
||||
# 1. Create analysis session
|
||||
analysis = await create_session(
|
||||
type="analysis",
|
||||
requirements=f"Analyze codebase for implementing: {feature_desc}"
|
||||
)
|
||||
|
||||
# 2. Wait for analysis to complete
|
||||
await wait_for_completion(analysis.id)
|
||||
|
||||
# 3. Create implementation session based on analysis
|
||||
implementation = await create_session(
|
||||
type="implementation",
|
||||
requirements=f"Implement {feature_desc} based on analysis",
|
||||
dependencies=[analysis.id]
|
||||
)
|
||||
|
||||
# 4. Create testing session
|
||||
testing = await create_session(
|
||||
type="testing",
|
||||
requirements=f"Write tests for {feature_desc}",
|
||||
dependencies=[implementation.id],
|
||||
options={"autoStart": true} # Auto-start when ready
|
||||
)
|
||||
|
||||
return {
|
||||
"analysis": analysis.id,
|
||||
"implementation": implementation.id,
|
||||
"testing": testing.id
|
||||
}
|
||||
```
|
||||
|
||||
### Polling Best Practices
|
||||
```javascript
|
||||
async function pollSession(sessionId, maxAttempts = 120) {
|
||||
const pollInterval = 5000; // 5 seconds
|
||||
let attempts = 0;
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
const response = await getSession(sessionId);
|
||||
const status = response.data.session.status;
|
||||
|
||||
if (['completed', 'failed', 'cancelled'].includes(status)) {
|
||||
return response.data.session;
|
||||
}
|
||||
|
||||
// Exponential backoff for long-running sessions
|
||||
const delay = status === 'pending' ? pollInterval * 2 : pollInterval;
|
||||
await sleep(delay);
|
||||
attempts++;
|
||||
}
|
||||
|
||||
throw new Error('Session polling timeout');
|
||||
}
|
||||
```
|
||||
|
||||
### Batch Processing Pattern
|
||||
Use metadata to group related sessions:
|
||||
```json
|
||||
{
|
||||
"type": "session.create",
|
||||
"session": {
|
||||
"type": "implementation",
|
||||
"project": { ... },
|
||||
"metadata": {
|
||||
"batchId": "feature-xyz-batch",
|
||||
"tags": ["feature", "priority-high"],
|
||||
"priority": "high"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then query all sessions in a batch:
|
||||
```json
|
||||
{
|
||||
"type": "session.list",
|
||||
"orchestrationId": "feature-xyz-batch"
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
```python
|
||||
def handle_api_response(response):
|
||||
if response.status_code == 429:
|
||||
# Rate limited - implement exponential backoff
|
||||
retry_after = int(response.headers.get('Retry-After', 60))
|
||||
time.sleep(retry_after)
|
||||
return retry_request()
|
||||
|
||||
data = response.json()
|
||||
if not data['success']:
|
||||
error = data.get('error', 'Unknown error')
|
||||
if 'not found' in error:
|
||||
# Handle missing session
|
||||
pass
|
||||
elif 'already started' in error:
|
||||
# Session already running - just poll for status
|
||||
pass
|
||||
else:
|
||||
raise ApiError(error)
|
||||
|
||||
return data['data']
|
||||
```
|
||||
|
||||
### Dependency Graph Building
|
||||
```typescript
|
||||
class SessionGraph {
|
||||
private sessions: Map<string, Session> = new Map();
|
||||
|
||||
addSession(session: Session) {
|
||||
this.sessions.set(session.id, session);
|
||||
}
|
||||
|
||||
getExecutionOrder(): string[] {
|
||||
// Topological sort to determine execution order
|
||||
const visited = new Set<string>();
|
||||
const order: string[] = [];
|
||||
|
||||
const visit = (id: string) => {
|
||||
if (visited.has(id)) return;
|
||||
visited.add(id);
|
||||
|
||||
const session = this.sessions.get(id);
|
||||
if (session?.dependencies) {
|
||||
session.dependencies.forEach(dep => visit(dep));
|
||||
}
|
||||
|
||||
order.push(id);
|
||||
};
|
||||
|
||||
this.sessions.forEach((_, id) => visit(id));
|
||||
return order;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Optimizing for Claude Code
|
||||
When creating sessions for Claude Code:
|
||||
|
||||
1. **Clear Requirements**: Be specific and actionable
|
||||
```json
|
||||
{
|
||||
"requirements": "Implement REST API endpoint POST /api/users with:\n- Request validation (email, password)\n- Password hashing with bcrypt\n- Store in PostgreSQL users table\n- Return JWT token\n- Handle duplicate email error",
|
||||
"context": "Using Express.js, TypeScript, Prisma ORM. Follow existing auth patterns in src/middleware/auth.ts"
|
||||
}
|
||||
```
|
||||
|
||||
2. **Provide Context**: Reference existing code patterns
|
||||
```json
|
||||
{
|
||||
"context": "Follow patterns in src/controllers/. Use existing error handling middleware. See src/types/user.ts for User interface."
|
||||
}
|
||||
```
|
||||
|
||||
3. **Use Session Types Effectively**:
|
||||
- `analysis` - Before implementing, understand the codebase
|
||||
- `implementation` - Write the actual code
|
||||
- `testing` - Ensure code works and has coverage
|
||||
- `review` - Final quality check
|
||||
- `coordination` - For complex multi-part tasks
|
||||
|
||||
### Performance Tips
|
||||
1. **Parallel Sessions**: Create independent sessions simultaneously
|
||||
2. **Reuse Analysis**: Cache analysis results for similar tasks
|
||||
3. **Smart Dependencies**: Only add dependencies when truly needed
|
||||
4. **Batch Operations**: Group related sessions with metadata
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Session Stuck in "pending"
|
||||
- Check if dependencies are completed
|
||||
- Verify Docker daemon is running
|
||||
- Check system resources (CPU, memory)
|
||||
- Use `session.get` to check dependency status
|
||||
|
||||
### Authentication Errors
|
||||
- Verify Bearer token matches CLAUDE_WEBHOOK_SECRET
|
||||
- Ensure Authorization header is properly formatted
|
||||
- Check token hasn't been rotated
|
||||
|
||||
### Session Failures
|
||||
- Review session output for error messages
|
||||
- Check Docker container logs
|
||||
- Verify repository access permissions
|
||||
- Ensure Claude API credentials are valid
|
||||
|
||||
### Timeout Issues
|
||||
- Default timeout is 30 minutes per session
|
||||
- For longer tasks, break into smaller sessions
|
||||
- Use custom timeout in options: `{"timeout": 3600}`
|
||||
|
||||
## Changelog
|
||||
|
||||
### v2.0.0 (2024-01-08)
|
||||
- **BREAKING**: Removed orchestration endpoint (use session.create with type="coordination")
|
||||
- **BREAKING**: Updated response structures (all data wrapped in `data.session` or `data.sessions`)
|
||||
- Added enhanced session creation with metadata and options
|
||||
- Added autoStart option for dependency-based execution
|
||||
- Added timeout and notification options
|
||||
- Improved dependency validation (filters invalid UUIDs)
|
||||
|
||||
### v1.0.0 (2024-01-06)
|
||||
- Initial release with session management
|
||||
- Support for 5 session types
|
||||
- Dependency management
|
||||
- Orchestration capabilities
|
||||
4
get-session.json
Normal file
4
get-session.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"type": "session.get",
|
||||
"sessionId": "d4ac40bf-1290-4237-83fe-53a4a6197dc5"
|
||||
}
|
||||
@@ -26,6 +26,7 @@ module.exports = {
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.{js,ts}',
|
||||
'!src/**/*.d.ts',
|
||||
'!src/types/**/*.ts',
|
||||
'!**/node_modules/**',
|
||||
'!**/dist/**'
|
||||
],
|
||||
|
||||
50
package-lock.json
generated
50
package-lock.json
generated
@@ -9,7 +9,6 @@
|
||||
"version": "0.1.1",
|
||||
"dependencies": {
|
||||
"@octokit/rest": "^22.0.0",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"axios": "^1.6.2",
|
||||
"body-parser": "^2.2.0",
|
||||
"commander": "^14.0.0",
|
||||
@@ -22,14 +21,15 @@
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.27.3",
|
||||
"@babel/core": "^7.27.4",
|
||||
"@babel/preset-env": "^7.27.2",
|
||||
"@jest/globals": "^30.0.0-beta.3",
|
||||
"@types/body-parser": "^1.19.5",
|
||||
"@types/body-parser": "^1.19.6",
|
||||
"@types/express": "^5.0.2",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.15.23",
|
||||
"@types/supertest": "^6.0.3",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.33.0",
|
||||
"@typescript-eslint/parser": "^8.33.0",
|
||||
"babel-jest": "^29.7.0",
|
||||
@@ -86,20 +86,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/core": {
|
||||
"version": "7.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.3.tgz",
|
||||
"integrity": "sha512-hyrN8ivxfvJ4i0fIJuV4EOlV0WDMz5Ui4StRTgVaAvWeiRCilXgwVvxJKtFQ3TKtHgJscB2YiXKGNJuVwhQMtA==",
|
||||
"version": "7.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz",
|
||||
"integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.27.3",
|
||||
"@babel/helper-compilation-targets": "^7.27.2",
|
||||
"@babel/helper-module-transforms": "^7.27.3",
|
||||
"@babel/helpers": "^7.27.3",
|
||||
"@babel/parser": "^7.27.3",
|
||||
"@babel/helpers": "^7.27.4",
|
||||
"@babel/parser": "^7.27.4",
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/traverse": "^7.27.3",
|
||||
"@babel/traverse": "^7.27.4",
|
||||
"@babel/types": "^7.27.3",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"debug": "^4.1.0",
|
||||
@@ -366,10 +367,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helpers": {
|
||||
"version": "7.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.3.tgz",
|
||||
"integrity": "sha512-h/eKy9agOya1IGuLaZ9tEUgz+uIRXcbtOhRtUyyMf8JFmn1iT13vnl/IGVWSkdOCG/pC57U4S1jnAabAavTMwg==",
|
||||
"version": "7.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.4.tgz",
|
||||
"integrity": "sha512-Y+bO6U+I7ZKaM5G5rDUZiYfUvQPUibYmAFe7EnKdnKBbVXDZxvp+MWOH5gYciY0EPk4EScsuFMQBbEfpdRKSCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/types": "^7.27.3"
|
||||
@@ -379,10 +381,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.3.tgz",
|
||||
"integrity": "sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw==",
|
||||
"version": "7.27.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz",
|
||||
"integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.27.3"
|
||||
},
|
||||
@@ -1636,14 +1639,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.3.tgz",
|
||||
"integrity": "sha512-lId/IfN/Ye1CIu8xG7oKBHXd2iNb2aW1ilPszzGcJug6M8RCKfVNcYhpI5+bMvFYjK7lXIM0R+a+6r8xhHp2FQ==",
|
||||
"version": "7.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz",
|
||||
"integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.27.3",
|
||||
"@babel/parser": "^7.27.3",
|
||||
"@babel/parser": "^7.27.4",
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/types": "^7.27.3",
|
||||
"debug": "^4.3.1",
|
||||
@@ -3108,10 +3112,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/body-parser": {
|
||||
"version": "1.19.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
|
||||
"integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
|
||||
"version": "1.19.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
||||
"integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/connect": "*",
|
||||
"@types/node": "*"
|
||||
@@ -3315,6 +3320,7 @@
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/yargs": {
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"test:coverage": "jest --coverage",
|
||||
"test:watch": "jest --watch",
|
||||
"test:ci": "jest --ci --coverage --testPathPattern='test/(unit|integration).*\\.test\\.(js|ts)$'",
|
||||
"test:combined-coverage": "./scripts/combine-coverage.js",
|
||||
"test:docker": "docker-compose -f docker-compose.test.yml run --rm test",
|
||||
"test:docker:integration": "docker-compose -f docker-compose.test.yml run --rm integration-test",
|
||||
"test:docker:e2e": "docker-compose -f docker-compose.test.yml run --rm e2e-test",
|
||||
@@ -47,10 +48,10 @@
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.27.3",
|
||||
"@babel/core": "^7.27.4",
|
||||
"@babel/preset-env": "^7.27.2",
|
||||
"@jest/globals": "^30.0.0-beta.3",
|
||||
"@types/body-parser": "^1.19.5",
|
||||
"@types/body-parser": "^1.19.6",
|
||||
"@types/express": "^5.0.2",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.15.23",
|
||||
|
||||
88
scripts/combine-coverage.js
Executable file
88
scripts/combine-coverage.js
Executable file
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* Combine coverage reports from main project and CLI
|
||||
*/
|
||||
|
||||
// Ensure coverage directories exist
|
||||
const mainCoverageDir = path.join(__dirname, '..', 'coverage');
|
||||
const cliCoverageDir = path.join(__dirname, '..', 'cli', 'coverage');
|
||||
const combinedCoverageDir = path.join(__dirname, '..', 'coverage-combined');
|
||||
|
||||
// Create combined coverage directory
|
||||
if (!fs.existsSync(combinedCoverageDir)) {
|
||||
fs.mkdirSync(combinedCoverageDir, { recursive: true });
|
||||
}
|
||||
|
||||
console.log('Generating main project coverage...');
|
||||
try {
|
||||
execSync('npm run test:ci', { stdio: 'inherit', cwd: path.join(__dirname, '..') });
|
||||
} catch (error) {
|
||||
console.error('Failed to generate main project coverage');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('\nGenerating CLI coverage...');
|
||||
try {
|
||||
execSync('npm run test:coverage', { stdio: 'inherit', cwd: path.join(__dirname, '..', 'cli') });
|
||||
} catch (error) {
|
||||
console.error('Failed to generate CLI coverage');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Check if both coverage files exist
|
||||
const mainLcov = path.join(mainCoverageDir, 'lcov.info');
|
||||
const cliLcov = path.join(cliCoverageDir, 'lcov.info');
|
||||
|
||||
if (!fs.existsSync(mainLcov)) {
|
||||
console.error('Main project lcov.info not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(cliLcov)) {
|
||||
console.error('CLI lcov.info not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Read both lcov files
|
||||
const mainLcovContent = fs.readFileSync(mainLcov, 'utf8');
|
||||
const cliLcovContent = fs.readFileSync(cliLcov, 'utf8');
|
||||
|
||||
// Adjust CLI paths to be relative to project root
|
||||
const adjustedCliLcov = cliLcovContent.replace(/SF:src\//g, 'SF:cli/src/');
|
||||
|
||||
// Combine lcov files
|
||||
const combinedLcov = mainLcovContent + '\n' + adjustedCliLcov;
|
||||
|
||||
// Write combined lcov file
|
||||
const combinedLcovPath = path.join(combinedCoverageDir, 'lcov.info');
|
||||
fs.writeFileSync(combinedLcovPath, combinedLcov);
|
||||
|
||||
console.log('\nCombined coverage report written to:', combinedLcovPath);
|
||||
|
||||
// Copy coverage-final.json files as well for better reporting
|
||||
if (fs.existsSync(path.join(mainCoverageDir, 'coverage-final.json'))) {
|
||||
const mainJson = JSON.parse(fs.readFileSync(path.join(mainCoverageDir, 'coverage-final.json'), 'utf8'));
|
||||
const cliJson = JSON.parse(fs.readFileSync(path.join(cliCoverageDir, 'coverage-final.json'), 'utf8'));
|
||||
|
||||
// Adjust CLI paths in JSON
|
||||
const adjustedCliJson = {};
|
||||
for (const [key, value] of Object.entries(cliJson)) {
|
||||
const adjustedKey = key.replace(/^src\//, 'cli/src/');
|
||||
adjustedCliJson[adjustedKey] = value;
|
||||
}
|
||||
|
||||
// Combine JSON coverage
|
||||
const combinedJson = { ...mainJson, ...adjustedCliJson };
|
||||
fs.writeFileSync(
|
||||
path.join(combinedCoverageDir, 'coverage-final.json'),
|
||||
JSON.stringify(combinedJson, null, 2)
|
||||
);
|
||||
}
|
||||
|
||||
console.log('\nCoverage combination complete!');
|
||||
console.log('Upload coverage-combined/lcov.info to Codecov for full project coverage.');
|
||||
@@ -149,19 +149,37 @@ else
|
||||
echo "DEBUG: Using $CLAUDE_USER_HOME as HOME for Claude CLI (fallback)" >&2
|
||||
fi
|
||||
|
||||
sudo -u node -E env \
|
||||
HOME="$CLAUDE_USER_HOME" \
|
||||
PATH="/usr/local/bin:/usr/local/share/npm-global/bin:$PATH" \
|
||||
ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY}" \
|
||||
GH_TOKEN="${GITHUB_TOKEN}" \
|
||||
GITHUB_TOKEN="${GITHUB_TOKEN}" \
|
||||
BASH_DEFAULT_TIMEOUT_MS="${BASH_DEFAULT_TIMEOUT_MS}" \
|
||||
BASH_MAX_TIMEOUT_MS="${BASH_MAX_TIMEOUT_MS}" \
|
||||
/usr/local/share/npm-global/bin/claude \
|
||||
--allowedTools "${ALLOWED_TOOLS}" \
|
||||
--verbose \
|
||||
--print "${COMMAND}" \
|
||||
> "${RESPONSE_FILE}" 2>&1
|
||||
if [ "${OUTPUT_FORMAT}" = "stream-json" ]; then
|
||||
# For stream-json, output directly to stdout for real-time processing
|
||||
exec sudo -u node -E env \
|
||||
HOME="$CLAUDE_USER_HOME" \
|
||||
PATH="/usr/local/bin:/usr/local/share/npm-global/bin:$PATH" \
|
||||
ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY}" \
|
||||
GH_TOKEN="${GITHUB_TOKEN}" \
|
||||
GITHUB_TOKEN="${GITHUB_TOKEN}" \
|
||||
BASH_DEFAULT_TIMEOUT_MS="${BASH_DEFAULT_TIMEOUT_MS}" \
|
||||
BASH_MAX_TIMEOUT_MS="${BASH_MAX_TIMEOUT_MS}" \
|
||||
/usr/local/share/npm-global/bin/claude \
|
||||
--allowedTools "${ALLOWED_TOOLS}" \
|
||||
--output-format stream-json \
|
||||
--verbose \
|
||||
--print "${COMMAND}"
|
||||
else
|
||||
# Default behavior - write to file
|
||||
sudo -u node -E env \
|
||||
HOME="$CLAUDE_USER_HOME" \
|
||||
PATH="/usr/local/bin:/usr/local/share/npm-global/bin:$PATH" \
|
||||
ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY}" \
|
||||
GH_TOKEN="${GITHUB_TOKEN}" \
|
||||
GITHUB_TOKEN="${GITHUB_TOKEN}" \
|
||||
BASH_DEFAULT_TIMEOUT_MS="${BASH_DEFAULT_TIMEOUT_MS}" \
|
||||
BASH_MAX_TIMEOUT_MS="${BASH_MAX_TIMEOUT_MS}" \
|
||||
/usr/local/share/npm-global/bin/claude \
|
||||
--allowedTools "${ALLOWED_TOOLS}" \
|
||||
--verbose \
|
||||
--print "${COMMAND}" \
|
||||
> "${RESPONSE_FILE}" 2>&1
|
||||
fi
|
||||
|
||||
# Check for errors
|
||||
if [ $? -ne 0 ]; then
|
||||
|
||||
9
session-request.json
Normal file
9
session-request.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"type": "session.create",
|
||||
"session": {
|
||||
"project": {
|
||||
"repository": "Cheffromspace/demo-repository",
|
||||
"requirements": "Implement a hello world program in Python that prints 'Hello, World!' to the console. Create the file as hello_world.py in the root directory. After implementing, create a pull request with the changes."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,9 @@ import { randomUUID } from 'crypto';
|
||||
|
||||
const logger = createLogger('SessionHandler');
|
||||
|
||||
// UUID validation regex pattern
|
||||
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
|
||||
interface SessionCreatePayload {
|
||||
type: 'session.create';
|
||||
session: Partial<ClaudeSession>;
|
||||
@@ -48,7 +51,7 @@ type SessionPayload =
|
||||
* Provides CRUD operations for MCP integration
|
||||
*/
|
||||
export class SessionHandler implements WebhookEventHandler<ClaudeWebhookPayload> {
|
||||
event = 'session';
|
||||
event = 'session*';
|
||||
private sessionManager: SessionManager;
|
||||
|
||||
constructor() {
|
||||
@@ -126,6 +129,27 @@ export class SessionHandler implements WebhookEventHandler<ClaudeWebhookPayload>
|
||||
};
|
||||
}
|
||||
|
||||
// Validate dependencies
|
||||
if (partialSession.dependencies && partialSession.dependencies.length > 0) {
|
||||
// Filter out invalid dependency values
|
||||
const validDependencies = partialSession.dependencies.filter(dep => {
|
||||
return dep && dep.trim() !== '' && dep.toLowerCase() !== 'none';
|
||||
});
|
||||
|
||||
// Check that all remaining dependencies are valid UUIDs
|
||||
const invalidDependencies = validDependencies.filter(dep => !UUID_REGEX.test(dep));
|
||||
|
||||
if (invalidDependencies.length > 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Invalid dependency IDs (not valid UUIDs): ${invalidDependencies.join(', ')}`
|
||||
};
|
||||
}
|
||||
|
||||
// Update dependencies to only include valid ones
|
||||
partialSession.dependencies = validDependencies;
|
||||
}
|
||||
|
||||
// Create full session object
|
||||
const session: ClaudeSession = {
|
||||
id: partialSession.id ?? randomUUID(),
|
||||
@@ -145,6 +169,9 @@ export class SessionHandler implements WebhookEventHandler<ClaudeWebhookPayload>
|
||||
status: 'initializing' as const
|
||||
};
|
||||
|
||||
// Update the session in SessionManager with containerId
|
||||
this.sessionManager.updateSession(createdSession);
|
||||
|
||||
logger.info('Session created', {
|
||||
sessionId: createdSession.id,
|
||||
type: createdSession.type,
|
||||
|
||||
@@ -23,51 +23,22 @@ export class SessionManager {
|
||||
// Generate container name
|
||||
const containerName = `claude-${session.type}-${session.id.substring(0, 8)}`;
|
||||
|
||||
// Get Docker image from environment
|
||||
const dockerImage = process.env.CLAUDE_CONTAINER_IMAGE ?? 'claudecode:latest';
|
||||
|
||||
// Set up volume mounts for persistent storage
|
||||
const volumeName = `claude-session-${session.id.substring(0, 8)}`;
|
||||
const volumeName = `${containerName}-volume`;
|
||||
|
||||
// Create container without starting it
|
||||
const createCmd = [
|
||||
'docker',
|
||||
'create',
|
||||
'--name',
|
||||
containerName,
|
||||
'--rm',
|
||||
'-v',
|
||||
`${volumeName}:/home/user/project`,
|
||||
'-v',
|
||||
`${volumeName}-claude:/home/user/.claude`,
|
||||
'-e',
|
||||
`SESSION_ID=${session.id}`,
|
||||
'-e',
|
||||
`SESSION_TYPE=${session.type}`,
|
||||
'-e',
|
||||
`GITHUB_TOKEN=${process.env.GITHUB_TOKEN ?? ''}`,
|
||||
'-e',
|
||||
`ANTHROPIC_API_KEY=${process.env.ANTHROPIC_API_KEY ?? ''}`,
|
||||
'-e',
|
||||
`REPOSITORY=${session.project.repository}`,
|
||||
'-e',
|
||||
`OPERATION_TYPE=session`,
|
||||
'--workdir',
|
||||
'/home/user/project',
|
||||
dockerImage,
|
||||
'/scripts/runtime/claudecode-entrypoint.sh'
|
||||
];
|
||||
logger.info('Creating container resources', { sessionId: session.id, containerName });
|
||||
|
||||
execSync(createCmd.join(' '), { stdio: 'pipe' });
|
||||
// Create volume for workspace
|
||||
execSync(`docker volume create ${volumeName}`, { stdio: 'pipe' });
|
||||
|
||||
logger.info('Container created', { sessionId: session.id, containerName });
|
||||
logger.info('Container resources created', { sessionId: session.id, containerName });
|
||||
|
||||
// Store session
|
||||
this.sessions.set(session.id, session);
|
||||
|
||||
return Promise.resolve(containerName);
|
||||
} catch (error) {
|
||||
logger.error('Failed to create container', { sessionId: session.id, error });
|
||||
logger.error('Failed to create container resources', { sessionId: session.id, error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -91,34 +62,84 @@ export class SessionManager {
|
||||
// Prepare the command based on session type
|
||||
const command = this.buildSessionCommand(session);
|
||||
|
||||
// Start the container and execute Claude
|
||||
// Get Docker image from environment
|
||||
const dockerImage = process.env.CLAUDE_CONTAINER_IMAGE ?? 'claudecode:latest';
|
||||
|
||||
// Start the container and execute Claude with stream-json output
|
||||
const execCmd = [
|
||||
'docker',
|
||||
'exec',
|
||||
'-i',
|
||||
'run',
|
||||
'--rm',
|
||||
'--name',
|
||||
session.containerId,
|
||||
'claude',
|
||||
'chat',
|
||||
'--no-prompt',
|
||||
'-m',
|
||||
command
|
||||
'-v',
|
||||
`${session.containerId}-volume:/home/user/project`,
|
||||
'-v',
|
||||
`${process.env.CLAUDE_AUTH_HOST_DIR ?? process.env.HOME + '/.claude-hub'}:/home/node/.claude`,
|
||||
'-e',
|
||||
`SESSION_ID=${session.id}`,
|
||||
'-e',
|
||||
`SESSION_TYPE=${session.type}`,
|
||||
'-e',
|
||||
`GITHUB_TOKEN=${process.env.GITHUB_TOKEN ?? ''}`,
|
||||
'-e',
|
||||
`REPO_FULL_NAME=${session.project.repository}`,
|
||||
'-e',
|
||||
`COMMAND=${command}`,
|
||||
'-e',
|
||||
`OPERATION_TYPE=session`,
|
||||
'-e',
|
||||
`OUTPUT_FORMAT=stream-json`,
|
||||
dockerImage
|
||||
];
|
||||
|
||||
// First start the container
|
||||
execSync(`docker start ${session.containerId}`, { stdio: 'pipe' });
|
||||
|
||||
// Then execute Claude command
|
||||
// Start the container with Claude command
|
||||
const dockerProcess = spawn(execCmd[0], execCmd.slice(1), {
|
||||
env: process.env
|
||||
env: process.env,
|
||||
detached: true
|
||||
});
|
||||
|
||||
// Collect output
|
||||
const logs: string[] = [];
|
||||
let firstLineProcessed = false;
|
||||
|
||||
dockerProcess.stdout.on('data', data => {
|
||||
const line = data.toString();
|
||||
logs.push(line);
|
||||
logger.debug('Session output', { sessionId: session.id, line });
|
||||
const lines = data
|
||||
.toString()
|
||||
.split('\n')
|
||||
.filter((line: string) => line.trim());
|
||||
|
||||
for (const line of lines) {
|
||||
logs.push(line);
|
||||
|
||||
// Process first line to get Claude session ID
|
||||
if (!firstLineProcessed && line.trim()) {
|
||||
firstLineProcessed = true;
|
||||
try {
|
||||
const initData = JSON.parse(line);
|
||||
if (
|
||||
initData.type === 'system' &&
|
||||
initData.subtype === 'init' &&
|
||||
initData.session_id
|
||||
) {
|
||||
session.claudeSessionId = initData.session_id;
|
||||
this.sessions.set(session.id, session);
|
||||
logger.info('Captured Claude session ID', {
|
||||
sessionId: session.id,
|
||||
claudeSessionId: session.claudeSessionId
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Failed to parse first line as JSON', {
|
||||
sessionId: session.id,
|
||||
line,
|
||||
err
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug('Session output', { sessionId: session.id, line });
|
||||
}
|
||||
});
|
||||
|
||||
dockerProcess.stderr.on('data', data => {
|
||||
@@ -143,6 +164,9 @@ export class SessionManager {
|
||||
this.notifyWaitingSessions(session.id);
|
||||
});
|
||||
|
||||
// Unref the process so it can run independently
|
||||
dockerProcess.unref();
|
||||
|
||||
return Promise.resolve();
|
||||
} catch (error) {
|
||||
logger.error('Failed to start session', { sessionId: session.id, error });
|
||||
@@ -157,6 +181,12 @@ export class SessionManager {
|
||||
* Queue a session to start when dependencies are met
|
||||
*/
|
||||
async queueSession(session: ClaudeSession): Promise<void> {
|
||||
// If session has no dependencies, start immediately
|
||||
if (!session.dependencies || session.dependencies.length === 0) {
|
||||
await this.startSession(session);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if all dependencies are completed
|
||||
const allDependenciesMet = session.dependencies.every(depId => {
|
||||
const dep = this.sessions.get(depId);
|
||||
@@ -183,6 +213,13 @@ export class SessionManager {
|
||||
return this.sessions.get(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update session
|
||||
*/
|
||||
updateSession(session: ClaudeSession): void {
|
||||
this.sessions.set(session.id, session);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all sessions for an orchestration
|
||||
*/
|
||||
|
||||
@@ -90,6 +90,7 @@ export interface ClaudeSession {
|
||||
type: SessionType;
|
||||
status: SessionStatus;
|
||||
containerId?: string;
|
||||
claudeSessionId?: string; // Claude's internal session ID
|
||||
project: ProjectInfo;
|
||||
dependencies: string[];
|
||||
startedAt?: Date;
|
||||
|
||||
@@ -38,6 +38,10 @@ class SecureCredentials {
|
||||
GITHUB_WEBHOOK_SECRET: {
|
||||
file: process.env['GITHUB_WEBHOOK_SECRET_FILE'] ?? '/run/secrets/webhook_secret',
|
||||
env: 'GITHUB_WEBHOOK_SECRET'
|
||||
},
|
||||
CLAUDE_WEBHOOK_SECRET: {
|
||||
file: process.env['CLAUDE_WEBHOOK_SECRET_FILE'] ?? '/run/secrets/claude_webhook_secret',
|
||||
env: 'CLAUDE_WEBHOOK_SECRET'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
4
start-session-new.json
Normal file
4
start-session-new.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"type": "session.start",
|
||||
"sessionId": "d4ac40bf-1290-4237-83fe-53a4a6197dc5"
|
||||
}
|
||||
4
start-session.json
Normal file
4
start-session.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"type": "session.start",
|
||||
"sessionId": "aa592787-6451-45fd-8413-229260a18b45"
|
||||
}
|
||||
@@ -1,394 +0,0 @@
|
||||
import request from 'supertest';
|
||||
import express from 'express';
|
||||
|
||||
// Mock child_process to prevent Docker commands
|
||||
jest.mock('child_process', () => ({
|
||||
execSync: jest.fn(() => ''),
|
||||
spawn: jest.fn(() => ({
|
||||
stdout: { on: jest.fn() },
|
||||
stderr: { on: jest.fn() },
|
||||
on: jest.fn((event, callback) => {
|
||||
if (event === 'close') {
|
||||
setTimeout(() => callback(0), 100);
|
||||
}
|
||||
})
|
||||
}))
|
||||
}));
|
||||
|
||||
// Mock SessionManager to avoid Docker calls in CI
|
||||
jest.mock('../../../src/providers/claude/services/SessionManager', () => {
|
||||
return {
|
||||
SessionManager: jest.fn().mockImplementation(() => ({
|
||||
createContainer: jest.fn().mockResolvedValue('mock-container-id'),
|
||||
startSession: jest.fn().mockResolvedValue(undefined),
|
||||
getSession: jest.fn().mockImplementation(id => ({
|
||||
id,
|
||||
status: 'running',
|
||||
type: 'implementation',
|
||||
project: { repository: 'test/repo', requirements: 'test' },
|
||||
dependencies: []
|
||||
})),
|
||||
listSessions: jest.fn().mockResolvedValue([]),
|
||||
getSessionOutput: jest.fn().mockResolvedValue({ output: 'test output' }),
|
||||
canStartSession: jest.fn().mockResolvedValue(true),
|
||||
updateSessionStatus: jest.fn().mockResolvedValue(undefined)
|
||||
}))
|
||||
};
|
||||
});
|
||||
|
||||
// Now we can import the routes
|
||||
import webhookRoutes from '../../../src/routes/webhooks';
|
||||
|
||||
// Mock environment variables
|
||||
process.env.CLAUDE_WEBHOOK_SECRET = 'test-secret';
|
||||
process.env.SKIP_WEBHOOK_VERIFICATION = '1';
|
||||
|
||||
describe('Claude Session Integration Tests', () => {
|
||||
let app: express.Application;
|
||||
|
||||
beforeAll(() => {
|
||||
// Import provider to register handlers
|
||||
require('../../../src/providers/claude');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
app = express();
|
||||
app.use(express.json());
|
||||
app.use('/api/webhooks', webhookRoutes);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('POST /api/webhooks/claude - Session Management', () => {
|
||||
it('should create a new session', async () => {
|
||||
const payload = {
|
||||
data: {
|
||||
type: 'session.create',
|
||||
session: {
|
||||
project: {
|
||||
repository: 'owner/repo',
|
||||
requirements: 'Test requirements'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/webhooks/claude')
|
||||
.set('Authorization', 'Bearer test-secret')
|
||||
.send(payload);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.data.session).toMatchObject({
|
||||
type: 'implementation',
|
||||
status: 'initializing',
|
||||
project: {
|
||||
repository: 'owner/repo',
|
||||
requirements: 'Test requirements'
|
||||
}
|
||||
});
|
||||
expect(response.body.data.session.id).toBeDefined();
|
||||
expect(response.body.data.session.containerId).toBeDefined();
|
||||
});
|
||||
|
||||
it('should create session with custom type', async () => {
|
||||
const payload = {
|
||||
data: {
|
||||
type: 'session.create',
|
||||
session: {
|
||||
type: 'analysis',
|
||||
project: {
|
||||
repository: 'owner/repo',
|
||||
requirements: 'Test requirements'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/webhooks/claude')
|
||||
.set('Authorization', 'Bearer test-secret')
|
||||
.send(payload);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.data.session.type).toBe('analysis');
|
||||
});
|
||||
|
||||
it('should reject session creation without repository', async () => {
|
||||
const payload = {
|
||||
data: {
|
||||
type: 'session.create',
|
||||
session: {
|
||||
project: {
|
||||
requirements: 'Test requirements'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/webhooks/claude')
|
||||
.set('Authorization', 'Bearer test-secret')
|
||||
.send(payload);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(false);
|
||||
expect(response.body.error).toBe('Repository is required for session creation');
|
||||
});
|
||||
|
||||
it('should reject session creation without requirements', async () => {
|
||||
const payload = {
|
||||
data: {
|
||||
type: 'session.create',
|
||||
session: {
|
||||
project: {
|
||||
repository: 'owner/repo'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/webhooks/claude')
|
||||
.set('Authorization', 'Bearer test-secret')
|
||||
.send(payload);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(false);
|
||||
expect(response.body.error).toBe('Requirements are required for session creation');
|
||||
});
|
||||
|
||||
it('should handle session.get request', async () => {
|
||||
// First create a session
|
||||
const createPayload = {
|
||||
data: {
|
||||
type: 'session.create',
|
||||
session: {
|
||||
project: {
|
||||
repository: 'owner/repo',
|
||||
requirements: 'Test requirements'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const createResponse = await request(app)
|
||||
.post('/api/webhooks/claude')
|
||||
.set('Authorization', 'Bearer test-secret')
|
||||
.send(createPayload);
|
||||
|
||||
const sessionId = createResponse.body.data.session.id;
|
||||
|
||||
// Then get the session
|
||||
const getPayload = {
|
||||
data: {
|
||||
type: 'session.get',
|
||||
sessionId
|
||||
}
|
||||
};
|
||||
|
||||
const getResponse = await request(app)
|
||||
.post('/api/webhooks/claude')
|
||||
.set('Authorization', 'Bearer test-secret')
|
||||
.send(getPayload);
|
||||
|
||||
expect(getResponse.status).toBe(200);
|
||||
expect(getResponse.body.success).toBe(true);
|
||||
expect(getResponse.body.data.session.id).toBe(sessionId);
|
||||
});
|
||||
|
||||
it('should handle session.list request', async () => {
|
||||
const payload = {
|
||||
data: {
|
||||
type: 'session.list'
|
||||
}
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/webhooks/claude')
|
||||
.set('Authorization', 'Bearer test-secret')
|
||||
.send(payload);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.data.sessions).toBeDefined();
|
||||
expect(Array.isArray(response.body.data.sessions)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle session.start request', async () => {
|
||||
// Create a session first
|
||||
const createPayload = {
|
||||
data: {
|
||||
type: 'session.create',
|
||||
session: {
|
||||
project: {
|
||||
repository: 'owner/repo',
|
||||
requirements: 'Test requirements'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const createResponse = await request(app)
|
||||
.post('/api/webhooks/claude')
|
||||
.set('Authorization', 'Bearer test-secret')
|
||||
.send(createPayload);
|
||||
|
||||
const sessionId = createResponse.body.data.session.id;
|
||||
|
||||
// Start the session
|
||||
const startPayload = {
|
||||
data: {
|
||||
type: 'session.start',
|
||||
sessionId
|
||||
}
|
||||
};
|
||||
|
||||
const startResponse = await request(app)
|
||||
.post('/api/webhooks/claude')
|
||||
.set('Authorization', 'Bearer test-secret')
|
||||
.send(startPayload);
|
||||
|
||||
expect(startResponse.status).toBe(200);
|
||||
expect(startResponse.body.success).toBe(true);
|
||||
expect(startResponse.body.message).toBe('Session started');
|
||||
});
|
||||
|
||||
it('should handle session.output request', async () => {
|
||||
// Create a session first
|
||||
const createPayload = {
|
||||
data: {
|
||||
type: 'session.create',
|
||||
session: {
|
||||
project: {
|
||||
repository: 'owner/repo',
|
||||
requirements: 'Test requirements'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const createResponse = await request(app)
|
||||
.post('/api/webhooks/claude')
|
||||
.set('Authorization', 'Bearer test-secret')
|
||||
.send(createPayload);
|
||||
|
||||
const sessionId = createResponse.body.data.session.id;
|
||||
|
||||
// Get session output
|
||||
const outputPayload = {
|
||||
data: {
|
||||
type: 'session.output',
|
||||
sessionId
|
||||
}
|
||||
};
|
||||
|
||||
const outputResponse = await request(app)
|
||||
.post('/api/webhooks/claude')
|
||||
.set('Authorization', 'Bearer test-secret')
|
||||
.send(outputPayload);
|
||||
|
||||
expect(outputResponse.status).toBe(200);
|
||||
expect(outputResponse.body.success).toBe(true);
|
||||
expect(outputResponse.body.data.sessionId).toBe(sessionId);
|
||||
expect(outputResponse.body.data.output).toBeNull(); // No output yet
|
||||
});
|
||||
|
||||
it('should reject requests without authentication', async () => {
|
||||
const payload = {
|
||||
data: {
|
||||
type: 'session.create',
|
||||
session: {
|
||||
project: {
|
||||
repository: 'owner/repo',
|
||||
requirements: 'Test'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const response = await request(app).post('/api/webhooks/claude').send(payload);
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body.error).toBe('Unauthorized');
|
||||
});
|
||||
|
||||
it('should reject requests with invalid authentication', async () => {
|
||||
const payload = {
|
||||
data: {
|
||||
type: 'session.create',
|
||||
session: {
|
||||
project: {
|
||||
repository: 'owner/repo',
|
||||
requirements: 'Test'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/webhooks/claude')
|
||||
.set('Authorization', 'Bearer wrong-secret')
|
||||
.send(payload);
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body.error).toBe('Unauthorized');
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/webhooks/claude - Orchestration', () => {
|
||||
it('should create orchestration session', async () => {
|
||||
const payload = {
|
||||
data: {
|
||||
type: 'orchestrate',
|
||||
project: {
|
||||
repository: 'owner/repo',
|
||||
requirements: 'Build a complete e-commerce platform'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/webhooks/claude')
|
||||
.set('Authorization', 'Bearer test-secret')
|
||||
.send(payload);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.message).toBe('Orchestration session created');
|
||||
expect(response.body.data).toMatchObject({
|
||||
status: 'initiated',
|
||||
summary: 'Created orchestration session for owner/repo'
|
||||
});
|
||||
expect(response.body.data.orchestrationId).toBeDefined();
|
||||
expect(response.body.data.sessions).toHaveLength(1);
|
||||
expect(response.body.data.sessions[0].type).toBe('coordination');
|
||||
});
|
||||
|
||||
it('should create orchestration session without auto-start', async () => {
|
||||
const payload = {
|
||||
data: {
|
||||
type: 'orchestrate',
|
||||
autoStart: false,
|
||||
project: {
|
||||
repository: 'owner/repo',
|
||||
requirements: 'Analyze and plan implementation'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/webhooks/claude')
|
||||
.set('Authorization', 'Bearer test-secret')
|
||||
.send(payload);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.data.sessions[0].status).toBe('initializing');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,174 +0,0 @@
|
||||
import request from 'supertest';
|
||||
import express from 'express';
|
||||
|
||||
// Mock child_process to prevent Docker commands
|
||||
jest.mock('child_process', () => ({
|
||||
execSync: jest.fn(() => ''),
|
||||
spawn: jest.fn(() => ({
|
||||
stdout: { on: jest.fn() },
|
||||
stderr: { on: jest.fn() },
|
||||
on: jest.fn((event, callback) => {
|
||||
if (event === 'close') {
|
||||
setTimeout(() => callback(0), 100);
|
||||
}
|
||||
})
|
||||
}))
|
||||
}));
|
||||
|
||||
// Mock SessionManager to avoid Docker calls in CI
|
||||
jest.mock('../../../src/providers/claude/services/SessionManager', () => {
|
||||
return {
|
||||
SessionManager: jest.fn().mockImplementation(() => ({
|
||||
createContainer: jest.fn().mockResolvedValue('mock-container-id'),
|
||||
startSession: jest.fn().mockResolvedValue(undefined),
|
||||
getSession: jest.fn().mockImplementation(id => ({
|
||||
id,
|
||||
status: 'running',
|
||||
type: 'implementation',
|
||||
project: { repository: 'test/repo', requirements: 'test' },
|
||||
dependencies: []
|
||||
})),
|
||||
listSessions: jest.fn().mockResolvedValue([]),
|
||||
getSessionOutput: jest.fn().mockResolvedValue({ output: 'test output' }),
|
||||
canStartSession: jest.fn().mockResolvedValue(true),
|
||||
updateSessionStatus: jest.fn().mockResolvedValue(undefined)
|
||||
}))
|
||||
};
|
||||
});
|
||||
|
||||
// Now we can import the routes
|
||||
import webhookRoutes from '../../../src/routes/webhooks';
|
||||
|
||||
// Set environment variables for testing
|
||||
process.env.CLAUDE_WEBHOOK_SECRET = 'test-claude-secret';
|
||||
process.env.SKIP_WEBHOOK_VERIFICATION = '1';
|
||||
|
||||
describe('Claude Webhook Integration', () => {
|
||||
let app: express.Application;
|
||||
|
||||
beforeAll(() => {
|
||||
// Import provider to register handlers
|
||||
require('../../../src/providers/claude');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
app = express();
|
||||
app.use(express.json());
|
||||
app.use('/api/webhooks', webhookRoutes);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('POST /api/webhooks/claude', () => {
|
||||
it('should accept valid orchestration request', async () => {
|
||||
const payload = {
|
||||
data: {
|
||||
type: 'orchestrate',
|
||||
project: {
|
||||
repository: 'test-owner/test-repo',
|
||||
requirements: 'Build a simple REST API with authentication'
|
||||
},
|
||||
strategy: {
|
||||
parallelSessions: 3,
|
||||
phases: ['analysis', 'implementation', 'testing']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/webhooks/claude')
|
||||
.set('Authorization', 'Bearer test-claude-secret')
|
||||
.send(payload)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toMatchObject({
|
||||
message: 'Webhook processed',
|
||||
event: 'orchestrate'
|
||||
});
|
||||
|
||||
expect(response.body.results).toBeDefined();
|
||||
expect(response.body.results[0].success).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject request without authorization', async () => {
|
||||
const payload = {
|
||||
data: {
|
||||
type: 'orchestrate',
|
||||
project: {
|
||||
repository: 'test-owner/test-repo',
|
||||
requirements: 'Build API'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Remove skip verification for this test
|
||||
const originalSkip = process.env.SKIP_WEBHOOK_VERIFICATION;
|
||||
delete process.env.SKIP_WEBHOOK_VERIFICATION;
|
||||
|
||||
const response = await request(app).post('/api/webhooks/claude').send(payload).expect(401);
|
||||
|
||||
expect(response.body).toMatchObject({
|
||||
error: 'Unauthorized'
|
||||
});
|
||||
|
||||
// Restore skip verification
|
||||
process.env.SKIP_WEBHOOK_VERIFICATION = originalSkip;
|
||||
});
|
||||
|
||||
it('should handle session management request', async () => {
|
||||
const payload = {
|
||||
data: {
|
||||
type: 'session',
|
||||
sessionId: 'test-session-123',
|
||||
project: {
|
||||
repository: 'test-owner/test-repo',
|
||||
requirements: 'Manage session'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/webhooks/claude')
|
||||
.set('Authorization', 'Bearer test-claude-secret')
|
||||
.send(payload)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toMatchObject({
|
||||
message: 'Webhook processed',
|
||||
event: 'session'
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject invalid payload', async () => {
|
||||
const payload = {
|
||||
data: {
|
||||
// Missing type field
|
||||
invalid: 'data'
|
||||
}
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/webhooks/claude')
|
||||
.set('Authorization', 'Bearer test-claude-secret')
|
||||
.send(payload)
|
||||
.expect(500);
|
||||
|
||||
expect(response.body.error).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/webhooks/health', () => {
|
||||
it('should show Claude provider in health check', async () => {
|
||||
const response = await request(app).get('/api/webhooks/health').expect(200);
|
||||
|
||||
expect(response.body.status).toBe('healthy');
|
||||
expect(response.body.providers).toBeDefined();
|
||||
|
||||
const claudeProvider = response.body.providers.find((p: any) => p.name === 'claude');
|
||||
expect(claudeProvider).toBeDefined();
|
||||
expect(claudeProvider.handlerCount).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -108,6 +108,156 @@ describe('SessionHandler', () => {
|
||||
expect(response.success).toBe(false);
|
||||
expect(response.error).toBe('Requirements are required for session creation');
|
||||
});
|
||||
|
||||
it('should filter out invalid dependency values', async () => {
|
||||
mockSessionManager.createContainer.mockResolvedValue('container-123');
|
||||
|
||||
const payload: ClaudeWebhookPayload = {
|
||||
data: {
|
||||
type: 'session.create',
|
||||
session: {
|
||||
project: {
|
||||
repository: 'owner/repo',
|
||||
requirements: 'Test requirements'
|
||||
},
|
||||
dependencies: ['', ' ', 'none', 'None', 'NONE', '550e8400-e29b-41d4-a716-446655440000']
|
||||
}
|
||||
},
|
||||
metadata: {}
|
||||
};
|
||||
|
||||
const response = await handler.handle(payload, mockContext);
|
||||
|
||||
expect(response.success).toBe(true);
|
||||
// Only the valid UUID should remain
|
||||
expect(response.data?.session.dependencies).toEqual(['550e8400-e29b-41d4-a716-446655440000']);
|
||||
});
|
||||
|
||||
it('should fail with invalid UUID format in dependencies', async () => {
|
||||
const payload: ClaudeWebhookPayload = {
|
||||
data: {
|
||||
type: 'session.create',
|
||||
session: {
|
||||
project: {
|
||||
repository: 'owner/repo',
|
||||
requirements: 'Test requirements'
|
||||
},
|
||||
dependencies: ['not-a-uuid', 'invalid-uuid-format', '12345']
|
||||
}
|
||||
},
|
||||
metadata: {}
|
||||
};
|
||||
|
||||
const response = await handler.handle(payload, mockContext);
|
||||
|
||||
expect(response.success).toBe(false);
|
||||
expect(response.error).toBe(
|
||||
'Invalid dependency IDs (not valid UUIDs): not-a-uuid, invalid-uuid-format, 12345'
|
||||
);
|
||||
});
|
||||
|
||||
it('should accept valid UUID dependencies', async () => {
|
||||
mockSessionManager.createContainer.mockResolvedValue('container-123');
|
||||
|
||||
const validUUIDs = [
|
||||
'550e8400-e29b-41d4-a716-446655440000',
|
||||
'f47ac10b-58cc-4372-a567-0e02b2c3d479',
|
||||
'6ba7b810-9dad-11d1-80b4-00c04fd430c8'
|
||||
];
|
||||
|
||||
const payload: ClaudeWebhookPayload = {
|
||||
data: {
|
||||
type: 'session.create',
|
||||
session: {
|
||||
project: {
|
||||
repository: 'owner/repo',
|
||||
requirements: 'Test requirements'
|
||||
},
|
||||
dependencies: validUUIDs
|
||||
}
|
||||
},
|
||||
metadata: {}
|
||||
};
|
||||
|
||||
const response = await handler.handle(payload, mockContext);
|
||||
|
||||
expect(response.success).toBe(true);
|
||||
expect(response.data?.session.dependencies).toEqual(validUUIDs);
|
||||
});
|
||||
|
||||
it('should handle mixed valid and invalid dependencies', async () => {
|
||||
const payload: ClaudeWebhookPayload = {
|
||||
data: {
|
||||
type: 'session.create',
|
||||
session: {
|
||||
project: {
|
||||
repository: 'owner/repo',
|
||||
requirements: 'Test requirements'
|
||||
},
|
||||
dependencies: [
|
||||
'550e8400-e29b-41d4-a716-446655440000', // valid
|
||||
'', // empty - filtered out
|
||||
'none', // filtered out
|
||||
'not-a-uuid', // invalid format
|
||||
'f47ac10b-58cc-4372-a567-0e02b2c3d479' // valid
|
||||
]
|
||||
}
|
||||
},
|
||||
metadata: {}
|
||||
};
|
||||
|
||||
const response = await handler.handle(payload, mockContext);
|
||||
|
||||
expect(response.success).toBe(false);
|
||||
expect(response.error).toBe('Invalid dependency IDs (not valid UUIDs): not-a-uuid');
|
||||
});
|
||||
|
||||
it('should handle empty dependencies array', async () => {
|
||||
mockSessionManager.createContainer.mockResolvedValue('container-123');
|
||||
|
||||
const payload: ClaudeWebhookPayload = {
|
||||
data: {
|
||||
type: 'session.create',
|
||||
session: {
|
||||
project: {
|
||||
repository: 'owner/repo',
|
||||
requirements: 'Test requirements'
|
||||
},
|
||||
dependencies: []
|
||||
}
|
||||
},
|
||||
metadata: {}
|
||||
};
|
||||
|
||||
const response = await handler.handle(payload, mockContext);
|
||||
|
||||
expect(response.success).toBe(true);
|
||||
expect(response.data?.session.dependencies).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle dependencies with only filtered values', async () => {
|
||||
mockSessionManager.createContainer.mockResolvedValue('container-123');
|
||||
|
||||
const payload: ClaudeWebhookPayload = {
|
||||
data: {
|
||||
type: 'session.create',
|
||||
session: {
|
||||
project: {
|
||||
repository: 'owner/repo',
|
||||
requirements: 'Test requirements'
|
||||
},
|
||||
dependencies: ['', ' ', 'none', 'None']
|
||||
}
|
||||
},
|
||||
metadata: {}
|
||||
};
|
||||
|
||||
const response = await handler.handle(payload, mockContext);
|
||||
|
||||
expect(response.success).toBe(true);
|
||||
// All values filtered out, should result in empty array
|
||||
expect(response.data?.session.dependencies).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('session.get', () => {
|
||||
|
||||
345
test/unit/providers/claude/services/SessionManager.test.ts
Normal file
345
test/unit/providers/claude/services/SessionManager.test.ts
Normal file
@@ -0,0 +1,345 @@
|
||||
import { SessionManager } from '../../../../../src/providers/claude/services/SessionManager';
|
||||
import { execSync, spawn } from 'child_process';
|
||||
import type { ClaudeSession } from '../../../../../src/types/claude-orchestration';
|
||||
|
||||
// Mock child_process
|
||||
jest.mock('child_process', () => ({
|
||||
execSync: jest.fn(),
|
||||
spawn: jest.fn()
|
||||
}));
|
||||
|
||||
// Mock logger
|
||||
jest.mock('../../../../../src/utils/logger', () => ({
|
||||
createLogger: () => ({
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
debug: jest.fn()
|
||||
})
|
||||
}));
|
||||
|
||||
describe('SessionManager', () => {
|
||||
let sessionManager: SessionManager;
|
||||
const mockExecSync = execSync as jest.MockedFunction<typeof execSync>;
|
||||
const mockSpawn = spawn as jest.MockedFunction<typeof spawn>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
sessionManager = new SessionManager();
|
||||
|
||||
// Setup default mocks
|
||||
mockExecSync.mockReturnValue(Buffer.from(''));
|
||||
mockSpawn.mockReturnValue({
|
||||
stdout: { on: jest.fn() },
|
||||
stderr: { on: jest.fn() },
|
||||
on: jest.fn()
|
||||
} as any);
|
||||
});
|
||||
|
||||
describe('createContainer', () => {
|
||||
it('should create a container for a session', async () => {
|
||||
const session: ClaudeSession = {
|
||||
id: 'test-session-123',
|
||||
type: 'analysis',
|
||||
status: 'pending',
|
||||
project: {
|
||||
repository: 'owner/repo',
|
||||
requirements: 'Test requirements',
|
||||
constraints: []
|
||||
},
|
||||
dependencies: [],
|
||||
createdAt: new Date()
|
||||
};
|
||||
|
||||
const containerName = await sessionManager.createContainer(session);
|
||||
|
||||
expect(containerName).toBe('claude-analysis-test-ses');
|
||||
expect(mockExecSync).toHaveBeenCalledWith(expect.stringContaining('docker volume create'), {
|
||||
stdio: 'pipe'
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle errors when creating container', () => {
|
||||
const session: ClaudeSession = {
|
||||
id: 'test-session-123',
|
||||
type: 'analysis',
|
||||
status: 'pending',
|
||||
project: {
|
||||
repository: 'owner/repo',
|
||||
requirements: 'Test requirements',
|
||||
constraints: []
|
||||
},
|
||||
dependencies: [],
|
||||
createdAt: new Date()
|
||||
};
|
||||
|
||||
mockExecSync.mockImplementation(() => {
|
||||
throw new Error('Docker error');
|
||||
});
|
||||
|
||||
expect(() => sessionManager.createContainer(session)).toThrow('Docker error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('startSession', () => {
|
||||
it('should start a session with a container', async () => {
|
||||
const session: ClaudeSession = {
|
||||
id: 'test-session-123',
|
||||
type: 'implementation',
|
||||
status: 'pending',
|
||||
containerId: 'container-123',
|
||||
project: {
|
||||
repository: 'owner/repo',
|
||||
requirements: 'Implement feature X',
|
||||
constraints: []
|
||||
},
|
||||
dependencies: [],
|
||||
createdAt: new Date()
|
||||
};
|
||||
|
||||
// Mock spawn to simulate successful execution
|
||||
const mockProcess = {
|
||||
stdout: {
|
||||
on: jest.fn((event, cb) => {
|
||||
if (event === 'data') {
|
||||
// Simulate stream-json output with Claude session ID
|
||||
cb(
|
||||
Buffer.from(
|
||||
'{"type":"system","subtype":"init","session_id":"claude-session-123"}\n'
|
||||
)
|
||||
);
|
||||
}
|
||||
})
|
||||
},
|
||||
stderr: { on: jest.fn() },
|
||||
on: jest.fn((event, cb) => {
|
||||
if (event === 'close') cb(0);
|
||||
}),
|
||||
unref: jest.fn()
|
||||
};
|
||||
mockSpawn.mockReturnValue(mockProcess as any);
|
||||
|
||||
await sessionManager.startSession(session);
|
||||
|
||||
expect(mockSpawn).toHaveBeenCalledWith(
|
||||
'docker',
|
||||
expect.arrayContaining(['run', '--rm', '--name', 'container-123']),
|
||||
expect.any(Object)
|
||||
);
|
||||
expect(mockProcess.unref).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw error if session has no container ID', () => {
|
||||
const session: ClaudeSession = {
|
||||
id: 'test-session-123',
|
||||
type: 'testing',
|
||||
status: 'pending',
|
||||
project: {
|
||||
repository: 'owner/repo',
|
||||
requirements: 'Test requirements',
|
||||
constraints: []
|
||||
},
|
||||
dependencies: [],
|
||||
createdAt: new Date()
|
||||
};
|
||||
|
||||
expect(() => sessionManager.startSession(session)).toThrow('Session has no container ID');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSession', () => {
|
||||
it('should return a session by ID', async () => {
|
||||
const session: ClaudeSession = {
|
||||
id: 'test-session-123',
|
||||
type: 'review',
|
||||
status: 'pending',
|
||||
project: {
|
||||
repository: 'owner/repo',
|
||||
requirements: 'Review code',
|
||||
constraints: []
|
||||
},
|
||||
dependencies: [],
|
||||
createdAt: new Date()
|
||||
};
|
||||
|
||||
await sessionManager.createContainer(session);
|
||||
const retrieved = sessionManager.getSession('test-session-123');
|
||||
|
||||
expect(retrieved).toBeDefined();
|
||||
expect(retrieved?.id).toBe('test-session-123');
|
||||
});
|
||||
|
||||
it('should return undefined for non-existent session', () => {
|
||||
const retrieved = sessionManager.getSession('non-existent');
|
||||
expect(retrieved).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAllSessions', () => {
|
||||
it('should return all sessions', async () => {
|
||||
const session1: ClaudeSession = {
|
||||
id: 'session-1',
|
||||
type: 'analysis',
|
||||
status: 'pending',
|
||||
project: {
|
||||
repository: 'owner/repo1',
|
||||
requirements: 'Analyze',
|
||||
constraints: []
|
||||
},
|
||||
dependencies: [],
|
||||
createdAt: new Date()
|
||||
};
|
||||
|
||||
const session2: ClaudeSession = {
|
||||
id: 'session-2',
|
||||
type: 'implementation',
|
||||
status: 'pending',
|
||||
project: {
|
||||
repository: 'owner/repo2',
|
||||
requirements: 'Implement',
|
||||
constraints: []
|
||||
},
|
||||
dependencies: [],
|
||||
createdAt: new Date()
|
||||
};
|
||||
|
||||
await sessionManager.createContainer(session1);
|
||||
await sessionManager.createContainer(session2);
|
||||
|
||||
const allSessions = sessionManager.getAllSessions();
|
||||
expect(allSessions).toHaveLength(2);
|
||||
expect(allSessions.map(s => s.id)).toEqual(['session-1', 'session-2']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOrchestrationSessions', () => {
|
||||
it('should return sessions for a specific orchestration', async () => {
|
||||
const session1: ClaudeSession = {
|
||||
id: 'orch-123-session-1',
|
||||
type: 'analysis',
|
||||
status: 'pending',
|
||||
project: {
|
||||
repository: 'owner/repo',
|
||||
requirements: 'Analyze',
|
||||
constraints: []
|
||||
},
|
||||
dependencies: [],
|
||||
createdAt: new Date()
|
||||
};
|
||||
|
||||
const session2: ClaudeSession = {
|
||||
id: 'orch-123-session-2',
|
||||
type: 'implementation',
|
||||
status: 'pending',
|
||||
project: {
|
||||
repository: 'owner/repo',
|
||||
requirements: 'Implement',
|
||||
constraints: []
|
||||
},
|
||||
dependencies: [],
|
||||
createdAt: new Date()
|
||||
};
|
||||
|
||||
const otherSession: ClaudeSession = {
|
||||
id: 'orch-456-session-1',
|
||||
type: 'testing',
|
||||
status: 'pending',
|
||||
project: {
|
||||
repository: 'owner/repo',
|
||||
requirements: 'Test',
|
||||
constraints: []
|
||||
},
|
||||
dependencies: [],
|
||||
createdAt: new Date()
|
||||
};
|
||||
|
||||
await sessionManager.createContainer(session1);
|
||||
await sessionManager.createContainer(session2);
|
||||
await sessionManager.createContainer(otherSession);
|
||||
|
||||
const orchSessions = sessionManager.getOrchestrationSessions('orch-123');
|
||||
expect(orchSessions).toHaveLength(2);
|
||||
expect(orchSessions.map(s => s.id)).toEqual(['orch-123-session-1', 'orch-123-session-2']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('queueSession', () => {
|
||||
it('should start session immediately if no dependencies', async () => {
|
||||
const session: ClaudeSession = {
|
||||
id: 'test-session',
|
||||
type: 'analysis',
|
||||
status: 'pending',
|
||||
containerId: 'container-123',
|
||||
project: {
|
||||
repository: 'owner/repo',
|
||||
requirements: 'Analyze',
|
||||
constraints: []
|
||||
},
|
||||
dependencies: [],
|
||||
createdAt: new Date()
|
||||
};
|
||||
|
||||
const mockProcess = {
|
||||
stdout: {
|
||||
on: jest.fn((event, cb) => {
|
||||
if (event === 'data') {
|
||||
cb(
|
||||
Buffer.from(
|
||||
'{"type":"system","subtype":"init","session_id":"claude-session-123"}\n'
|
||||
)
|
||||
);
|
||||
}
|
||||
})
|
||||
},
|
||||
stderr: { on: jest.fn() },
|
||||
on: jest.fn((event, cb) => {
|
||||
if (event === 'close') cb(0);
|
||||
}),
|
||||
unref: jest.fn()
|
||||
};
|
||||
mockSpawn.mockReturnValue(mockProcess as any);
|
||||
|
||||
await sessionManager.queueSession(session);
|
||||
|
||||
expect(mockSpawn).toHaveBeenCalledWith(
|
||||
'docker',
|
||||
expect.arrayContaining(['run', '--rm', '--name', 'container-123']),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('should queue session if dependencies not met', async () => {
|
||||
const depSession: ClaudeSession = {
|
||||
id: 'dep-session',
|
||||
type: 'analysis',
|
||||
status: 'running',
|
||||
project: {
|
||||
repository: 'owner/repo',
|
||||
requirements: 'Analyze',
|
||||
constraints: []
|
||||
},
|
||||
dependencies: [],
|
||||
createdAt: new Date()
|
||||
};
|
||||
|
||||
const session: ClaudeSession = {
|
||||
id: 'test-session',
|
||||
type: 'implementation',
|
||||
status: 'pending',
|
||||
containerId: 'container-123',
|
||||
project: {
|
||||
repository: 'owner/repo',
|
||||
requirements: 'Implement',
|
||||
constraints: []
|
||||
},
|
||||
dependencies: ['dep-session'],
|
||||
createdAt: new Date()
|
||||
};
|
||||
|
||||
await sessionManager.createContainer(depSession);
|
||||
await sessionManager.queueSession(session);
|
||||
|
||||
// Should not start immediately
|
||||
expect(mockSpawn).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
111
test/unit/providers/github/handlers/IssueHandler.test.ts
Normal file
111
test/unit/providers/github/handlers/IssueHandler.test.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { IssueOpenedHandler } from '../../../../../src/providers/github/handlers/IssueHandler';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('../../../../../src/utils/logger', () => ({
|
||||
createLogger: () => ({
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
warn: jest.fn()
|
||||
})
|
||||
}));
|
||||
|
||||
jest.mock('../../../../../src/utils/secureCredentials', () => ({
|
||||
SecureCredentials: jest.fn().mockImplementation(() => ({
|
||||
loadCredentials: jest.fn(),
|
||||
getCredential: jest.fn().mockReturnValue('mock-value')
|
||||
})),
|
||||
secureCredentials: {
|
||||
loadCredentials: jest.fn(),
|
||||
getCredential: jest.fn().mockReturnValue('mock-value')
|
||||
}
|
||||
}));
|
||||
|
||||
jest.mock('../../../../../src/services/claudeService');
|
||||
jest.mock('../../../../../src/services/githubService');
|
||||
|
||||
const claudeService = require('../../../../../src/services/claudeService');
|
||||
|
||||
describe('IssueOpenedHandler', () => {
|
||||
let handler: IssueOpenedHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
handler = new IssueOpenedHandler();
|
||||
});
|
||||
|
||||
describe('handle', () => {
|
||||
const mockPayload = {
|
||||
event: 'issues.opened',
|
||||
data: {
|
||||
action: 'opened',
|
||||
issue: {
|
||||
id: 123,
|
||||
number: 1,
|
||||
title: 'Test Issue',
|
||||
body: 'This is a test issue about authentication and API integration',
|
||||
labels: [],
|
||||
state: 'open',
|
||||
user: {
|
||||
login: 'testuser',
|
||||
id: 1
|
||||
},
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
},
|
||||
repository: {
|
||||
id: 456,
|
||||
name: 'test-repo',
|
||||
full_name: 'owner/test-repo',
|
||||
owner: {
|
||||
login: 'owner',
|
||||
id: 2
|
||||
},
|
||||
private: false
|
||||
},
|
||||
sender: {
|
||||
login: 'testuser',
|
||||
id: 1
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const mockContext = {
|
||||
timestamp: new Date(),
|
||||
requestId: 'test-request-id'
|
||||
};
|
||||
|
||||
it('should analyze and label new issues', async () => {
|
||||
claudeService.processCommand = jest.fn().mockResolvedValue('Labels applied successfully');
|
||||
|
||||
const result = await handler.handle(mockPayload as any, mockContext);
|
||||
|
||||
expect(claudeService.processCommand).toHaveBeenCalledWith({
|
||||
repoFullName: 'owner/test-repo',
|
||||
issueNumber: 1,
|
||||
command: expect.stringContaining('Analyze this GitHub issue'),
|
||||
isPullRequest: false,
|
||||
branchName: null,
|
||||
operationType: 'auto-tagging'
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
message: 'Issue auto-tagged successfully',
|
||||
data: {
|
||||
repo: 'owner/test-repo',
|
||||
issue: 1
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle errors gracefully', async () => {
|
||||
claudeService.processCommand = jest.fn().mockRejectedValue(new Error('Analysis failed'));
|
||||
|
||||
const result = await handler.handle(mockPayload as any, mockContext);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('Analysis failed');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user