@stubbedev/atlassian-mcp 0.2.10 → 0.2.12
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.
- package/dist/bitbucket.js +60 -1
- package/dist/index.js +148 -19
- package/dist/jira.js +16 -0
- package/package.json +1 -1
package/dist/bitbucket.js
CHANGED
|
@@ -340,6 +340,55 @@ export class BitbucketClient {
|
|
|
340
340
|
const lines = data.values.map((r, i) => `${start + i + 1}. ${r.project.key}/${r.slug} — ${r.name}`);
|
|
341
341
|
return text(`${data.values.length} repo(s)${pageHint(data)}:\n${lines.join('\n')}`);
|
|
342
342
|
}
|
|
343
|
+
async searchUsers(args) {
|
|
344
|
+
const params = new URLSearchParams();
|
|
345
|
+
if (args.query)
|
|
346
|
+
params.set('filter', args.query);
|
|
347
|
+
params.set('limit', String(args.limit ?? 25));
|
|
348
|
+
if (args.start)
|
|
349
|
+
params.set('start', String(args.start));
|
|
350
|
+
let path;
|
|
351
|
+
if (args.projectKey && args.repoSlug) {
|
|
352
|
+
path = `${this.rp(args.projectKey, args.repoSlug)}/permissions/users?${params}`;
|
|
353
|
+
}
|
|
354
|
+
else if (args.projectKey) {
|
|
355
|
+
path = `/projects/${encodeURIComponent(args.projectKey)}/permissions/users?${params}`;
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
path = `/users?${params}`;
|
|
359
|
+
}
|
|
360
|
+
const data = await this.request('GET', path);
|
|
361
|
+
if (!data || data.values.length === 0)
|
|
362
|
+
return text('No users found.');
|
|
363
|
+
const lines = data.values.map((entry, i) => {
|
|
364
|
+
const user = entry.user ?? entry;
|
|
365
|
+
const parts = [`${i + 1}. ${user.displayName} (${user.name})`];
|
|
366
|
+
if (user.emailAddress)
|
|
367
|
+
parts.push(`— ${user.emailAddress}`);
|
|
368
|
+
if (user.active === false)
|
|
369
|
+
parts.push('[inactive]');
|
|
370
|
+
return parts.join(' ');
|
|
371
|
+
});
|
|
372
|
+
return text(`${data.values.length} user(s)${pageHint(data)}:\n${lines.join('\n')}`);
|
|
373
|
+
}
|
|
374
|
+
async searchUsersRaw(args) {
|
|
375
|
+
const params = new URLSearchParams();
|
|
376
|
+
if (args.query)
|
|
377
|
+
params.set('filter', args.query);
|
|
378
|
+
params.set('limit', String(args.limit ?? 50));
|
|
379
|
+
let path;
|
|
380
|
+
if (args.projectKey && args.repoSlug) {
|
|
381
|
+
path = `${this.rp(args.projectKey, args.repoSlug)}/permissions/users?${params}`;
|
|
382
|
+
}
|
|
383
|
+
else if (args.projectKey) {
|
|
384
|
+
path = `/projects/${encodeURIComponent(args.projectKey)}/permissions/users?${params}`;
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
path = `/users?${params}`;
|
|
388
|
+
}
|
|
389
|
+
const data = await this.request('GET', path);
|
|
390
|
+
return (data?.values ?? []).map((entry) => entry.user ?? entry);
|
|
391
|
+
}
|
|
343
392
|
async listPullRequests(args) {
|
|
344
393
|
const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
|
|
345
394
|
const { state = 'OPEN', fromBranch, text: searchText, limit = 25, start = 0 } = args;
|
|
@@ -730,6 +779,16 @@ export class BitbucketClient {
|
|
|
730
779
|
}
|
|
731
780
|
return text(content);
|
|
732
781
|
}
|
|
782
|
+
async fetchFileText(projectKey, repoSlug, filePath) {
|
|
783
|
+
try {
|
|
784
|
+
const encoded = filePath.split('/').map(encodeURIComponent).join('/');
|
|
785
|
+
const content = await this.requestText(`${this.rp(projectKey, repoSlug)}/raw/${encoded}`);
|
|
786
|
+
return content;
|
|
787
|
+
}
|
|
788
|
+
catch {
|
|
789
|
+
return null;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
733
792
|
async getPrComments(args) {
|
|
734
793
|
const { projectKey, repoSlug } = this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
|
|
735
794
|
const limit = args.limit ?? 50;
|
|
@@ -972,7 +1031,7 @@ export class BitbucketClient {
|
|
|
972
1031
|
return text(`${data.values.length} task(s) on PR #${args.prId} (${open} open)${pageHint(data)}:\n${lines.join('\n')}`);
|
|
973
1032
|
}
|
|
974
1033
|
async mutatePrTask(args) {
|
|
975
|
-
|
|
1034
|
+
this.resolveProjectAndRepo(args.projectKey, args.repoSlug);
|
|
976
1035
|
if (args.action === 'create') {
|
|
977
1036
|
if (!args.text)
|
|
978
1037
|
throw new Error('text is required to create a task.');
|
package/dist/index.js
CHANGED
|
@@ -132,18 +132,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
132
132
|
...(jira ? [
|
|
133
133
|
{
|
|
134
134
|
name: 'start_work',
|
|
135
|
-
description: 'Start working on a Jira ticket:
|
|
135
|
+
description: 'Start working on a Jira ticket end-to-end: resolves the ticket (by key or free-text search with a picker when multiple match), creates a local branch with an auto-generated name, fetches the project README from Bitbucket so you have commit/PR conventions in context, and prints a ready-to-use next-steps summary. Use when told "make a branch for FOO-123", "start working on this ticket", "I want to work on the login bug", or "begin work on the payment gateway story". If issueKey is omitted, provide query for free-text search.',
|
|
136
136
|
inputSchema: {
|
|
137
137
|
type: 'object',
|
|
138
138
|
properties: {
|
|
139
|
-
issueKey: { type: 'string', description: 'Jira issue key, e.g. FOO-123' },
|
|
139
|
+
issueKey: { type: 'string', description: 'Jira issue key, e.g. FOO-123 (provide this OR query)' },
|
|
140
|
+
query: { type: 'string', description: 'Free-text search when issueKey is unknown — shows a picker if multiple tickets match' },
|
|
140
141
|
repoPath: { type: 'string', description: 'Local repo path (defaults to cwd)' },
|
|
141
142
|
baseBranch: { type: 'string', description: 'Branch to base off (default: master)' },
|
|
142
143
|
branchName: { type: 'string', description: 'Override the generated branch name' },
|
|
143
144
|
transitionName: { type: 'string', description: 'Jira transition to apply, e.g. "In Progress" (optional)' },
|
|
144
145
|
push: { type: 'boolean', description: 'Push branch to remote after creation (default false)', default: false },
|
|
145
146
|
},
|
|
146
|
-
required: ['issueKey'],
|
|
147
147
|
},
|
|
148
148
|
},
|
|
149
149
|
{
|
|
@@ -263,17 +263,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
263
263
|
] : []),
|
|
264
264
|
...(bitbucket ? [{
|
|
265
265
|
name: 'bitbucket_search',
|
|
266
|
-
description: 'Discover Bitbucket resources. Use when asked "what PRs are open?", "show me the repos", "find the PR for this branch", or "list branches". Set resource:\n• "pull_requests" (default) — list PRs by state/branch/text; mine=true for your inbox\n• "repos" — list repositories in a project\n• "branches" — list or filter branches in a repo',
|
|
266
|
+
description: 'Discover Bitbucket resources. Use when asked "what PRs are open?", "show me the repos", "find the PR for this branch", or "list branches". Set resource:\n• "pull_requests" (default) — list PRs by state/branch/text; mine=true for your inbox\n• "repos" — list repositories in a project\n• "branches" — list or filter branches in a repo\n• "users" — find users by name/email (pass query); add projectKey+repoSlug to restrict to users with repo access. ALWAYS use this to look up valid usernames before adding reviewers to a PR.',
|
|
267
267
|
inputSchema: {
|
|
268
268
|
type: 'object',
|
|
269
269
|
properties: {
|
|
270
|
-
resource: { type: 'string', enum: ['pull_requests', 'repos', 'branches'], description: 'What to search (default: pull_requests)' },
|
|
270
|
+
resource: { type: 'string', enum: ['pull_requests', 'repos', 'branches', 'users'], description: 'What to search (default: pull_requests)' },
|
|
271
271
|
mine: { type: 'boolean', description: 'Return your own PRs by role (resource=pull_requests only)' },
|
|
272
272
|
role: { type: 'string', enum: ['author', 'reviewer', 'participant'], description: 'Your role filter when mine=true' },
|
|
273
273
|
projectKey: { type: 'string', description: 'Bitbucket project code, e.g. "ENG"' },
|
|
274
274
|
project: { type: 'string', description: 'Alias for projectKey' },
|
|
275
275
|
repoSlug: { type: 'string', description: 'Repository slug' },
|
|
276
276
|
repo: { type: 'string', description: 'Alias for repoSlug' },
|
|
277
|
+
query: { type: 'string', description: 'Name or email filter (resource=users only)' },
|
|
277
278
|
state: { type: 'string', enum: ['OPEN', 'MERGED', 'DECLINED'], description: 'PR state filter (default OPEN)' },
|
|
278
279
|
fromBranch: { type: 'string', description: 'Filter PRs from this source branch' },
|
|
279
280
|
text: { type: 'string', description: 'Filter PRs by title/description text' },
|
|
@@ -285,7 +286,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
285
286
|
},
|
|
286
287
|
{
|
|
287
288
|
name: 'bitbucket_get_pr',
|
|
288
|
-
description: 'Full details for one PR: metadata, commits, open comments, blockers, and optional diff. Use when asked to "review this PR", "show me the review comments", "what\'s blocking the merge", or after get_dev_context surfaces a prId.
|
|
289
|
+
description: 'Full details for one PR: metadata, commits, open comments, blockers, and optional diff. Use when asked to "review this PR", "show me the review comments", "what\'s blocking the merge", or after get_dev_context surfaces a prId. IMPORTANT: The PR branch is often not the locally checked-out branch. Do NOT read files with local tools (Read, git_get_diff, etc.) for PR context — use bitbucket_get_file with the PR\'s source branch instead. The response includes a "Viewing as" line — if it says "you are the author", do NOT add review comments or a summary unless explicitly asked; just answer questions about the PR. If it says "you are a reviewer", default to posting inline comments for suggested changes and a final summary comment.',
|
|
289
290
|
inputSchema: {
|
|
290
291
|
type: 'object',
|
|
291
292
|
properties: {
|
|
@@ -330,7 +331,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
330
331
|
description: { type: 'string', description: 'PR description (optional)' },
|
|
331
332
|
fromBranch: { type: 'string', description: 'Source branch (defaults to current branch)' },
|
|
332
333
|
toBranch: { type: 'string', description: 'Target branch (default: master)' },
|
|
333
|
-
reviewers: { type: 'array', items: { type: 'string' }, description: 'Reviewer usernames
|
|
334
|
+
reviewers: { type: 'array', items: { type: 'string' }, description: 'Reviewer usernames. Use bitbucket_search resource=users to look up valid usernames before setting this.' },
|
|
335
|
+
pickReviewers: { type: 'boolean', description: 'Show an interactive reviewer picker before creating the PR (lists users with repo access)' },
|
|
334
336
|
},
|
|
335
337
|
required: ['title'],
|
|
336
338
|
},
|
|
@@ -340,7 +342,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
340
342
|
title: { type: 'string', description: 'Updated PR title (optional)' },
|
|
341
343
|
description: { type: 'string', description: 'Updated description, or empty string to clear (optional)' },
|
|
342
344
|
toBranch: { type: 'string', description: 'Updated target branch (optional)' },
|
|
343
|
-
reviewers: { type: 'array', items: { type: 'string' }, description: 'Updated reviewer usernames. Empty array clears reviewers.' },
|
|
345
|
+
reviewers: { type: 'array', items: { type: 'string' }, description: 'Updated reviewer usernames. Empty array clears reviewers. Use bitbucket_search resource=users to look up valid usernames before setting this.' },
|
|
344
346
|
},
|
|
345
347
|
},
|
|
346
348
|
},
|
|
@@ -377,7 +379,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
377
379
|
},
|
|
378
380
|
{
|
|
379
381
|
name: 'bitbucket_get_file',
|
|
380
|
-
description: 'Raw file content from Bitbucket at a branch, tag, or commit.
|
|
382
|
+
description: 'Raw file content from Bitbucket at a branch, tag, or commit. CRITICAL: if the PR branch being reviewed is NOT the currently checked-out local branch, ALL additional file context for that review MUST come from this tool — never from local Read, git_get_diff, or any tool that reads local disk. Pass the PR source branch as ref.',
|
|
381
383
|
inputSchema: {
|
|
382
384
|
type: 'object',
|
|
383
385
|
properties: {
|
|
@@ -461,8 +463,69 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
461
463
|
if (!jira)
|
|
462
464
|
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
463
465
|
const a = args;
|
|
464
|
-
|
|
465
|
-
|
|
466
|
+
if (!a.issueKey && !a.query) {
|
|
467
|
+
throw new Error('Provide issueKey (e.g. FOO-123) or query (free-text search).');
|
|
468
|
+
}
|
|
469
|
+
// Resolve issueKey from free-text query if not provided directly
|
|
470
|
+
let issueKey = a.issueKey;
|
|
471
|
+
if (!issueKey && a.query) {
|
|
472
|
+
const candidates = await jira.findIssues(a.query, 10);
|
|
473
|
+
if (candidates.length === 0) {
|
|
474
|
+
return { content: [{ type: 'text', text: `No Jira tickets found for: "${a.query}"` }] };
|
|
475
|
+
}
|
|
476
|
+
if (candidates.length === 1) {
|
|
477
|
+
issueKey = candidates[0].key;
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
// Multiple matches — present a picker
|
|
481
|
+
const pickerMessage = [
|
|
482
|
+
`Found ${candidates.length} tickets matching "${a.query}". Which one do you want to work on?`,
|
|
483
|
+
...candidates.map((c, i) => `${i + 1}. [${c.key}] ${c.summary} (${c.status})`),
|
|
484
|
+
].join('\n');
|
|
485
|
+
try {
|
|
486
|
+
const pickerResult = await server.elicitInput({
|
|
487
|
+
message: pickerMessage,
|
|
488
|
+
requestedSchema: {
|
|
489
|
+
type: 'object',
|
|
490
|
+
properties: {
|
|
491
|
+
ticket: {
|
|
492
|
+
type: 'string',
|
|
493
|
+
title: 'Select a ticket',
|
|
494
|
+
oneOf: [
|
|
495
|
+
...candidates.map((c) => ({ const: c.key, title: `[${c.key}] ${c.summary}` })),
|
|
496
|
+
{ const: '__cancel__', title: 'Cancel' },
|
|
497
|
+
],
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
required: ['ticket'],
|
|
501
|
+
},
|
|
502
|
+
});
|
|
503
|
+
if (pickerResult.action === 'cancel' ||
|
|
504
|
+
pickerResult.action === 'decline' ||
|
|
505
|
+
pickerResult.content?.ticket === '__cancel__') {
|
|
506
|
+
return { content: [{ type: 'text', text: 'Cancelled.' }] };
|
|
507
|
+
}
|
|
508
|
+
issueKey = pickerResult.content?.ticket;
|
|
509
|
+
if (!issueKey || issueKey === '__cancel__') {
|
|
510
|
+
return { content: [{ type: 'text', text: 'Cancelled.' }] };
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
catch {
|
|
514
|
+
// Client doesn't support elicitation — list options and ask caller to retry with issueKey
|
|
515
|
+
const list = candidates.map((c) => ` • ${c.key} — ${c.summary} (${c.status})`).join('\n');
|
|
516
|
+
return {
|
|
517
|
+
content: [{
|
|
518
|
+
type: 'text',
|
|
519
|
+
text: `Found ${candidates.length} tickets matching "${a.query}":\n${list}\n\nRe-run start_work with the desired issueKey.`,
|
|
520
|
+
}],
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if (!issueKey)
|
|
526
|
+
throw new Error('Could not resolve issue key.');
|
|
527
|
+
const fields = await jira.getIssueFields(issueKey);
|
|
528
|
+
const branchName = a.branchName ?? slugifyBranchName(issueKey, fields.summary, fields.type);
|
|
466
529
|
const repoPath = a.repoPath ?? process.cwd();
|
|
467
530
|
// Check if branch already exists on remote before creating
|
|
468
531
|
const remote = checkRemoteBranch(branchName, repoPath);
|
|
@@ -474,7 +537,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
474
537
|
const contextLines = [authorLine, commitLine].filter(Boolean).join('\n');
|
|
475
538
|
const message = [
|
|
476
539
|
`Branch "${branchName}" already exists on remote.`,
|
|
477
|
-
`Ticket: ${
|
|
540
|
+
`Ticket: ${issueKey} — ${fields.summary}`,
|
|
478
541
|
contextLines,
|
|
479
542
|
].filter(Boolean).join('\n');
|
|
480
543
|
try {
|
|
@@ -508,7 +571,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
508
571
|
if (chosen === 'cancel') {
|
|
509
572
|
return { content: [{ type: 'text', text: 'Cancelled.' }] };
|
|
510
573
|
}
|
|
511
|
-
// new_name — instruct the model to re-run with a custom name
|
|
512
574
|
return {
|
|
513
575
|
content: [{
|
|
514
576
|
type: 'text',
|
|
@@ -516,11 +578,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
516
578
|
}],
|
|
517
579
|
};
|
|
518
580
|
}
|
|
519
|
-
// Fallback: unknown action
|
|
520
581
|
return { content: [{ type: 'text', text: 'Cancelled.' }] };
|
|
521
582
|
}
|
|
522
583
|
catch {
|
|
523
|
-
// Client doesn't support elicitation — fall back to informational text
|
|
524
584
|
return {
|
|
525
585
|
content: [{
|
|
526
586
|
type: 'text',
|
|
@@ -542,21 +602,52 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
542
602
|
push: a.push ?? false,
|
|
543
603
|
});
|
|
544
604
|
const lines = [
|
|
545
|
-
`Ticket: ${
|
|
605
|
+
`Ticket: ${issueKey} — ${fields.summary}`,
|
|
546
606
|
`Status: ${fields.status}`,
|
|
547
607
|
branchResult.content[0].text,
|
|
548
608
|
];
|
|
549
609
|
if (a.transitionName) {
|
|
550
610
|
try {
|
|
551
|
-
await jira.mutateIssue({ issueKey
|
|
611
|
+
await jira.mutateIssue({ issueKey, transitionName: a.transitionName });
|
|
552
612
|
lines.push(`Jira: transitioned → ${a.transitionName}`);
|
|
553
613
|
}
|
|
554
614
|
catch (err) {
|
|
555
615
|
lines.push(`Jira: could not transition — ${err.message}`);
|
|
556
616
|
}
|
|
557
617
|
}
|
|
558
|
-
|
|
559
|
-
|
|
618
|
+
// Fetch README from Bitbucket for project conventions
|
|
619
|
+
if (bitbucket) {
|
|
620
|
+
try {
|
|
621
|
+
const remoteUrl = (() => {
|
|
622
|
+
try {
|
|
623
|
+
return execFileSync('git', ['remote', 'get-url', 'origin'], { cwd: repoPath, encoding: 'utf-8' }).trim();
|
|
624
|
+
}
|
|
625
|
+
catch {
|
|
626
|
+
return '';
|
|
627
|
+
}
|
|
628
|
+
})();
|
|
629
|
+
const parsed = parseBitbucketRemote(remoteUrl);
|
|
630
|
+
if (parsed) {
|
|
631
|
+
const readme = await bitbucket.fetchFileText(parsed.projectKey, parsed.repoSlug, 'README.md');
|
|
632
|
+
if (readme) {
|
|
633
|
+
const maxLen = 4000;
|
|
634
|
+
const truncated = readme.length > maxLen ? readme.slice(0, maxLen) + '\n... (truncated)' : readme;
|
|
635
|
+
lines.push('');
|
|
636
|
+
lines.push('Project conventions (from README.md):');
|
|
637
|
+
lines.push('────────────────────────────────────');
|
|
638
|
+
lines.push(truncated);
|
|
639
|
+
lines.push('────────────────────────────────────');
|
|
640
|
+
lines.push('Follow the conventions above when writing commit messages and the PR description.');
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
catch { /* README fetch is best-effort */ }
|
|
645
|
+
lines.push('');
|
|
646
|
+
lines.push('Next steps:');
|
|
647
|
+
lines.push(' 1. Make your changes and commit following the project conventions.');
|
|
648
|
+
lines.push(' 2. Use bitbucket_mutate (create) to open a PR — the Jira summary and ticket key make a good title/description starting point.');
|
|
649
|
+
lines.push(' 3. Add reviewers: bitbucket_search resource=users to find colleagues, or pass pickReviewers=true in create to get an interactive picker.');
|
|
650
|
+
}
|
|
560
651
|
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
561
652
|
}
|
|
562
653
|
// Jira
|
|
@@ -611,6 +702,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
611
702
|
return await bitbucket.listRepos(a);
|
|
612
703
|
if (resource === 'branches')
|
|
613
704
|
return await bitbucket.getBranches(a);
|
|
705
|
+
if (resource === 'users')
|
|
706
|
+
return await bitbucket.searchUsers({ projectKey: a.projectKey, repoSlug: a.repoSlug, query: a.query, limit: a.limit, start: a.start });
|
|
614
707
|
// pull_requests (default)
|
|
615
708
|
if (a.mine)
|
|
616
709
|
return await bitbucket.myPrs({ limit: a.limit, start: a.start, role: a.role });
|
|
@@ -633,6 +726,42 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
633
726
|
return await bitbucket.declinePr({ ...a, message: a.declineMessage });
|
|
634
727
|
if (action === 'merge')
|
|
635
728
|
return await bitbucket.mergePr({ ...a, message: a.mergeMessage, mergeStrategy: a.mergeStrategy });
|
|
729
|
+
// Handle interactive reviewer picker for PR creation
|
|
730
|
+
const createArgs = a.create;
|
|
731
|
+
if (createArgs?.pickReviewers && !a.prId) {
|
|
732
|
+
const projectKey = a.projectKey;
|
|
733
|
+
const repoSlug = a.repoSlug;
|
|
734
|
+
const users = await bitbucket.searchUsersRaw({ projectKey, repoSlug, limit: 30 });
|
|
735
|
+
if (users.length > 0) {
|
|
736
|
+
// Build boolean checkbox schema — one field per user
|
|
737
|
+
// key must be safe for JSON schema property names
|
|
738
|
+
const toSchemaKey = (uname) => uname.replace(/[^a-zA-Z0-9]/g, '_');
|
|
739
|
+
const userMap = new Map(); // schemaKey -> username
|
|
740
|
+
const properties = {};
|
|
741
|
+
for (const u of users) {
|
|
742
|
+
const key = toSchemaKey(u.name);
|
|
743
|
+
userMap.set(key, u.name);
|
|
744
|
+
properties[key] = { type: 'boolean', title: `${u.displayName} (${u.name})` };
|
|
745
|
+
}
|
|
746
|
+
try {
|
|
747
|
+
const pickerResult = await server.elicitInput({
|
|
748
|
+
message: 'Select reviewers to add to this PR:',
|
|
749
|
+
requestedSchema: { type: 'object', properties },
|
|
750
|
+
});
|
|
751
|
+
if (pickerResult.action === 'accept' && pickerResult.content) {
|
|
752
|
+
const selected = [];
|
|
753
|
+
for (const [key, username] of userMap) {
|
|
754
|
+
if (pickerResult.content[key] === true)
|
|
755
|
+
selected.push(username);
|
|
756
|
+
}
|
|
757
|
+
createArgs.reviewers = selected;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
catch {
|
|
761
|
+
// elicitation not supported — proceed without reviewer picker
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
636
765
|
return await bitbucket.mutatePullRequest(a);
|
|
637
766
|
}
|
|
638
767
|
case 'bitbucket_comment': {
|
package/dist/jira.js
CHANGED
|
@@ -285,6 +285,22 @@ export class JiraClient {
|
|
|
285
285
|
const page = pagination(data.total, startAt, data.issues.length);
|
|
286
286
|
return text(`Found ${data.total} issues${page}:\n${lines.join('\n')}`);
|
|
287
287
|
}
|
|
288
|
+
async findIssues(query, maxResults = 10) {
|
|
289
|
+
const jql = buildJQL({ query });
|
|
290
|
+
const params = new URLSearchParams({
|
|
291
|
+
jql,
|
|
292
|
+
maxResults: String(maxResults),
|
|
293
|
+
startAt: '0',
|
|
294
|
+
fields: 'summary,status,issuetype',
|
|
295
|
+
});
|
|
296
|
+
const data = await this.request('GET', `/search?${params}`);
|
|
297
|
+
return (data?.issues ?? []).map((i) => ({
|
|
298
|
+
key: i.key,
|
|
299
|
+
summary: i.fields.summary,
|
|
300
|
+
status: i.fields.status.name,
|
|
301
|
+
type: i.fields.issuetype.name,
|
|
302
|
+
}));
|
|
303
|
+
}
|
|
288
304
|
async myIssues(args) {
|
|
289
305
|
return this.searchIssues({
|
|
290
306
|
jql: 'assignee = currentUser() ORDER BY updated DESC',
|