feat: Add code snippet support and suggestions to add_comment tool
- Added code_snippet parameter to find line numbers automatically using code text - Added search_context parameter with before/after arrays for disambiguation - Added match_strategy parameter (strict/best) for handling multiple matches - Added suggestion and suggestion_end_line parameters for code suggestions - Created suggestion-formatter utility for formatting suggestion comments - Enhanced tool definitions with clearer descriptions and examples - Updated README with comprehensive usage guide and decision flow - Removed all debug information from responses for production readiness - Added detailed error responses when multiple code matches are found - Improved type definitions and guards for new parameters This makes the add_comment tool more intelligent and user-friendly, especially for AI-powered code review scenarios.
This commit is contained in:
parent
d649a116fe
commit
f602840c97
10 changed files with 746 additions and 73 deletions
36
CHANGELOG.md
36
CHANGELOG.md
|
@ -5,6 +5,42 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.9.0] - 2025-01-26
|
||||
|
||||
### Added
|
||||
- **Code snippet support in `add_comment` tool**:
|
||||
- Added `code_snippet` parameter to find line numbers automatically using code text
|
||||
- Added `search_context` parameter with `before` and `after` arrays to disambiguate multiple matches
|
||||
- Added `match_strategy` parameter with options:
|
||||
- `"strict"` (default): Fails with detailed error when multiple matches found
|
||||
- `"best"`: Auto-selects the highest confidence match
|
||||
- Returns detailed error with all occurrences when multiple matches found in strict mode
|
||||
- Particularly useful for AI-powered code review tools that analyze diffs
|
||||
- Created comprehensive line matching algorithm that:
|
||||
- Parses diffs to find exact code snippets
|
||||
- Calculates confidence scores based on context matching
|
||||
- Handles added, removed, and context lines appropriately
|
||||
|
||||
### Changed
|
||||
- Enhanced `add_comment` tool to resolve line numbers from code snippets when `line_number` is not provided
|
||||
- Improved error messages to include preview and suggestions for resolving ambiguous matches
|
||||
|
||||
## [0.8.0] - 2025-01-26
|
||||
|
||||
### Added
|
||||
- **Code suggestions support in `add_comment` tool**:
|
||||
- Added `suggestion` parameter to add code suggestions in comments
|
||||
- Added `suggestion_end_line` parameter for multi-line suggestions
|
||||
- Suggestions are formatted using GitHub-style markdown ````suggestion` blocks
|
||||
- Works with both single-line and multi-line code replacements
|
||||
- Requires `file_path` and `line_number` to be specified when using suggestions
|
||||
- Compatible with both Bitbucket Cloud and Server
|
||||
- Created `suggestion-formatter.ts` utility for formatting suggestion comments
|
||||
|
||||
### Changed
|
||||
- Enhanced `add_comment` tool to validate suggestion requirements
|
||||
- Updated tool response to indicate when a comment contains a suggestion
|
||||
|
||||
## [0.7.0] - 2025-01-26
|
||||
|
||||
### Added
|
||||
|
|
207
README.md
207
README.md
|
@ -271,9 +271,9 @@ Returns a paginated list of pull requests with:
|
|||
|
||||
### Add Comment
|
||||
|
||||
Add general comments, reply to existing comments, or add inline comments on specific lines of code:
|
||||
Add a comment to a pull request, either as a general comment or inline on specific code:
|
||||
|
||||
```typescript
|
||||
```javascript
|
||||
// General comment
|
||||
{
|
||||
"tool": "add_comment",
|
||||
|
@ -281,37 +281,150 @@ Add general comments, reply to existing comments, or add inline comments on spec
|
|||
"workspace": "PROJ",
|
||||
"repository": "my-repo",
|
||||
"pull_request_id": 123,
|
||||
"comment_text": "Great work! Just one small suggestion..."
|
||||
"comment_text": "Great work on this PR!"
|
||||
}
|
||||
}
|
||||
|
||||
// Reply to an existing comment
|
||||
// Inline comment on specific line
|
||||
{
|
||||
"tool": "add_comment",
|
||||
"arguments": {
|
||||
"workspace": "PROJ",
|
||||
"repository": "my-repo",
|
||||
"pull_request_id": 123,
|
||||
"comment_text": "Thanks for the feedback! I've updated the code.",
|
||||
"parent_comment_id": 456 // ID of the comment you're replying to
|
||||
}
|
||||
}
|
||||
|
||||
// Inline comment on specific code
|
||||
{
|
||||
"tool": "add_comment",
|
||||
"arguments": {
|
||||
"workspace": "PROJ",
|
||||
"repository": "my-repo",
|
||||
"pull_request_id": 123,
|
||||
"comment_text": "This variable should be renamed for clarity",
|
||||
"file_path": "src/main.js",
|
||||
"comment_text": "Consider extracting this into a separate function",
|
||||
"file_path": "src/utils/helpers.js",
|
||||
"line_number": 42,
|
||||
"line_type": "ADDED" // ADDED, REMOVED, or CONTEXT
|
||||
"line_type": "CONTEXT" // ADDED, REMOVED, or CONTEXT
|
||||
}
|
||||
}
|
||||
|
||||
// Reply to existing comment
|
||||
{
|
||||
"tool": "add_comment",
|
||||
"arguments": {
|
||||
"workspace": "PROJ",
|
||||
"repository": "my-repo",
|
||||
"pull_request_id": 123,
|
||||
"comment_text": "I agree with this suggestion",
|
||||
"parent_comment_id": 456
|
||||
}
|
||||
}
|
||||
|
||||
// Add comment with code suggestion (single line)
|
||||
{
|
||||
"tool": "add_comment",
|
||||
"arguments": {
|
||||
"workspace": "PROJ",
|
||||
"repository": "my-repo",
|
||||
"pull_request_id": 123,
|
||||
"comment_text": "This variable name could be more descriptive.",
|
||||
"file_path": "src/utils/helpers.js",
|
||||
"line_number": 42,
|
||||
"line_type": "CONTEXT",
|
||||
"suggestion": "const userAuthenticationToken = token;"
|
||||
}
|
||||
}
|
||||
|
||||
// Add comment with multi-line code suggestion
|
||||
{
|
||||
"tool": "add_comment",
|
||||
"arguments": {
|
||||
"workspace": "PROJ",
|
||||
"repository": "my-repo",
|
||||
"pull_request_id": 123,
|
||||
"comment_text": "This function could be simplified using array methods.",
|
||||
"file_path": "src/utils/calculations.js",
|
||||
"line_number": 50,
|
||||
"suggestion_end_line": 55,
|
||||
"line_type": "CONTEXT",
|
||||
"suggestion": "function calculateTotal(items) {\n return items.reduce((sum, item) => sum + item.price, 0);\n}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The suggestion feature formats comments using GitHub-style markdown suggestion blocks that Bitbucket can render. When adding a suggestion:
|
||||
- `suggestion` is required and contains the replacement code
|
||||
- `file_path` and `line_number` are required when using suggestions
|
||||
- `suggestion_end_line` is optional and used for multi-line suggestions (defaults to `line_number`)
|
||||
- The comment will be formatted with a ````suggestion` markdown block that may be applicable in the Bitbucket UI
|
||||
|
||||
### Using Code Snippets Instead of Line Numbers
|
||||
|
||||
The `add_comment` tool now supports finding line numbers automatically using code snippets. This is especially useful when AI tools analyze diffs and may struggle with exact line numbers:
|
||||
|
||||
```javascript
|
||||
// Add comment using code snippet
|
||||
{
|
||||
"tool": "add_comment",
|
||||
"arguments": {
|
||||
"workspace": "PROJ",
|
||||
"repository": "my-repo",
|
||||
"pull_request_id": 123,
|
||||
"comment_text": "This variable name could be more descriptive",
|
||||
"file_path": "src/components/Button.res",
|
||||
"code_snippet": "let isDisabled = false",
|
||||
"search_context": {
|
||||
"before": ["let onClick = () => {"],
|
||||
"after": ["setLoading(true)"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle multiple matches with strategy
|
||||
{
|
||||
"tool": "add_comment",
|
||||
"arguments": {
|
||||
"workspace": "PROJ",
|
||||
"repository": "my-repo",
|
||||
"pull_request_id": 123,
|
||||
"comment_text": "Consider extracting this",
|
||||
"file_path": "src/utils/helpers.js",
|
||||
"code_snippet": "return result;",
|
||||
"search_context": {
|
||||
"before": ["const result = calculate();"],
|
||||
"after": ["}"]
|
||||
},
|
||||
"match_strategy": "best" // Auto-select highest confidence match
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Code Snippet Parameters:**
|
||||
- `code_snippet`: The exact code line to find (alternative to `line_number`)
|
||||
- `search_context`: Optional context to disambiguate multiple matches
|
||||
- `before`: Array of lines that should appear before the target
|
||||
- `after`: Array of lines that should appear after the target
|
||||
- `match_strategy`: How to handle multiple matches
|
||||
- `"strict"` (default): Fail with error showing all matches
|
||||
- `"best"`: Auto-select the highest confidence match
|
||||
|
||||
**Error Response for Multiple Matches (strict mode):**
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "MULTIPLE_MATCHES_FOUND",
|
||||
"message": "Code snippet 'return result;' found in 3 locations",
|
||||
"occurrences": [
|
||||
{
|
||||
"line_number": 42,
|
||||
"file_path": "src/utils/helpers.js",
|
||||
"preview": " const result = calculate();\n> return result;\n}",
|
||||
"confidence": 0.9,
|
||||
"line_type": "ADDED"
|
||||
},
|
||||
// ... more matches
|
||||
],
|
||||
"suggestion": "To resolve, either:\n1. Add more context...\n2. Use match_strategy: 'best'...\n3. Use line_number directly"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This feature is particularly useful for:
|
||||
- AI-powered code review tools that analyze diffs
|
||||
- Scripts that automatically add comments based on code patterns
|
||||
- Avoiding line number confusion in large diffs
|
||||
|
||||
**Note on comment replies:**
|
||||
- Use `parent_comment_id` to reply to any comment (general or inline)
|
||||
- In `get_pull_request` responses:
|
||||
|
@ -327,6 +440,62 @@ Add general comments, reply to existing comments, or add inline comments on spec
|
|||
- `REMOVED` - For deleted lines (red in diff)
|
||||
- `CONTEXT` - For unchanged context lines
|
||||
|
||||
#### Add Comment - Complete Usage Guide
|
||||
|
||||
The `add_comment` tool supports multiple scenarios. Here's when and how to use each approach:
|
||||
|
||||
**1. General PR Comments (No file/line)**
|
||||
- Use when: Making overall feedback about the PR
|
||||
- Required params: `comment_text` only
|
||||
- Example: "LGTM!", "Please update the documentation"
|
||||
|
||||
**2. Reply to Existing Comments**
|
||||
- Use when: Continuing a conversation thread
|
||||
- Required params: `comment_text`, `parent_comment_id`
|
||||
- Works for both general and inline comment replies
|
||||
|
||||
**3. Inline Comments with Line Number**
|
||||
- Use when: You know the exact line number from the diff
|
||||
- Required params: `comment_text`, `file_path`, `line_number`
|
||||
- Optional: `line_type` (defaults to CONTEXT)
|
||||
|
||||
**4. Inline Comments with Code Snippet**
|
||||
- Use when: You have the code but not the line number (common for AI tools)
|
||||
- Required params: `comment_text`, `file_path`, `code_snippet`
|
||||
- The tool will automatically find the line number
|
||||
- Add `search_context` if the code appears multiple times
|
||||
- Use `match_strategy: "best"` to auto-select when multiple matches exist
|
||||
|
||||
**5. Code Suggestions**
|
||||
- Use when: Proposing specific code changes
|
||||
- Required params: `comment_text`, `file_path`, `line_number`, `suggestion`
|
||||
- For multi-line: also add `suggestion_end_line`
|
||||
- Creates applicable suggestion blocks in Bitbucket UI
|
||||
|
||||
**Decision Flow for AI/Automated Tools:**
|
||||
```
|
||||
1. Do you want to suggest code changes?
|
||||
→ Use suggestion with line_number
|
||||
|
||||
2. Do you have the exact line number?
|
||||
→ Use line_number directly
|
||||
|
||||
3. Do you have the code snippet but not line number?
|
||||
→ Use code_snippet (add search_context if needed)
|
||||
|
||||
4. Is it a general comment about the PR?
|
||||
→ Use comment_text only
|
||||
|
||||
5. Are you replying to another comment?
|
||||
→ Add parent_comment_id
|
||||
```
|
||||
|
||||
**Common Pitfalls to Avoid:**
|
||||
- Don't use both `line_number` and `code_snippet` - pick one
|
||||
- Suggestions always need `file_path` and `line_number`
|
||||
- Code snippets must match exactly (including whitespace)
|
||||
- REMOVED lines reference the source file, ADDED/CONTEXT reference the destination
|
||||
|
||||
### Merge Pull Request
|
||||
|
||||
```typescript
|
||||
|
|
12
package-lock.json
generated
12
package-lock.json
generated
|
@ -1,16 +1,16 @@
|
|||
{
|
||||
"name": "@nexus2520/bitbucket-mcp-server",
|
||||
"version": "0.7.0",
|
||||
"version": "0.9.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@nexus2520/bitbucket-mcp-server",
|
||||
"version": "0.7.0",
|
||||
"version": "0.9.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||
"axios": "^1.9.0",
|
||||
"axios": "^1.10.0",
|
||||
"minimatch": "^9.0.3"
|
||||
},
|
||||
"bin": {
|
||||
|
@ -94,9 +94,9 @@
|
|||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
|
||||
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
|
||||
"integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@nexus2520/bitbucket-mcp-server",
|
||||
"version": "0.7.0",
|
||||
"version": "0.9.0",
|
||||
"description": "MCP server for Bitbucket API integration - supports both Cloud and Server",
|
||||
"type": "module",
|
||||
"main": "./build/index.js",
|
||||
|
@ -44,7 +44,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||
"axios": "^1.9.0",
|
||||
"axios": "^1.10.0",
|
||||
"minimatch": "^9.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { BitbucketApiClient } from '../utils/api-client.js';
|
||||
import { formatServerResponse, formatCloudResponse } from '../utils/formatters.js';
|
||||
import { formatSuggestionComment } from '../utils/suggestion-formatter.js';
|
||||
import { DiffParser } from '../utils/diff-parser.js';
|
||||
import {
|
||||
BitbucketServerPullRequest,
|
||||
BitbucketCloudPullRequest,
|
||||
|
@ -9,7 +11,9 @@ import {
|
|||
BitbucketCloudComment,
|
||||
BitbucketCloudFileChange,
|
||||
FormattedComment,
|
||||
FormattedFileChange
|
||||
FormattedFileChange,
|
||||
CodeMatch,
|
||||
MultipleMatchesError
|
||||
} from '../types/bitbucket.js';
|
||||
import {
|
||||
isGetPullRequestArgs,
|
||||
|
@ -27,6 +31,43 @@ export class PullRequestHandlers {
|
|||
private username: string
|
||||
) {}
|
||||
|
||||
private async getFilteredPullRequestDiff(
|
||||
workspace: string,
|
||||
repository: string,
|
||||
pullRequestId: number,
|
||||
filePath: string,
|
||||
contextLines: number = 3
|
||||
): Promise<string> {
|
||||
let apiPath: string;
|
||||
let config: any = {};
|
||||
|
||||
if (this.apiClient.getIsServer()) {
|
||||
// Bitbucket Server API
|
||||
apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pullRequestId}/diff`;
|
||||
config.params = { contextLines };
|
||||
} else {
|
||||
// Bitbucket Cloud API
|
||||
apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pullRequestId}/diff`;
|
||||
config.params = { context: contextLines };
|
||||
}
|
||||
|
||||
config.headers = { 'Accept': 'text/plain' };
|
||||
|
||||
const rawDiff = await this.apiClient.makeRequest<string>('get', apiPath, undefined, config);
|
||||
|
||||
const diffParser = new DiffParser();
|
||||
const sections = diffParser.parseDiffIntoSections(rawDiff);
|
||||
|
||||
const filterOptions = {
|
||||
filePath: filePath
|
||||
};
|
||||
|
||||
const filteredResult = diffParser.filterSections(sections, filterOptions);
|
||||
const filteredDiff = diffParser.reconstructDiff(filteredResult.sections);
|
||||
|
||||
return filteredDiff;
|
||||
}
|
||||
|
||||
async handleGetPullRequest(args: any) {
|
||||
if (!isGetPullRequestArgs(args)) {
|
||||
throw new McpError(
|
||||
|
@ -38,7 +79,6 @@ export class PullRequestHandlers {
|
|||
const { workspace, repository, pull_request_id } = args;
|
||||
|
||||
try {
|
||||
// Different API paths for Server vs Cloud
|
||||
const apiPath = this.apiClient.getIsServer()
|
||||
? `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}`
|
||||
: `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}`;
|
||||
|
@ -47,10 +87,8 @@ export class PullRequestHandlers {
|
|||
|
||||
let mergeInfo: MergeInfo = {};
|
||||
|
||||
// For Bitbucket Server, fetch additional merge information if PR is merged
|
||||
if (this.apiClient.getIsServer() && pr.state === 'MERGED') {
|
||||
try {
|
||||
// Try to get activities to find merge information
|
||||
const activitiesPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/activities`;
|
||||
const activitiesResponse = await this.apiClient.makeRequest<any>('get', activitiesPath, undefined, {
|
||||
params: { limit: 100 }
|
||||
|
@ -64,25 +102,21 @@ export class PullRequestHandlers {
|
|||
mergeInfo.mergedBy = mergeActivity.user?.displayName || null;
|
||||
mergeInfo.mergedAt = new Date(mergeActivity.createdDate).toISOString();
|
||||
|
||||
// Try to get commit message if we have the hash
|
||||
if (mergeActivity.commit?.id) {
|
||||
try {
|
||||
const commitPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/commits/${mergeActivity.commit.id}`;
|
||||
const commitResponse = await this.apiClient.makeRequest<any>('get', commitPath);
|
||||
mergeInfo.mergeCommitMessage = commitResponse.message || null;
|
||||
} catch (commitError) {
|
||||
// If we can't get the commit message, continue without it
|
||||
console.error('Failed to fetch merge commit message:', commitError);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (activitiesError) {
|
||||
// If we can't get activities, continue without merge info
|
||||
console.error('Failed to fetch PR activities:', activitiesError);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch comments and file changes in parallel
|
||||
let comments: FormattedComment[] = [];
|
||||
let activeCommentCount = 0;
|
||||
let totalCommentCount = 0;
|
||||
|
@ -101,16 +135,13 @@ export class PullRequestHandlers {
|
|||
fileChanges = fileChangesResult.fileChanges;
|
||||
fileChangesSummary = fileChangesResult.summary;
|
||||
} catch (error) {
|
||||
// Log error but continue with PR data
|
||||
console.error('Failed to fetch additional PR data:', error);
|
||||
}
|
||||
|
||||
// Format the response based on server type
|
||||
const formattedResponse = this.apiClient.getIsServer()
|
||||
? formatServerResponse(pr as BitbucketServerPullRequest, mergeInfo, this.baseUrl)
|
||||
: formatCloudResponse(pr as BitbucketCloudPullRequest);
|
||||
|
||||
// Add comments and file changes to the response
|
||||
const enhancedResponse = {
|
||||
...formattedResponse,
|
||||
active_comments: comments,
|
||||
|
@ -174,7 +205,6 @@ export class PullRequestHandlers {
|
|||
|
||||
const response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, { params });
|
||||
|
||||
// Format the response
|
||||
let pullRequests: any[] = [];
|
||||
let totalCount = 0;
|
||||
let nextPageStart = null;
|
||||
|
@ -386,10 +416,62 @@ export class PullRequestHandlers {
|
|||
);
|
||||
}
|
||||
|
||||
const { workspace, repository, pull_request_id, comment_text, parent_comment_id, file_path, line_number, line_type } = args;
|
||||
let {
|
||||
workspace,
|
||||
repository,
|
||||
pull_request_id,
|
||||
comment_text,
|
||||
parent_comment_id,
|
||||
file_path,
|
||||
line_number,
|
||||
line_type,
|
||||
suggestion,
|
||||
suggestion_end_line,
|
||||
code_snippet,
|
||||
search_context,
|
||||
match_strategy = 'strict'
|
||||
} = args;
|
||||
|
||||
let sequentialPosition: number | undefined;
|
||||
if (code_snippet && !line_number && file_path) {
|
||||
try {
|
||||
const resolved = await this.resolveLineFromCode(
|
||||
workspace,
|
||||
repository,
|
||||
pull_request_id,
|
||||
file_path,
|
||||
code_snippet,
|
||||
search_context,
|
||||
match_strategy
|
||||
);
|
||||
|
||||
line_number = resolved.line_number;
|
||||
line_type = resolved.line_type;
|
||||
sequentialPosition = resolved.sequential_position;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (suggestion && (!file_path || !line_number)) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
'Suggestions require file_path and line_number to be specified'
|
||||
);
|
||||
}
|
||||
|
||||
const isInlineComment = file_path !== undefined && line_number !== undefined;
|
||||
|
||||
let finalCommentText = comment_text;
|
||||
if (suggestion) {
|
||||
finalCommentText = formatSuggestionComment(
|
||||
comment_text,
|
||||
suggestion,
|
||||
line_number,
|
||||
suggestion_end_line || line_number
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
let apiPath: string;
|
||||
let requestBody: any;
|
||||
|
@ -398,7 +480,7 @@ export class PullRequestHandlers {
|
|||
// Bitbucket Server API
|
||||
apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pull_request_id}/comments`;
|
||||
requestBody = {
|
||||
text: comment_text
|
||||
text: finalCommentText
|
||||
};
|
||||
|
||||
if (parent_comment_id !== undefined) {
|
||||
|
@ -413,13 +495,14 @@ export class PullRequestHandlers {
|
|||
path: file_path,
|
||||
diffType: 'EFFECTIVE'
|
||||
};
|
||||
|
||||
}
|
||||
} else {
|
||||
// Bitbucket Cloud API
|
||||
apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pull_request_id}/comments`;
|
||||
requestBody = {
|
||||
content: {
|
||||
raw: comment_text
|
||||
raw: finalCommentText
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -437,12 +520,16 @@ export class PullRequestHandlers {
|
|||
|
||||
const comment = await this.apiClient.makeRequest<any>('post', apiPath, requestBody);
|
||||
|
||||
const responseMessage = suggestion
|
||||
? 'Comment with code suggestion added successfully'
|
||||
: (isInlineComment ? 'Inline comment added successfully' : 'Comment added successfully');
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
message: isInlineComment ? 'Inline comment added successfully' : 'Comment added successfully',
|
||||
message: responseMessage,
|
||||
comment: {
|
||||
id: comment.id,
|
||||
text: this.apiClient.getIsServer() ? comment.text : comment.content.raw,
|
||||
|
@ -450,7 +537,9 @@ export class PullRequestHandlers {
|
|||
created_on: this.apiClient.getIsServer() ? new Date(comment.createdDate).toLocaleString() : comment.created_on,
|
||||
file_path: isInlineComment ? file_path : undefined,
|
||||
line_number: isInlineComment ? line_number : undefined,
|
||||
line_type: isInlineComment ? (line_type || 'CONTEXT') : undefined
|
||||
line_type: isInlineComment ? (line_type || 'CONTEXT') : undefined,
|
||||
has_suggestion: !!suggestion,
|
||||
suggestion_lines: suggestion ? (suggestion_end_line ? `${line_number}-${suggestion_end_line}` : `${line_number}`) : undefined
|
||||
}
|
||||
}, null, 2),
|
||||
},
|
||||
|
@ -532,7 +621,6 @@ export class PullRequestHandlers {
|
|||
let totalCount = 0;
|
||||
|
||||
if (this.apiClient.getIsServer()) {
|
||||
// Helper function to process nested comments recursively
|
||||
const processNestedComments = (comment: any, anchor: any): FormattedComment => {
|
||||
const formattedComment: FormattedComment = {
|
||||
id: comment.id,
|
||||
|
@ -545,11 +633,9 @@ export class PullRequestHandlers {
|
|||
state: comment.state
|
||||
};
|
||||
|
||||
// Process nested replies
|
||||
if (comment.comments && comment.comments.length > 0) {
|
||||
formattedComment.replies = comment.comments
|
||||
.filter((reply: any) => {
|
||||
// Apply same filters to replies
|
||||
if (reply.state === 'RESOLVED') return false;
|
||||
if (anchor && anchor.orphaned === true) return false;
|
||||
return true;
|
||||
|
@ -560,7 +646,6 @@ export class PullRequestHandlers {
|
|||
return formattedComment;
|
||||
};
|
||||
|
||||
// Helper to count all comments including nested ones
|
||||
const countAllComments = (comment: any): number => {
|
||||
let count = 1;
|
||||
if (comment.comments && comment.comments.length > 0) {
|
||||
|
@ -569,16 +654,13 @@ export class PullRequestHandlers {
|
|||
return count;
|
||||
};
|
||||
|
||||
// Helper to count active comments including nested ones
|
||||
const countActiveComments = (comment: any, anchor: any): number => {
|
||||
let count = 0;
|
||||
|
||||
// Check if this comment is active
|
||||
if (comment.state !== 'RESOLVED' && (!anchor || anchor.orphaned !== true)) {
|
||||
count = 1;
|
||||
}
|
||||
|
||||
// Count active nested comments
|
||||
if (comment.comments && comment.comments.length > 0) {
|
||||
count += comment.comments.reduce((sum: number, reply: any) => sum + countActiveComments(reply, anchor), 0);
|
||||
}
|
||||
|
@ -586,7 +668,6 @@ export class PullRequestHandlers {
|
|||
return count;
|
||||
};
|
||||
|
||||
// Bitbucket Server API - fetch from activities
|
||||
const apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pullRequestId}/activities`;
|
||||
const response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, {
|
||||
params: { limit: 1000 }
|
||||
|
@ -594,41 +675,32 @@ export class PullRequestHandlers {
|
|||
|
||||
const activities = response.values || [];
|
||||
|
||||
// Filter for comment activities
|
||||
const commentActivities = activities.filter((a: any) =>
|
||||
a.action === 'COMMENTED' && a.comment
|
||||
);
|
||||
|
||||
// Count all comments including nested ones
|
||||
totalCount = commentActivities.reduce((sum: number, activity: any) => {
|
||||
return sum + countAllComments(activity.comment);
|
||||
}, 0);
|
||||
|
||||
// Count active comments including nested ones
|
||||
activeCount = commentActivities.reduce((sum: number, activity: any) => {
|
||||
return sum + countActiveComments(activity.comment, activity.commentAnchor);
|
||||
}, 0);
|
||||
|
||||
// Process top-level comments and their nested replies
|
||||
const processedComments = commentActivities
|
||||
.filter((a: any) => {
|
||||
const c = a.comment;
|
||||
const anchor = a.commentAnchor;
|
||||
|
||||
// Skip resolved comments
|
||||
if (c.state === 'RESOLVED') return false;
|
||||
|
||||
// Skip orphaned inline comments
|
||||
if (anchor && anchor.orphaned === true) return false;
|
||||
|
||||
return true;
|
||||
})
|
||||
.map((a: any) => processNestedComments(a.comment, a.commentAnchor));
|
||||
|
||||
// Limit to 20 top-level comments
|
||||
comments = processedComments.slice(0, 20);
|
||||
} else {
|
||||
// Bitbucket Cloud API
|
||||
const apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pullRequestId}/comments`;
|
||||
const response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, {
|
||||
params: { pagelen: 100 }
|
||||
|
@ -637,7 +709,6 @@ export class PullRequestHandlers {
|
|||
const allComments = response.values || [];
|
||||
totalCount = allComments.length;
|
||||
|
||||
// Filter for active comments (not deleted or resolved) and limit to 20
|
||||
const activeComments = allComments
|
||||
.filter((c: BitbucketCloudComment) => !c.deleted && !c.resolved)
|
||||
.slice(0, 20);
|
||||
|
@ -673,7 +744,6 @@ export class PullRequestHandlers {
|
|||
let totalLinesRemoved = 0;
|
||||
|
||||
if (this.apiClient.getIsServer()) {
|
||||
// Bitbucket Server API - use changes endpoint
|
||||
const apiPath = `/rest/api/1.0/projects/${workspace}/repos/${repository}/pull-requests/${pullRequestId}/changes`;
|
||||
const response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, {
|
||||
params: { limit: 1000 }
|
||||
|
@ -694,7 +764,6 @@ export class PullRequestHandlers {
|
|||
};
|
||||
});
|
||||
} else {
|
||||
// Bitbucket Cloud API - use diffstat endpoint (has line statistics)
|
||||
const apiPath = `/repositories/${workspace}/${repository}/pullrequests/${pullRequestId}/diffstat`;
|
||||
const response = await this.apiClient.makeRequest<any>('get', apiPath, undefined, {
|
||||
params: { pagelen: 100 }
|
||||
|
@ -729,4 +798,299 @@ export class PullRequestHandlers {
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async resolveLineFromCode(
|
||||
workspace: string,
|
||||
repository: string,
|
||||
pullRequestId: number,
|
||||
filePath: string,
|
||||
codeSnippet: string,
|
||||
searchContext?: { before?: string[]; after?: string[] },
|
||||
matchStrategy: 'strict' | 'best' = 'strict'
|
||||
): Promise<{
|
||||
line_number: number;
|
||||
line_type: 'ADDED' | 'REMOVED' | 'CONTEXT';
|
||||
sequential_position?: number;
|
||||
hunk_info?: any;
|
||||
diff_context?: string;
|
||||
diff_content_preview?: string;
|
||||
calculation_details?: string;
|
||||
}> {
|
||||
try {
|
||||
const diffContent = await this.getFilteredPullRequestDiff(workspace, repository, pullRequestId, filePath);
|
||||
|
||||
const parser = new DiffParser();
|
||||
const sections = parser.parseDiffIntoSections(diffContent);
|
||||
|
||||
let fileSection = sections[0];
|
||||
if (!this.apiClient.getIsServer()) {
|
||||
fileSection = sections.find(s => s.filePath === filePath) || sections[0];
|
||||
}
|
||||
|
||||
if (!fileSection) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
`File ${filePath} not found in pull request diff`
|
||||
);
|
||||
}
|
||||
|
||||
const matches = this.findCodeMatches(
|
||||
fileSection.content,
|
||||
codeSnippet,
|
||||
searchContext
|
||||
);
|
||||
|
||||
if (matches.length === 0) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
`Code snippet not found in ${filePath}`
|
||||
);
|
||||
}
|
||||
|
||||
if (matches.length === 1) {
|
||||
return {
|
||||
line_number: matches[0].line_number,
|
||||
line_type: matches[0].line_type,
|
||||
sequential_position: matches[0].sequential_position,
|
||||
hunk_info: matches[0].hunk_info,
|
||||
diff_context: matches[0].preview,
|
||||
diff_content_preview: diffContent.split('\n').slice(0, 50).join('\n'),
|
||||
calculation_details: `Direct line number from diff: ${matches[0].line_number}`
|
||||
};
|
||||
}
|
||||
|
||||
if (matchStrategy === 'best') {
|
||||
const best = this.selectBestMatch(matches);
|
||||
|
||||
return {
|
||||
line_number: best.line_number,
|
||||
line_type: best.line_type,
|
||||
sequential_position: best.sequential_position,
|
||||
hunk_info: best.hunk_info,
|
||||
diff_context: best.preview,
|
||||
diff_content_preview: diffContent.split('\n').slice(0, 50).join('\n'),
|
||||
calculation_details: `Best match selected from ${matches.length} matches, line: ${best.line_number}`
|
||||
};
|
||||
}
|
||||
|
||||
const error: MultipleMatchesError = {
|
||||
code: 'MULTIPLE_MATCHES_FOUND',
|
||||
message: `Code snippet '${codeSnippet.substring(0, 50)}...' found in ${matches.length} locations`,
|
||||
occurrences: matches.map(m => ({
|
||||
line_number: m.line_number,
|
||||
file_path: filePath,
|
||||
preview: m.preview,
|
||||
confidence: m.confidence,
|
||||
line_type: m.line_type
|
||||
})),
|
||||
suggestion: 'To resolve, either:\n1. Add more context to uniquely identify the location\n2. Use match_strategy: \'best\' to auto-select highest confidence match\n3. Use line_number directly'
|
||||
};
|
||||
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
JSON.stringify({ error })
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof McpError) {
|
||||
throw error;
|
||||
}
|
||||
throw new McpError(
|
||||
ErrorCode.InternalError,
|
||||
`Failed to resolve line from code: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private findCodeMatches(
|
||||
diffContent: string,
|
||||
codeSnippet: string,
|
||||
searchContext?: { before?: string[]; after?: string[] }
|
||||
): CodeMatch[] {
|
||||
const lines = diffContent.split('\n');
|
||||
const matches: CodeMatch[] = [];
|
||||
let currentDestLine = 0; // Destination file line number
|
||||
let currentSrcLine = 0; // Source file line number
|
||||
let inHunk = false;
|
||||
let sequentialAddedCount = 0; // Track sequential ADDED lines
|
||||
let currentHunkIndex = -1;
|
||||
let currentHunkDestStart = 0;
|
||||
let currentHunkSrcStart = 0;
|
||||
let destPositionInHunk = 0; // Track position in destination file relative to hunk start
|
||||
let srcPositionInHunk = 0; // Track position in source file relative to hunk start
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
if (line.startsWith('@@')) {
|
||||
const match = line.match(/@@ -(\d+),\d+ \+(\d+),\d+ @@/);
|
||||
if (match) {
|
||||
currentHunkSrcStart = parseInt(match[1]);
|
||||
currentHunkDestStart = parseInt(match[2]);
|
||||
currentSrcLine = currentHunkSrcStart;
|
||||
currentDestLine = currentHunkDestStart;
|
||||
inHunk = true;
|
||||
currentHunkIndex++;
|
||||
destPositionInHunk = 0;
|
||||
srcPositionInHunk = 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!inHunk) continue;
|
||||
|
||||
if (line === '') {
|
||||
inHunk = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
let lineType: 'ADDED' | 'REMOVED' | 'CONTEXT';
|
||||
let lineContent = '';
|
||||
let lineNumber = 0;
|
||||
|
||||
if (line.startsWith('+')) {
|
||||
lineType = 'ADDED';
|
||||
lineContent = line.substring(1);
|
||||
lineNumber = currentHunkDestStart + destPositionInHunk;
|
||||
destPositionInHunk++;
|
||||
sequentialAddedCount++;
|
||||
} else if (line.startsWith('-')) {
|
||||
lineType = 'REMOVED';
|
||||
lineContent = line.substring(1);
|
||||
lineNumber = currentHunkSrcStart + srcPositionInHunk;
|
||||
srcPositionInHunk++;
|
||||
} else if (line.startsWith(' ')) {
|
||||
lineType = 'CONTEXT';
|
||||
lineContent = line.substring(1);
|
||||
lineNumber = currentHunkDestStart + destPositionInHunk;
|
||||
destPositionInHunk++;
|
||||
srcPositionInHunk++;
|
||||
} else {
|
||||
inHunk = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lineContent.trim() === codeSnippet.trim()) {
|
||||
const confidence = this.calculateConfidence(
|
||||
lines,
|
||||
i,
|
||||
searchContext,
|
||||
lineType
|
||||
);
|
||||
|
||||
matches.push({
|
||||
line_number: lineNumber,
|
||||
line_type: lineType,
|
||||
exact_content: codeSnippet,
|
||||
preview: this.getPreview(lines, i),
|
||||
confidence,
|
||||
context: this.extractContext(lines, i),
|
||||
sequential_position: lineType === 'ADDED' ? sequentialAddedCount : undefined,
|
||||
hunk_info: {
|
||||
hunk_index: currentHunkIndex,
|
||||
destination_start: currentHunkDestStart,
|
||||
line_in_hunk: destPositionInHunk
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (lineType === 'ADDED') {
|
||||
currentDestLine++;
|
||||
} else if (lineType === 'REMOVED') {
|
||||
currentSrcLine++;
|
||||
} else if (lineType === 'CONTEXT') {
|
||||
currentSrcLine++;
|
||||
currentDestLine++;
|
||||
}
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
private calculateConfidence(
|
||||
lines: string[],
|
||||
index: number,
|
||||
searchContext?: { before?: string[]; after?: string[] },
|
||||
lineType?: 'ADDED' | 'REMOVED' | 'CONTEXT'
|
||||
): number {
|
||||
let confidence = 0.5; // Base confidence
|
||||
|
||||
if (!searchContext) {
|
||||
return confidence;
|
||||
}
|
||||
|
||||
if (searchContext.before) {
|
||||
let matchedBefore = 0;
|
||||
for (let j = 0; j < searchContext.before.length; j++) {
|
||||
const contextLine = searchContext.before[searchContext.before.length - 1 - j];
|
||||
const checkIndex = index - j - 1;
|
||||
if (checkIndex >= 0) {
|
||||
const checkLine = lines[checkIndex].substring(1);
|
||||
if (checkLine.trim() === contextLine.trim()) {
|
||||
matchedBefore++;
|
||||
}
|
||||
}
|
||||
}
|
||||
confidence += (matchedBefore / searchContext.before.length) * 0.3;
|
||||
}
|
||||
|
||||
if (searchContext.after) {
|
||||
let matchedAfter = 0;
|
||||
for (let j = 0; j < searchContext.after.length; j++) {
|
||||
const contextLine = searchContext.after[j];
|
||||
const checkIndex = index + j + 1;
|
||||
if (checkIndex < lines.length) {
|
||||
const checkLine = lines[checkIndex].substring(1);
|
||||
if (checkLine.trim() === contextLine.trim()) {
|
||||
matchedAfter++;
|
||||
}
|
||||
}
|
||||
}
|
||||
confidence += (matchedAfter / searchContext.after.length) * 0.3;
|
||||
}
|
||||
|
||||
if (lineType === 'ADDED') {
|
||||
confidence += 0.1;
|
||||
}
|
||||
|
||||
return Math.min(confidence, 1.0);
|
||||
}
|
||||
|
||||
private getPreview(lines: string[], index: number): string {
|
||||
const start = Math.max(0, index - 1);
|
||||
const end = Math.min(lines.length, index + 2);
|
||||
const previewLines = [];
|
||||
|
||||
for (let i = start; i < end; i++) {
|
||||
const prefix = i === index ? '> ' : ' ';
|
||||
previewLines.push(prefix + lines[i]);
|
||||
}
|
||||
|
||||
return previewLines.join('\n');
|
||||
}
|
||||
|
||||
private extractContext(lines: string[], index: number): { lines_before: string[]; lines_after: string[] } {
|
||||
const linesBefore: string[] = [];
|
||||
const linesAfter: string[] = [];
|
||||
|
||||
for (let i = Math.max(0, index - 2); i < index; i++) {
|
||||
if (lines[i].match(/^[+\- ]/)) {
|
||||
linesBefore.push(lines[i].substring(1));
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = index + 1; i < Math.min(lines.length, index + 3); i++) {
|
||||
if (lines[i].match(/^[+\- ]/)) {
|
||||
linesAfter.push(lines[i].substring(1));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
lines_before: linesBefore,
|
||||
lines_after: linesAfter
|
||||
};
|
||||
}
|
||||
|
||||
private selectBestMatch(matches: CodeMatch[]): CodeMatch {
|
||||
return matches.sort((a, b) => b.confidence - a.confidence)[0];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ class BitbucketMCPServer {
|
|||
this.server = new Server(
|
||||
{
|
||||
name: 'bitbucket-mcp-server',
|
||||
version: '0.7.0',
|
||||
version: '0.9.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
|
|
|
@ -140,7 +140,7 @@ export const toolDefinitions = [
|
|||
},
|
||||
{
|
||||
name: 'add_comment',
|
||||
description: 'Add a comment to a pull request (general or inline on specific code)',
|
||||
description: 'Add a comment to a pull request. Supports: 1) General PR comments, 2) Replies to existing comments, 3) Inline comments on specific code lines (using line_number OR code_snippet), 4) Code suggestions for single or multi-line replacements. For inline comments, you can either provide exact line_number or use code_snippet to auto-detect the line.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
@ -158,25 +158,58 @@ export const toolDefinitions = [
|
|||
},
|
||||
comment_text: {
|
||||
type: 'string',
|
||||
description: 'Comment text',
|
||||
description: 'The main comment text. For suggestions, this is the explanation before the code suggestion.',
|
||||
},
|
||||
parent_comment_id: {
|
||||
type: 'number',
|
||||
description: 'Parent comment ID for replies (optional)',
|
||||
description: 'ID of comment to reply to. Use this to create threaded conversations (optional)',
|
||||
},
|
||||
file_path: {
|
||||
type: 'string',
|
||||
description: 'File path for inline comment (optional, e.g., "src/main.js")',
|
||||
description: 'File path for inline comment. Required for inline comments. Example: "src/components/Button.js" (optional)',
|
||||
},
|
||||
line_number: {
|
||||
type: 'number',
|
||||
description: 'Line number for inline comment (optional, required with file_path)',
|
||||
description: 'Exact line number in the file. Use this OR code_snippet, not both. Required with file_path unless using code_snippet (optional)',
|
||||
},
|
||||
line_type: {
|
||||
type: 'string',
|
||||
description: 'Type of line for inline comment: ADDED, REMOVED, or CONTEXT (optional, default: CONTEXT)',
|
||||
description: 'Type of line: ADDED (green/new lines), REMOVED (red/deleted lines), or CONTEXT (unchanged lines). Default: CONTEXT',
|
||||
enum: ['ADDED', 'REMOVED', 'CONTEXT'],
|
||||
},
|
||||
suggestion: {
|
||||
type: 'string',
|
||||
description: 'Replacement code for a suggestion. Creates a suggestion block that can be applied in Bitbucket UI. Requires file_path and line_number. For multi-line, include newlines in the string (optional)',
|
||||
},
|
||||
suggestion_end_line: {
|
||||
type: 'number',
|
||||
description: 'For multi-line suggestions: the last line number to replace. If not provided, only replaces the single line at line_number (optional)',
|
||||
},
|
||||
code_snippet: {
|
||||
type: 'string',
|
||||
description: 'Exact code text from the diff to find and comment on. Use this instead of line_number for auto-detection. Must match exactly including whitespace (optional)',
|
||||
},
|
||||
search_context: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
before: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Array of code lines that appear BEFORE the target line. Helps disambiguate when code_snippet appears multiple times',
|
||||
},
|
||||
after: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Array of code lines that appear AFTER the target line. Helps disambiguate when code_snippet appears multiple times',
|
||||
},
|
||||
},
|
||||
description: 'Additional context lines to help locate the exact position when using code_snippet. Useful when the same code appears multiple times (optional)',
|
||||
},
|
||||
match_strategy: {
|
||||
type: 'string',
|
||||
enum: ['strict', 'best'],
|
||||
description: 'How to handle multiple matches when using code_snippet. "strict": fail with detailed error showing all matches. "best": automatically pick the highest confidence match. Default: "strict"',
|
||||
},
|
||||
},
|
||||
required: ['workspace', 'repository', 'pull_request_id', 'comment_text'],
|
||||
},
|
||||
|
|
|
@ -352,3 +352,35 @@ export interface FormattedFileChange {
|
|||
status: 'added' | 'modified' | 'removed' | 'renamed';
|
||||
old_path?: string;
|
||||
}
|
||||
|
||||
// Types for code snippet matching
|
||||
export interface CodeMatch {
|
||||
line_number: number;
|
||||
line_type: 'ADDED' | 'REMOVED' | 'CONTEXT';
|
||||
exact_content: string;
|
||||
preview: string;
|
||||
confidence: number;
|
||||
context: {
|
||||
lines_before: string[];
|
||||
lines_after: string[];
|
||||
};
|
||||
sequential_position?: number; // Position within diff (for ADDED lines)
|
||||
hunk_info?: {
|
||||
hunk_index: number;
|
||||
destination_start: number;
|
||||
line_in_hunk: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MultipleMatchesError {
|
||||
code: 'MULTIPLE_MATCHES_FOUND';
|
||||
message: string;
|
||||
occurrences: Array<{
|
||||
line_number: number;
|
||||
file_path: string;
|
||||
preview: string;
|
||||
confidence: number;
|
||||
line_type: 'ADDED' | 'REMOVED' | 'CONTEXT';
|
||||
}>;
|
||||
suggestion: string;
|
||||
}
|
||||
|
|
|
@ -82,6 +82,14 @@ export const isAddCommentArgs = (
|
|||
file_path?: string;
|
||||
line_number?: number;
|
||||
line_type?: 'ADDED' | 'REMOVED' | 'CONTEXT';
|
||||
suggestion?: string;
|
||||
suggestion_end_line?: number;
|
||||
code_snippet?: string;
|
||||
search_context?: {
|
||||
before?: string[];
|
||||
after?: string[];
|
||||
};
|
||||
match_strategy?: 'strict' | 'best';
|
||||
} =>
|
||||
typeof args === 'object' &&
|
||||
args !== null &&
|
||||
|
@ -92,7 +100,16 @@ export const isAddCommentArgs = (
|
|||
(args.parent_comment_id === undefined || typeof args.parent_comment_id === 'number') &&
|
||||
(args.file_path === undefined || typeof args.file_path === 'string') &&
|
||||
(args.line_number === undefined || typeof args.line_number === 'number') &&
|
||||
(args.line_type === undefined || ['ADDED', 'REMOVED', 'CONTEXT'].includes(args.line_type));
|
||||
(args.line_type === undefined || ['ADDED', 'REMOVED', 'CONTEXT'].includes(args.line_type)) &&
|
||||
(args.suggestion === undefined || typeof args.suggestion === 'string') &&
|
||||
(args.suggestion_end_line === undefined || typeof args.suggestion_end_line === 'number') &&
|
||||
(args.code_snippet === undefined || typeof args.code_snippet === 'string') &&
|
||||
(args.search_context === undefined || (
|
||||
typeof args.search_context === 'object' &&
|
||||
(args.search_context.before === undefined || Array.isArray(args.search_context.before)) &&
|
||||
(args.search_context.after === undefined || Array.isArray(args.search_context.after))
|
||||
)) &&
|
||||
(args.match_strategy === undefined || ['strict', 'best'].includes(args.match_strategy));
|
||||
|
||||
export const isMergePullRequestArgs = (
|
||||
args: any
|
||||
|
|
22
src/utils/suggestion-formatter.ts
Normal file
22
src/utils/suggestion-formatter.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* Formats a comment with a code suggestion in markdown format
|
||||
* that Bitbucket can render as an applicable suggestion
|
||||
*/
|
||||
export function formatSuggestionComment(
|
||||
commentText: string,
|
||||
suggestion: string,
|
||||
startLine?: number,
|
||||
endLine?: number
|
||||
): string {
|
||||
// Add line range info if it's a multi-line suggestion
|
||||
const lineInfo = startLine && endLine && endLine > startLine
|
||||
? ` (lines ${startLine}-${endLine})`
|
||||
: '';
|
||||
|
||||
// Format with GitHub-style suggestion markdown
|
||||
return `${commentText}${lineInfo}
|
||||
|
||||
\`\`\`suggestion
|
||||
${suggestion}
|
||||
\`\`\``;
|
||||
}
|
Loading…
Reference in a new issue