opencode-gitlab-plugin 3.0.0 → 3.1.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 +14 -0
- package/dist/index.js +185 -48
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.cjs +190 -53
- package/dist/mcp-server.cjs.map +1 -1
- package/package.json +1 -1
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.1.0](https://gitlab.com/vglafirov/opencode-gitlab-plugin/compare/v3.0.1...v3.1.0) (2026-10-13)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### ✨ Features
|
|
9
|
+
|
|
10
|
+
* **orbit:** derive neighbors default rel_types from the schema ([567a114](https://gitlab.com/vglafirov/opencode-gitlab-plugin/commit/567a114be061e6fc5e6d59984c497f8621a9a582))
|
|
11
|
+
|
|
12
|
+
## [3.0.1](https://gitlab.com/vglafirov/opencode-gitlab-plugin/compare/v3.0.0...v3.0.1) (2026-10-06)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### 🐛 Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **orbit:** update tools to current Knowledge Graph query DSL ([a62c60b](https://gitlab.com/vglafirov/opencode-gitlab-plugin/commit/a62c60bc195b63928868424cb5412bf579f23597))
|
|
18
|
+
|
|
5
19
|
## [3.0.0](https://gitlab.com/vglafirov/opencode-gitlab-plugin/compare/v2.8.2...v3.0.0) (2026-09-21)
|
|
6
20
|
|
|
7
21
|
|
package/dist/index.js
CHANGED
|
@@ -5450,6 +5450,109 @@ Examples:
|
|
|
5450
5450
|
// src/tools/orbit.ts
|
|
5451
5451
|
import { tool as tool18 } from "@opencode-ai/plugin";
|
|
5452
5452
|
var z18 = tool18.schema;
|
|
5453
|
+
var MAX_REL_TYPES = 10;
|
|
5454
|
+
var SCHEMA_CACHE_TTL_MS = 10 * 60 * 1e3;
|
|
5455
|
+
var schemaCache = /* @__PURE__ */ new Map();
|
|
5456
|
+
var schemaInflight = /* @__PURE__ */ new Map();
|
|
5457
|
+
async function loadOrbitSchema(client) {
|
|
5458
|
+
const key = process.env["GITLAB_INSTANCE_URL"] || "https://gitlab.com";
|
|
5459
|
+
const cached = schemaCache.get(key);
|
|
5460
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
5461
|
+
return cached.schema;
|
|
5462
|
+
}
|
|
5463
|
+
const inflight = schemaInflight.get(key);
|
|
5464
|
+
if (inflight) {
|
|
5465
|
+
return inflight;
|
|
5466
|
+
}
|
|
5467
|
+
const request = (async () => {
|
|
5468
|
+
try {
|
|
5469
|
+
const schema = await client.fetch("GET", "/orbit/schema");
|
|
5470
|
+
if (!schema || !Array.isArray(schema.edges)) {
|
|
5471
|
+
return void 0;
|
|
5472
|
+
}
|
|
5473
|
+
schemaCache.set(key, { schema, expiresAt: Date.now() + SCHEMA_CACHE_TTL_MS });
|
|
5474
|
+
return schema;
|
|
5475
|
+
} catch {
|
|
5476
|
+
return void 0;
|
|
5477
|
+
}
|
|
5478
|
+
})();
|
|
5479
|
+
schemaInflight.set(key, request);
|
|
5480
|
+
void request.finally(() => {
|
|
5481
|
+
if (schemaInflight.get(key) === request) {
|
|
5482
|
+
schemaInflight.delete(key);
|
|
5483
|
+
}
|
|
5484
|
+
});
|
|
5485
|
+
return request;
|
|
5486
|
+
}
|
|
5487
|
+
function deriveRelTypes(schema, entityType, preferred) {
|
|
5488
|
+
const touching = /* @__PURE__ */ new Set();
|
|
5489
|
+
for (const edge of schema.edges ?? []) {
|
|
5490
|
+
const touchesEntity = (edge.variants ?? []).some(
|
|
5491
|
+
(variant) => variant.source_type === entityType || variant.target_type === entityType
|
|
5492
|
+
);
|
|
5493
|
+
if (touchesEntity) {
|
|
5494
|
+
touching.add(edge.name);
|
|
5495
|
+
}
|
|
5496
|
+
}
|
|
5497
|
+
const rank = (name) => {
|
|
5498
|
+
const index = preferred.indexOf(name);
|
|
5499
|
+
return index === -1 ? preferred.length : index;
|
|
5500
|
+
};
|
|
5501
|
+
return [...touching].sort((a, b) => rank(a) - rank(b) || a.localeCompare(b)).slice(0, MAX_REL_TYPES);
|
|
5502
|
+
}
|
|
5503
|
+
async function defaultRelTypes(client, entityType) {
|
|
5504
|
+
const fallback = orbitRelTypes(entityType);
|
|
5505
|
+
const schema = await loadOrbitSchema(client);
|
|
5506
|
+
if (!schema) {
|
|
5507
|
+
return fallback;
|
|
5508
|
+
}
|
|
5509
|
+
const derived = deriveRelTypes(schema, entityType, fallback);
|
|
5510
|
+
return derived.length > 0 ? derived : fallback;
|
|
5511
|
+
}
|
|
5512
|
+
function orbitRelTypes(entityType) {
|
|
5513
|
+
switch (entityType) {
|
|
5514
|
+
case "MergeRequest":
|
|
5515
|
+
return [
|
|
5516
|
+
"AUTHORED",
|
|
5517
|
+
"REVIEWER",
|
|
5518
|
+
"ASSIGNED",
|
|
5519
|
+
"CLOSES",
|
|
5520
|
+
"MENTIONS",
|
|
5521
|
+
"IN_PROJECT",
|
|
5522
|
+
"HAS_HEAD_PIPELINE",
|
|
5523
|
+
"TRIGGERED",
|
|
5524
|
+
"IN_MILESTONE",
|
|
5525
|
+
"HAS_LABEL"
|
|
5526
|
+
];
|
|
5527
|
+
case "WorkItem":
|
|
5528
|
+
return [
|
|
5529
|
+
"AUTHORED",
|
|
5530
|
+
"ASSIGNED",
|
|
5531
|
+
"CLOSES",
|
|
5532
|
+
"MENTIONS",
|
|
5533
|
+
"RELATED_TO",
|
|
5534
|
+
"CONTAINS",
|
|
5535
|
+
"IN_PROJECT",
|
|
5536
|
+
"IN_GROUP",
|
|
5537
|
+
"IN_MILESTONE",
|
|
5538
|
+
"HAS_LABEL"
|
|
5539
|
+
];
|
|
5540
|
+
case "User":
|
|
5541
|
+
return ["AUTHORED", "ASSIGNED", "REVIEWER", "MEMBER_OF", "OWNER", "CREATOR"];
|
|
5542
|
+
case "Project":
|
|
5543
|
+
return ["CONTAINS", "IN_PROJECT", "MEMBER_OF", "CREATOR"];
|
|
5544
|
+
case "Pipeline":
|
|
5545
|
+
return ["HAS_HEAD_PIPELINE", "TRIGGERED", "IN_PROJECT", "HAS_JOB", "HAS_STAGE"];
|
|
5546
|
+
case "File":
|
|
5547
|
+
return ["CONTAINS", "IMPORTS", "DEFINES", "CALLS", "ON_BRANCH"];
|
|
5548
|
+
case "Group":
|
|
5549
|
+
return ["CONTAINS", "MEMBER_OF", "OWNER", "IN_GROUP"];
|
|
5550
|
+
case "Commit":
|
|
5551
|
+
return ["MERGED_AT_COMMIT"];
|
|
5552
|
+
default:
|
|
5553
|
+
return ["AUTHORED", "ASSIGNED", "REVIEWER", "CLOSES", "MENTIONS", "RELATED_TO", "IN_PROJECT"];
|
|
5554
|
+
}
|
|
5555
|
+
}
|
|
5453
5556
|
var orbitTools = {
|
|
5454
5557
|
gitlab_orbit_status: tool18({
|
|
5455
5558
|
description: `Check GitLab Orbit Knowledge Graph API availability and health.
|
|
@@ -5583,47 +5686,64 @@ Note: Requires Orbit API to be available (Premium/Ultimate tier).`,
|
|
|
5583
5686
|
description: `Execute a query against the Orbit Knowledge Graph.
|
|
5584
5687
|
|
|
5585
5688
|
Query types:
|
|
5586
|
-
- traversal:
|
|
5587
|
-
- neighbors:
|
|
5689
|
+
- traversal: Fetch matching nodes or follow relationships between nodes
|
|
5690
|
+
- neighbors: Return nodes connected to one bounded node
|
|
5691
|
+
- aggregation: Count/sum/avg/group graph results
|
|
5692
|
+
- path_finding: Find a bounded path between two nodes
|
|
5588
5693
|
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5694
|
+
QUERY SHAPE (current DSL, format_version 5.x):
|
|
5695
|
+
Every query has a "query_type" and a "nodes" array of node selectors (max 5).
|
|
5696
|
+
A node selector names an entity and is made SELECTIVE via "node_ids" (exact
|
|
5697
|
+
internal ids) or "filters" (natural properties). Relationships between selectors
|
|
5698
|
+
go in a "relationships" array referencing selector "id" aliases.
|
|
5592
5699
|
|
|
5593
|
-
|
|
5700
|
+
IMPORTANT changes from older docs:
|
|
5701
|
+
- Use a top-level "nodes" ARRAY, not a single "node" object.
|
|
5702
|
+
- Pin ids with "node_ids": [123], NOT filters:{id:{op:"eq",value:123}}.
|
|
5703
|
+
- Filters use {op: value}, e.g. {"state": "merged"} or {"created_at": {"gte": "2026-01-01"}}.
|
|
5704
|
+
- A "neighbors" query uses a 1-element "nodes" array plus a top-level
|
|
5705
|
+
"neighbors": {direction, rel_types:[...]} object. rel_types is REQUIRED to
|
|
5706
|
+
bound the lookup and is capped at 10 entries.
|
|
5707
|
+
|
|
5708
|
+
Example neighbors query (an MR's graph neighbours):
|
|
5594
5709
|
{
|
|
5595
5710
|
"query_type": "neighbors",
|
|
5596
|
-
"
|
|
5597
|
-
|
|
5598
|
-
"
|
|
5599
|
-
"
|
|
5711
|
+
"nodes": [{ "id": "mr", "entity": "MergeRequest", "node_ids": [507483917] }],
|
|
5712
|
+
"neighbors": {
|
|
5713
|
+
"direction": "both",
|
|
5714
|
+
"rel_types": ["AUTHORED", "REVIEWER", "IN_PROJECT", "CLOSES", "HAS_HEAD_PIPELINE"]
|
|
5600
5715
|
},
|
|
5601
|
-
"
|
|
5602
|
-
"aggregations": [],
|
|
5603
|
-
"path": {"type": "shortest", "from": "p", "to": "p", "max_depth": 1}
|
|
5716
|
+
"limit": 30
|
|
5604
5717
|
}
|
|
5605
5718
|
|
|
5606
|
-
Example traversal
|
|
5719
|
+
Example traversal (merged MRs in a project):
|
|
5607
5720
|
{
|
|
5608
5721
|
"query_type": "traversal",
|
|
5609
|
-
"
|
|
5610
|
-
"entity": "
|
|
5611
|
-
"id": "mr",
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
"
|
|
5615
|
-
"
|
|
5616
|
-
"path": {"type": "shortest", "from": "mr", "to": "mr", "max_depth": 1}
|
|
5722
|
+
"nodes": [
|
|
5723
|
+
{ "id": "project", "entity": "Project", "filters": {"full_path": "gitlab-org/gitlab"} },
|
|
5724
|
+
{ "id": "mr", "entity": "MergeRequest", "filters": {"state": "merged"},
|
|
5725
|
+
"columns": ["iid", "title", "merged_at"] }
|
|
5726
|
+
],
|
|
5727
|
+
"relationships": [{ "type": "IN_PROJECT", "from": "mr", "to": "project" }],
|
|
5728
|
+
"limit": 25
|
|
5617
5729
|
}
|
|
5618
5730
|
|
|
5619
|
-
|
|
5620
|
-
|
|
5731
|
+
Notes:
|
|
5732
|
+
- node_ids are GitLab *internal* numeric ids (not IIDs). For an MR, get it via
|
|
5733
|
+
gitlab_get_merge_request (the "id" field). For epics, use work_item_id.
|
|
5734
|
+
- Traversal/aggregation queries must include at least one selective node.
|
|
5735
|
+
- aggregation adds an "aggregations" array (e.g. [{"count":"mr"}]) with optional
|
|
5736
|
+
"group_by"; path_finding adds a "path":{from,to,max_depth} object between two
|
|
5737
|
+
node selectors. Both use the same envelope; see gitlab_orbit_schema and the
|
|
5738
|
+
Orbit query-language docs for the full grammar.
|
|
5739
|
+
- Use gitlab_orbit_schema to discover entities and relationship types.
|
|
5740
|
+
Returns nodes and edges arrays.`,
|
|
5621
5741
|
args: {
|
|
5622
5742
|
// Using a JSON string rather than a structured zod schema because the Orbit query
|
|
5623
5743
|
// format is still evolving. This allows flexibility as the API changes without
|
|
5624
5744
|
// requiring schema updates for each iteration.
|
|
5625
5745
|
query: z18.string().describe(
|
|
5626
|
-
|
|
5746
|
+
'JSON string containing the full query object. Must include "query_type" and a top-level "nodes" array of selectors (each pinned by "node_ids" or "filters"); add "relationships"/"neighbors"/"aggregations"/"path" per the query_type. See the tool description for the current DSL and worked examples.'
|
|
5627
5747
|
),
|
|
5628
5748
|
limit: z18.number().optional().describe("Maximum number of results (default: 30)")
|
|
5629
5749
|
},
|
|
@@ -5642,12 +5762,23 @@ Returns nodes array and edges array with relationship types.`,
|
|
|
5642
5762
|
2
|
|
5643
5763
|
);
|
|
5644
5764
|
}
|
|
5765
|
+
if (typeof queryObj !== "object" || queryObj === null || Array.isArray(queryObj)) {
|
|
5766
|
+
return JSON.stringify(
|
|
5767
|
+
{
|
|
5768
|
+
error: "Invalid query parameter",
|
|
5769
|
+
suggestion: 'The query must be a JSON object with a "query_type" and a "nodes" array.'
|
|
5770
|
+
},
|
|
5771
|
+
null,
|
|
5772
|
+
2
|
|
5773
|
+
);
|
|
5774
|
+
}
|
|
5775
|
+
if (args.limit !== void 0) {
|
|
5776
|
+
queryObj.limit = args.limit;
|
|
5777
|
+
}
|
|
5645
5778
|
const requestBody = {
|
|
5646
|
-
query: queryObj
|
|
5779
|
+
query: queryObj,
|
|
5780
|
+
response_format: "raw"
|
|
5647
5781
|
};
|
|
5648
|
-
if (args.limit) {
|
|
5649
|
-
requestBody.limit = args.limit;
|
|
5650
|
-
}
|
|
5651
5782
|
try {
|
|
5652
5783
|
const result = await client.fetch("POST", "/orbit/query", requestBody);
|
|
5653
5784
|
if (result.code) {
|
|
@@ -5656,7 +5787,7 @@ Returns nodes array and edges array with relationship types.`,
|
|
|
5656
5787
|
error: "Query compilation error",
|
|
5657
5788
|
code: result.code,
|
|
5658
5789
|
message: result.message,
|
|
5659
|
-
suggestion: 'Check query structure
|
|
5790
|
+
suggestion: 'Check query structure against the current DSL: top-level "nodes" array, pin ids with "node_ids", filters use {op: value}, and a "neighbors" query needs neighbors:{direction, rel_types} (max 10 rel_types).'
|
|
5660
5791
|
},
|
|
5661
5792
|
null,
|
|
5662
5793
|
2
|
|
@@ -5704,7 +5835,7 @@ Common use cases:
|
|
|
5704
5835
|
|
|
5705
5836
|
Note: Use GitLab internal numeric IDs (not IIDs).
|
|
5706
5837
|
Example: For MR !123 in gitlab-org/gitlab, first get the internal ID via gitlab_get_merge_request,
|
|
5707
|
-
then use that ID (e.g., 377844873).`,
|
|
5838
|
+
then use that ID (e.g., 377844873). For epics, use the work_item_id.`,
|
|
5708
5839
|
args: {
|
|
5709
5840
|
entity_type: z18.string().describe(
|
|
5710
5841
|
"Type of the entity (e.g., Project, MergeRequest, User, Pipeline, Commit, WorkItem, Group)"
|
|
@@ -5713,33 +5844,38 @@ then use that ID (e.g., 377844873).`,
|
|
|
5713
5844
|
"GitLab internal numeric ID (not IID). Get this from REST/GraphQL API responses."
|
|
5714
5845
|
),
|
|
5715
5846
|
direction: z18.enum(["incoming", "outgoing", "both"]).optional().describe("Direction of relationships to traverse (default: both)"),
|
|
5847
|
+
rel_types: z18.array(z18.string()).optional().describe(
|
|
5848
|
+
'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.'
|
|
5849
|
+
),
|
|
5716
5850
|
limit: z18.number().optional().describe("Maximum number of neighbors to return (default: 30)")
|
|
5717
5851
|
},
|
|
5718
5852
|
execute: async (args, _ctx) => {
|
|
5719
5853
|
const client = getGitLabClient();
|
|
5720
5854
|
const direction = args.direction || "both";
|
|
5855
|
+
const requestedRelTypes = args.rel_types?.length ? args.rel_types : await defaultRelTypes(client, args.entity_type);
|
|
5856
|
+
const relTypes = requestedRelTypes.slice(0, MAX_REL_TYPES);
|
|
5857
|
+
const warnings = requestedRelTypes.length > relTypes.length ? [`rel_types truncated to 10 (got ${requestedRelTypes.length})`] : [];
|
|
5721
5858
|
const queryObj = {
|
|
5722
5859
|
query_type: "neighbors",
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5860
|
+
nodes: [
|
|
5861
|
+
{
|
|
5862
|
+
id: "n",
|
|
5863
|
+
entity: args.entity_type,
|
|
5864
|
+
node_ids: [args.entity_id]
|
|
5728
5865
|
}
|
|
5729
|
-
|
|
5866
|
+
],
|
|
5730
5867
|
neighbors: {
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
}
|
|
5734
|
-
aggregations: [],
|
|
5735
|
-
path: { type: "shortest", from: "n", to: "n", max_depth: 1 }
|
|
5868
|
+
direction,
|
|
5869
|
+
rel_types: relTypes
|
|
5870
|
+
}
|
|
5736
5871
|
};
|
|
5872
|
+
if (args.limit !== void 0) {
|
|
5873
|
+
queryObj.limit = args.limit;
|
|
5874
|
+
}
|
|
5737
5875
|
const requestBody = {
|
|
5738
|
-
query: queryObj
|
|
5876
|
+
query: queryObj,
|
|
5877
|
+
response_format: "raw"
|
|
5739
5878
|
};
|
|
5740
|
-
if (args.limit) {
|
|
5741
|
-
requestBody.limit = args.limit;
|
|
5742
|
-
}
|
|
5743
5879
|
try {
|
|
5744
5880
|
const result = await client.fetch("POST", "/orbit/query", requestBody);
|
|
5745
5881
|
if (result.code) {
|
|
@@ -5749,13 +5885,14 @@ then use that ID (e.g., 377844873).`,
|
|
|
5749
5885
|
code: result.code,
|
|
5750
5886
|
message: result.message,
|
|
5751
5887
|
entity_type: args.entity_type,
|
|
5752
|
-
entity_id: args.entity_id
|
|
5888
|
+
entity_id: args.entity_id,
|
|
5889
|
+
...warnings.length > 0 && { warnings }
|
|
5753
5890
|
},
|
|
5754
5891
|
null,
|
|
5755
5892
|
2
|
|
5756
5893
|
);
|
|
5757
5894
|
}
|
|
5758
|
-
return JSON.stringify(result, null, 2);
|
|
5895
|
+
return JSON.stringify(warnings.length > 0 ? { ...result, warnings } : result, null, 2);
|
|
5759
5896
|
} catch (error) {
|
|
5760
5897
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
5761
5898
|
if (errorMessage.includes("404") || errorMessage.includes("not found")) {
|