@atlassian-dc-mcp/bitbucket 0.25.0 → 0.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,6 +23,84 @@ function resolveToken(token: string | (() => string | undefined), missingTokenMe
23
23
  };
24
24
  }
25
25
 
26
+ type DiffLineType = 'ADDED' | 'REMOVED' | 'CONTEXT';
27
+
28
+ /**
29
+ * Build a Bitbucket DC inline comment anchor.
30
+ *
31
+ * For a multiline range, Bitbucket DC (>= 9.3.0) requires `multilineMarker` + `multilineSpan`.
32
+ * The legacy `multilineStartLine`/`multilineStartLineType`/`multilineAnchor`/`multilineDestinationRange`
33
+ * fields are silently ignored by the server, which then stores a single-line anchor — so a multiline
34
+ * `​```suggestion` only replaces its first line on Apply. Field names verified against BB DC 9.3.2.
35
+ */
36
+ function buildCommentAnchor(params: {
37
+ filePath: string;
38
+ line?: number;
39
+ lineType?: DiffLineType;
40
+ startLine?: number;
41
+ startLineType?: DiffLineType;
42
+ }): Record<string, any> {
43
+ const { filePath, line, lineType } = params;
44
+ const anchor: Record<string, any> = { path: filePath, diffType: 'EFFECTIVE' };
45
+ if (line === undefined || !lineType) {
46
+ return anchor;
47
+ }
48
+
49
+ if (params.startLine === undefined) {
50
+ anchor.line = line;
51
+ anchor.lineType = lineType;
52
+ anchor.fileType = lineType === 'REMOVED' ? 'FROM' : 'TO';
53
+ return anchor;
54
+ }
55
+
56
+ // Normalize so the marker sits on the lower line and `line` on the upper line.
57
+ let startLine = params.startLine;
58
+ let startLineType: DiffLineType = params.startLineType ?? lineType;
59
+ let endLine = line;
60
+ let endLineType: DiffLineType = lineType;
61
+ if (startLine > endLine) {
62
+ [startLine, endLine] = [endLine, startLine];
63
+ [startLineType, endLineType] = [endLineType, startLineType];
64
+ }
65
+
66
+ const fileType = endLineType === 'REMOVED' ? 'FROM' : 'TO';
67
+ anchor.line = endLine;
68
+ anchor.lineType = endLineType;
69
+ anchor.fileType = fileType;
70
+ anchor.multilineMarker = { startLine, startLineType };
71
+ anchor.multilineSpan =
72
+ fileType === 'FROM'
73
+ ? { srcSpanStart: startLine, srcSpanEnd: endLine }
74
+ : { dstSpanStart: startLine, dstSpanEnd: endLine };
75
+ return anchor;
76
+ }
77
+
78
+ const SUGGESTION_BLOCK_RE = /```suggestion\b[^\n]*\n([\s\S]*?)```/;
79
+
80
+ /**
81
+ * Warn when a multi-line `​```suggestion` block is anchored to a single line: Bitbucket only replaces
82
+ * the anchored line on Apply, leaving the rest (duplicated code). Returns undefined when there is no
83
+ * concern. Heuristic — a 1-line anchor with a multi-line suggestion is the common failure shape.
84
+ */
85
+ function multilineSuggestionWarning(text: string, startLine?: number): string | undefined {
86
+ if (startLine !== undefined) {
87
+ return undefined;
88
+ }
89
+ const match = SUGGESTION_BLOCK_RE.exec(text);
90
+ if (!match) {
91
+ return undefined;
92
+ }
93
+ const suggestionLineCount = match[1].replace(/\n$/, '').split('\n').length;
94
+ if (suggestionLineCount <= 1) {
95
+ return undefined;
96
+ }
97
+ return (
98
+ 'The comment contains a multi-line ```suggestion block but is anchored to a single line. ' +
99
+ 'On Apply, Bitbucket replaces only that one line and leaves the rest. To replace multiple lines, ' +
100
+ 'set startLine (first replaced line) and line (last replaced line) to span the whole range.'
101
+ );
102
+ }
103
+
26
104
  export class BitbucketService {
27
105
  private readonly getPageSize: () => number;
28
106
 
@@ -349,27 +427,11 @@ export class BitbucketService {
349
427
 
350
428
  // Add anchor for file/line comments
351
429
  if (filePath) {
352
- comment.anchor = {
353
- path: filePath,
354
- diffType: 'EFFECTIVE'
355
- };
356
-
357
- // Add line-specific anchor properties
358
- if (line !== undefined && lineType) {
359
- comment.anchor.line = line;
360
- comment.anchor.lineType = lineType;
361
- comment.anchor.fileType = 'TO'; // Default to destination file
362
-
363
- if (startLine !== undefined) {
364
- const resolvedStartLineType = startLineType ?? lineType;
365
- comment.anchor.multilineAnchor = true;
366
- comment.anchor.multilineStartLine = startLine;
367
- comment.anchor.multilineStartLineType = resolvedStartLineType;
368
- comment.anchor.multilineDestinationRange = { minimum: startLine, maximum: line };
369
- }
370
- }
430
+ comment.anchor = buildCommentAnchor({ filePath, line, lineType, startLine, startLineType });
371
431
  }
372
432
 
433
+ const suggestionWarning = filePath ? multilineSuggestionWarning(text, startLine) : undefined;
434
+
373
435
  const result = await handleApiOperation(
374
436
  () => PullRequestsService.createComment2(
375
437
  projectKey,
@@ -383,10 +445,17 @@ export class BitbucketService {
383
445
  if (result.success && result.data && output !== 'full') {
384
446
  return {
385
447
  ...result,
386
- data: shapePullRequestCommentAck(result.data),
448
+ data: {
449
+ ...shapePullRequestCommentAck(result.data),
450
+ ...(suggestionWarning ? { warning: suggestionWarning } : {}),
451
+ },
387
452
  };
388
453
  }
389
454
 
455
+ if (result.success && suggestionWarning) {
456
+ return { ...result, warning: suggestionWarning };
457
+ }
458
+
390
459
  return result;
391
460
  }
392
461
 
@@ -911,10 +980,10 @@ export const bitbucketToolSchemas = {
911
980
  text: z.string().describe("The comment text"),
912
981
  parentId: z.number().optional().describe("Parent comment ID for replies"),
913
982
  filePath: z.string().optional().describe("File path for file-specific comments"),
914
- startLine: z.number().optional().describe("Start line of a multiline comment range. When provided together with 'line' (end line), creates a multiline comment spanning from startLine to line."),
915
- startLineType: z.enum(['ADDED', 'REMOVED', 'CONTEXT']).optional().describe("Line type for the start line of a multiline comment. Defaults to the same value as lineType if omitted."),
916
- line: z.number().optional().describe("Line number for single-line comments, or end line for multiline comments"),
917
- lineType: z.enum(['ADDED', 'REMOVED', 'CONTEXT']).optional().describe("Line type for the end line (or the only line for single-line comments)"),
983
+ startLine: z.number().optional().describe("First line of a multiline range. Provide together with 'line' (the last line) to span multiple lines. REQUIRED for a multi-line ```suggestion: the anchored range (startLine..line) is exactly what 'Apply suggestion' replaces — omit it and only the single 'line' is replaced, leaving the rest. Requires Bitbucket DC >= 9.3.0 for multiline suggestions."),
984
+ startLineType: z.enum(['ADDED', 'REMOVED', 'CONTEXT']).optional().describe("Line type for the start line of a multiline range. Defaults to the same value as lineType if omitted."),
985
+ line: z.number().optional().describe("Single-line comments: the line. Multiline: the LAST line of the range (use startLine for the first). For a ```suggestion this is the last replaced line."),
986
+ lineType: z.enum(['ADDED', 'REMOVED', 'CONTEXT']).optional().describe("Line type for 'line' (the end line, or the only line for single-line comments). Use 'ADDED'/'CONTEXT' for the new/target file, 'REMOVED' for the original/source file."),
918
987
  pending: z.boolean().optional().describe("If true, creates a pending (draft) comment not visible to others until the review is submitted via bitbucket_submitPullRequestReview. Only works when filePath is provided — top-level PR comments (no filePath) are always posted live."),
919
988
  severity: z.enum(['NORMAL', 'BLOCKER']).optional().describe("Comment severity. Use 'BLOCKER' to post the comment as a task that must be resolved before the PR can be merged. Defaults to 'NORMAL' (regular comment)."),
920
989
  output: z.enum(['ack', 'full']).optional().describe("Return a compact acknowledgement or the full API response. Defaults to ack.")
@@ -24,6 +24,10 @@ interface CommentAnchor {
24
24
  path: string;
25
25
  diffType: string;
26
26
  orphaned: boolean;
27
+ multilineMarker?: {
28
+ startLine: number;
29
+ startLineType: string;
30
+ };
27
31
  }
28
32
 
29
33
  interface CommentProperties {
@@ -131,6 +135,8 @@ interface SimplifiedAnchor {
131
135
  line: number;
132
136
  path: string;
133
137
  fileType?: string;
138
+ startLine?: number;
139
+ startLineType?: string;
134
140
  }
135
141
 
136
142
  interface SimplifiedComment {
@@ -240,7 +246,13 @@ function simplifyAnchor(anchor: CommentAnchor): SimplifiedAnchor {
240
246
  return {
241
247
  line: anchor.line,
242
248
  path: anchor.path,
243
- fileType: anchor.fileType
249
+ fileType: anchor.fileType,
250
+ ...(anchor.multilineMarker
251
+ ? {
252
+ startLine: anchor.multilineMarker.startLine,
253
+ startLineType: anchor.multilineMarker.startLineType
254
+ }
255
+ : {})
244
256
  };
245
257
  }
246
258