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.
@@ -29275,9 +29275,22 @@ var MergeRequestsClient = class _MergeRequestsClient extends GitLabApiClient {
29275
29275
  requestBody
29276
29276
  );
29277
29277
  }
29278
- async createMrNote(projectId, mrIid, body, discussionId) {
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
- async createIssueNote(projectId, issueIid, body, discussionId) {
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
- async createEpicNote(groupId, epicIid, body, discussionId) {
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 void 0;
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 void 0;
31619
+ return {};
31557
31620
  }
31558
- if (gitlabAuth.type === "oauth" && gitlabAuth.access) {
31559
- return gitlabAuth.access;
31621
+ const token = credentialToToken(gitlabAuth);
31622
+ if (token) {
31623
+ return { token };
31560
31624
  }
31561
- if (gitlabAuth.type === "api" && gitlabAuth.key) {
31562
- return gitlabAuth.key;
31625
+ if (gitlabAuth.type === "oauth" && isExpired(gitlabAuth.expires)) {
31626
+ return { expired: new Date(gitlabAuth.expires) };
31563
31627
  }
31564
- return gitlabAuth.token || void 0;
31628
+ return {};
31565
31629
  } catch (error45) {
31566
- return void 0;
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 configure it in OpenCode auth storage (~/.local/share/opencode/auth.json)."
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(args.project_id, args.iid, args.body),
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(args.project_id, args.iid, args.body),
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(args.group_id, args.iid, args.body),
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.0" : "0.0.0";
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();