opencode-gitlab-plugin 3.0.1 → 3.2.0

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 CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
4
4
 
5
+ ## [3.2.0](https://gitlab.com/vglafirov/opencode-gitlab-plugin/compare/v3.1.0...v3.2.0) (2026-10-25)
6
+
7
+
8
+ ### ✨ Features
9
+
10
+ * **notes:** support confidential/internal notes in gitlab_create_note ([90cedca](https://gitlab.com/vglafirov/opencode-gitlab-plugin/commit/90cedcac2ab128a2f2377a2951776e47a0d395ba))
11
+
12
+ ## [3.1.0](https://gitlab.com/vglafirov/opencode-gitlab-plugin/compare/v3.0.1...v3.1.0) (2026-10-13)
13
+
14
+
15
+ ### ✨ Features
16
+
17
+ * **orbit:** derive neighbors default rel_types from the schema ([567a114](https://gitlab.com/vglafirov/opencode-gitlab-plugin/commit/567a114be061e6fc5e6d59984c497f8621a9a582))
18
+
5
19
  ## [3.0.1](https://gitlab.com/vglafirov/opencode-gitlab-plugin/compare/v3.0.0...v3.0.1) (2026-10-06)
6
20
 
7
21
 
package/README.md CHANGED
@@ -477,11 +477,11 @@ The plugin provides **64 tools** organized into the following categories:
477
477
 
478
478
  ### Notes Tools (3 tools)
479
479
 
480
- | Tool | Description |
481
- | -------------------- | --------------------------------------------------------------------------------------------------- |
482
- | `gitlab_list_notes` | List all notes/comments on any resource (MRs, issues, epics, snippets) with cursor-based pagination |
483
- | `gitlab_get_note` | Get a single note by ID (issues and epics only) |
484
- | `gitlab_create_note` | Add a simple comment to any resource (for thread replies, use `gitlab_create_discussion`) |
480
+ | Tool | Description |
481
+ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
482
+ | `gitlab_list_notes` | List all notes/comments on any resource (MRs, issues, epics, snippets) with cursor-based pagination |
483
+ | `gitlab_get_note` | Get a single note by ID (issues and epics only) |
484
+ | `gitlab_create_note` | Add a simple comment to any resource (for thread replies, use `gitlab_create_discussion`). Pass `internal: true` to post a confidential/internal note on merge requests, issues, and epics |
485
485
 
486
486
  ### Audit Event Tools (3 tools)
487
487
 
@@ -534,6 +534,15 @@ await plugin.tool.gitlab_create_note.execute({
534
534
  body: 'I will start working on this today.',
535
535
  });
536
536
 
537
+ // Add a confidential/internal note (visible only to project members; merge requests, issues, and epics)
538
+ await plugin.tool.gitlab_create_note.execute({
539
+ resource_type: 'issue',
540
+ project_id: 'my-group/my-project',
541
+ iid: issue.iid,
542
+ body: 'Internal triage note - not visible to the reporter.',
543
+ internal: true,
544
+ });
545
+
537
546
  // List all issues with specific labels
538
547
  const issues = await plugin.tool.gitlab_list_issues.execute({
539
548
  project_id: 'my-group/my-project',
package/dist/index.js CHANGED
@@ -511,9 +511,22 @@ var MergeRequestsClient = class _MergeRequestsClient extends GitLabApiClient {
511
511
  requestBody
512
512
  );
513
513
  }
514
- async createMrNote(projectId, mrIid, body, discussionId) {
514
+ /**
515
+ * Create a note on a merge request.
516
+ *
517
+ * When `discussionId` is set the note is a reply on that discussion thread, which the
518
+ * GitLab Notes API does not allow to be internal. Passing `internal=true` together with a
519
+ * `discussionId` therefore throws (fail-closed) rather than silently dropping the
520
+ * confidentiality flag. Post a standalone internal note (omit `discussionId`) instead.
521
+ */
522
+ async createMrNote(projectId, mrIid, body, discussionId, internal) {
515
523
  const encodedProject = this.encodeProjectId(projectId);
516
524
  if (discussionId) {
525
+ if (internal) {
526
+ throw new Error(
527
+ "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."
528
+ );
529
+ }
517
530
  return this.fetch(
518
531
  "POST",
519
532
  `/projects/${encodedProject}/merge_requests/${mrIid}/discussions/${discussionId}/notes`,
@@ -523,7 +536,7 @@ var MergeRequestsClient = class _MergeRequestsClient extends GitLabApiClient {
523
536
  return this.fetch(
524
537
  "POST",
525
538
  `/projects/${encodedProject}/merge_requests/${mrIid}/notes`,
526
- { body }
539
+ internal ? { body, internal: true } : { body }
527
540
  );
528
541
  }
529
542
  async updateMrNote(projectId, mrIid, noteId, body) {
@@ -989,9 +1002,22 @@ var IssuesClient = class extends GitLabApiClient {
989
1002
  `/projects/${encodedProject}/issues/${issueIid}/discussions/${discussionId}`
990
1003
  );
991
1004
  }
992
- async createIssueNote(projectId, issueIid, body, discussionId) {
1005
+ /**
1006
+ * Create a note on an issue.
1007
+ *
1008
+ * When `discussionId` is set the note is a reply on that discussion thread, which the
1009
+ * GitLab Notes API does not allow to be internal. Passing `internal=true` together with a
1010
+ * `discussionId` therefore throws (fail-closed) rather than silently dropping the
1011
+ * confidentiality flag. Post a standalone internal note (omit `discussionId`) instead.
1012
+ */
1013
+ async createIssueNote(projectId, issueIid, body, discussionId, internal) {
993
1014
  const encodedProject = this.encodeProjectId(projectId);
994
1015
  if (discussionId) {
1016
+ if (internal) {
1017
+ throw new Error(
1018
+ "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."
1019
+ );
1020
+ }
995
1021
  return this.fetch(
996
1022
  "POST",
997
1023
  `/projects/${encodedProject}/issues/${issueIid}/discussions/${discussionId}/notes`,
@@ -1001,7 +1027,7 @@ var IssuesClient = class extends GitLabApiClient {
1001
1027
  return this.fetch(
1002
1028
  "POST",
1003
1029
  `/projects/${encodedProject}/issues/${issueIid}/notes`,
1004
- { body }
1030
+ internal ? { body, internal: true } : { body }
1005
1031
  );
1006
1032
  }
1007
1033
  async resolveIssueDiscussion(projectId, issueIid, discussionId) {
@@ -2332,9 +2358,22 @@ var EpicsClient = class extends GitLabApiClient {
2332
2358
  `/groups/${encodedGroup}/epics/${epicIid}/discussions/${discussionId}`
2333
2359
  );
2334
2360
  }
2335
- async createEpicNote(groupId, epicIid, body, discussionId) {
2361
+ /**
2362
+ * Create a note on an epic.
2363
+ *
2364
+ * When `discussionId` is set the note is a reply on that discussion thread, which the
2365
+ * GitLab Notes API does not allow to be internal. Passing `internal=true` together with a
2366
+ * `discussionId` therefore throws (fail-closed) rather than silently dropping the
2367
+ * confidentiality flag. Post a standalone internal note (omit `discussionId`) instead.
2368
+ */
2369
+ async createEpicNote(groupId, epicIid, body, discussionId, internal) {
2336
2370
  const encodedGroup = encodeURIComponent(groupId);
2337
2371
  if (discussionId) {
2372
+ if (internal) {
2373
+ throw new Error(
2374
+ "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."
2375
+ );
2376
+ }
2338
2377
  return this.fetch(
2339
2378
  "POST",
2340
2379
  `/groups/${encodedGroup}/epics/${epicIid}/discussions/${discussionId}/notes`,
@@ -2344,7 +2383,7 @@ var EpicsClient = class extends GitLabApiClient {
2344
2383
  return this.fetch(
2345
2384
  "POST",
2346
2385
  `/groups/${encodedGroup}/epics/${epicIid}/notes`,
2347
- { body }
2386
+ internal ? { body, internal: true } : { body }
2348
2387
  );
2349
2388
  }
2350
2389
  async getEpicNote(groupId, epicIid, noteId) {
@@ -4902,9 +4941,14 @@ Creates a standalone comment (not part of a thread).
4902
4941
  For replying to existing discussion threads, use gitlab_create_discussion
4903
4942
  with discussion_id parameter instead.
4904
4943
 
4944
+ Set internal=true to post a confidential/internal note visible only to project members
4945
+ (supported for merge requests, issues, and epics; not supported for snippets, where passing
4946
+ internal=true raises an error rather than silently posting a public note).
4947
+
4905
4948
  Examples:
4906
4949
  - MR comment: resource_type="merge_request", project_id="group/project", iid=123, body="LGTM!"
4907
4950
  - Issue comment: resource_type="issue", project_id="group/project", iid=456, body="Working on this"
4951
+ - Internal issue note: resource_type="issue", project_id="group/project", iid=456, body="Internal only", internal=true
4908
4952
  - Epic comment: resource_type="epic", group_id="my-group", iid=1, body="Planning complete"
4909
4953
  - Snippet comment: resource_type="snippet", project_id="group/project", snippet_id=789, body="Nice code!"`,
4910
4954
  args: {
@@ -4913,27 +4957,53 @@ Examples:
4913
4957
  project_id: z14.string().optional().describe("Project ID or path. Required for merge_request, issue, snippet"),
4914
4958
  group_id: z14.string().optional().describe("Group ID or path. Required for epic"),
4915
4959
  iid: z14.number().optional().describe("Internal ID of the resource (for merge_request, issue, epic)"),
4916
- snippet_id: z14.number().optional().describe("Snippet ID (required for snippet)")
4960
+ snippet_id: z14.number().optional().describe("Snippet ID (required for snippet)"),
4961
+ internal: z14.boolean().optional().describe(
4962
+ "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)."
4963
+ )
4917
4964
  },
4918
4965
  execute: async (args, _ctx) => {
4919
4966
  validateNoteParams(args.resource_type, args, { allowedTypes: NOTE_RESOURCE_TYPES });
4967
+ if (args.internal && args.resource_type === "snippet") {
4968
+ throw new Error(
4969
+ `internal=true is not supported for snippet notes. Refusing to post a note that was intended to be confidential as a public note.`
4970
+ );
4971
+ }
4920
4972
  const client = getGitLabClient();
4921
4973
  switch (args.resource_type) {
4922
4974
  case "merge_request":
4923
4975
  return JSON.stringify(
4924
- await client.createMrNote(args.project_id, args.iid, args.body),
4976
+ await client.createMrNote(
4977
+ args.project_id,
4978
+ args.iid,
4979
+ args.body,
4980
+ void 0,
4981
+ args.internal
4982
+ ),
4925
4983
  null,
4926
4984
  2
4927
4985
  );
4928
4986
  case "issue":
4929
4987
  return JSON.stringify(
4930
- await client.createIssueNote(args.project_id, args.iid, args.body),
4988
+ await client.createIssueNote(
4989
+ args.project_id,
4990
+ args.iid,
4991
+ args.body,
4992
+ void 0,
4993
+ args.internal
4994
+ ),
4931
4995
  null,
4932
4996
  2
4933
4997
  );
4934
4998
  case "epic":
4935
4999
  return JSON.stringify(
4936
- await client.createEpicNote(args.group_id, args.iid, args.body),
5000
+ await client.createEpicNote(
5001
+ args.group_id,
5002
+ args.iid,
5003
+ args.body,
5004
+ void 0,
5005
+ args.internal
5006
+ ),
4937
5007
  null,
4938
5008
  2
4939
5009
  );
@@ -5450,6 +5520,65 @@ Examples:
5450
5520
  // src/tools/orbit.ts
5451
5521
  import { tool as tool18 } from "@opencode-ai/plugin";
5452
5522
  var z18 = tool18.schema;
5523
+ var MAX_REL_TYPES = 10;
5524
+ var SCHEMA_CACHE_TTL_MS = 10 * 60 * 1e3;
5525
+ var schemaCache = /* @__PURE__ */ new Map();
5526
+ var schemaInflight = /* @__PURE__ */ new Map();
5527
+ async function loadOrbitSchema(client) {
5528
+ const key = process.env["GITLAB_INSTANCE_URL"] || "https://gitlab.com";
5529
+ const cached = schemaCache.get(key);
5530
+ if (cached && cached.expiresAt > Date.now()) {
5531
+ return cached.schema;
5532
+ }
5533
+ const inflight = schemaInflight.get(key);
5534
+ if (inflight) {
5535
+ return inflight;
5536
+ }
5537
+ const request = (async () => {
5538
+ try {
5539
+ const schema = await client.fetch("GET", "/orbit/schema");
5540
+ if (!schema || !Array.isArray(schema.edges)) {
5541
+ return void 0;
5542
+ }
5543
+ schemaCache.set(key, { schema, expiresAt: Date.now() + SCHEMA_CACHE_TTL_MS });
5544
+ return schema;
5545
+ } catch {
5546
+ return void 0;
5547
+ }
5548
+ })();
5549
+ schemaInflight.set(key, request);
5550
+ void request.finally(() => {
5551
+ if (schemaInflight.get(key) === request) {
5552
+ schemaInflight.delete(key);
5553
+ }
5554
+ });
5555
+ return request;
5556
+ }
5557
+ function deriveRelTypes(schema, entityType, preferred) {
5558
+ const touching = /* @__PURE__ */ new Set();
5559
+ for (const edge of schema.edges ?? []) {
5560
+ const touchesEntity = (edge.variants ?? []).some(
5561
+ (variant) => variant.source_type === entityType || variant.target_type === entityType
5562
+ );
5563
+ if (touchesEntity) {
5564
+ touching.add(edge.name);
5565
+ }
5566
+ }
5567
+ const rank = (name) => {
5568
+ const index = preferred.indexOf(name);
5569
+ return index === -1 ? preferred.length : index;
5570
+ };
5571
+ return [...touching].sort((a, b) => rank(a) - rank(b) || a.localeCompare(b)).slice(0, MAX_REL_TYPES);
5572
+ }
5573
+ async function defaultRelTypes(client, entityType) {
5574
+ const fallback = orbitRelTypes(entityType);
5575
+ const schema = await loadOrbitSchema(client);
5576
+ if (!schema) {
5577
+ return fallback;
5578
+ }
5579
+ const derived = deriveRelTypes(schema, entityType, fallback);
5580
+ return derived.length > 0 ? derived : fallback;
5581
+ }
5453
5582
  function orbitRelTypes(entityType) {
5454
5583
  switch (entityType) {
5455
5584
  case "MergeRequest":
@@ -5786,15 +5915,15 @@ then use that ID (e.g., 377844873). For epics, use the work_item_id.`,
5786
5915
  ),
5787
5916
  direction: z18.enum(["incoming", "outgoing", "both"]).optional().describe("Direction of relationships to traverse (default: both)"),
5788
5917
  rel_types: z18.array(z18.string()).optional().describe(
5789
- 'Relationship types to follow. Required by the API to bound the lookup; if omitted, a sensible default set for the entity type is used. The API caps this at 10 per selector, so more than 10 entries are truncated to the first 10 and the response includes a "warnings" field. See gitlab_orbit_schema for valid edge types.'
5918
+ 'Relationship types to follow. Required by the API to bound the lookup; if omitted, up to 10 edge types that touch the entity type are taken from gitlab_orbit_schema (with a built-in default set as fallback if the schema is unavailable). The API caps this at 10 per selector, so more than 10 entries are truncated to the first 10 and the response includes a "warnings" field. See gitlab_orbit_schema for valid edge types.'
5790
5919
  ),
5791
5920
  limit: z18.number().optional().describe("Maximum number of neighbors to return (default: 30)")
5792
5921
  },
5793
5922
  execute: async (args, _ctx) => {
5794
5923
  const client = getGitLabClient();
5795
5924
  const direction = args.direction || "both";
5796
- const requestedRelTypes = args.rel_types?.length ? args.rel_types : orbitRelTypes(args.entity_type);
5797
- const relTypes = requestedRelTypes.slice(0, 10);
5925
+ const requestedRelTypes = args.rel_types?.length ? args.rel_types : await defaultRelTypes(client, args.entity_type);
5926
+ const relTypes = requestedRelTypes.slice(0, MAX_REL_TYPES);
5798
5927
  const warnings = requestedRelTypes.length > relTypes.length ? [`rel_types truncated to 10 (got ${requestedRelTypes.length})`] : [];
5799
5928
  const queryObj = {
5800
5929
  query_type: "neighbors",