opencode-gitlab-plugin 3.1.0 → 3.2.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.
- package/CHANGELOG.md +14 -0
- package/README.md +14 -5
- package/dist/index.js +144 -22
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.cjs +123 -22
- package/dist/mcp-server.cjs.map +1 -1
- package/package.json +1 -1
package/dist/mcp-server.cjs
CHANGED
|
@@ -29275,9 +29275,22 @@ var MergeRequestsClient = class _MergeRequestsClient extends GitLabApiClient {
|
|
|
29275
29275
|
requestBody
|
|
29276
29276
|
);
|
|
29277
29277
|
}
|
|
29278
|
-
|
|
29278
|
+
/**
|
|
29279
|
+
* Create a note on a merge request.
|
|
29280
|
+
*
|
|
29281
|
+
* When `discussionId` is set the note is a reply on that discussion thread, which the
|
|
29282
|
+
* GitLab Notes API does not allow to be internal. Passing `internal=true` together with a
|
|
29283
|
+
* `discussionId` therefore throws (fail-closed) rather than silently dropping the
|
|
29284
|
+
* confidentiality flag. Post a standalone internal note (omit `discussionId`) instead.
|
|
29285
|
+
*/
|
|
29286
|
+
async createMrNote(projectId, mrIid, body, discussionId, internal) {
|
|
29279
29287
|
const encodedProject = this.encodeProjectId(projectId);
|
|
29280
29288
|
if (discussionId) {
|
|
29289
|
+
if (internal) {
|
|
29290
|
+
throw new Error(
|
|
29291
|
+
"internal=true is not supported when replying to a discussion. The GitLab Notes API does not accept an internal flag on discussion replies, so honouring it silently would drop a confidentiality flag. Post a standalone internal note (omit discussionId) instead."
|
|
29292
|
+
);
|
|
29293
|
+
}
|
|
29281
29294
|
return this.fetch(
|
|
29282
29295
|
"POST",
|
|
29283
29296
|
`/projects/${encodedProject}/merge_requests/${mrIid}/discussions/${discussionId}/notes`,
|
|
@@ -29287,7 +29300,7 @@ var MergeRequestsClient = class _MergeRequestsClient extends GitLabApiClient {
|
|
|
29287
29300
|
return this.fetch(
|
|
29288
29301
|
"POST",
|
|
29289
29302
|
`/projects/${encodedProject}/merge_requests/${mrIid}/notes`,
|
|
29290
|
-
{ body }
|
|
29303
|
+
internal ? { body, internal: true } : { body }
|
|
29291
29304
|
);
|
|
29292
29305
|
}
|
|
29293
29306
|
async updateMrNote(projectId, mrIid, noteId, body) {
|
|
@@ -29753,9 +29766,22 @@ var IssuesClient = class extends GitLabApiClient {
|
|
|
29753
29766
|
`/projects/${encodedProject}/issues/${issueIid}/discussions/${discussionId}`
|
|
29754
29767
|
);
|
|
29755
29768
|
}
|
|
29756
|
-
|
|
29769
|
+
/**
|
|
29770
|
+
* Create a note on an issue.
|
|
29771
|
+
*
|
|
29772
|
+
* When `discussionId` is set the note is a reply on that discussion thread, which the
|
|
29773
|
+
* GitLab Notes API does not allow to be internal. Passing `internal=true` together with a
|
|
29774
|
+
* `discussionId` therefore throws (fail-closed) rather than silently dropping the
|
|
29775
|
+
* confidentiality flag. Post a standalone internal note (omit `discussionId`) instead.
|
|
29776
|
+
*/
|
|
29777
|
+
async createIssueNote(projectId, issueIid, body, discussionId, internal) {
|
|
29757
29778
|
const encodedProject = this.encodeProjectId(projectId);
|
|
29758
29779
|
if (discussionId) {
|
|
29780
|
+
if (internal) {
|
|
29781
|
+
throw new Error(
|
|
29782
|
+
"internal=true is not supported when replying to a discussion. The GitLab Notes API does not accept an internal flag on discussion replies, so honouring it silently would drop a confidentiality flag. Post a standalone internal note (omit discussionId) instead."
|
|
29783
|
+
);
|
|
29784
|
+
}
|
|
29759
29785
|
return this.fetch(
|
|
29760
29786
|
"POST",
|
|
29761
29787
|
`/projects/${encodedProject}/issues/${issueIid}/discussions/${discussionId}/notes`,
|
|
@@ -29765,7 +29791,7 @@ var IssuesClient = class extends GitLabApiClient {
|
|
|
29765
29791
|
return this.fetch(
|
|
29766
29792
|
"POST",
|
|
29767
29793
|
`/projects/${encodedProject}/issues/${issueIid}/notes`,
|
|
29768
|
-
{ body }
|
|
29794
|
+
internal ? { body, internal: true } : { body }
|
|
29769
29795
|
);
|
|
29770
29796
|
}
|
|
29771
29797
|
async resolveIssueDiscussion(projectId, issueIid, discussionId) {
|
|
@@ -31096,9 +31122,22 @@ var EpicsClient = class extends GitLabApiClient {
|
|
|
31096
31122
|
`/groups/${encodedGroup}/epics/${epicIid}/discussions/${discussionId}`
|
|
31097
31123
|
);
|
|
31098
31124
|
}
|
|
31099
|
-
|
|
31125
|
+
/**
|
|
31126
|
+
* Create a note on an epic.
|
|
31127
|
+
*
|
|
31128
|
+
* When `discussionId` is set the note is a reply on that discussion thread, which the
|
|
31129
|
+
* GitLab Notes API does not allow to be internal. Passing `internal=true` together with a
|
|
31130
|
+
* `discussionId` therefore throws (fail-closed) rather than silently dropping the
|
|
31131
|
+
* confidentiality flag. Post a standalone internal note (omit `discussionId`) instead.
|
|
31132
|
+
*/
|
|
31133
|
+
async createEpicNote(groupId, epicIid, body, discussionId, internal) {
|
|
31100
31134
|
const encodedGroup = encodeURIComponent(groupId);
|
|
31101
31135
|
if (discussionId) {
|
|
31136
|
+
if (internal) {
|
|
31137
|
+
throw new Error(
|
|
31138
|
+
"internal=true is not supported when replying to a discussion. The GitLab Notes API does not accept an internal flag on discussion replies, so honouring it silently would drop a confidentiality flag. Post a standalone internal note (omit discussionId) instead."
|
|
31139
|
+
);
|
|
31140
|
+
}
|
|
31102
31141
|
return this.fetch(
|
|
31103
31142
|
"POST",
|
|
31104
31143
|
`/groups/${encodedGroup}/epics/${epicIid}/discussions/${discussionId}/notes`,
|
|
@@ -31108,7 +31147,7 @@ var EpicsClient = class extends GitLabApiClient {
|
|
|
31108
31147
|
return this.fetch(
|
|
31109
31148
|
"POST",
|
|
31110
31149
|
`/groups/${encodedGroup}/epics/${epicIid}/notes`,
|
|
31111
|
-
{ body }
|
|
31150
|
+
internal ? { body, internal: true } : { body }
|
|
31112
31151
|
);
|
|
31113
31152
|
}
|
|
31114
31153
|
async getEpicNote(groupId, epicIid, noteId) {
|
|
@@ -31544,36 +31583,67 @@ applyMixins(UnifiedGitLabClient, [
|
|
|
31544
31583
|
]);
|
|
31545
31584
|
|
|
31546
31585
|
// src/utils.ts
|
|
31586
|
+
var integrationToken;
|
|
31587
|
+
function credentialToToken(value) {
|
|
31588
|
+
if (!value || typeof value !== "object") {
|
|
31589
|
+
return void 0;
|
|
31590
|
+
}
|
|
31591
|
+
const credential = value;
|
|
31592
|
+
if (credential["type"] === "oauth") {
|
|
31593
|
+
const access = credential["access"];
|
|
31594
|
+
if (typeof access !== "string" || !access) {
|
|
31595
|
+
return void 0;
|
|
31596
|
+
}
|
|
31597
|
+
return isExpired(credential["expires"]) ? void 0 : access;
|
|
31598
|
+
}
|
|
31599
|
+
if (typeof credential["key"] === "string" && credential["key"]) {
|
|
31600
|
+
return credential["key"];
|
|
31601
|
+
}
|
|
31602
|
+
if (typeof credential["token"] === "string" && credential["token"]) {
|
|
31603
|
+
return credential["token"];
|
|
31604
|
+
}
|
|
31605
|
+
return void 0;
|
|
31606
|
+
}
|
|
31607
|
+
function isExpired(expires) {
|
|
31608
|
+
return typeof expires === "number" && expires > 0 && expires <= Date.now();
|
|
31609
|
+
}
|
|
31547
31610
|
function readTokenFromAuthStorage() {
|
|
31548
31611
|
try {
|
|
31549
31612
|
const authPath = import_path.default.join(import_os.default.homedir(), ".local", "share", "opencode", "auth.json");
|
|
31550
31613
|
if (!import_fs.default.existsSync(authPath)) {
|
|
31551
|
-
return
|
|
31614
|
+
return {};
|
|
31552
31615
|
}
|
|
31553
31616
|
const authData = JSON.parse(import_fs.default.readFileSync(authPath, "utf-8"));
|
|
31554
31617
|
const gitlabAuth = authData?.gitlab;
|
|
31555
31618
|
if (!gitlabAuth) {
|
|
31556
|
-
return
|
|
31619
|
+
return {};
|
|
31557
31620
|
}
|
|
31558
|
-
|
|
31559
|
-
|
|
31621
|
+
const token = credentialToToken(gitlabAuth);
|
|
31622
|
+
if (token) {
|
|
31623
|
+
return { token };
|
|
31560
31624
|
}
|
|
31561
|
-
if (gitlabAuth.type === "
|
|
31562
|
-
return gitlabAuth.
|
|
31625
|
+
if (gitlabAuth.type === "oauth" && isExpired(gitlabAuth.expires)) {
|
|
31626
|
+
return { expired: new Date(gitlabAuth.expires) };
|
|
31563
31627
|
}
|
|
31564
|
-
return
|
|
31628
|
+
return {};
|
|
31565
31629
|
} catch (error45) {
|
|
31566
|
-
return
|
|
31630
|
+
return {};
|
|
31567
31631
|
}
|
|
31568
31632
|
}
|
|
31569
31633
|
function getGitLabClient() {
|
|
31570
|
-
let token = process.env["GITLAB_TOKEN"];
|
|
31634
|
+
let token = process.env["GITLAB_TOKEN"] || integrationToken;
|
|
31635
|
+
let expired;
|
|
31571
31636
|
if (!token) {
|
|
31572
|
-
token = readTokenFromAuthStorage();
|
|
31637
|
+
({ token, expired } = readTokenFromAuthStorage());
|
|
31573
31638
|
}
|
|
31574
31639
|
if (!token) {
|
|
31640
|
+
if (expired) {
|
|
31641
|
+
throw new Error(
|
|
31642
|
+
`GitLab OAuth token in ~/.local/share/opencode/auth.json expired on ${expired.toISOString()}. Re-authenticate GitLab in OpenCode or set the GITLAB_TOKEN environment variable.`
|
|
31643
|
+
);
|
|
31644
|
+
}
|
|
31575
31645
|
throw new Error(
|
|
31576
|
-
"GitLab API token not found. Set GITLAB_TOKEN environment variable or
|
|
31646
|
+
"GitLab API token not found. Set GITLAB_TOKEN environment variable or connect GitLab in OpenCode."
|
|
31577
31647
|
);
|
|
31578
31648
|
}
|
|
31579
31649
|
const instanceUrl = process.env["GITLAB_INSTANCE_URL"] || "https://gitlab.com";
|
|
@@ -33659,9 +33729,14 @@ Creates a standalone comment (not part of a thread).
|
|
|
33659
33729
|
For replying to existing discussion threads, use gitlab_create_discussion
|
|
33660
33730
|
with discussion_id parameter instead.
|
|
33661
33731
|
|
|
33732
|
+
Set internal=true to post a confidential/internal note visible only to project members
|
|
33733
|
+
(supported for merge requests, issues, and epics; not supported for snippets, where passing
|
|
33734
|
+
internal=true raises an error rather than silently posting a public note).
|
|
33735
|
+
|
|
33662
33736
|
Examples:
|
|
33663
33737
|
- MR comment: resource_type="merge_request", project_id="group/project", iid=123, body="LGTM!"
|
|
33664
33738
|
- Issue comment: resource_type="issue", project_id="group/project", iid=456, body="Working on this"
|
|
33739
|
+
- Internal issue note: resource_type="issue", project_id="group/project", iid=456, body="Internal only", internal=true
|
|
33665
33740
|
- Epic comment: resource_type="epic", group_id="my-group", iid=1, body="Planning complete"
|
|
33666
33741
|
- Snippet comment: resource_type="snippet", project_id="group/project", snippet_id=789, body="Nice code!"`,
|
|
33667
33742
|
args: {
|
|
@@ -33670,27 +33745,53 @@ Examples:
|
|
|
33670
33745
|
project_id: z14.string().optional().describe("Project ID or path. Required for merge_request, issue, snippet"),
|
|
33671
33746
|
group_id: z14.string().optional().describe("Group ID or path. Required for epic"),
|
|
33672
33747
|
iid: z14.number().optional().describe("Internal ID of the resource (for merge_request, issue, epic)"),
|
|
33673
|
-
snippet_id: z14.number().optional().describe("Snippet ID (required for snippet)")
|
|
33748
|
+
snippet_id: z14.number().optional().describe("Snippet ID (required for snippet)"),
|
|
33749
|
+
internal: z14.boolean().optional().describe(
|
|
33750
|
+
"Post as a confidential/internal note visible only to project members (merge requests, issues, and epics; not supported for snippets, where passing true raises an error). Defaults to false (public note)."
|
|
33751
|
+
)
|
|
33674
33752
|
},
|
|
33675
33753
|
execute: async (args, _ctx) => {
|
|
33676
33754
|
validateNoteParams(args.resource_type, args, { allowedTypes: NOTE_RESOURCE_TYPES });
|
|
33755
|
+
if (args.internal && args.resource_type === "snippet") {
|
|
33756
|
+
throw new Error(
|
|
33757
|
+
`internal=true is not supported for snippet notes. Refusing to post a note that was intended to be confidential as a public note.`
|
|
33758
|
+
);
|
|
33759
|
+
}
|
|
33677
33760
|
const client = getGitLabClient();
|
|
33678
33761
|
switch (args.resource_type) {
|
|
33679
33762
|
case "merge_request":
|
|
33680
33763
|
return JSON.stringify(
|
|
33681
|
-
await client.createMrNote(
|
|
33764
|
+
await client.createMrNote(
|
|
33765
|
+
args.project_id,
|
|
33766
|
+
args.iid,
|
|
33767
|
+
args.body,
|
|
33768
|
+
void 0,
|
|
33769
|
+
args.internal
|
|
33770
|
+
),
|
|
33682
33771
|
null,
|
|
33683
33772
|
2
|
|
33684
33773
|
);
|
|
33685
33774
|
case "issue":
|
|
33686
33775
|
return JSON.stringify(
|
|
33687
|
-
await client.createIssueNote(
|
|
33776
|
+
await client.createIssueNote(
|
|
33777
|
+
args.project_id,
|
|
33778
|
+
args.iid,
|
|
33779
|
+
args.body,
|
|
33780
|
+
void 0,
|
|
33781
|
+
args.internal
|
|
33782
|
+
),
|
|
33688
33783
|
null,
|
|
33689
33784
|
2
|
|
33690
33785
|
);
|
|
33691
33786
|
case "epic":
|
|
33692
33787
|
return JSON.stringify(
|
|
33693
|
-
await client.createEpicNote(
|
|
33788
|
+
await client.createEpicNote(
|
|
33789
|
+
args.group_id,
|
|
33790
|
+
args.iid,
|
|
33791
|
+
args.body,
|
|
33792
|
+
void 0,
|
|
33793
|
+
args.internal
|
|
33794
|
+
),
|
|
33694
33795
|
null,
|
|
33695
33796
|
2
|
|
33696
33797
|
);
|
|
@@ -34727,7 +34828,7 @@ async function main() {
|
|
|
34727
34828
|
`);
|
|
34728
34829
|
process.exit(1);
|
|
34729
34830
|
}
|
|
34730
|
-
const version2 = true ? "3.1
|
|
34831
|
+
const version2 = true ? "3.2.1" : "0.0.0";
|
|
34731
34832
|
const server = new McpServer({ name: "gitlab", version: version2 });
|
|
34732
34833
|
adaptToolsToMcp(server, mcpTools);
|
|
34733
34834
|
const transport = new StdioServerTransport();
|