opencode-gitlab-dap 1.6.0 → 1.6.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/dist/chunk-DPR6OUYG.js +119 -0
- package/dist/chunk-DPR6OUYG.js.map +1 -0
- package/dist/index.cjs +1205 -1081
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +958 -969
- package/dist/index.js.map +1 -1
- package/dist/mcp-servers-2HCDB7XM.js +11 -0
- package/dist/mcp-servers-2HCDB7XM.js.map +1 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __esm = (fn, res) => function __init() {
|
|
9
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
+
};
|
|
8
11
|
var __export = (target, all) => {
|
|
9
12
|
for (var name in all)
|
|
10
13
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -27,17 +30,147 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
30
|
));
|
|
28
31
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
32
|
|
|
33
|
+
// src/graphql.ts
|
|
34
|
+
async function gql(instanceUrl, token, query, variables) {
|
|
35
|
+
const res = await fetch(`${instanceUrl.replace(/\/$/, "")}/api/graphql`, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
38
|
+
body: JSON.stringify({ query, variables })
|
|
39
|
+
});
|
|
40
|
+
const json = await res.json();
|
|
41
|
+
if (json.errors?.length) throw new Error(json.errors[0].message);
|
|
42
|
+
return json.data;
|
|
43
|
+
}
|
|
44
|
+
var init_graphql = __esm({
|
|
45
|
+
"src/graphql.ts"() {
|
|
46
|
+
"use strict";
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// src/mcp-servers.ts
|
|
51
|
+
var mcp_servers_exports = {};
|
|
52
|
+
__export(mcp_servers_exports, {
|
|
53
|
+
discoverMcpToolNames: () => discoverMcpToolNames,
|
|
54
|
+
fetchMcpServers: () => fetchMcpServers,
|
|
55
|
+
listMcpServerTools: () => listMcpServerTools
|
|
56
|
+
});
|
|
57
|
+
async function fetchMcpServers(instanceUrl, token, projectId, agents) {
|
|
58
|
+
try {
|
|
59
|
+
let after = null;
|
|
60
|
+
const serversByAgent = /* @__PURE__ */ new Map();
|
|
61
|
+
for (; ; ) {
|
|
62
|
+
const data = await gql(instanceUrl, token, MCP_SERVERS_QUERY, {
|
|
63
|
+
projectId,
|
|
64
|
+
...after ? { after } : {}
|
|
65
|
+
});
|
|
66
|
+
const page = data?.aiCatalogConfiguredItems;
|
|
67
|
+
if (!page) break;
|
|
68
|
+
for (const node of page.nodes ?? []) {
|
|
69
|
+
const item = node.item;
|
|
70
|
+
const version = item?.latestVersion;
|
|
71
|
+
if (!version?.mcpServers?.nodes?.length) continue;
|
|
72
|
+
const servers = version.mcpServers.nodes.map((s) => ({
|
|
73
|
+
id: s.id,
|
|
74
|
+
name: s.name,
|
|
75
|
+
description: s.description ?? "",
|
|
76
|
+
url: s.url,
|
|
77
|
+
transport: s.transport,
|
|
78
|
+
authType: s.authType,
|
|
79
|
+
currentUserConnected: !!s.currentUserConnected
|
|
80
|
+
}));
|
|
81
|
+
serversByAgent.set(item.id, servers);
|
|
82
|
+
}
|
|
83
|
+
if (!page.pageInfo?.hasNextPage) break;
|
|
84
|
+
after = page.pageInfo.endCursor;
|
|
85
|
+
}
|
|
86
|
+
for (const agent of agents) {
|
|
87
|
+
const servers = serversByAgent.get(agent.identifier);
|
|
88
|
+
if (servers?.length) agent.mcpServers = servers;
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function listMcpServerTools(server) {
|
|
94
|
+
try {
|
|
95
|
+
const res = await fetch(server.url, {
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers: {
|
|
98
|
+
"Content-Type": "application/json",
|
|
99
|
+
Accept: "application/json, text/event-stream"
|
|
100
|
+
},
|
|
101
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list", params: {} })
|
|
102
|
+
});
|
|
103
|
+
if (!res.ok) return [];
|
|
104
|
+
const data = await res.json();
|
|
105
|
+
const tools = data?.result?.tools;
|
|
106
|
+
if (!Array.isArray(tools)) return [];
|
|
107
|
+
return tools.map((t) => ({ name: t.name, description: t.description ?? "" }));
|
|
108
|
+
} catch {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function discoverMcpToolNames(agents) {
|
|
113
|
+
const result = /* @__PURE__ */ new Map();
|
|
114
|
+
const seen = /* @__PURE__ */ new Set();
|
|
115
|
+
for (const agent of agents) {
|
|
116
|
+
if (!agent.mcpServers?.length) continue;
|
|
117
|
+
const agentToolNames = [];
|
|
118
|
+
for (const server of agent.mcpServers) {
|
|
119
|
+
if (seen.has(server.id)) {
|
|
120
|
+
const key2 = server.name.toLowerCase().replace(/[^a-z0-9]+/g, "_");
|
|
121
|
+
const cached = result.get(server.id);
|
|
122
|
+
if (cached) agentToolNames.push(...cached.map((t) => `${key2}_${t}`));
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
seen.add(server.id);
|
|
126
|
+
const tools = await listMcpServerTools(server);
|
|
127
|
+
const toolNames = tools.map((t) => t.name);
|
|
128
|
+
result.set(server.id, toolNames);
|
|
129
|
+
const key = server.name.toLowerCase().replace(/[^a-z0-9]+/g, "_");
|
|
130
|
+
agentToolNames.push(...toolNames.map((t) => `${key}_${t}`));
|
|
131
|
+
}
|
|
132
|
+
result.set(agent.identifier, agentToolNames);
|
|
133
|
+
}
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
var MCP_SERVERS_QUERY;
|
|
137
|
+
var init_mcp_servers = __esm({
|
|
138
|
+
"src/mcp-servers.ts"() {
|
|
139
|
+
"use strict";
|
|
140
|
+
init_graphql();
|
|
141
|
+
MCP_SERVERS_QUERY = `
|
|
142
|
+
query AiCatalogMcpServers($projectId: ProjectID!, $after: String) {
|
|
143
|
+
aiCatalogConfiguredItems(first: 50, projectId: $projectId, itemTypes: [AGENT], after: $after) {
|
|
144
|
+
pageInfo { hasNextPage endCursor }
|
|
145
|
+
nodes {
|
|
146
|
+
item {
|
|
147
|
+
id
|
|
148
|
+
name
|
|
149
|
+
latestVersion {
|
|
150
|
+
... on AiCatalogAgentVersion {
|
|
151
|
+
mcpServers {
|
|
152
|
+
nodes { id name description url transport authType currentUserConnected }
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}`;
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
30
163
|
// src/index.ts
|
|
31
164
|
var index_exports = {};
|
|
32
165
|
__export(index_exports, {
|
|
33
166
|
default: () => index_default
|
|
34
167
|
});
|
|
35
168
|
module.exports = __toCommonJS(index_exports);
|
|
36
|
-
var import_plugin = require("@opencode-ai/plugin");
|
|
37
169
|
var import_gitlab_ai_provider = require("gitlab-ai-provider");
|
|
38
170
|
|
|
39
|
-
// src/
|
|
171
|
+
// src/flow-execution.ts
|
|
40
172
|
var import_js_yaml = require("js-yaml");
|
|
173
|
+
init_graphql();
|
|
41
174
|
|
|
42
175
|
// src/generated/foundational-flows.ts
|
|
43
176
|
var FOUNDATIONAL_FLOWS = {
|
|
@@ -1214,7 +1347,7 @@ flow:
|
|
|
1214
1347
|
"slack_assistant": 'version: "v1"\nenvironment: chat\ncomponents:\n - name: "slack_assistant"\n type: AgentComponent\n prompt_id: "slack_assistant_prompt"\n inputs:\n - from: "context:goal"\n as: "goal"\n toolset:\n - "gitlab_api_get"\n - "gitlab_graphql"\n - "gitlab_issue_search"\n - "get_issue"\n - "get_merge_request"\n - "get_wiki_page"\n - "get_repository_file"\n - "gitlab_documentation_search"\n ui_log_events:\n - "on_agent_final_answer"\n - "on_tool_execution_success"\n - "on_tool_execution_failed"\nrouters:\n - from: "slack_assistant"\n to: "end"\nflow:\n entry_point: "slack_assistant"\nprompts:\n - name: "slack_assistant_prompt"\n prompt_id: "slack_assistant_prompt"\n unit_primitives:\n - duo_agent_platform\n prompt_template:\n system: |\n You are a helpful GitLab assistant that people talk to in Slack.\n\n You receive the Slack thread as context (in <slack-thread-context> tags) and a user_context with the invoking user\'s identity and their group memberships. Use the groups to broaden your searches \u2014 search across multiple groups, not just the default namespace. When scoped searches return no results, always fall back to instance-wide search using `GET /api/v4/search?scope=issues&search=...` or `GET /api/v4/projects?search=...&search_namespaces=true`.\n\n Keep Slack responses short. Use bullet points and link to GitLab so people can click through. Format your responses using Slack mrkdwn syntax:\n - *bold* for emphasis\n - _italic_ for secondary emphasis\n - `code` for inline code or commands\n - ```multi-line code``` for code blocks\n - <url|link text> for URLs (e.g., <https://gitlab.com/my-issue|View issue>)\n - <#channel_id> to link channels\n - <@user_id> to mention users\n - <!subteam^group_id> to mention groups\n - @here, @channel, @everyone for special mentions\n - ~strikethrough~ to strikethrough text\n - > quote for block quotes\n - - item for bullet points\n - \\n for line breaks\n\n You are currently *read-only* and act on behalf of the person who mentioned you. You can search and retrieve GitLab data (issues, projects, merge requests, users, etc.) but you cannot create, update, or delete anything. If someone asks you to create an issue or make changes, let them know this capability is not available yet and suggest they do it in GitLab directly.\n\n When a specific tool exists for the task (e.g. `get_issue`, `get_merge_request`, `get_wiki_page`, `get_repository_file`), prefer it over `gitlab_api_get` \u2014 it handles URL parsing and compound operations automatically. Use `gitlab_api_get` and `gitlab_graphql` as fallbacks for resources not covered by a specific tool.\n\n The GitLab API accepts URL-encoded project paths (like `gitlab-org%2Fgitlab`) wherever it accepts numeric project IDs. Use this when people reference projects by path.\n user: |\n {{goal}}\n placeholder: history\n'
|
|
1215
1348
|
};
|
|
1216
1349
|
|
|
1217
|
-
// src/
|
|
1350
|
+
// src/flow-execution.ts
|
|
1218
1351
|
function extractFlowInputs(flowConfig) {
|
|
1219
1352
|
if (!flowConfig) return [];
|
|
1220
1353
|
const flow = flowConfig.flow;
|
|
@@ -1270,16 +1403,41 @@ query FlowVersionDefinition($itemId: AiCatalogItemID!) {
|
|
|
1270
1403
|
}
|
|
1271
1404
|
}
|
|
1272
1405
|
}`;
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
}
|
|
1406
|
+
var RESOLVE_ROOT_NAMESPACE_QUERY = `
|
|
1407
|
+
query resolveRootNamespace($projectPath: ID!) {
|
|
1408
|
+
project(fullPath: $projectPath) {
|
|
1409
|
+
id
|
|
1410
|
+
group {
|
|
1411
|
+
id
|
|
1412
|
+
rootNamespace { id }
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
}`;
|
|
1416
|
+
var WORKFLOW_STATUS_QUERY = `
|
|
1417
|
+
query getWorkflowStatus($workflowId: AiDuoWorkflowsWorkflowID!) {
|
|
1418
|
+
duoWorkflowWorkflows(workflowId: $workflowId) {
|
|
1419
|
+
nodes {
|
|
1420
|
+
id
|
|
1421
|
+
status
|
|
1422
|
+
humanStatus
|
|
1423
|
+
createdAt
|
|
1424
|
+
updatedAt
|
|
1425
|
+
workflowDefinition
|
|
1426
|
+
lastExecutorLogsUrl
|
|
1427
|
+
latestCheckpoint {
|
|
1428
|
+
duoMessages {
|
|
1429
|
+
content
|
|
1430
|
+
correlationId
|
|
1431
|
+
role
|
|
1432
|
+
messageType
|
|
1433
|
+
status
|
|
1434
|
+
timestamp
|
|
1435
|
+
toolInfo
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}`;
|
|
1283
1441
|
async function fetchFoundationalChatAgents(instanceUrl, token, projectId) {
|
|
1284
1442
|
const agents = [];
|
|
1285
1443
|
let after = null;
|
|
@@ -1364,59 +1522,18 @@ async function fetchCustomAgents(instanceUrl, token, projectId) {
|
|
|
1364
1522
|
}
|
|
1365
1523
|
return agents;
|
|
1366
1524
|
}
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
aiCatalogConfiguredItems(first: 50, projectId: $projectId, itemTypes: [AGENT], after: $after) {
|
|
1370
|
-
pageInfo { hasNextPage endCursor }
|
|
1371
|
-
nodes {
|
|
1372
|
-
item {
|
|
1373
|
-
id
|
|
1374
|
-
name
|
|
1375
|
-
latestVersion {
|
|
1376
|
-
... on AiCatalogAgentVersion {
|
|
1377
|
-
mcpServers {
|
|
1378
|
-
nodes { id name description url transport authType currentUserConnected }
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
}
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
}`;
|
|
1386
|
-
async function fetchMcpServers(instanceUrl, token, projectId, agents) {
|
|
1525
|
+
async function fetchCatalogAgents(instanceUrl, token, projectId) {
|
|
1526
|
+
const { fetchMcpServers: fetchMcpServers2 } = await Promise.resolve().then(() => (init_mcp_servers(), mcp_servers_exports));
|
|
1387
1527
|
try {
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
const page = data?.aiCatalogConfiguredItems;
|
|
1396
|
-
if (!page) break;
|
|
1397
|
-
for (const node of page.nodes ?? []) {
|
|
1398
|
-
const item = node.item;
|
|
1399
|
-
const version = item?.latestVersion;
|
|
1400
|
-
if (!version?.mcpServers?.nodes?.length) continue;
|
|
1401
|
-
const servers = version.mcpServers.nodes.map((s) => ({
|
|
1402
|
-
id: s.id,
|
|
1403
|
-
name: s.name,
|
|
1404
|
-
description: s.description ?? "",
|
|
1405
|
-
url: s.url,
|
|
1406
|
-
transport: s.transport,
|
|
1407
|
-
authType: s.authType,
|
|
1408
|
-
currentUserConnected: !!s.currentUserConnected
|
|
1409
|
-
}));
|
|
1410
|
-
serversByAgent.set(item.id, servers);
|
|
1411
|
-
}
|
|
1412
|
-
if (!page.pageInfo?.hasNextPage) break;
|
|
1413
|
-
after = page.pageInfo.endCursor;
|
|
1414
|
-
}
|
|
1415
|
-
for (const agent of agents) {
|
|
1416
|
-
const servers = serversByAgent.get(agent.identifier);
|
|
1417
|
-
if (servers?.length) agent.mcpServers = servers;
|
|
1418
|
-
}
|
|
1528
|
+
const [foundational, custom] = await Promise.all([
|
|
1529
|
+
fetchFoundationalChatAgents(instanceUrl, token, projectId),
|
|
1530
|
+
fetchCustomAgents(instanceUrl, token, projectId)
|
|
1531
|
+
]);
|
|
1532
|
+
const agents = [...foundational, ...custom];
|
|
1533
|
+
await fetchMcpServers2(instanceUrl, token, projectId, agents);
|
|
1534
|
+
return agents;
|
|
1419
1535
|
} catch {
|
|
1536
|
+
return [];
|
|
1420
1537
|
}
|
|
1421
1538
|
}
|
|
1422
1539
|
async function getFlowDefinition(instanceUrl, token, opts) {
|
|
@@ -1448,16 +1565,6 @@ async function getFlowDefinition(instanceUrl, token, opts) {
|
|
|
1448
1565
|
}
|
|
1449
1566
|
return { config, name, ...apiError && !config ? { error: apiError } : {} };
|
|
1450
1567
|
}
|
|
1451
|
-
var RESOLVE_ROOT_NAMESPACE_QUERY = `
|
|
1452
|
-
query resolveRootNamespace($projectPath: ID!) {
|
|
1453
|
-
project(fullPath: $projectPath) {
|
|
1454
|
-
id
|
|
1455
|
-
group {
|
|
1456
|
-
id
|
|
1457
|
-
rootNamespace { id }
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
}`;
|
|
1461
1568
|
async function resolveRootNamespaceId(instanceUrl, token, projectPath) {
|
|
1462
1569
|
try {
|
|
1463
1570
|
const data = await gql(instanceUrl, token, RESOLVE_ROOT_NAMESPACE_QUERY, {
|
|
@@ -1514,31 +1621,6 @@ async function executeFlow(instanceUrl, token, projectPath, consumerId, goal, op
|
|
|
1514
1621
|
}
|
|
1515
1622
|
return res.json();
|
|
1516
1623
|
}
|
|
1517
|
-
var WORKFLOW_STATUS_QUERY = `
|
|
1518
|
-
query getWorkflowStatus($workflowId: AiDuoWorkflowsWorkflowID!) {
|
|
1519
|
-
duoWorkflowWorkflows(workflowId: $workflowId) {
|
|
1520
|
-
nodes {
|
|
1521
|
-
id
|
|
1522
|
-
status
|
|
1523
|
-
humanStatus
|
|
1524
|
-
createdAt
|
|
1525
|
-
updatedAt
|
|
1526
|
-
workflowDefinition
|
|
1527
|
-
lastExecutorLogsUrl
|
|
1528
|
-
latestCheckpoint {
|
|
1529
|
-
duoMessages {
|
|
1530
|
-
content
|
|
1531
|
-
correlationId
|
|
1532
|
-
role
|
|
1533
|
-
messageType
|
|
1534
|
-
status
|
|
1535
|
-
timestamp
|
|
1536
|
-
toolInfo
|
|
1537
|
-
}
|
|
1538
|
-
}
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1541
|
-
}`;
|
|
1542
1624
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1543
1625
|
async function getWorkflowStatus(instanceUrl, token, workflowId) {
|
|
1544
1626
|
const gid = `gid://gitlab/Ai::DuoWorkflows::Workflow/${workflowId}`;
|
|
@@ -1558,19 +1640,9 @@ async function getWorkflowStatus(instanceUrl, token, workflowId) {
|
|
|
1558
1640
|
if (!nodes?.length) throw new Error(`Workflow ${workflowId} not found`);
|
|
1559
1641
|
return nodes[0];
|
|
1560
1642
|
}
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
fetchFoundationalChatAgents(instanceUrl, token, projectId),
|
|
1565
|
-
fetchCustomAgents(instanceUrl, token, projectId)
|
|
1566
|
-
]);
|
|
1567
|
-
const agents = [...foundational, ...custom];
|
|
1568
|
-
await fetchMcpServers(instanceUrl, token, projectId, agents);
|
|
1569
|
-
return agents;
|
|
1570
|
-
} catch {
|
|
1571
|
-
return [];
|
|
1572
|
-
}
|
|
1573
|
-
}
|
|
1643
|
+
|
|
1644
|
+
// src/catalog-items.ts
|
|
1645
|
+
init_graphql();
|
|
1574
1646
|
var LIST_AI_CATALOG_ITEMS_QUERY = `
|
|
1575
1647
|
query listAiCatalogItems(
|
|
1576
1648
|
$itemTypes: [AiCatalogItemType!]
|
|
@@ -1705,24 +1777,99 @@ query resolveProjectIds($projectPath: ID!) {
|
|
|
1705
1777
|
namespace { id }
|
|
1706
1778
|
}
|
|
1707
1779
|
}`;
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1780
|
+
function normalizeItemGid(id) {
|
|
1781
|
+
if (id.startsWith("gid://")) return id;
|
|
1782
|
+
if (!/^\d+$/.test(id)) throw new Error(`Invalid catalog item ID: "${id}"`);
|
|
1783
|
+
return `gid://gitlab/Ai::Catalog::Item/${id}`;
|
|
1784
|
+
}
|
|
1785
|
+
async function resolveProjectGid(instanceUrl, token, projectPath) {
|
|
1786
|
+
const result = await gql(instanceUrl, token, RESOLVE_PROJECT_IDS_FOR_TOOLS_QUERY, {
|
|
1787
|
+
projectPath
|
|
1788
|
+
});
|
|
1789
|
+
return result.project.id;
|
|
1790
|
+
}
|
|
1791
|
+
async function listAiCatalogItems(instanceUrl, token, itemTypes, options) {
|
|
1792
|
+
const variables = {
|
|
1793
|
+
itemTypes,
|
|
1794
|
+
first: options?.first ?? 20
|
|
1795
|
+
};
|
|
1796
|
+
if (options?.search) variables.search = options.search;
|
|
1797
|
+
if (options?.after) variables.after = options.after;
|
|
1798
|
+
const result = await gql(instanceUrl, token, LIST_AI_CATALOG_ITEMS_QUERY, variables);
|
|
1799
|
+
return result.aiCatalogItems;
|
|
1800
|
+
}
|
|
1801
|
+
async function getAiCatalogItem(instanceUrl, token, itemId) {
|
|
1802
|
+
const gid = normalizeItemGid(itemId);
|
|
1803
|
+
const result = await gql(instanceUrl, token, GET_AI_CATALOG_ITEM_QUERY, { id: gid });
|
|
1804
|
+
return result.aiCatalogItem;
|
|
1805
|
+
}
|
|
1806
|
+
async function listProjectAiCatalogItems(instanceUrl, token, projectPath, itemTypes, options) {
|
|
1807
|
+
const projectGid = await resolveProjectGid(instanceUrl, token, projectPath);
|
|
1808
|
+
const variables = {
|
|
1809
|
+
projectId: projectGid,
|
|
1810
|
+
itemTypes,
|
|
1811
|
+
includeFoundationalConsumers: true,
|
|
1812
|
+
first: options?.first ?? 20
|
|
1813
|
+
};
|
|
1814
|
+
if (options?.after) variables.after = options.after;
|
|
1815
|
+
const result = await gql(instanceUrl, token, LIST_PROJECT_CONFIGURED_ITEMS_QUERY, variables);
|
|
1816
|
+
return result.aiCatalogConfiguredItems;
|
|
1817
|
+
}
|
|
1818
|
+
async function enableAiCatalogItemForProject(instanceUrl, token, projectPath, itemId) {
|
|
1819
|
+
const projectGid = await resolveProjectGid(instanceUrl, token, projectPath);
|
|
1820
|
+
const gid = normalizeItemGid(itemId);
|
|
1821
|
+
const result = await gql(instanceUrl, token, ENABLE_AI_CATALOG_ITEM_MUTATION, {
|
|
1822
|
+
input: { itemId: gid, target: { projectId: projectGid } }
|
|
1823
|
+
});
|
|
1824
|
+
if (result.aiCatalogItemConsumerCreate.errors.length > 0) {
|
|
1825
|
+
throw new Error(
|
|
1826
|
+
`Failed to enable item: ${result.aiCatalogItemConsumerCreate.errors.join(", ")}`
|
|
1827
|
+
);
|
|
1828
|
+
}
|
|
1829
|
+
return result.aiCatalogItemConsumerCreate;
|
|
1830
|
+
}
|
|
1831
|
+
async function disableAiCatalogItemForProject(instanceUrl, token, projectPath, itemId) {
|
|
1832
|
+
const projectGid = await resolveProjectGid(instanceUrl, token, projectPath);
|
|
1833
|
+
const gid = normalizeItemGid(itemId);
|
|
1834
|
+
const consumerResult = await gql(instanceUrl, token, FIND_ITEM_CONSUMER_FOR_DISABLE_QUERY, {
|
|
1835
|
+
projectId: projectGid,
|
|
1836
|
+
itemTypes: ["AGENT", "FLOW", "THIRD_PARTY_FLOW"]
|
|
1837
|
+
});
|
|
1838
|
+
const consumer = consumerResult.aiCatalogConfiguredItems.nodes.find(
|
|
1839
|
+
(n) => n.item.id === gid
|
|
1840
|
+
);
|
|
1841
|
+
if (!consumer?.id) throw new Error("Agent/flow is not enabled in this project");
|
|
1842
|
+
const result = await gql(instanceUrl, token, DISABLE_AI_CATALOG_ITEM_MUTATION, {
|
|
1843
|
+
input: { id: consumer.id }
|
|
1844
|
+
});
|
|
1845
|
+
if (result.aiCatalogItemConsumerDelete.errors.length > 0) {
|
|
1846
|
+
throw new Error(
|
|
1847
|
+
`Failed to disable item: ${result.aiCatalogItemConsumerDelete.errors.join(", ")}`
|
|
1848
|
+
);
|
|
1849
|
+
}
|
|
1850
|
+
return result.aiCatalogItemConsumerDelete;
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
// src/catalog-crud.ts
|
|
1854
|
+
init_graphql();
|
|
1855
|
+
var CREATE_AGENT_MUTATION = `
|
|
1856
|
+
mutation AiCatalogAgentCreate($input: AiCatalogAgentCreateInput!) {
|
|
1857
|
+
aiCatalogAgentCreate(input: $input) {
|
|
1858
|
+
errors
|
|
1859
|
+
item {
|
|
1860
|
+
id
|
|
1861
|
+
name
|
|
1862
|
+
description
|
|
1863
|
+
itemType
|
|
1864
|
+
public
|
|
1865
|
+
project { id, nameWithNamespace, webUrl }
|
|
1866
|
+
latestVersion {
|
|
1867
|
+
id
|
|
1868
|
+
humanVersionName
|
|
1869
|
+
released
|
|
1870
|
+
... on AiCatalogAgentVersion {
|
|
1871
|
+
systemPrompt
|
|
1872
|
+
userPrompt
|
|
1726
1873
|
tools { nodes { id, name, description } }
|
|
1727
1874
|
mcpServers { nodes { id, name, url } }
|
|
1728
1875
|
}
|
|
@@ -1806,78 +1953,11 @@ mutation AiCatalogFlowUpdate($input: AiCatalogFlowUpdateInput!) {
|
|
|
1806
1953
|
}
|
|
1807
1954
|
}
|
|
1808
1955
|
}`;
|
|
1809
|
-
function
|
|
1956
|
+
function normalizeItemGid2(id) {
|
|
1810
1957
|
if (id.startsWith("gid://")) return id;
|
|
1811
1958
|
if (!/^\d+$/.test(id)) throw new Error(`Invalid catalog item ID: "${id}"`);
|
|
1812
1959
|
return `gid://gitlab/Ai::Catalog::Item/${id}`;
|
|
1813
1960
|
}
|
|
1814
|
-
async function listAiCatalogItems(instanceUrl, token, itemTypes, options) {
|
|
1815
|
-
const variables = {
|
|
1816
|
-
itemTypes,
|
|
1817
|
-
first: options?.first ?? 20
|
|
1818
|
-
};
|
|
1819
|
-
if (options?.search) variables.search = options.search;
|
|
1820
|
-
if (options?.after) variables.after = options.after;
|
|
1821
|
-
const result = await gql(instanceUrl, token, LIST_AI_CATALOG_ITEMS_QUERY, variables);
|
|
1822
|
-
return result.aiCatalogItems;
|
|
1823
|
-
}
|
|
1824
|
-
async function getAiCatalogItem(instanceUrl, token, itemId) {
|
|
1825
|
-
const gid = normalizeItemGid(itemId);
|
|
1826
|
-
const result = await gql(instanceUrl, token, GET_AI_CATALOG_ITEM_QUERY, { id: gid });
|
|
1827
|
-
return result.aiCatalogItem;
|
|
1828
|
-
}
|
|
1829
|
-
async function resolveProjectGid(instanceUrl, token, projectPath) {
|
|
1830
|
-
const result = await gql(instanceUrl, token, RESOLVE_PROJECT_IDS_FOR_TOOLS_QUERY, {
|
|
1831
|
-
projectPath
|
|
1832
|
-
});
|
|
1833
|
-
return result.project.id;
|
|
1834
|
-
}
|
|
1835
|
-
async function listProjectAiCatalogItems(instanceUrl, token, projectPath, itemTypes, options) {
|
|
1836
|
-
const projectGid = await resolveProjectGid(instanceUrl, token, projectPath);
|
|
1837
|
-
const variables = {
|
|
1838
|
-
projectId: projectGid,
|
|
1839
|
-
itemTypes,
|
|
1840
|
-
includeFoundationalConsumers: true,
|
|
1841
|
-
first: options?.first ?? 20
|
|
1842
|
-
};
|
|
1843
|
-
if (options?.after) variables.after = options.after;
|
|
1844
|
-
const result = await gql(instanceUrl, token, LIST_PROJECT_CONFIGURED_ITEMS_QUERY, variables);
|
|
1845
|
-
return result.aiCatalogConfiguredItems;
|
|
1846
|
-
}
|
|
1847
|
-
async function enableAiCatalogItemForProject(instanceUrl, token, projectPath, itemId) {
|
|
1848
|
-
const projectGid = await resolveProjectGid(instanceUrl, token, projectPath);
|
|
1849
|
-
const gid = normalizeItemGid(itemId);
|
|
1850
|
-
const result = await gql(instanceUrl, token, ENABLE_AI_CATALOG_ITEM_MUTATION, {
|
|
1851
|
-
input: { itemId: gid, target: { projectId: projectGid } }
|
|
1852
|
-
});
|
|
1853
|
-
if (result.aiCatalogItemConsumerCreate.errors.length > 0) {
|
|
1854
|
-
throw new Error(
|
|
1855
|
-
`Failed to enable item: ${result.aiCatalogItemConsumerCreate.errors.join(", ")}`
|
|
1856
|
-
);
|
|
1857
|
-
}
|
|
1858
|
-
return result.aiCatalogItemConsumerCreate;
|
|
1859
|
-
}
|
|
1860
|
-
async function disableAiCatalogItemForProject(instanceUrl, token, projectPath, itemId) {
|
|
1861
|
-
const projectGid = await resolveProjectGid(instanceUrl, token, projectPath);
|
|
1862
|
-
const gid = normalizeItemGid(itemId);
|
|
1863
|
-
const consumerResult = await gql(instanceUrl, token, FIND_ITEM_CONSUMER_FOR_DISABLE_QUERY, {
|
|
1864
|
-
projectId: projectGid,
|
|
1865
|
-
itemTypes: ["AGENT", "FLOW", "THIRD_PARTY_FLOW"]
|
|
1866
|
-
});
|
|
1867
|
-
const consumer = consumerResult.aiCatalogConfiguredItems.nodes.find(
|
|
1868
|
-
(n) => n.item.id === gid
|
|
1869
|
-
);
|
|
1870
|
-
if (!consumer?.id) throw new Error("Agent/flow is not enabled in this project");
|
|
1871
|
-
const result = await gql(instanceUrl, token, DISABLE_AI_CATALOG_ITEM_MUTATION, {
|
|
1872
|
-
input: { id: consumer.id }
|
|
1873
|
-
});
|
|
1874
|
-
if (result.aiCatalogItemConsumerDelete.errors.length > 0) {
|
|
1875
|
-
throw new Error(
|
|
1876
|
-
`Failed to disable item: ${result.aiCatalogItemConsumerDelete.errors.join(", ")}`
|
|
1877
|
-
);
|
|
1878
|
-
}
|
|
1879
|
-
return result.aiCatalogItemConsumerDelete;
|
|
1880
|
-
}
|
|
1881
1961
|
async function createAgent(instanceUrl, token, projectPath, params) {
|
|
1882
1962
|
const projectGid = await resolveProjectGid(instanceUrl, token, projectPath);
|
|
1883
1963
|
const input = {
|
|
@@ -1899,7 +1979,7 @@ async function createAgent(instanceUrl, token, projectPath, params) {
|
|
|
1899
1979
|
return result.aiCatalogAgentCreate.item;
|
|
1900
1980
|
}
|
|
1901
1981
|
async function updateAgent(instanceUrl, token, itemId, params) {
|
|
1902
|
-
const gid =
|
|
1982
|
+
const gid = normalizeItemGid2(itemId);
|
|
1903
1983
|
const input = { id: gid };
|
|
1904
1984
|
if (params.name !== void 0) input.name = params.name;
|
|
1905
1985
|
if (params.description !== void 0) input.description = params.description;
|
|
@@ -1938,7 +2018,7 @@ async function createFlow(instanceUrl, token, projectPath, params) {
|
|
|
1938
2018
|
return result.aiCatalogFlowCreate.item;
|
|
1939
2019
|
}
|
|
1940
2020
|
async function updateFlow(instanceUrl, token, itemId, params) {
|
|
1941
|
-
const gid =
|
|
2021
|
+
const gid = normalizeItemGid2(itemId);
|
|
1942
2022
|
const input = { id: gid };
|
|
1943
2023
|
if (params.name !== void 0) input.name = params.name;
|
|
1944
2024
|
if (params.description !== void 0) input.description = params.description;
|
|
@@ -1953,101 +2033,582 @@ async function updateFlow(instanceUrl, token, itemId, params) {
|
|
|
1953
2033
|
return result.aiCatalogFlowUpdate.item;
|
|
1954
2034
|
}
|
|
1955
2035
|
|
|
1956
|
-
// src/
|
|
1957
|
-
|
|
1958
|
-
var import_js_yaml2 = __toESM(require("js-yaml"), 1);
|
|
2036
|
+
// src/catalog.ts
|
|
2037
|
+
init_mcp_servers();
|
|
1959
2038
|
|
|
1960
|
-
//
|
|
1961
|
-
var
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
"
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
"
|
|
1971
|
-
|
|
1972
|
-
"
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2039
|
+
// src/auth.ts
|
|
2040
|
+
var import_fs = require("fs");
|
|
2041
|
+
var import_path = require("path");
|
|
2042
|
+
var import_os = __toESM(require("os"), 1);
|
|
2043
|
+
function readAuth() {
|
|
2044
|
+
try {
|
|
2045
|
+
const authPath = (0, import_path.join)(import_os.default.homedir(), ".local", "share", "opencode", "auth.json");
|
|
2046
|
+
const data = JSON.parse((0, import_fs.readFileSync)(authPath, "utf-8"));
|
|
2047
|
+
const gitlab = data?.gitlab;
|
|
2048
|
+
if (!gitlab) return null;
|
|
2049
|
+
const token = gitlab.type === "oauth" ? gitlab.access : gitlab.type === "api" ? gitlab.key : null;
|
|
2050
|
+
if (!token) return null;
|
|
2051
|
+
const instanceUrl = gitlab.enterpriseUrl ?? process.env.GITLAB_INSTANCE_URL ?? "https://gitlab.com";
|
|
2052
|
+
return { token, instanceUrl };
|
|
2053
|
+
} catch {
|
|
2054
|
+
return null;
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
// src/agents.ts
|
|
2059
|
+
function resolveModelId(entry) {
|
|
2060
|
+
const ref = entry.selectedModelRef ?? entry.discovery?.defaultModel?.ref;
|
|
2061
|
+
if (!ref) return "duo-workflow-default";
|
|
2062
|
+
return `duo-workflow-${ref.replace(/[/_]/g, "-")}`;
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
// src/prompts.ts
|
|
2066
|
+
var FLOW_DISPATCH_GUIDELINES = [
|
|
2067
|
+
`## GitLab Flow Dispatch Guidelines`,
|
|
2068
|
+
``,
|
|
2069
|
+
`CRITICAL: You must NEVER call gitlab_execute_project_flow or gitlab_get_flow_definition directly.`,
|
|
2070
|
+
`Flows are ALWAYS executed via the Task tool with subagent_type "general".`,
|
|
2071
|
+
`When the user's message contains flow dispatch instructions (starting with "IMPORTANT: You MUST"),`,
|
|
2072
|
+
`follow those instructions exactly \u2014 call the Task tool with the provided parameters.`,
|
|
2073
|
+
``,
|
|
2074
|
+
`### Multiple Flows or Resources`,
|
|
2075
|
+
`When multiple flows need to run (multiple @mentions, or batch across resources), dispatch them`,
|
|
2076
|
+
`via a SINGLE "general" subagent. The general subagent can execute multiple tool calls in parallel,`,
|
|
2077
|
+
`so all flows fire simultaneously. Do NOT dispatch multiple Task calls \u2014 use ONE Task with a prompt`,
|
|
2078
|
+
`that lists all the flows to execute, so the subagent runs them concurrently.`,
|
|
2079
|
+
``,
|
|
2080
|
+
`### Batch Operations (Multiple Resources)`,
|
|
2081
|
+
`If the user asks to run flows on multiple resources (e.g., "for each MR"), first list the`,
|
|
2082
|
+
`resources yourself using GitLab API tools, then dispatch ONE general subagent whose prompt`,
|
|
2083
|
+
`includes all flow executions (N flows x M resources) to run in parallel.`
|
|
2084
|
+
].join("\n");
|
|
2085
|
+
var AGENT_CREATION_GUIDELINES = `## Creating Custom GitLab Agents
|
|
2086
|
+
|
|
2087
|
+
Before calling gitlab_create_agent, you MUST:
|
|
2088
|
+
1. Call gitlab_list_builtin_tools and gitlab_list_project_mcp_servers.
|
|
2089
|
+
2. Ask the user 4 questions using the question tool (one call, all 4 questions):
|
|
2090
|
+
- Agent name (suggest one, allow custom)
|
|
2091
|
+
- Visibility: Public or Private
|
|
2092
|
+
- Tools: show tools grouped by category as multi-select (Search, Issues, MRs, Epics, Files, Git, CI/CD, Security, Audit, Planning, Wiki, API)
|
|
2093
|
+
- MCP servers: multi-select from available servers
|
|
2094
|
+
3. Show the generated system prompt and ask for confirmation.
|
|
2095
|
+
4. Only then call gitlab_create_agent. Use full tool GIDs like "gid://gitlab/Ai::Catalog::BuiltInTool/1".
|
|
2096
|
+
5. Ask if the user wants to enable it on the current project.`;
|
|
2097
|
+
var FLOW_SCHEMA_REFERENCE = `## Flow YAML Schema Reference
|
|
2098
|
+
|
|
2099
|
+
### Top-level structure (all required unless noted):
|
|
2100
|
+
version: "v1" # Always "v1"
|
|
2101
|
+
environment: ambient # Always "ambient"
|
|
2102
|
+
components: [...] # Array of components (min 1)
|
|
2103
|
+
routers: [...] # Array of routers connecting components
|
|
2104
|
+
flow:
|
|
2105
|
+
entry_point: "component_name" # First component to run
|
|
2106
|
+
inputs: [...] # Optional: additional context inputs
|
|
2107
|
+
prompts: [...] # Optional: inline prompt definitions
|
|
2108
|
+
|
|
2109
|
+
### Component types:
|
|
2110
|
+
|
|
2111
|
+
1. DeterministicStepComponent \u2014 runs ONE tool, no LLM call:
|
|
2112
|
+
- name: "fetch_data" # alphanumeric + underscore only
|
|
2113
|
+
type: DeterministicStepComponent
|
|
2114
|
+
tool_name: "get_merge_request"
|
|
2115
|
+
inputs: # map tool parameters
|
|
2116
|
+
- { from: "context:goal", as: "merge_request_iid" }
|
|
2117
|
+
- { from: "context:project_id", as: "project_id" }
|
|
2118
|
+
|
|
2119
|
+
2. OneOffComponent \u2014 single LLM call + tool execution:
|
|
2120
|
+
- name: "process_data"
|
|
2121
|
+
type: OneOffComponent
|
|
2122
|
+
prompt_id: "my_prompt" # references inline prompt
|
|
2123
|
+
prompt_version: null # null = use inline prompt from prompts section
|
|
2124
|
+
toolset: ["read_file", "edit_file"]
|
|
2125
|
+
inputs:
|
|
2126
|
+
- { from: "context:fetch_data.tool_responses", as: "data" }
|
|
2127
|
+
max_correction_attempts: 3 # retries on tool failure (default 3)
|
|
2128
|
+
|
|
2129
|
+
3. AgentComponent \u2014 multi-turn LLM reasoning loop:
|
|
2130
|
+
- name: "analyze"
|
|
2131
|
+
type: AgentComponent
|
|
2132
|
+
prompt_id: "my_agent_prompt"
|
|
2133
|
+
prompt_version: null
|
|
2134
|
+
toolset: ["read_file", "grep"]
|
|
2135
|
+
inputs:
|
|
2136
|
+
- { from: "context:goal", as: "user_goal" }
|
|
2137
|
+
ui_log_events: ["on_agent_final_answer"]
|
|
2138
|
+
|
|
2139
|
+
### IOKey syntax (inputs/from values):
|
|
2140
|
+
"context:goal" # The flow goal (user input)
|
|
2141
|
+
"context:project_id" # Project ID (auto-injected)
|
|
2142
|
+
"context:<component_name>.tool_responses" # Tool output from a component
|
|
2143
|
+
"context:<component_name>.final_answer" # Agent's final text answer
|
|
2144
|
+
"context:<component_name>.execution_result" # OneOff execution result
|
|
2145
|
+
{ from: "context:x", as: "var_name" } # Rename for prompt template
|
|
2146
|
+
{ from: "true", as: "flag", literal: true } # Literal value
|
|
2147
|
+
|
|
2148
|
+
### Router patterns:
|
|
2149
|
+
Direct: { from: "step1", to: "step2" }
|
|
2150
|
+
To end: { from: "last_step", to: "end" }
|
|
2151
|
+
Conditional: { from: "step1", condition: { input: "context:step1.final_answer.decision", routes: { "yes": "step2", "no": "end" } } }
|
|
2152
|
+
|
|
2153
|
+
### Inline prompts (in prompts section):
|
|
2154
|
+
- prompt_id: "my_prompt"
|
|
2155
|
+
name: "My Prompt"
|
|
2156
|
+
unit_primitives: ["duo_agent_platform"] # Always this value
|
|
2157
|
+
prompt_template:
|
|
2158
|
+
system: "You are a helpful assistant. {{var_name}} is available."
|
|
2159
|
+
user: "Process: {{data}}"
|
|
2160
|
+
placeholder: "history" # Optional, for AgentComponent conversation history
|
|
2161
|
+
|
|
2162
|
+
### Tool names: use names from gitlab_list_builtin_tools (e.g., "read_file", "get_merge_request", "create_merge_request_note").`;
|
|
2163
|
+
var FLOW_EXAMPLE_LINEAR = `## Example: Simple linear flow (fetch MR \u2192 analyze \u2192 post comment)
|
|
2164
|
+
\`\`\`yaml
|
|
2165
|
+
version: "v1"
|
|
2166
|
+
environment: ambient
|
|
2167
|
+
components:
|
|
2168
|
+
- name: fetch_mr
|
|
2169
|
+
type: DeterministicStepComponent
|
|
2170
|
+
tool_name: build_review_merge_request_context
|
|
2171
|
+
inputs:
|
|
2172
|
+
- { from: "context:project_id", as: "project_id" }
|
|
2173
|
+
- { from: "context:goal", as: "merge_request_iid" }
|
|
2174
|
+
ui_log_events: ["on_tool_execution_success", "on_tool_execution_failed"]
|
|
2175
|
+
- name: analyze_and_comment
|
|
2176
|
+
type: OneOffComponent
|
|
2177
|
+
prompt_id: review_prompt
|
|
2178
|
+
prompt_version: null
|
|
2179
|
+
toolset: ["create_merge_request_note"]
|
|
2180
|
+
inputs:
|
|
2181
|
+
- { from: "context:fetch_mr.tool_responses", as: "mr_data" }
|
|
2182
|
+
- { from: "context:project_id", as: "project_id" }
|
|
2183
|
+
- { from: "context:goal", as: "merge_request_iid" }
|
|
2184
|
+
max_correction_attempts: 3
|
|
2185
|
+
ui_log_events: ["on_tool_execution_success"]
|
|
2186
|
+
routers:
|
|
2187
|
+
- { from: fetch_mr, to: analyze_and_comment }
|
|
2188
|
+
- { from: analyze_and_comment, to: end }
|
|
2189
|
+
flow:
|
|
2190
|
+
entry_point: fetch_mr
|
|
2191
|
+
prompts:
|
|
2192
|
+
- prompt_id: review_prompt
|
|
2193
|
+
name: MR Review Prompt
|
|
2194
|
+
unit_primitives: ["duo_agent_platform"]
|
|
2195
|
+
prompt_template:
|
|
2196
|
+
system: |
|
|
2197
|
+
You review merge requests. Analyze the MR data and post a concise review comment.
|
|
2198
|
+
Focus on code quality, potential bugs, and improvements.
|
|
2199
|
+
user: "Review MR !{{merge_request_iid}} in project {{project_id}}: {{mr_data}}"
|
|
2200
|
+
\`\`\``;
|
|
2201
|
+
var FLOW_EXAMPLE_CONDITIONAL = `## Example: Conditional flow (gather data \u2192 decide \u2192 branch)
|
|
2202
|
+
\`\`\`yaml
|
|
2203
|
+
version: "v1"
|
|
2204
|
+
environment: ambient
|
|
2205
|
+
components:
|
|
2206
|
+
- name: gather_context
|
|
2207
|
+
type: DeterministicStepComponent
|
|
2208
|
+
tool_name: get_vulnerability_details
|
|
2209
|
+
inputs:
|
|
2210
|
+
- { from: "context:goal", as: "vulnerability_id" }
|
|
2211
|
+
ui_log_events: ["on_tool_execution_success"]
|
|
2212
|
+
- name: evaluate
|
|
2213
|
+
type: AgentComponent
|
|
2214
|
+
prompt_id: eval_prompt
|
|
2215
|
+
prompt_version: null
|
|
2216
|
+
toolset: []
|
|
2217
|
+
inputs:
|
|
2218
|
+
- { from: "context:gather_context.tool_responses", as: "vuln_data" }
|
|
2219
|
+
ui_log_events: ["on_agent_final_answer"]
|
|
2220
|
+
- name: create_fix
|
|
2221
|
+
type: AgentComponent
|
|
2222
|
+
prompt_id: fix_prompt
|
|
2223
|
+
prompt_version: null
|
|
2224
|
+
toolset: ["read_file", "edit_file", "grep"]
|
|
2225
|
+
inputs:
|
|
2226
|
+
- { from: "context:gather_context.tool_responses", as: "vuln_data" }
|
|
2227
|
+
ui_log_events: ["on_agent_final_answer", "on_tool_execution_success"]
|
|
2228
|
+
routers:
|
|
2229
|
+
- { from: gather_context, to: evaluate }
|
|
2230
|
+
- from: evaluate
|
|
2231
|
+
condition:
|
|
2232
|
+
input: "context:evaluate.final_answer"
|
|
2233
|
+
routes:
|
|
2234
|
+
"fix_needed": create_fix
|
|
2235
|
+
"false_positive": end
|
|
2236
|
+
default_route: end
|
|
2237
|
+
- { from: create_fix, to: end }
|
|
2238
|
+
flow:
|
|
2239
|
+
entry_point: gather_context
|
|
2240
|
+
prompts:
|
|
2241
|
+
- prompt_id: eval_prompt
|
|
2242
|
+
name: Vulnerability Evaluator
|
|
2243
|
+
unit_primitives: ["duo_agent_platform"]
|
|
2244
|
+
prompt_template:
|
|
2245
|
+
system: |
|
|
2246
|
+
Evaluate if a vulnerability needs fixing. Respond with exactly "fix_needed" or "false_positive".
|
|
2247
|
+
user: "Evaluate: {{vuln_data}}"
|
|
2248
|
+
- prompt_id: fix_prompt
|
|
2249
|
+
name: Fix Generator
|
|
2250
|
+
unit_primitives: ["duo_agent_platform"]
|
|
2251
|
+
prompt_template:
|
|
2252
|
+
system: |
|
|
2253
|
+
You fix security vulnerabilities. Read the relevant code and apply the fix.
|
|
2254
|
+
user: "Fix this vulnerability: {{vuln_data}}"
|
|
2255
|
+
placeholder: history
|
|
2256
|
+
\`\`\``;
|
|
2257
|
+
|
|
2258
|
+
// src/hooks.ts
|
|
2259
|
+
function buildFlowSubagentPrompt(flow, projectPath, projectUrl) {
|
|
2260
|
+
return [
|
|
2261
|
+
`You execute the "${flow.name}" GitLab flow. Project: ${projectPath} (${projectUrl}).`,
|
|
2262
|
+
``,
|
|
2263
|
+
`STEP 1: Call gitlab_get_flow_definition with consumer_id=${flow.consumerId}, foundational=${!!flow.foundational}.`,
|
|
2264
|
+
`Parse the YAML config:`,
|
|
2265
|
+
`- In "components", find { from: "context:goal", as: "<name>" }. The "as" value is what the goal parameter must contain:`,
|
|
2266
|
+
` "merge_request_iid" -> just the number (e.g. 14)`,
|
|
2267
|
+
` "pipeline_url"/"url"/"issue_url" -> full URL (e.g. ${projectUrl}/-/merge_requests/5)`,
|
|
2268
|
+
` "vulnerability_id" -> just the ID number`,
|
|
2269
|
+
` "goal" -> free-form text`,
|
|
2270
|
+
`- In "flow.inputs", find additional_context categories (skip "agent_platform_standard_context").`,
|
|
2271
|
+
``,
|
|
2272
|
+
`STEP 2: Resolve the goal to the EXACT value the flow expects (from step 1).`,
|
|
2273
|
+
`The goal parameter has a 10000 char limit and is used directly as the flow parameter \u2014 pass ONLY the raw value, never a sentence.`,
|
|
2274
|
+
`Use GitLab API tools if needed to look up IDs or construct URLs from the user's message.`,
|
|
2275
|
+
``,
|
|
2276
|
+
`STEP 3: Gather additional_context values (if any from step 1) using available tools.`,
|
|
2277
|
+
``,
|
|
2278
|
+
`STEP 4: Call gitlab_execute_project_flow with:`,
|
|
2279
|
+
` project_id: "${projectPath}"`,
|
|
2280
|
+
` consumer_id: ${flow.consumerId}`,
|
|
2281
|
+
` goal: <resolved value from step 2>`,
|
|
2282
|
+
` additional_context (if needed): [{"Category":"<cat>","Content":"{\\"field\\":\\"val\\"}"}]`,
|
|
2283
|
+
``,
|
|
2284
|
+
`STEP 5: Call gitlab_get_workflow_status with the workflow_id. Report status and URL: ${projectUrl}/-/automate/agent-sessions/<id>`
|
|
2285
|
+
].join("\n");
|
|
2286
|
+
}
|
|
2287
|
+
function makeChatMessageHook(getAuthCache, flowAgents, getProjectPath) {
|
|
2288
|
+
return async (_input, output) => {
|
|
2289
|
+
const projectPath = getProjectPath();
|
|
2290
|
+
const indicesToRemove = [];
|
|
2291
|
+
const flowMentions = [];
|
|
2292
|
+
for (let i = 0; i < output.parts.length; i++) {
|
|
2293
|
+
const part = output.parts[i];
|
|
2294
|
+
if (part.type !== "agent") continue;
|
|
2295
|
+
const flow = flowAgents.get(part.name);
|
|
2296
|
+
if (!flow || !flow.consumerId || !projectPath) continue;
|
|
2297
|
+
flowMentions.push({ idx: i, flow, displayName: part.name });
|
|
2298
|
+
if (i + 1 < output.parts.length) {
|
|
2299
|
+
const next = output.parts[i + 1];
|
|
2300
|
+
if (next.type === "text" && next.synthetic && next.text?.includes("call the task tool with subagent")) {
|
|
2301
|
+
indicesToRemove.push(i + 1);
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
if (flowMentions.length === 0) {
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
2308
|
+
const authCache = getAuthCache();
|
|
2309
|
+
if (flowMentions.length === 1) {
|
|
2310
|
+
const { idx, flow } = flowMentions[0];
|
|
2311
|
+
const baseUrl2 = authCache?.instanceUrl?.replace(/\/$/, "") ?? "https://gitlab.com";
|
|
2312
|
+
const projectUrl2 = `${baseUrl2}/${projectPath}`;
|
|
2313
|
+
const rawText2 = output.parts.filter((p) => p.type === "text" && !p.synthetic).map((p) => p.text).join(" ").trim() || "Execute the flow";
|
|
2314
|
+
const subagentPrompt = buildFlowSubagentPrompt(flow, projectPath, projectUrl2) + `
|
|
2315
|
+
|
|
2316
|
+
User goal: "${rawText2}"`;
|
|
2317
|
+
const resultText = [
|
|
2318
|
+
`IMPORTANT: You MUST call the Task tool RIGHT NOW to dispatch a subagent. Do NOT execute these steps yourself.`,
|
|
2319
|
+
``,
|
|
2320
|
+
`Call the Task tool with:`,
|
|
2321
|
+
` subagent_type: "general"`,
|
|
2322
|
+
` description: "Execute ${flow.name} flow"`,
|
|
2323
|
+
` prompt: ${JSON.stringify(subagentPrompt)}`,
|
|
2324
|
+
``,
|
|
2325
|
+
`Do not do anything else. Just call the Task tool with the above parameters.`
|
|
2326
|
+
].join("\n");
|
|
2327
|
+
const original2 = output.parts[idx];
|
|
2328
|
+
output.parts[idx] = { ...original2, type: "text", text: resultText };
|
|
2329
|
+
delete output.parts[idx].name;
|
|
2330
|
+
delete output.parts[idx].source;
|
|
2331
|
+
for (const rmIdx of indicesToRemove.reverse()) {
|
|
2332
|
+
output.parts.splice(rmIdx, 1);
|
|
2333
|
+
}
|
|
2334
|
+
return;
|
|
2335
|
+
}
|
|
2336
|
+
const baseUrl = authCache?.instanceUrl?.replace(/\/$/, "") ?? "https://gitlab.com";
|
|
2337
|
+
const projectUrl = `${baseUrl}/${projectPath}`;
|
|
2338
|
+
const flowNames = new Set(flowMentions.map((m) => m.displayName));
|
|
2339
|
+
let rawText = output.parts.filter((p) => p.type === "text" && !p.synthetic).map((p) => p.text).join(" ").trim() || "Execute the flows";
|
|
2340
|
+
for (const name of flowNames) {
|
|
2341
|
+
rawText = rawText.replace(new RegExp(`@${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "g"), "").trim();
|
|
2342
|
+
}
|
|
2343
|
+
rawText = rawText.replace(/\s{2,}/g, " ").trim() || "Execute the flows";
|
|
2344
|
+
const flowList = flowMentions.map(
|
|
2345
|
+
({ flow, displayName }, i) => `${i + 1}. "${displayName}" \u2014 consumer_id=${flow.consumerId}, foundational=${!!flow.foundational}`
|
|
2346
|
+
);
|
|
2347
|
+
const batchPrompt = [
|
|
2348
|
+
`Execute ${flowMentions.length} GitLab flows on project ${projectPath} (${projectUrl}).`,
|
|
2349
|
+
`User goal: "${rawText}"`,
|
|
2350
|
+
``,
|
|
2351
|
+
`Flows to execute:`,
|
|
2352
|
+
...flowList,
|
|
2353
|
+
``,
|
|
2354
|
+
`EXECUTION PLAN:`,
|
|
2355
|
+
`1. Call gitlab_get_flow_definition for ALL flows listed above in a SINGLE response (${flowMentions.length} tool calls at once).`,
|
|
2356
|
+
` Parse each YAML to find what "context:goal" maps to (the "as" field in components).`,
|
|
2357
|
+
``,
|
|
2358
|
+
`2. If the user's goal involves multiple resources (e.g., "for each MR"), list them using GitLab API tools.`,
|
|
2359
|
+
``,
|
|
2360
|
+
`3. Call gitlab_execute_project_flow for EVERY flow+resource combination in a SINGLE response.`,
|
|
2361
|
+
` For each call, set the goal to the EXACT value the flow expects (e.g., just "14" for merge_request_iid).`,
|
|
2362
|
+
` project_id: "${projectPath}"`,
|
|
2363
|
+
` You MUST emit ALL execute calls in ONE response \u2014 do NOT wait for one to finish before calling the next.`,
|
|
2364
|
+
``,
|
|
2365
|
+
`4. Collect all workflow_ids from step 3. Call gitlab_get_workflow_status for ALL of them in a SINGLE response.`,
|
|
2366
|
+
``,
|
|
2367
|
+
`5. Present a summary table: flow name, resource, status, URL (${projectUrl}/-/automate/agent-sessions/<id>).`,
|
|
2368
|
+
``,
|
|
2369
|
+
`CRITICAL: In steps 1, 3, and 4 you MUST make multiple tool calls in the SAME response for parallel execution.`
|
|
2370
|
+
].join("\n");
|
|
2371
|
+
const combinedText = [
|
|
2372
|
+
`IMPORTANT: You MUST call the Task tool RIGHT NOW with subagent_type "general" to dispatch all flows in parallel.`,
|
|
2373
|
+
`Do NOT call flow tools yourself. Do NOT dispatch multiple Task calls \u2014 use ONE.`,
|
|
2374
|
+
``,
|
|
2375
|
+
`Call the Task tool with:`,
|
|
2376
|
+
` subagent_type: "general"`,
|
|
2377
|
+
` description: "Execute ${flowMentions.length} flows in parallel"`,
|
|
2378
|
+
` prompt: ${JSON.stringify(batchPrompt)}`,
|
|
2379
|
+
``,
|
|
2380
|
+
`Do not do anything else. Just call the Task tool with the above parameters.`
|
|
2381
|
+
].join("\n");
|
|
2382
|
+
const firstIdx = flowMentions[0].idx;
|
|
2383
|
+
const original = output.parts[firstIdx];
|
|
2384
|
+
output.parts[firstIdx] = { ...original, type: "text", text: combinedText };
|
|
2385
|
+
delete output.parts[firstIdx].name;
|
|
2386
|
+
delete output.parts[firstIdx].source;
|
|
2387
|
+
for (let i = flowMentions.length - 1; i >= 1; i--) {
|
|
2388
|
+
indicesToRemove.push(flowMentions[i].idx);
|
|
2389
|
+
}
|
|
2390
|
+
for (const idx of [...new Set(indicesToRemove)].sort((a, b) => b - a)) {
|
|
2391
|
+
output.parts.splice(idx, 1);
|
|
2392
|
+
}
|
|
2393
|
+
};
|
|
2394
|
+
}
|
|
2395
|
+
function makeChatParamsHook(gitlabAgentNames) {
|
|
2396
|
+
return async (input, _output) => {
|
|
2397
|
+
if (!gitlabAgentNames.has(input.agent)) return;
|
|
2398
|
+
const model = input.model;
|
|
2399
|
+
const modelId = model?.modelID ?? model?.id ?? "";
|
|
2400
|
+
const isDWS = modelId.includes("duo-workflow");
|
|
2401
|
+
if (!isDWS) {
|
|
2402
|
+
const name = model?.name ?? modelId ?? "unknown";
|
|
2403
|
+
throw new Error(
|
|
2404
|
+
`GitLab agent "${input.agent}" requires an Agent Platform model but the current model is "${name}". Please switch to an Agent Platform model (duo-workflow-*) in the model picker to use GitLab agents.`
|
|
2405
|
+
);
|
|
2406
|
+
}
|
|
2407
|
+
};
|
|
2408
|
+
}
|
|
2409
|
+
function makeSystemTransformHook(flowAgents, getAuthCache) {
|
|
2410
|
+
return async (_input, output) => {
|
|
2411
|
+
if (flowAgents.size) {
|
|
2412
|
+
output.system.push(FLOW_DISPATCH_GUIDELINES);
|
|
2413
|
+
}
|
|
2414
|
+
if (getAuthCache()) {
|
|
2415
|
+
output.system.push(AGENT_CREATION_GUIDELINES);
|
|
2416
|
+
}
|
|
2417
|
+
};
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
// src/tools/flow-tools.ts
|
|
2421
|
+
var import_plugin = require("@opencode-ai/plugin");
|
|
2422
|
+
var z = import_plugin.tool.schema;
|
|
2423
|
+
function makeFlowTools(ctx) {
|
|
2424
|
+
return {
|
|
2425
|
+
gitlab_execute_project_flow: (0, import_plugin.tool)({
|
|
2426
|
+
description: "Execute a GitLab DAP flow on a project.\nTriggers a flow via the Duo Workflow Service REST API.\nThe flow runs asynchronously and is visible in the GitLab UI.\nReturns the workflow record with ID and status.\nThe additional_context parameter accepts flow-specific inputs as a JSON array of {Category, Content} objects.",
|
|
2427
|
+
args: {
|
|
2428
|
+
project_id: z.string().describe('Project path (e.g., "gitlab-org/gitlab")'),
|
|
2429
|
+
consumer_id: z.number().describe("AI Catalog ItemConsumer numeric ID"),
|
|
2430
|
+
goal: z.string().describe("User prompt/goal for the flow, include relevant URLs"),
|
|
2431
|
+
additional_context: z.string().optional().describe(
|
|
2432
|
+
'JSON array of flow inputs: [{"Category":"merge_request","Content":"{\\"url\\":\\"https://...\\"}"}]'
|
|
2433
|
+
),
|
|
2434
|
+
issue_id: z.number().optional().describe("Issue IID for context"),
|
|
2435
|
+
merge_request_id: z.number().optional().describe("Merge request IID for context")
|
|
2436
|
+
},
|
|
2437
|
+
execute: async (args) => {
|
|
2438
|
+
const auth = ctx.ensureAuth();
|
|
2439
|
+
if (!auth) return "Error: GitLab authentication not available";
|
|
2440
|
+
let flowInputs;
|
|
2441
|
+
if (args.additional_context) {
|
|
2442
|
+
try {
|
|
2443
|
+
flowInputs = JSON.parse(args.additional_context);
|
|
2444
|
+
} catch {
|
|
2445
|
+
return "Error: additional_context must be a valid JSON array";
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
try {
|
|
2449
|
+
const result = await executeFlow(
|
|
2450
|
+
auth.instanceUrl,
|
|
2451
|
+
auth.token,
|
|
2452
|
+
args.project_id,
|
|
2453
|
+
args.consumer_id,
|
|
2454
|
+
args.goal,
|
|
2455
|
+
{
|
|
2456
|
+
issueId: args.issue_id,
|
|
2457
|
+
mergeRequestId: args.merge_request_id,
|
|
2458
|
+
namespaceId: ctx.getNamespaceId(),
|
|
2459
|
+
flowInputs
|
|
2460
|
+
}
|
|
2461
|
+
);
|
|
2462
|
+
return JSON.stringify(result, null, 2);
|
|
2463
|
+
} catch (err) {
|
|
2464
|
+
return `Error executing flow: ${err.message}`;
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
}),
|
|
2468
|
+
gitlab_get_flow_definition: (0, import_plugin.tool)({
|
|
2469
|
+
description: "Get the YAML configuration of a GitLab DAP flow.\nReturns the flow config YAML which contains the flow.inputs section\ndescribing what additional_context categories and fields the flow requires.\nUse this before executing a flow to understand what inputs to gather.\nSet foundational=true for GitLab built-in flows, foundational=false for custom flows.",
|
|
2470
|
+
args: {
|
|
2471
|
+
consumer_id: z.number().describe("AI Catalog ItemConsumer numeric ID"),
|
|
2472
|
+
foundational: z.boolean().describe("true for GitLab foundational flows, false for custom flows")
|
|
2473
|
+
},
|
|
2474
|
+
execute: async (args) => {
|
|
2475
|
+
const auth = ctx.ensureAuth();
|
|
2476
|
+
if (!auth) return "Error: GitLab authentication not available";
|
|
2477
|
+
const flow = [...ctx.getFlowAgents().values()].find(
|
|
2478
|
+
(f) => f.consumerId === args.consumer_id
|
|
2479
|
+
);
|
|
2480
|
+
try {
|
|
2481
|
+
const result = await getFlowDefinition(auth.instanceUrl, auth.token, {
|
|
2482
|
+
consumerId: args.consumer_id,
|
|
2483
|
+
flowName: flow?.name,
|
|
2484
|
+
foundational: args.foundational,
|
|
2485
|
+
catalogItemVersionId: flow?.catalogItemVersionId,
|
|
2486
|
+
itemIdentifier: flow?.identifier,
|
|
2487
|
+
workflowDefinition: flow?.workflowDefinition
|
|
2488
|
+
});
|
|
2489
|
+
return JSON.stringify(result, null, 2);
|
|
2490
|
+
} catch (err) {
|
|
2491
|
+
return `Error getting flow definition: ${err.message}`;
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
}),
|
|
2495
|
+
gitlab_get_workflow_status: (0, import_plugin.tool)({
|
|
2496
|
+
description: "Get the status and latest messages of a GitLab DAP workflow.\nUse this to monitor a running flow after executing it.\nReturns the workflow status, latest checkpoint messages, and timestamps.\nPoll every 10 seconds until status is completed, failed, or cancelled.",
|
|
2497
|
+
args: {
|
|
2498
|
+
workflow_id: z.number().describe("Workflow numeric ID (from gitlab_execute_project_flow result)")
|
|
2499
|
+
},
|
|
2500
|
+
execute: async (args) => {
|
|
2501
|
+
const auth = ctx.ensureAuth();
|
|
2502
|
+
if (!auth) return "Error: GitLab authentication not available";
|
|
2503
|
+
try {
|
|
2504
|
+
const result = await getWorkflowStatus(auth.instanceUrl, auth.token, args.workflow_id);
|
|
2505
|
+
return JSON.stringify(result, null, 2);
|
|
2506
|
+
} catch (err) {
|
|
2507
|
+
return `Error getting workflow status: ${err.message}`;
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
})
|
|
2511
|
+
};
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
// src/tools/catalog-crud-tools.ts
|
|
2515
|
+
var import_plugin2 = require("@opencode-ai/plugin");
|
|
2516
|
+
|
|
2517
|
+
// src/flow-validator.ts
|
|
2518
|
+
var import_ajv = __toESM(require("ajv"), 1);
|
|
2519
|
+
var import_js_yaml2 = __toESM(require("js-yaml"), 1);
|
|
2520
|
+
|
|
2521
|
+
// vendor/schemas/flow_v2.json
|
|
2522
|
+
var flow_v2_default = {
|
|
2523
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
2524
|
+
title: "Flow Registry v1 Configuration Schema",
|
|
2525
|
+
description: "JSON Schema for validating Flow Registry v1 YAML configuration files",
|
|
2526
|
+
type: "object",
|
|
2527
|
+
required: [
|
|
2528
|
+
"version",
|
|
2529
|
+
"environment",
|
|
2530
|
+
"components",
|
|
2531
|
+
"routers",
|
|
2532
|
+
"flow",
|
|
2533
|
+
"yaml_definition"
|
|
2534
|
+
],
|
|
2535
|
+
additionalProperties: false,
|
|
2536
|
+
properties: {
|
|
2537
|
+
version: {
|
|
2538
|
+
type: "string",
|
|
2539
|
+
const: "v1",
|
|
2540
|
+
description: "Framework version - must be 'v1' for current stable version"
|
|
2541
|
+
},
|
|
2542
|
+
environment: {
|
|
2543
|
+
type: "string",
|
|
2544
|
+
enum: [
|
|
2545
|
+
"ambient"
|
|
2546
|
+
],
|
|
2547
|
+
description: "Flow environment declaring expected level of interaction between human and AI agent"
|
|
2548
|
+
},
|
|
2549
|
+
components: {
|
|
2550
|
+
type: "array",
|
|
2551
|
+
minItems: 1,
|
|
2552
|
+
description: "List of components that make up the flow",
|
|
2553
|
+
items: {
|
|
2554
|
+
oneOf: [
|
|
2555
|
+
{
|
|
2556
|
+
$ref: "#/definitions/AgentComponent"
|
|
2557
|
+
},
|
|
2558
|
+
{
|
|
2559
|
+
$ref: "#/definitions/DeterministicStepComponent"
|
|
2560
|
+
},
|
|
2561
|
+
{
|
|
2562
|
+
$ref: "#/definitions/OneOffComponent"
|
|
2563
|
+
}
|
|
2564
|
+
]
|
|
2565
|
+
}
|
|
2566
|
+
},
|
|
2567
|
+
routers: {
|
|
2568
|
+
type: "array",
|
|
2569
|
+
description: "Define how components connect to each other",
|
|
2570
|
+
items: {
|
|
2571
|
+
$ref: "#/definitions/Router"
|
|
2572
|
+
}
|
|
2573
|
+
},
|
|
2574
|
+
flow: {
|
|
2575
|
+
type: "object",
|
|
2576
|
+
description: "Specify the entry point component and other flow options",
|
|
2577
|
+
properties: {
|
|
2578
|
+
entry_point: {
|
|
2579
|
+
type: "string",
|
|
2580
|
+
description: "Name of first component to run. Examples: 'main_agent', 'initial_step'",
|
|
2581
|
+
pattern: "^[a-zA-Z0-9_]+$"
|
|
2582
|
+
},
|
|
2583
|
+
inputs: {
|
|
2584
|
+
type: "array",
|
|
2585
|
+
description: "Optional additional context schema definitions that can be passed to the flow (in addition to the 'goal')",
|
|
2586
|
+
items: {
|
|
2587
|
+
$ref: "#/definitions/FlowInputCategory"
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
},
|
|
2591
|
+
additionalProperties: false
|
|
2592
|
+
},
|
|
2593
|
+
prompts: {
|
|
2594
|
+
type: "array",
|
|
2595
|
+
description: "List of inline prompt templates for flow components to use",
|
|
2596
|
+
items: {
|
|
2597
|
+
$ref: "#/definitions/LocalPrompt"
|
|
2598
|
+
}
|
|
2599
|
+
},
|
|
2600
|
+
yaml_definition: {
|
|
2601
|
+
type: "string"
|
|
2602
|
+
}
|
|
2603
|
+
},
|
|
2604
|
+
definitions: {
|
|
2605
|
+
ComponentName: {
|
|
2606
|
+
type: "string",
|
|
2607
|
+
pattern: "^[a-zA-Z0-9_]+$",
|
|
2608
|
+
description: "Component name must use alphanumeric characters or underscore. Must not include characters such as : and . Examples: 'my_agent', 'step1', 'dataProcessor'"
|
|
2609
|
+
},
|
|
2610
|
+
AgentComponent: {
|
|
2611
|
+
type: "object",
|
|
2051
2612
|
required: [
|
|
2052
2613
|
"name",
|
|
2053
2614
|
"type",
|
|
@@ -2560,30 +3121,257 @@ function validateFlowYaml(yamlString) {
|
|
|
2560
3121
|
return { valid: true, errors: [] };
|
|
2561
3122
|
}
|
|
2562
3123
|
|
|
2563
|
-
// src/
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
3124
|
+
// src/tools/catalog-crud-tools.ts
|
|
3125
|
+
var z2 = import_plugin2.tool.schema;
|
|
3126
|
+
function makeCatalogCrudTools(ctx) {
|
|
3127
|
+
return {
|
|
3128
|
+
gitlab_create_agent: (0, import_plugin2.tool)({
|
|
3129
|
+
description: "Create a new custom agent in the GitLab AI Catalog.\nFirst call: set confirmed=false (or omit). The tool returns without creating anything and instructs you to ask the user for agent properties using the question tool.\nSecond call: after the user confirms, set confirmed=true to actually create the agent.\nAfter creation, use gitlab_enable_project_agent to enable it on a project.",
|
|
3130
|
+
args: {
|
|
3131
|
+
project_id: z2.string().describe('Project path (e.g., "gitlab-org/gitlab")'),
|
|
3132
|
+
name: z2.string().describe("Display name for the agent"),
|
|
3133
|
+
description: z2.string().describe("Description of what the agent does"),
|
|
3134
|
+
public: z2.boolean().describe("Whether the agent is publicly visible in the AI Catalog"),
|
|
3135
|
+
system_prompt: z2.string().describe("System prompt that defines the agent's behavior"),
|
|
3136
|
+
user_prompt: z2.string().optional().describe("User prompt template (optional)"),
|
|
3137
|
+
tools: z2.array(z2.string()).optional().describe(
|
|
3138
|
+
'Array of built-in tool Global IDs from gitlab_list_builtin_tools. Must be full GIDs like "gid://gitlab/Ai::Catalog::BuiltInTool/1", NOT tool names.'
|
|
3139
|
+
),
|
|
3140
|
+
mcp_tools: z2.array(z2.string()).optional().describe("Array of MCP tool names to enable"),
|
|
3141
|
+
mcp_servers: z2.array(z2.string()).optional().describe(
|
|
3142
|
+
'Array of MCP server Global IDs from gitlab_list_project_mcp_servers. Must be full GIDs like "gid://gitlab/Ai::Catalog::McpServer/1", NOT server names.'
|
|
3143
|
+
),
|
|
3144
|
+
release: z2.boolean().optional().describe("Whether to release the version immediately (default: false)"),
|
|
3145
|
+
confirmed: z2.boolean().optional().describe(
|
|
3146
|
+
"Set to true only after the user has reviewed and confirmed all parameters. Omit or set false on first call."
|
|
3147
|
+
)
|
|
3148
|
+
},
|
|
3149
|
+
execute: async (args) => {
|
|
3150
|
+
if (!args.confirmed) {
|
|
3151
|
+
return [
|
|
3152
|
+
"STOP: Do not create the agent yet. You must ask the user to confirm the configuration first.",
|
|
3153
|
+
"",
|
|
3154
|
+
"Follow these steps NOW:",
|
|
3155
|
+
"1. Call gitlab_list_builtin_tools and gitlab_list_project_mcp_servers to discover options.",
|
|
3156
|
+
"2. Use the question tool to ask the user ALL 4 of these (in one call):",
|
|
3157
|
+
" - Agent name (suggest one, allow custom input)",
|
|
3158
|
+
" - Visibility: Public or Private",
|
|
3159
|
+
" - Tools: group by category (Search, Issues, MRs, Epics, Files, Git, CI/CD, Security, Audit, Planning, Wiki, API) as multi-select",
|
|
3160
|
+
" - MCP servers: multi-select from available servers",
|
|
3161
|
+
"3. Generate a system prompt and show it to the user for approval.",
|
|
3162
|
+
"4. Call gitlab_create_agent again with confirmed=true after the user approves."
|
|
3163
|
+
].join("\n");
|
|
3164
|
+
}
|
|
3165
|
+
const auth = ctx.ensureAuth();
|
|
3166
|
+
if (!auth) throw new Error("Not authenticated");
|
|
3167
|
+
const result = await createAgent(auth.instanceUrl, auth.token, args.project_id, {
|
|
3168
|
+
name: args.name,
|
|
3169
|
+
description: args.description,
|
|
3170
|
+
public: args.public,
|
|
3171
|
+
systemPrompt: args.system_prompt,
|
|
3172
|
+
userPrompt: args.user_prompt,
|
|
3173
|
+
tools: args.tools,
|
|
3174
|
+
mcpTools: args.mcp_tools,
|
|
3175
|
+
mcpServers: args.mcp_servers,
|
|
3176
|
+
release: args.release
|
|
3177
|
+
});
|
|
3178
|
+
await ctx.refreshAgents();
|
|
3179
|
+
return JSON.stringify(result, null, 2);
|
|
3180
|
+
}
|
|
3181
|
+
}),
|
|
3182
|
+
gitlab_update_agent: (0, import_plugin2.tool)({
|
|
3183
|
+
description: "Update an existing custom agent in the GitLab AI Catalog.\nOnly provided fields are updated; omitted fields remain unchanged.",
|
|
3184
|
+
args: {
|
|
3185
|
+
id: z2.string().describe("Agent ID (numeric or full GID)"),
|
|
3186
|
+
name: z2.string().optional().describe("New display name"),
|
|
3187
|
+
description: z2.string().optional().describe("New description"),
|
|
3188
|
+
public: z2.boolean().optional().describe("Whether publicly visible"),
|
|
3189
|
+
system_prompt: z2.string().optional().describe("New system prompt"),
|
|
3190
|
+
user_prompt: z2.string().optional().describe("New user prompt template"),
|
|
3191
|
+
tools: z2.array(z2.string()).optional().describe(
|
|
3192
|
+
'New set of built-in tool Global IDs (full GIDs like "gid://gitlab/Ai::Catalog::BuiltInTool/1")'
|
|
3193
|
+
),
|
|
3194
|
+
mcp_tools: z2.array(z2.string()).optional().describe("New set of MCP tool names"),
|
|
3195
|
+
mcp_servers: z2.array(z2.string()).optional().describe(
|
|
3196
|
+
'New set of MCP server Global IDs (full GIDs like "gid://gitlab/Ai::Catalog::McpServer/1")'
|
|
3197
|
+
),
|
|
3198
|
+
release: z2.boolean().optional().describe("Whether to release the latest version"),
|
|
3199
|
+
version_bump: z2.enum(["MAJOR", "MINOR", "PATCH"]).optional().describe("Version bump type")
|
|
3200
|
+
},
|
|
3201
|
+
execute: async (args) => {
|
|
3202
|
+
const auth = ctx.ensureAuth();
|
|
3203
|
+
if (!auth) throw new Error("Not authenticated");
|
|
3204
|
+
const result = await updateAgent(auth.instanceUrl, auth.token, args.id, {
|
|
3205
|
+
name: args.name,
|
|
3206
|
+
description: args.description,
|
|
3207
|
+
public: args.public,
|
|
3208
|
+
systemPrompt: args.system_prompt,
|
|
3209
|
+
userPrompt: args.user_prompt,
|
|
3210
|
+
tools: args.tools,
|
|
3211
|
+
mcpTools: args.mcp_tools,
|
|
3212
|
+
mcpServers: args.mcp_servers,
|
|
3213
|
+
release: args.release,
|
|
3214
|
+
versionBump: args.version_bump
|
|
3215
|
+
});
|
|
3216
|
+
await ctx.refreshAgents();
|
|
3217
|
+
return JSON.stringify(result, null, 2);
|
|
3218
|
+
}
|
|
3219
|
+
}),
|
|
3220
|
+
gitlab_list_builtin_tools: (0, import_plugin2.tool)({
|
|
3221
|
+
description: "List available built-in GitLab tools that can be assigned to custom agents.\nReturns tool IDs, names, and descriptions. Use the IDs when creating or updating agents.",
|
|
3222
|
+
args: {},
|
|
3223
|
+
execute: async () => {
|
|
3224
|
+
const auth = ctx.ensureAuth();
|
|
3225
|
+
if (!auth) throw new Error("Not authenticated");
|
|
3226
|
+
const tools = await listBuiltInTools(auth.instanceUrl, auth.token);
|
|
3227
|
+
if (!tools.length) return "No built-in tools available.";
|
|
3228
|
+
return JSON.stringify(tools, null, 2);
|
|
3229
|
+
}
|
|
3230
|
+
}),
|
|
3231
|
+
gitlab_design_flow: (0, import_plugin2.tool)({
|
|
3232
|
+
description: "Interactive flow design tool. Returns the flow YAML schema reference, examples, and instructions.\nUse this BEFORE gitlab_create_flow to design the flow definition interactively with the user.\nThe tool also validates generated YAML against the flow_v2 JSON schema.",
|
|
3233
|
+
args: {
|
|
3234
|
+
action: z2.enum(["get_schema", "validate"]).describe(
|
|
3235
|
+
'"get_schema" returns the schema reference and examples. "validate" validates a YAML definition.'
|
|
3236
|
+
),
|
|
3237
|
+
definition: z2.string().optional().describe("YAML definition to validate (required when action=validate)")
|
|
3238
|
+
},
|
|
3239
|
+
execute: async (args) => {
|
|
3240
|
+
if (args.action === "validate") {
|
|
3241
|
+
if (!args.definition) return "Error: definition is required for validate action.";
|
|
3242
|
+
const result = validateFlowYaml(args.definition);
|
|
3243
|
+
if (result.valid) return "VALID: Flow definition passes schema validation.";
|
|
3244
|
+
return `INVALID: ${result.errors.length} error(s):
|
|
3245
|
+
${result.errors.map((e, i) => ` ${i + 1}. ${e}`).join("\n")}`;
|
|
3246
|
+
}
|
|
3247
|
+
return [
|
|
3248
|
+
"Follow this multi-round workflow to design a flow:",
|
|
3249
|
+
"",
|
|
3250
|
+
"ROUND 1: Call gitlab_list_builtin_tools to discover available tool names for the flow.",
|
|
3251
|
+
" Then use the question tool to ask the user:",
|
|
3252
|
+
" - Flow name",
|
|
3253
|
+
" - Visibility: Public or Private",
|
|
3254
|
+
" - What the flow should do (step-by-step description)",
|
|
3255
|
+
" - What GitLab resource it operates on (MR, issue, pipeline, vulnerability, etc.)",
|
|
3256
|
+
"",
|
|
3257
|
+
"ROUND 2: Based on the user's answers, propose a component architecture in plain text:",
|
|
3258
|
+
" - List each step with its type (DeterministicStep, OneOff, or Agent)",
|
|
3259
|
+
" - Explain what each step does and what tools it uses",
|
|
3260
|
+
" - Show the routing (linear or conditional)",
|
|
3261
|
+
" Ask the user to confirm or adjust.",
|
|
3262
|
+
"",
|
|
3263
|
+
"ROUND 3: Generate the full YAML definition using the schema below.",
|
|
3264
|
+
" Call gitlab_design_flow with action='validate' to check it.",
|
|
3265
|
+
" Show the validated YAML to the user for final approval.",
|
|
3266
|
+
" Then call gitlab_create_flow with confirmed=true.",
|
|
3267
|
+
"",
|
|
3268
|
+
"=== FLOW YAML SCHEMA ===",
|
|
3269
|
+
FLOW_SCHEMA_REFERENCE,
|
|
3270
|
+
"",
|
|
3271
|
+
"=== EXAMPLE: Linear flow ===",
|
|
3272
|
+
FLOW_EXAMPLE_LINEAR,
|
|
3273
|
+
"",
|
|
3274
|
+
"=== EXAMPLE: Conditional flow ===",
|
|
3275
|
+
FLOW_EXAMPLE_CONDITIONAL
|
|
3276
|
+
].join("\n");
|
|
3277
|
+
}
|
|
3278
|
+
}),
|
|
3279
|
+
gitlab_create_flow: (0, import_plugin2.tool)({
|
|
3280
|
+
description: "Create a custom flow in the GitLab AI Catalog.\nFirst call: set confirmed=false (or omit). Returns instructions to use gitlab_design_flow first.\nSecond call: after the user confirms, set confirmed=true to create the flow.\nAfter creation, use gitlab_enable_project_flow to enable it on a project.",
|
|
3281
|
+
args: {
|
|
3282
|
+
project_id: z2.string().describe('Project path (e.g., "gitlab-org/gitlab")'),
|
|
3283
|
+
name: z2.string().describe("Display name for the flow"),
|
|
3284
|
+
description: z2.string().describe("Description of what the flow does"),
|
|
3285
|
+
public: z2.boolean().describe("Whether publicly visible in the AI Catalog"),
|
|
3286
|
+
definition: z2.string().describe("Flow YAML definition (validated via gitlab_design_flow)"),
|
|
3287
|
+
release: z2.boolean().optional().describe("Whether to release the version immediately"),
|
|
3288
|
+
confirmed: z2.boolean().optional().describe("Set true only after user has reviewed the YAML")
|
|
3289
|
+
},
|
|
3290
|
+
execute: async (args) => {
|
|
3291
|
+
if (!args.confirmed) {
|
|
3292
|
+
return [
|
|
3293
|
+
"STOP: Do not create the flow yet.",
|
|
3294
|
+
"",
|
|
3295
|
+
"Call gitlab_design_flow with action='get_schema' first to get the interactive workflow.",
|
|
3296
|
+
"Follow the multi-round design process, then call this tool with confirmed=true."
|
|
3297
|
+
].join("\n");
|
|
3298
|
+
}
|
|
3299
|
+
const validation = validateFlowYaml(args.definition);
|
|
3300
|
+
if (!validation.valid) {
|
|
3301
|
+
return `Flow YAML validation failed:
|
|
3302
|
+
${validation.errors.map((e, i) => ` ${i + 1}. ${e}`).join("\n")}
|
|
3303
|
+
|
|
3304
|
+
Fix the errors and try again.`;
|
|
3305
|
+
}
|
|
3306
|
+
const auth = ctx.ensureAuth();
|
|
3307
|
+
if (!auth) throw new Error("Not authenticated");
|
|
3308
|
+
const result = await createFlow(auth.instanceUrl, auth.token, args.project_id, {
|
|
3309
|
+
name: args.name,
|
|
3310
|
+
description: args.description,
|
|
3311
|
+
public: args.public,
|
|
3312
|
+
definition: args.definition,
|
|
3313
|
+
release: args.release
|
|
3314
|
+
});
|
|
3315
|
+
await ctx.refreshAgents();
|
|
3316
|
+
const json = JSON.stringify(result, null, 2);
|
|
3317
|
+
return `${json}
|
|
3318
|
+
|
|
3319
|
+
Flow created successfully. Ask the user if they want to enable it on the current project using gitlab_enable_project_flow.`;
|
|
3320
|
+
}
|
|
3321
|
+
}),
|
|
3322
|
+
gitlab_update_flow: (0, import_plugin2.tool)({
|
|
3323
|
+
description: "Update an existing custom flow in the GitLab AI Catalog.\nOnly provided fields are updated; omitted fields remain unchanged.\nUse gitlab_design_flow with action='validate' to check YAML before updating.",
|
|
3324
|
+
args: {
|
|
3325
|
+
id: z2.string().describe("Flow ID (numeric or full GID)"),
|
|
3326
|
+
name: z2.string().optional().describe("New display name"),
|
|
3327
|
+
description: z2.string().optional().describe("New description"),
|
|
3328
|
+
public: z2.boolean().optional().describe("Whether publicly visible"),
|
|
3329
|
+
definition: z2.string().optional().describe("New flow YAML definition"),
|
|
3330
|
+
release: z2.boolean().optional().describe("Whether to release the latest version"),
|
|
3331
|
+
version_bump: z2.enum(["MAJOR", "MINOR", "PATCH"]).optional().describe("Version bump type")
|
|
3332
|
+
},
|
|
3333
|
+
execute: async (args) => {
|
|
3334
|
+
if (args.definition) {
|
|
3335
|
+
const validation = validateFlowYaml(args.definition);
|
|
3336
|
+
if (!validation.valid) {
|
|
3337
|
+
return `Flow YAML validation failed:
|
|
3338
|
+
${validation.errors.map((e, i) => ` ${i + 1}. ${e}`).join("\n")}
|
|
3339
|
+
|
|
3340
|
+
Fix the errors and try again.`;
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
const auth = ctx.ensureAuth();
|
|
3344
|
+
if (!auth) throw new Error("Not authenticated");
|
|
3345
|
+
const result = await updateFlow(auth.instanceUrl, auth.token, args.id, {
|
|
3346
|
+
name: args.name,
|
|
3347
|
+
description: args.description,
|
|
3348
|
+
public: args.public,
|
|
3349
|
+
definition: args.definition,
|
|
3350
|
+
release: args.release,
|
|
3351
|
+
versionBump: args.version_bump
|
|
3352
|
+
});
|
|
3353
|
+
await ctx.refreshAgents();
|
|
3354
|
+
return JSON.stringify(result, null, 2);
|
|
3355
|
+
}
|
|
3356
|
+
})
|
|
3357
|
+
};
|
|
2568
3358
|
}
|
|
2569
3359
|
|
|
2570
|
-
// src/
|
|
2571
|
-
var
|
|
2572
|
-
var
|
|
2573
|
-
|
|
2574
|
-
var z = import_plugin.tool.schema;
|
|
2575
|
-
function makeItemTools(z2, itemType, label, getAuth, ensureAuth, onChanged) {
|
|
3360
|
+
// src/tools/catalog-item-tools.ts
|
|
3361
|
+
var import_plugin3 = require("@opencode-ai/plugin");
|
|
3362
|
+
var z3 = import_plugin3.tool.schema;
|
|
3363
|
+
function makeItemTools(itemType, label, getAuth, ensureAuth, onChanged) {
|
|
2576
3364
|
const itemTypes = [itemType];
|
|
2577
3365
|
const Label = label.charAt(0).toUpperCase() + label.slice(1);
|
|
2578
3366
|
return {
|
|
2579
|
-
[`gitlab_list_${label}s`]: (0,
|
|
3367
|
+
[`gitlab_list_${label}s`]: (0, import_plugin3.tool)({
|
|
2580
3368
|
description: `List ${label}s in the GitLab AI Catalog.
|
|
2581
3369
|
Returns ${label}s with name, description, visibility, foundational flag, and version info.
|
|
2582
3370
|
Supports search and cursor-based pagination.`,
|
|
2583
3371
|
args: {
|
|
2584
|
-
search:
|
|
2585
|
-
first:
|
|
2586
|
-
after:
|
|
3372
|
+
search: z3.string().optional().describe(`Search query to filter ${label}s by name or description`),
|
|
3373
|
+
first: z3.number().optional().describe("Number of items to return (default 20)"),
|
|
3374
|
+
after: z3.string().optional().describe("Cursor for pagination (from pageInfo.endCursor)")
|
|
2587
3375
|
},
|
|
2588
3376
|
execute: async (args) => {
|
|
2589
3377
|
const auth = ensureAuth();
|
|
@@ -2596,12 +3384,12 @@ Supports search and cursor-based pagination.`,
|
|
|
2596
3384
|
}
|
|
2597
3385
|
}
|
|
2598
3386
|
}),
|
|
2599
|
-
[`gitlab_get_${label}`]: (0,
|
|
3387
|
+
[`gitlab_get_${label}`]: (0, import_plugin3.tool)({
|
|
2600
3388
|
description: `Get details of a specific ${label} from the AI Catalog.
|
|
2601
3389
|
Returns full details including name, description, visibility, versions, creator, and permissions.
|
|
2602
3390
|
Accepts either a full Global ID (gid://gitlab/Ai::Catalog::Item/123) or just the numeric ID.`,
|
|
2603
3391
|
args: {
|
|
2604
|
-
[`${label}_id`]:
|
|
3392
|
+
[`${label}_id`]: z3.string().describe(`${Label} ID: full GID or numeric ID`)
|
|
2605
3393
|
},
|
|
2606
3394
|
execute: async (args) => {
|
|
2607
3395
|
const auth = ensureAuth();
|
|
@@ -2614,14 +3402,14 @@ Accepts either a full Global ID (gid://gitlab/Ai::Catalog::Item/123) or just the
|
|
|
2614
3402
|
}
|
|
2615
3403
|
}
|
|
2616
3404
|
}),
|
|
2617
|
-
[`gitlab_list_project_${label}s`]: (0,
|
|
3405
|
+
[`gitlab_list_project_${label}s`]: (0, import_plugin3.tool)({
|
|
2618
3406
|
description: `List ${label}s enabled for a specific project.
|
|
2619
3407
|
Returns all configured ${label}s including foundational ones.
|
|
2620
3408
|
Each result includes the ${label} details and its consumer configuration.`,
|
|
2621
3409
|
args: {
|
|
2622
|
-
project_id:
|
|
2623
|
-
first:
|
|
2624
|
-
after:
|
|
3410
|
+
project_id: z3.string().describe('Project path (e.g., "gitlab-org/gitlab")'),
|
|
3411
|
+
first: z3.number().optional().describe("Number of items to return (default 20)"),
|
|
3412
|
+
after: z3.string().optional().describe("Cursor for pagination")
|
|
2625
3413
|
},
|
|
2626
3414
|
execute: async (args) => {
|
|
2627
3415
|
const auth = ensureAuth();
|
|
@@ -2640,14 +3428,14 @@ Each result includes the ${label} details and its consumer configuration.`,
|
|
|
2640
3428
|
}
|
|
2641
3429
|
}
|
|
2642
3430
|
}),
|
|
2643
|
-
[`gitlab_enable_project_${label}`]: (0,
|
|
3431
|
+
[`gitlab_enable_project_${label}`]: (0, import_plugin3.tool)({
|
|
2644
3432
|
description: `Enable a ${label} in a project.
|
|
2645
3433
|
Requires Maintainer or Owner role.
|
|
2646
3434
|
Enabling in a project also enables at the group level.
|
|
2647
3435
|
Foundational ${label}s cannot be enabled this way (use admin settings).`,
|
|
2648
3436
|
args: {
|
|
2649
|
-
project_id:
|
|
2650
|
-
[`${label}_id`]:
|
|
3437
|
+
project_id: z3.string().describe('Project path (e.g., "gitlab-org/gitlab")'),
|
|
3438
|
+
[`${label}_id`]: z3.string().describe(`${Label} ID: full GID or numeric ID`)
|
|
2651
3439
|
},
|
|
2652
3440
|
execute: async (args) => {
|
|
2653
3441
|
const auth = ensureAuth();
|
|
@@ -2660,287 +3448,81 @@ Foundational ${label}s cannot be enabled this way (use admin settings).`,
|
|
|
2660
3448
|
args[`${label}_id`]
|
|
2661
3449
|
);
|
|
2662
3450
|
await onChanged?.();
|
|
2663
|
-
return JSON.stringify(result, null, 2) + "\n\nNote: Restart opencode for the @ menu to reflect changes.";
|
|
2664
|
-
} catch (err) {
|
|
2665
|
-
return `Error: ${err.message}`;
|
|
2666
|
-
}
|
|
2667
|
-
}
|
|
2668
|
-
}),
|
|
2669
|
-
[`gitlab_disable_project_${label}`]: (0,
|
|
2670
|
-
description: `Disable a ${label} in a project.
|
|
2671
|
-
Requires Maintainer or Owner role.
|
|
2672
|
-
Resolves the consumer ID internally from the ${label} ID.`,
|
|
2673
|
-
args: {
|
|
2674
|
-
project_id:
|
|
2675
|
-
[`${label}_id`]:
|
|
2676
|
-
},
|
|
2677
|
-
execute: async (args) => {
|
|
2678
|
-
const auth = ensureAuth();
|
|
2679
|
-
if (!auth) return "Error: GitLab authentication not available";
|
|
2680
|
-
try {
|
|
2681
|
-
const result = await disableAiCatalogItemForProject(
|
|
2682
|
-
auth.instanceUrl,
|
|
2683
|
-
auth.token,
|
|
2684
|
-
args.project_id,
|
|
2685
|
-
args[`${label}_id`]
|
|
2686
|
-
);
|
|
2687
|
-
await onChanged?.();
|
|
2688
|
-
return JSON.stringify(result, null, 2) + "\n\nNote: Restart opencode for the @ menu to reflect changes.";
|
|
2689
|
-
} catch (err) {
|
|
2690
|
-
return `Error: ${err.message}`;
|
|
2691
|
-
}
|
|
2692
|
-
}
|
|
2693
|
-
})
|
|
2694
|
-
};
|
|
2695
|
-
}
|
|
2696
|
-
function
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
}
|
|
2703
|
-
return auth;
|
|
2704
|
-
};
|
|
2705
|
-
return {
|
|
2706
|
-
...makeItemTools(z2, "AGENT", "agent", getAuth, ensureAuth, onChanged),
|
|
2707
|
-
...makeItemTools(z2, "FLOW", "flow", getAuth, ensureAuth, onChanged)
|
|
2708
|
-
};
|
|
2709
|
-
}
|
|
2710
|
-
function readAuth() {
|
|
2711
|
-
try {
|
|
2712
|
-
const authPath = (0, import_path.join)(import_os.default.homedir(), ".local", "share", "opencode", "auth.json");
|
|
2713
|
-
const data = JSON.parse((0, import_fs.readFileSync)(authPath, "utf-8"));
|
|
2714
|
-
const gitlab = data?.gitlab;
|
|
2715
|
-
if (!gitlab) return null;
|
|
2716
|
-
const token = gitlab.type === "oauth" ? gitlab.access : gitlab.type === "api" ? gitlab.key : null;
|
|
2717
|
-
if (!token) return null;
|
|
2718
|
-
const instanceUrl = gitlab.enterpriseUrl ?? process.env.GITLAB_INSTANCE_URL ?? "https://gitlab.com";
|
|
2719
|
-
return { token, instanceUrl };
|
|
2720
|
-
} catch {
|
|
2721
|
-
return null;
|
|
2722
|
-
}
|
|
2723
|
-
}
|
|
2724
|
-
function buildFlowSubagentPrompt(flow, projectPath, projectUrl) {
|
|
2725
|
-
return [
|
|
2726
|
-
`You execute the "${flow.name}" GitLab flow. Project: ${projectPath} (${projectUrl}).`,
|
|
2727
|
-
``,
|
|
2728
|
-
`STEP 1: Call gitlab_get_flow_definition with consumer_id=${flow.consumerId}, foundational=${!!flow.foundational}.`,
|
|
2729
|
-
`Parse the YAML config:`,
|
|
2730
|
-
`- In "components", find { from: "context:goal", as: "<name>" }. The "as" value is what the goal parameter must contain:`,
|
|
2731
|
-
` "merge_request_iid" -> just the number (e.g. 14)`,
|
|
2732
|
-
` "pipeline_url"/"url"/"issue_url" -> full URL (e.g. ${projectUrl}/-/merge_requests/5)`,
|
|
2733
|
-
` "vulnerability_id" -> just the ID number`,
|
|
2734
|
-
` "goal" -> free-form text`,
|
|
2735
|
-
`- In "flow.inputs", find additional_context categories (skip "agent_platform_standard_context").`,
|
|
2736
|
-
``,
|
|
2737
|
-
`STEP 2: Resolve the goal to the EXACT value the flow expects (from step 1).`,
|
|
2738
|
-
`The goal parameter has a 10000 char limit and is used directly as the flow parameter \u2014 pass ONLY the raw value, never a sentence.`,
|
|
2739
|
-
`Use GitLab API tools if needed to look up IDs or construct URLs from the user's message.`,
|
|
2740
|
-
``,
|
|
2741
|
-
`STEP 3: Gather additional_context values (if any from step 1) using available tools.`,
|
|
2742
|
-
``,
|
|
2743
|
-
`STEP 4: Call gitlab_execute_project_flow with:`,
|
|
2744
|
-
` project_id: "${projectPath}"`,
|
|
2745
|
-
` consumer_id: ${flow.consumerId}`,
|
|
2746
|
-
` goal: <resolved value from step 2>`,
|
|
2747
|
-
` additional_context (if needed): [{"Category":"<cat>","Content":"{\\"field\\":\\"val\\"}"}]`,
|
|
2748
|
-
``,
|
|
2749
|
-
`STEP 5: Call gitlab_get_workflow_status with the workflow_id. Report status and URL: ${projectUrl}/-/automate/agent-sessions/<id>`
|
|
2750
|
-
].join("\n");
|
|
2751
|
-
}
|
|
2752
|
-
var memo = /* @__PURE__ */ new Map();
|
|
2753
|
-
var FLOW_DISPATCH_GUIDELINES = [
|
|
2754
|
-
`## GitLab Flow Dispatch Guidelines`,
|
|
2755
|
-
``,
|
|
2756
|
-
`CRITICAL: You must NEVER call gitlab_execute_project_flow or gitlab_get_flow_definition directly.`,
|
|
2757
|
-
`Flows are ALWAYS executed via the Task tool with subagent_type "general".`,
|
|
2758
|
-
`When the user's message contains flow dispatch instructions (starting with "IMPORTANT: You MUST"),`,
|
|
2759
|
-
`follow those instructions exactly \u2014 call the Task tool with the provided parameters.`,
|
|
2760
|
-
``,
|
|
2761
|
-
`### Multiple Flows or Resources`,
|
|
2762
|
-
`When multiple flows need to run (multiple @mentions, or batch across resources), dispatch them`,
|
|
2763
|
-
`via a SINGLE "general" subagent. The general subagent can execute multiple tool calls in parallel,`,
|
|
2764
|
-
`so all flows fire simultaneously. Do NOT dispatch multiple Task calls \u2014 use ONE Task with a prompt`,
|
|
2765
|
-
`that lists all the flows to execute, so the subagent runs them concurrently.`,
|
|
2766
|
-
``,
|
|
2767
|
-
`### Batch Operations (Multiple Resources)`,
|
|
2768
|
-
`If the user asks to run flows on multiple resources (e.g., "for each MR"), first list the`,
|
|
2769
|
-
`resources yourself using GitLab API tools, then dispatch ONE general subagent whose prompt`,
|
|
2770
|
-
`includes all flow executions (N flows x M resources) to run in parallel.`
|
|
2771
|
-
].join("\n");
|
|
2772
|
-
var AGENT_CREATION_GUIDELINES = `## Creating Custom GitLab Agents
|
|
2773
|
-
|
|
2774
|
-
Before calling gitlab_create_agent, you MUST:
|
|
2775
|
-
1. Call gitlab_list_builtin_tools and gitlab_list_project_mcp_servers.
|
|
2776
|
-
2. Ask the user 4 questions using the question tool (one call, all 4 questions):
|
|
2777
|
-
- Agent name (suggest one, allow custom)
|
|
2778
|
-
- Visibility: Public or Private
|
|
2779
|
-
- Tools: show tools grouped by category as multi-select (Search, Issues, MRs, Epics, Files, Git, CI/CD, Security, Audit, Planning, Wiki, API)
|
|
2780
|
-
- MCP servers: multi-select from available servers
|
|
2781
|
-
3. Show the generated system prompt and ask for confirmation.
|
|
2782
|
-
4. Only then call gitlab_create_agent. Use full tool GIDs like "gid://gitlab/Ai::Catalog::BuiltInTool/1".
|
|
2783
|
-
5. Ask if the user wants to enable it on the current project.`;
|
|
2784
|
-
var FLOW_SCHEMA_REFERENCE = `## Flow YAML Schema Reference
|
|
2785
|
-
|
|
2786
|
-
### Top-level structure (all required unless noted):
|
|
2787
|
-
version: "v1" # Always "v1"
|
|
2788
|
-
environment: ambient # Always "ambient"
|
|
2789
|
-
components: [...] # Array of components (min 1)
|
|
2790
|
-
routers: [...] # Array of routers connecting components
|
|
2791
|
-
flow:
|
|
2792
|
-
entry_point: "component_name" # First component to run
|
|
2793
|
-
inputs: [...] # Optional: additional context inputs
|
|
2794
|
-
prompts: [...] # Optional: inline prompt definitions
|
|
2795
|
-
|
|
2796
|
-
### Component types:
|
|
2797
|
-
|
|
2798
|
-
1. DeterministicStepComponent \u2014 runs ONE tool, no LLM call:
|
|
2799
|
-
- name: "fetch_data" # alphanumeric + underscore only
|
|
2800
|
-
type: DeterministicStepComponent
|
|
2801
|
-
tool_name: "get_merge_request"
|
|
2802
|
-
inputs: # map tool parameters
|
|
2803
|
-
- { from: "context:goal", as: "merge_request_iid" }
|
|
2804
|
-
- { from: "context:project_id", as: "project_id" }
|
|
2805
|
-
|
|
2806
|
-
2. OneOffComponent \u2014 single LLM call + tool execution:
|
|
2807
|
-
- name: "process_data"
|
|
2808
|
-
type: OneOffComponent
|
|
2809
|
-
prompt_id: "my_prompt" # references inline prompt
|
|
2810
|
-
prompt_version: null # null = use inline prompt from prompts section
|
|
2811
|
-
toolset: ["read_file", "edit_file"]
|
|
2812
|
-
inputs:
|
|
2813
|
-
- { from: "context:fetch_data.tool_responses", as: "data" }
|
|
2814
|
-
max_correction_attempts: 3 # retries on tool failure (default 3)
|
|
2815
|
-
|
|
2816
|
-
3. AgentComponent \u2014 multi-turn LLM reasoning loop:
|
|
2817
|
-
- name: "analyze"
|
|
2818
|
-
type: AgentComponent
|
|
2819
|
-
prompt_id: "my_agent_prompt"
|
|
2820
|
-
prompt_version: null
|
|
2821
|
-
toolset: ["read_file", "grep"]
|
|
2822
|
-
inputs:
|
|
2823
|
-
- { from: "context:goal", as: "user_goal" }
|
|
2824
|
-
ui_log_events: ["on_agent_final_answer"]
|
|
2825
|
-
|
|
2826
|
-
### IOKey syntax (inputs/from values):
|
|
2827
|
-
"context:goal" # The flow goal (user input)
|
|
2828
|
-
"context:project_id" # Project ID (auto-injected)
|
|
2829
|
-
"context:<component_name>.tool_responses" # Tool output from a component
|
|
2830
|
-
"context:<component_name>.final_answer" # Agent's final text answer
|
|
2831
|
-
"context:<component_name>.execution_result" # OneOff execution result
|
|
2832
|
-
{ from: "context:x", as: "var_name" } # Rename for prompt template
|
|
2833
|
-
{ from: "true", as: "flag", literal: true } # Literal value
|
|
2834
|
-
|
|
2835
|
-
### Router patterns:
|
|
2836
|
-
Direct: { from: "step1", to: "step2" }
|
|
2837
|
-
To end: { from: "last_step", to: "end" }
|
|
2838
|
-
Conditional: { from: "step1", condition: { input: "context:step1.final_answer.decision", routes: { "yes": "step2", "no": "end" } } }
|
|
3451
|
+
return JSON.stringify(result, null, 2) + "\n\nNote: Restart opencode for the @ menu to reflect changes.";
|
|
3452
|
+
} catch (err) {
|
|
3453
|
+
return `Error: ${err.message}`;
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
}),
|
|
3457
|
+
[`gitlab_disable_project_${label}`]: (0, import_plugin3.tool)({
|
|
3458
|
+
description: `Disable a ${label} in a project.
|
|
3459
|
+
Requires Maintainer or Owner role.
|
|
3460
|
+
Resolves the consumer ID internally from the ${label} ID.`,
|
|
3461
|
+
args: {
|
|
3462
|
+
project_id: z3.string().describe('Project path (e.g., "gitlab-org/gitlab")'),
|
|
3463
|
+
[`${label}_id`]: z3.string().describe(`${Label} ID: full GID or numeric ID`)
|
|
3464
|
+
},
|
|
3465
|
+
execute: async (args) => {
|
|
3466
|
+
const auth = ensureAuth();
|
|
3467
|
+
if (!auth) return "Error: GitLab authentication not available";
|
|
3468
|
+
try {
|
|
3469
|
+
const result = await disableAiCatalogItemForProject(
|
|
3470
|
+
auth.instanceUrl,
|
|
3471
|
+
auth.token,
|
|
3472
|
+
args.project_id,
|
|
3473
|
+
args[`${label}_id`]
|
|
3474
|
+
);
|
|
3475
|
+
await onChanged?.();
|
|
3476
|
+
return JSON.stringify(result, null, 2) + "\n\nNote: Restart opencode for the @ menu to reflect changes.";
|
|
3477
|
+
} catch (err) {
|
|
3478
|
+
return `Error: ${err.message}`;
|
|
3479
|
+
}
|
|
3480
|
+
}
|
|
3481
|
+
})
|
|
3482
|
+
};
|
|
3483
|
+
}
|
|
3484
|
+
function makeCatalogItemTools(ctx) {
|
|
3485
|
+
return {
|
|
3486
|
+
...makeItemTools("AGENT", "agent", ctx.getAuth, ctx.ensureAuth, ctx.refreshAgents),
|
|
3487
|
+
...makeItemTools("FLOW", "flow", ctx.getAuth, ctx.ensureAuth, ctx.refreshAgents)
|
|
3488
|
+
};
|
|
3489
|
+
}
|
|
2839
3490
|
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
3491
|
+
// src/tools/mcp-tools.ts
|
|
3492
|
+
var import_plugin4 = require("@opencode-ai/plugin");
|
|
3493
|
+
var z4 = import_plugin4.tool.schema;
|
|
3494
|
+
function makeMcpTools(getCachedAgents) {
|
|
3495
|
+
return {
|
|
3496
|
+
gitlab_list_project_mcp_servers: (0, import_plugin4.tool)({
|
|
3497
|
+
description: "List MCP servers available through agents enabled for a project.\nReturns deduplicated servers with name, URL, auth type, connection status, and which agents use them.",
|
|
3498
|
+
args: {
|
|
3499
|
+
project_id: z4.string().describe('Project path (e.g., "gitlab-org/gitlab")')
|
|
3500
|
+
},
|
|
3501
|
+
execute: async (_args) => {
|
|
3502
|
+
const serverMap = /* @__PURE__ */ new Map();
|
|
3503
|
+
for (const agent of getCachedAgents()) {
|
|
3504
|
+
if (!agent.mcpServers?.length) continue;
|
|
3505
|
+
for (const server of agent.mcpServers) {
|
|
3506
|
+
const existing = serverMap.get(server.id);
|
|
3507
|
+
if (existing) {
|
|
3508
|
+
if (!existing.usedBy.includes(agent.name)) existing.usedBy.push(agent.name);
|
|
3509
|
+
} else {
|
|
3510
|
+
serverMap.set(server.id, { ...server, usedBy: [agent.name] });
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
const servers = [...serverMap.values()];
|
|
3515
|
+
if (!servers.length) {
|
|
3516
|
+
return "No MCP servers found for agents enabled in this project.";
|
|
3517
|
+
}
|
|
3518
|
+
return JSON.stringify(servers, null, 2);
|
|
3519
|
+
}
|
|
3520
|
+
})
|
|
3521
|
+
};
|
|
3522
|
+
}
|
|
2848
3523
|
|
|
2849
|
-
|
|
2850
|
-
var
|
|
2851
|
-
\`\`\`yaml
|
|
2852
|
-
version: "v1"
|
|
2853
|
-
environment: ambient
|
|
2854
|
-
components:
|
|
2855
|
-
- name: fetch_mr
|
|
2856
|
-
type: DeterministicStepComponent
|
|
2857
|
-
tool_name: build_review_merge_request_context
|
|
2858
|
-
inputs:
|
|
2859
|
-
- { from: "context:project_id", as: "project_id" }
|
|
2860
|
-
- { from: "context:goal", as: "merge_request_iid" }
|
|
2861
|
-
ui_log_events: ["on_tool_execution_success", "on_tool_execution_failed"]
|
|
2862
|
-
- name: analyze_and_comment
|
|
2863
|
-
type: OneOffComponent
|
|
2864
|
-
prompt_id: review_prompt
|
|
2865
|
-
prompt_version: null
|
|
2866
|
-
toolset: ["create_merge_request_note"]
|
|
2867
|
-
inputs:
|
|
2868
|
-
- { from: "context:fetch_mr.tool_responses", as: "mr_data" }
|
|
2869
|
-
- { from: "context:project_id", as: "project_id" }
|
|
2870
|
-
- { from: "context:goal", as: "merge_request_iid" }
|
|
2871
|
-
max_correction_attempts: 3
|
|
2872
|
-
ui_log_events: ["on_tool_execution_success"]
|
|
2873
|
-
routers:
|
|
2874
|
-
- { from: fetch_mr, to: analyze_and_comment }
|
|
2875
|
-
- { from: analyze_and_comment, to: end }
|
|
2876
|
-
flow:
|
|
2877
|
-
entry_point: fetch_mr
|
|
2878
|
-
prompts:
|
|
2879
|
-
- prompt_id: review_prompt
|
|
2880
|
-
name: MR Review Prompt
|
|
2881
|
-
unit_primitives: ["duo_agent_platform"]
|
|
2882
|
-
prompt_template:
|
|
2883
|
-
system: |
|
|
2884
|
-
You review merge requests. Analyze the MR data and post a concise review comment.
|
|
2885
|
-
Focus on code quality, potential bugs, and improvements.
|
|
2886
|
-
user: "Review MR !{{merge_request_iid}} in project {{project_id}}: {{mr_data}}"
|
|
2887
|
-
\`\`\``;
|
|
2888
|
-
var FLOW_EXAMPLE_CONDITIONAL = `## Example: Conditional flow (gather data \u2192 decide \u2192 branch)
|
|
2889
|
-
\`\`\`yaml
|
|
2890
|
-
version: "v1"
|
|
2891
|
-
environment: ambient
|
|
2892
|
-
components:
|
|
2893
|
-
- name: gather_context
|
|
2894
|
-
type: DeterministicStepComponent
|
|
2895
|
-
tool_name: get_vulnerability_details
|
|
2896
|
-
inputs:
|
|
2897
|
-
- { from: "context:goal", as: "vulnerability_id" }
|
|
2898
|
-
ui_log_events: ["on_tool_execution_success"]
|
|
2899
|
-
- name: evaluate
|
|
2900
|
-
type: AgentComponent
|
|
2901
|
-
prompt_id: eval_prompt
|
|
2902
|
-
prompt_version: null
|
|
2903
|
-
toolset: []
|
|
2904
|
-
inputs:
|
|
2905
|
-
- { from: "context:gather_context.tool_responses", as: "vuln_data" }
|
|
2906
|
-
ui_log_events: ["on_agent_final_answer"]
|
|
2907
|
-
- name: create_fix
|
|
2908
|
-
type: AgentComponent
|
|
2909
|
-
prompt_id: fix_prompt
|
|
2910
|
-
prompt_version: null
|
|
2911
|
-
toolset: ["read_file", "edit_file", "grep"]
|
|
2912
|
-
inputs:
|
|
2913
|
-
- { from: "context:gather_context.tool_responses", as: "vuln_data" }
|
|
2914
|
-
ui_log_events: ["on_agent_final_answer", "on_tool_execution_success"]
|
|
2915
|
-
routers:
|
|
2916
|
-
- { from: gather_context, to: evaluate }
|
|
2917
|
-
- from: evaluate
|
|
2918
|
-
condition:
|
|
2919
|
-
input: "context:evaluate.final_answer"
|
|
2920
|
-
routes:
|
|
2921
|
-
"fix_needed": create_fix
|
|
2922
|
-
"false_positive": end
|
|
2923
|
-
default_route: end
|
|
2924
|
-
- { from: create_fix, to: end }
|
|
2925
|
-
flow:
|
|
2926
|
-
entry_point: gather_context
|
|
2927
|
-
prompts:
|
|
2928
|
-
- prompt_id: eval_prompt
|
|
2929
|
-
name: Vulnerability Evaluator
|
|
2930
|
-
unit_primitives: ["duo_agent_platform"]
|
|
2931
|
-
prompt_template:
|
|
2932
|
-
system: |
|
|
2933
|
-
Evaluate if a vulnerability needs fixing. Respond with exactly "fix_needed" or "false_positive".
|
|
2934
|
-
user: "Evaluate: {{vuln_data}}"
|
|
2935
|
-
- prompt_id: fix_prompt
|
|
2936
|
-
name: Fix Generator
|
|
2937
|
-
unit_primitives: ["duo_agent_platform"]
|
|
2938
|
-
prompt_template:
|
|
2939
|
-
system: |
|
|
2940
|
-
You fix security vulnerabilities. Read the relevant code and apply the fix.
|
|
2941
|
-
user: "Fix this vulnerability: {{vuln_data}}"
|
|
2942
|
-
placeholder: history
|
|
2943
|
-
\`\`\``;
|
|
3524
|
+
// src/index.ts
|
|
3525
|
+
var memo = /* @__PURE__ */ new Map();
|
|
2944
3526
|
var plugin = async (input) => {
|
|
2945
3527
|
let authCache = null;
|
|
2946
3528
|
let projectPath;
|
|
@@ -2950,6 +3532,13 @@ var plugin = async (input) => {
|
|
|
2950
3532
|
let cachedAgents = [];
|
|
2951
3533
|
let cfgRef = null;
|
|
2952
3534
|
let baseModelIdRef;
|
|
3535
|
+
function ensureAuth() {
|
|
3536
|
+
if (!authCache) {
|
|
3537
|
+
const auth = readAuth();
|
|
3538
|
+
if (auth) authCache = auth;
|
|
3539
|
+
}
|
|
3540
|
+
return authCache;
|
|
3541
|
+
}
|
|
2953
3542
|
async function load() {
|
|
2954
3543
|
const auth = readAuth();
|
|
2955
3544
|
if (!auth) return null;
|
|
@@ -3023,6 +3612,14 @@ var plugin = async (input) => {
|
|
|
3023
3612
|
}
|
|
3024
3613
|
}
|
|
3025
3614
|
}
|
|
3615
|
+
const ctx = {
|
|
3616
|
+
getAuth: () => authCache,
|
|
3617
|
+
ensureAuth,
|
|
3618
|
+
getFlowAgents: () => flowAgents,
|
|
3619
|
+
getCachedAgents: () => cachedAgents,
|
|
3620
|
+
getNamespaceId: () => namespaceId,
|
|
3621
|
+
refreshAgents
|
|
3622
|
+
};
|
|
3026
3623
|
return {
|
|
3027
3624
|
async config(cfg) {
|
|
3028
3625
|
const result = await load();
|
|
@@ -3066,491 +3663,18 @@ var plugin = async (input) => {
|
|
|
3066
3663
|
}
|
|
3067
3664
|
}
|
|
3068
3665
|
},
|
|
3069
|
-
"chat.message":
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
if (!flow || !flow.consumerId || !projectPath) continue;
|
|
3077
|
-
flowMentions.push({ idx: i, flow, displayName: part.name });
|
|
3078
|
-
if (i + 1 < output.parts.length) {
|
|
3079
|
-
const next = output.parts[i + 1];
|
|
3080
|
-
if (next.type === "text" && next.synthetic && next.text?.includes("call the task tool with subagent")) {
|
|
3081
|
-
indicesToRemove.push(i + 1);
|
|
3082
|
-
}
|
|
3083
|
-
}
|
|
3084
|
-
}
|
|
3085
|
-
if (flowMentions.length === 0) {
|
|
3086
|
-
return;
|
|
3087
|
-
}
|
|
3088
|
-
if (flowMentions.length === 1) {
|
|
3089
|
-
const { idx, flow } = flowMentions[0];
|
|
3090
|
-
const baseUrl2 = authCache?.instanceUrl?.replace(/\/$/, "") ?? "https://gitlab.com";
|
|
3091
|
-
const projectUrl2 = `${baseUrl2}/${projectPath}`;
|
|
3092
|
-
const rawText2 = output.parts.filter((p) => p.type === "text" && !p.synthetic).map((p) => p.text).join(" ").trim() || "Execute the flow";
|
|
3093
|
-
const subagentPrompt = buildFlowSubagentPrompt(flow, projectPath, projectUrl2) + `
|
|
3094
|
-
|
|
3095
|
-
User goal: "${rawText2}"`;
|
|
3096
|
-
const resultText = [
|
|
3097
|
-
`IMPORTANT: You MUST call the Task tool RIGHT NOW to dispatch a subagent. Do NOT execute these steps yourself.`,
|
|
3098
|
-
``,
|
|
3099
|
-
`Call the Task tool with:`,
|
|
3100
|
-
` subagent_type: "general"`,
|
|
3101
|
-
` description: "Execute ${flow.name} flow"`,
|
|
3102
|
-
` prompt: ${JSON.stringify(subagentPrompt)}`,
|
|
3103
|
-
``,
|
|
3104
|
-
`Do not do anything else. Just call the Task tool with the above parameters.`
|
|
3105
|
-
].join("\n");
|
|
3106
|
-
const original2 = output.parts[idx];
|
|
3107
|
-
output.parts[idx] = { ...original2, type: "text", text: resultText };
|
|
3108
|
-
delete output.parts[idx].name;
|
|
3109
|
-
delete output.parts[idx].source;
|
|
3110
|
-
for (const rmIdx of indicesToRemove.reverse()) {
|
|
3111
|
-
output.parts.splice(rmIdx, 1);
|
|
3112
|
-
}
|
|
3113
|
-
return;
|
|
3114
|
-
}
|
|
3115
|
-
const baseUrl = authCache?.instanceUrl?.replace(/\/$/, "") ?? "https://gitlab.com";
|
|
3116
|
-
const projectUrl = `${baseUrl}/${projectPath}`;
|
|
3117
|
-
const flowNames = new Set(flowMentions.map((m) => m.displayName));
|
|
3118
|
-
let rawText = output.parts.filter((p) => p.type === "text" && !p.synthetic).map((p) => p.text).join(" ").trim() || "Execute the flows";
|
|
3119
|
-
for (const name of flowNames) {
|
|
3120
|
-
rawText = rawText.replace(new RegExp(`@${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "g"), "").trim();
|
|
3121
|
-
}
|
|
3122
|
-
rawText = rawText.replace(/\s{2,}/g, " ").trim() || "Execute the flows";
|
|
3123
|
-
const flowList = flowMentions.map(
|
|
3124
|
-
({ flow, displayName }, i) => `${i + 1}. "${displayName}" \u2014 consumer_id=${flow.consumerId}, foundational=${!!flow.foundational}`
|
|
3125
|
-
);
|
|
3126
|
-
const batchPrompt = [
|
|
3127
|
-
`Execute ${flowMentions.length} GitLab flows on project ${projectPath} (${projectUrl}).`,
|
|
3128
|
-
`User goal: "${rawText}"`,
|
|
3129
|
-
``,
|
|
3130
|
-
`Flows to execute:`,
|
|
3131
|
-
...flowList,
|
|
3132
|
-
``,
|
|
3133
|
-
`EXECUTION PLAN:`,
|
|
3134
|
-
`1. Call gitlab_get_flow_definition for ALL flows listed above in a SINGLE response (${flowMentions.length} tool calls at once).`,
|
|
3135
|
-
` Parse each YAML to find what "context:goal" maps to (the "as" field in components).`,
|
|
3136
|
-
``,
|
|
3137
|
-
`2. If the user's goal involves multiple resources (e.g., "for each MR"), list them using GitLab API tools.`,
|
|
3138
|
-
``,
|
|
3139
|
-
`3. Call gitlab_execute_project_flow for EVERY flow+resource combination in a SINGLE response.`,
|
|
3140
|
-
` For each call, set the goal to the EXACT value the flow expects (e.g., just "14" for merge_request_iid).`,
|
|
3141
|
-
` project_id: "${projectPath}"`,
|
|
3142
|
-
` You MUST emit ALL execute calls in ONE response \u2014 do NOT wait for one to finish before calling the next.`,
|
|
3143
|
-
``,
|
|
3144
|
-
`4. Collect all workflow_ids from step 3. Call gitlab_get_workflow_status for ALL of them in a SINGLE response.`,
|
|
3145
|
-
``,
|
|
3146
|
-
`5. Present a summary table: flow name, resource, status, URL (${projectUrl}/-/automate/agent-sessions/<id>).`,
|
|
3147
|
-
``,
|
|
3148
|
-
`CRITICAL: In steps 1, 3, and 4 you MUST make multiple tool calls in the SAME response for parallel execution.`
|
|
3149
|
-
].join("\n");
|
|
3150
|
-
const combinedText = [
|
|
3151
|
-
`IMPORTANT: You MUST call the Task tool RIGHT NOW with subagent_type "general" to dispatch all flows in parallel.`,
|
|
3152
|
-
`Do NOT call flow tools yourself. Do NOT dispatch multiple Task calls \u2014 use ONE.`,
|
|
3153
|
-
``,
|
|
3154
|
-
`Call the Task tool with:`,
|
|
3155
|
-
` subagent_type: "general"`,
|
|
3156
|
-
` description: "Execute ${flowMentions.length} flows in parallel"`,
|
|
3157
|
-
` prompt: ${JSON.stringify(batchPrompt)}`,
|
|
3158
|
-
``,
|
|
3159
|
-
`Do not do anything else. Just call the Task tool with the above parameters.`
|
|
3160
|
-
].join("\n");
|
|
3161
|
-
const firstIdx = flowMentions[0].idx;
|
|
3162
|
-
const original = output.parts[firstIdx];
|
|
3163
|
-
output.parts[firstIdx] = { ...original, type: "text", text: combinedText };
|
|
3164
|
-
delete output.parts[firstIdx].name;
|
|
3165
|
-
delete output.parts[firstIdx].source;
|
|
3166
|
-
for (let i = flowMentions.length - 1; i >= 1; i--) {
|
|
3167
|
-
indicesToRemove.push(flowMentions[i].idx);
|
|
3168
|
-
}
|
|
3169
|
-
for (const idx of [...new Set(indicesToRemove)].sort((a, b) => b - a)) {
|
|
3170
|
-
output.parts.splice(idx, 1);
|
|
3171
|
-
}
|
|
3172
|
-
},
|
|
3173
|
-
"chat.params": async (input2, _output) => {
|
|
3174
|
-
if (!gitlabAgentNames.has(input2.agent)) return;
|
|
3175
|
-
const model = input2.model;
|
|
3176
|
-
const modelId = model?.modelID ?? model?.id ?? "";
|
|
3177
|
-
const isDWS = modelId.includes("duo-workflow");
|
|
3178
|
-
if (!isDWS) {
|
|
3179
|
-
const name = model?.name ?? modelId ?? "unknown";
|
|
3180
|
-
throw new Error(
|
|
3181
|
-
`GitLab agent "${input2.agent}" requires an Agent Platform model but the current model is "${name}". Please switch to an Agent Platform model (duo-workflow-*) in the model picker to use GitLab agents.`
|
|
3182
|
-
);
|
|
3183
|
-
}
|
|
3184
|
-
},
|
|
3185
|
-
"experimental.chat.system.transform": async (_input, output) => {
|
|
3186
|
-
if (flowAgents.size) {
|
|
3187
|
-
output.system.push(FLOW_DISPATCH_GUIDELINES);
|
|
3188
|
-
}
|
|
3189
|
-
if (authCache) {
|
|
3190
|
-
output.system.push(AGENT_CREATION_GUIDELINES);
|
|
3191
|
-
}
|
|
3192
|
-
},
|
|
3666
|
+
"chat.message": makeChatMessageHook(
|
|
3667
|
+
() => authCache,
|
|
3668
|
+
flowAgents,
|
|
3669
|
+
() => projectPath
|
|
3670
|
+
),
|
|
3671
|
+
"chat.params": makeChatParamsHook(gitlabAgentNames),
|
|
3672
|
+
"experimental.chat.system.transform": makeSystemTransformHook(flowAgents, () => authCache),
|
|
3193
3673
|
tool: {
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
consumer_id: z.number().describe("AI Catalog ItemConsumer numeric ID"),
|
|
3199
|
-
goal: z.string().describe("User prompt/goal for the flow, include relevant URLs"),
|
|
3200
|
-
additional_context: z.string().optional().describe(
|
|
3201
|
-
'JSON array of flow inputs: [{"Category":"merge_request","Content":"{\\"url\\":\\"https://...\\"}"}]'
|
|
3202
|
-
),
|
|
3203
|
-
issue_id: z.number().optional().describe("Issue IID for context"),
|
|
3204
|
-
merge_request_id: z.number().optional().describe("Merge request IID for context")
|
|
3205
|
-
},
|
|
3206
|
-
execute: async (args) => {
|
|
3207
|
-
if (!authCache) {
|
|
3208
|
-
const auth = readAuth();
|
|
3209
|
-
if (!auth) return "Error: GitLab authentication not available";
|
|
3210
|
-
authCache = auth;
|
|
3211
|
-
}
|
|
3212
|
-
let flowInputs;
|
|
3213
|
-
if (args.additional_context) {
|
|
3214
|
-
try {
|
|
3215
|
-
flowInputs = JSON.parse(args.additional_context);
|
|
3216
|
-
} catch {
|
|
3217
|
-
return "Error: additional_context must be a valid JSON array";
|
|
3218
|
-
}
|
|
3219
|
-
}
|
|
3220
|
-
try {
|
|
3221
|
-
const result = await executeFlow(
|
|
3222
|
-
authCache.instanceUrl,
|
|
3223
|
-
authCache.token,
|
|
3224
|
-
args.project_id,
|
|
3225
|
-
args.consumer_id,
|
|
3226
|
-
args.goal,
|
|
3227
|
-
{
|
|
3228
|
-
issueId: args.issue_id,
|
|
3229
|
-
mergeRequestId: args.merge_request_id,
|
|
3230
|
-
namespaceId,
|
|
3231
|
-
flowInputs
|
|
3232
|
-
}
|
|
3233
|
-
);
|
|
3234
|
-
return JSON.stringify(result, null, 2);
|
|
3235
|
-
} catch (err) {
|
|
3236
|
-
return `Error executing flow: ${err.message}`;
|
|
3237
|
-
}
|
|
3238
|
-
}
|
|
3239
|
-
}),
|
|
3240
|
-
gitlab_get_flow_definition: (0, import_plugin.tool)({
|
|
3241
|
-
description: "Get the YAML configuration of a GitLab DAP flow.\nReturns the flow config YAML which contains the flow.inputs section\ndescribing what additional_context categories and fields the flow requires.\nUse this before executing a flow to understand what inputs to gather.\nSet foundational=true for GitLab built-in flows, foundational=false for custom flows.",
|
|
3242
|
-
args: {
|
|
3243
|
-
consumer_id: z.number().describe("AI Catalog ItemConsumer numeric ID"),
|
|
3244
|
-
foundational: z.boolean().describe("true for GitLab foundational flows, false for custom flows")
|
|
3245
|
-
},
|
|
3246
|
-
execute: async (args) => {
|
|
3247
|
-
if (!authCache) {
|
|
3248
|
-
const auth = readAuth();
|
|
3249
|
-
if (!auth) return "Error: GitLab authentication not available";
|
|
3250
|
-
authCache = auth;
|
|
3251
|
-
}
|
|
3252
|
-
const flow = [...flowAgents.values()].find((f) => f.consumerId === args.consumer_id);
|
|
3253
|
-
try {
|
|
3254
|
-
const result = await getFlowDefinition(authCache.instanceUrl, authCache.token, {
|
|
3255
|
-
consumerId: args.consumer_id,
|
|
3256
|
-
flowName: flow?.name,
|
|
3257
|
-
foundational: args.foundational,
|
|
3258
|
-
catalogItemVersionId: flow?.catalogItemVersionId,
|
|
3259
|
-
itemIdentifier: flow?.identifier,
|
|
3260
|
-
workflowDefinition: flow?.workflowDefinition
|
|
3261
|
-
});
|
|
3262
|
-
return JSON.stringify(result, null, 2);
|
|
3263
|
-
} catch (err) {
|
|
3264
|
-
return `Error getting flow definition: ${err.message}`;
|
|
3265
|
-
}
|
|
3266
|
-
}
|
|
3267
|
-
}),
|
|
3268
|
-
gitlab_get_workflow_status: (0, import_plugin.tool)({
|
|
3269
|
-
description: "Get the status and latest messages of a GitLab DAP workflow.\nUse this to monitor a running flow after executing it.\nReturns the workflow status, latest checkpoint messages, and timestamps.\nPoll every 10 seconds until status is completed, failed, or cancelled.",
|
|
3270
|
-
args: {
|
|
3271
|
-
workflow_id: z.number().describe("Workflow numeric ID (from gitlab_execute_project_flow result)")
|
|
3272
|
-
},
|
|
3273
|
-
execute: async (args) => {
|
|
3274
|
-
if (!authCache) {
|
|
3275
|
-
const auth = readAuth();
|
|
3276
|
-
if (!auth) return "Error: GitLab authentication not available";
|
|
3277
|
-
authCache = auth;
|
|
3278
|
-
}
|
|
3279
|
-
try {
|
|
3280
|
-
const result = await getWorkflowStatus(
|
|
3281
|
-
authCache.instanceUrl,
|
|
3282
|
-
authCache.token,
|
|
3283
|
-
args.workflow_id
|
|
3284
|
-
);
|
|
3285
|
-
return JSON.stringify(result, null, 2);
|
|
3286
|
-
} catch (err) {
|
|
3287
|
-
return `Error getting workflow status: ${err.message}`;
|
|
3288
|
-
}
|
|
3289
|
-
}
|
|
3290
|
-
}),
|
|
3291
|
-
gitlab_list_project_mcp_servers: (0, import_plugin.tool)({
|
|
3292
|
-
description: "List MCP servers available through agents enabled for a project.\nReturns deduplicated servers with name, URL, auth type, connection status, and which agents use them.",
|
|
3293
|
-
args: {
|
|
3294
|
-
project_id: z.string().describe('Project path (e.g., "gitlab-org/gitlab")')
|
|
3295
|
-
},
|
|
3296
|
-
execute: async (_args) => {
|
|
3297
|
-
const serverMap = /* @__PURE__ */ new Map();
|
|
3298
|
-
for (const agent of cachedAgents) {
|
|
3299
|
-
if (!agent.mcpServers?.length) continue;
|
|
3300
|
-
for (const server of agent.mcpServers) {
|
|
3301
|
-
const existing = serverMap.get(server.id);
|
|
3302
|
-
if (existing) {
|
|
3303
|
-
if (!existing.usedBy.includes(agent.name)) existing.usedBy.push(agent.name);
|
|
3304
|
-
} else {
|
|
3305
|
-
serverMap.set(server.id, { ...server, usedBy: [agent.name] });
|
|
3306
|
-
}
|
|
3307
|
-
}
|
|
3308
|
-
}
|
|
3309
|
-
const servers = [...serverMap.values()];
|
|
3310
|
-
if (!servers.length) {
|
|
3311
|
-
return "No MCP servers found for agents enabled in this project.";
|
|
3312
|
-
}
|
|
3313
|
-
return JSON.stringify(servers, null, 2);
|
|
3314
|
-
}
|
|
3315
|
-
}),
|
|
3316
|
-
gitlab_create_agent: (0, import_plugin.tool)({
|
|
3317
|
-
description: "Create a new custom agent in the GitLab AI Catalog.\nFirst call: set confirmed=false (or omit). The tool returns without creating anything and instructs you to ask the user for agent properties using the question tool.\nSecond call: after the user confirms, set confirmed=true to actually create the agent.\nAfter creation, use gitlab_enable_project_agent to enable it on a project.",
|
|
3318
|
-
args: {
|
|
3319
|
-
project_id: z.string().describe('Project path (e.g., "gitlab-org/gitlab")'),
|
|
3320
|
-
name: z.string().describe("Display name for the agent"),
|
|
3321
|
-
description: z.string().describe("Description of what the agent does"),
|
|
3322
|
-
public: z.boolean().describe("Whether the agent is publicly visible in the AI Catalog"),
|
|
3323
|
-
system_prompt: z.string().describe("System prompt that defines the agent's behavior"),
|
|
3324
|
-
user_prompt: z.string().optional().describe("User prompt template (optional)"),
|
|
3325
|
-
tools: z.array(z.string()).optional().describe(
|
|
3326
|
-
'Array of built-in tool Global IDs from gitlab_list_builtin_tools. Must be full GIDs like "gid://gitlab/Ai::Catalog::BuiltInTool/1", NOT tool names.'
|
|
3327
|
-
),
|
|
3328
|
-
mcp_tools: z.array(z.string()).optional().describe("Array of MCP tool names to enable"),
|
|
3329
|
-
mcp_servers: z.array(z.string()).optional().describe(
|
|
3330
|
-
'Array of MCP server Global IDs from gitlab_list_project_mcp_servers. Must be full GIDs like "gid://gitlab/Ai::Catalog::McpServer/1", NOT server names.'
|
|
3331
|
-
),
|
|
3332
|
-
release: z.boolean().optional().describe("Whether to release the version immediately (default: false)"),
|
|
3333
|
-
confirmed: z.boolean().optional().describe(
|
|
3334
|
-
"Set to true only after the user has reviewed and confirmed all parameters. Omit or set false on first call."
|
|
3335
|
-
)
|
|
3336
|
-
},
|
|
3337
|
-
execute: async (args) => {
|
|
3338
|
-
if (!args.confirmed) {
|
|
3339
|
-
return [
|
|
3340
|
-
"STOP: Do not create the agent yet. You must ask the user to confirm the configuration first.",
|
|
3341
|
-
"",
|
|
3342
|
-
"Follow these steps NOW:",
|
|
3343
|
-
"1. Call gitlab_list_builtin_tools and gitlab_list_project_mcp_servers to discover options.",
|
|
3344
|
-
"2. Use the question tool to ask the user ALL 4 of these (in one call):",
|
|
3345
|
-
" - Agent name (suggest one, allow custom input)",
|
|
3346
|
-
" - Visibility: Public or Private",
|
|
3347
|
-
" - Tools: group by category (Search, Issues, MRs, Epics, Files, Git, CI/CD, Security, Audit, Planning, Wiki, API) as multi-select",
|
|
3348
|
-
" - MCP servers: multi-select from available servers",
|
|
3349
|
-
"3. Generate a system prompt and show it to the user for approval.",
|
|
3350
|
-
"4. Call gitlab_create_agent again with confirmed=true after the user approves."
|
|
3351
|
-
].join("\n");
|
|
3352
|
-
}
|
|
3353
|
-
const auth = authCache ?? readAuth();
|
|
3354
|
-
if (!auth) throw new Error("Not authenticated");
|
|
3355
|
-
const result = await createAgent(auth.instanceUrl, auth.token, args.project_id, {
|
|
3356
|
-
name: args.name,
|
|
3357
|
-
description: args.description,
|
|
3358
|
-
public: args.public,
|
|
3359
|
-
systemPrompt: args.system_prompt,
|
|
3360
|
-
userPrompt: args.user_prompt,
|
|
3361
|
-
tools: args.tools,
|
|
3362
|
-
mcpTools: args.mcp_tools,
|
|
3363
|
-
mcpServers: args.mcp_servers,
|
|
3364
|
-
release: args.release
|
|
3365
|
-
});
|
|
3366
|
-
await refreshAgents();
|
|
3367
|
-
return JSON.stringify(result, null, 2);
|
|
3368
|
-
}
|
|
3369
|
-
}),
|
|
3370
|
-
gitlab_update_agent: (0, import_plugin.tool)({
|
|
3371
|
-
description: "Update an existing custom agent in the GitLab AI Catalog.\nOnly provided fields are updated; omitted fields remain unchanged.",
|
|
3372
|
-
args: {
|
|
3373
|
-
id: z.string().describe("Agent ID (numeric or full GID)"),
|
|
3374
|
-
name: z.string().optional().describe("New display name"),
|
|
3375
|
-
description: z.string().optional().describe("New description"),
|
|
3376
|
-
public: z.boolean().optional().describe("Whether publicly visible"),
|
|
3377
|
-
system_prompt: z.string().optional().describe("New system prompt"),
|
|
3378
|
-
user_prompt: z.string().optional().describe("New user prompt template"),
|
|
3379
|
-
tools: z.array(z.string()).optional().describe(
|
|
3380
|
-
'New set of built-in tool Global IDs (full GIDs like "gid://gitlab/Ai::Catalog::BuiltInTool/1")'
|
|
3381
|
-
),
|
|
3382
|
-
mcp_tools: z.array(z.string()).optional().describe("New set of MCP tool names"),
|
|
3383
|
-
mcp_servers: z.array(z.string()).optional().describe(
|
|
3384
|
-
'New set of MCP server Global IDs (full GIDs like "gid://gitlab/Ai::Catalog::McpServer/1")'
|
|
3385
|
-
),
|
|
3386
|
-
release: z.boolean().optional().describe("Whether to release the latest version"),
|
|
3387
|
-
version_bump: z.enum(["MAJOR", "MINOR", "PATCH"]).optional().describe("Version bump type")
|
|
3388
|
-
},
|
|
3389
|
-
execute: async (args) => {
|
|
3390
|
-
const auth = authCache ?? readAuth();
|
|
3391
|
-
if (!auth) throw new Error("Not authenticated");
|
|
3392
|
-
const result = await updateAgent(auth.instanceUrl, auth.token, args.id, {
|
|
3393
|
-
name: args.name,
|
|
3394
|
-
description: args.description,
|
|
3395
|
-
public: args.public,
|
|
3396
|
-
systemPrompt: args.system_prompt,
|
|
3397
|
-
userPrompt: args.user_prompt,
|
|
3398
|
-
tools: args.tools,
|
|
3399
|
-
mcpTools: args.mcp_tools,
|
|
3400
|
-
mcpServers: args.mcp_servers,
|
|
3401
|
-
release: args.release,
|
|
3402
|
-
versionBump: args.version_bump
|
|
3403
|
-
});
|
|
3404
|
-
await refreshAgents();
|
|
3405
|
-
return JSON.stringify(result, null, 2);
|
|
3406
|
-
}
|
|
3407
|
-
}),
|
|
3408
|
-
gitlab_list_builtin_tools: (0, import_plugin.tool)({
|
|
3409
|
-
description: "List available built-in GitLab tools that can be assigned to custom agents.\nReturns tool IDs, names, and descriptions. Use the IDs when creating or updating agents.",
|
|
3410
|
-
args: {},
|
|
3411
|
-
execute: async () => {
|
|
3412
|
-
const auth = authCache ?? readAuth();
|
|
3413
|
-
if (!auth) throw new Error("Not authenticated");
|
|
3414
|
-
const tools = await listBuiltInTools(auth.instanceUrl, auth.token);
|
|
3415
|
-
if (!tools.length) return "No built-in tools available.";
|
|
3416
|
-
return JSON.stringify(tools, null, 2);
|
|
3417
|
-
}
|
|
3418
|
-
}),
|
|
3419
|
-
gitlab_design_flow: (0, import_plugin.tool)({
|
|
3420
|
-
description: "Interactive flow design tool. Returns the flow YAML schema reference, examples, and instructions.\nUse this BEFORE gitlab_create_flow to design the flow definition interactively with the user.\nThe tool also validates generated YAML against the flow_v2 JSON schema.",
|
|
3421
|
-
args: {
|
|
3422
|
-
action: z.enum(["get_schema", "validate"]).describe(
|
|
3423
|
-
'"get_schema" returns the schema reference and examples. "validate" validates a YAML definition.'
|
|
3424
|
-
),
|
|
3425
|
-
definition: z.string().optional().describe("YAML definition to validate (required when action=validate)")
|
|
3426
|
-
},
|
|
3427
|
-
execute: async (args) => {
|
|
3428
|
-
if (args.action === "validate") {
|
|
3429
|
-
if (!args.definition) return "Error: definition is required for validate action.";
|
|
3430
|
-
const result = validateFlowYaml(args.definition);
|
|
3431
|
-
if (result.valid) return "VALID: Flow definition passes schema validation.";
|
|
3432
|
-
return `INVALID: ${result.errors.length} error(s):
|
|
3433
|
-
${result.errors.map((e, i) => ` ${i + 1}. ${e}`).join("\n")}`;
|
|
3434
|
-
}
|
|
3435
|
-
return [
|
|
3436
|
-
"Follow this multi-round workflow to design a flow:",
|
|
3437
|
-
"",
|
|
3438
|
-
"ROUND 1: Call gitlab_list_builtin_tools to discover available tool names for the flow.",
|
|
3439
|
-
" Then use the question tool to ask the user:",
|
|
3440
|
-
" - Flow name",
|
|
3441
|
-
" - Visibility: Public or Private",
|
|
3442
|
-
" - What the flow should do (step-by-step description)",
|
|
3443
|
-
" - What GitLab resource it operates on (MR, issue, pipeline, vulnerability, etc.)",
|
|
3444
|
-
"",
|
|
3445
|
-
"ROUND 2: Based on the user's answers, propose a component architecture in plain text:",
|
|
3446
|
-
" - List each step with its type (DeterministicStep, OneOff, or Agent)",
|
|
3447
|
-
" - Explain what each step does and what tools it uses",
|
|
3448
|
-
" - Show the routing (linear or conditional)",
|
|
3449
|
-
" Ask the user to confirm or adjust.",
|
|
3450
|
-
"",
|
|
3451
|
-
"ROUND 3: Generate the full YAML definition using the schema below.",
|
|
3452
|
-
" Call gitlab_design_flow with action='validate' to check it.",
|
|
3453
|
-
" Show the validated YAML to the user for final approval.",
|
|
3454
|
-
" Then call gitlab_create_flow with confirmed=true.",
|
|
3455
|
-
"",
|
|
3456
|
-
"=== FLOW YAML SCHEMA ===",
|
|
3457
|
-
FLOW_SCHEMA_REFERENCE,
|
|
3458
|
-
"",
|
|
3459
|
-
"=== EXAMPLE: Linear flow ===",
|
|
3460
|
-
FLOW_EXAMPLE_LINEAR,
|
|
3461
|
-
"",
|
|
3462
|
-
"=== EXAMPLE: Conditional flow ===",
|
|
3463
|
-
FLOW_EXAMPLE_CONDITIONAL
|
|
3464
|
-
].join("\n");
|
|
3465
|
-
}
|
|
3466
|
-
}),
|
|
3467
|
-
gitlab_create_flow: (0, import_plugin.tool)({
|
|
3468
|
-
description: "Create a custom flow in the GitLab AI Catalog.\nFirst call: set confirmed=false (or omit). Returns instructions to use gitlab_design_flow first.\nSecond call: after the user confirms, set confirmed=true to create the flow.\nAfter creation, use gitlab_enable_project_flow to enable it on a project.",
|
|
3469
|
-
args: {
|
|
3470
|
-
project_id: z.string().describe('Project path (e.g., "gitlab-org/gitlab")'),
|
|
3471
|
-
name: z.string().describe("Display name for the flow"),
|
|
3472
|
-
description: z.string().describe("Description of what the flow does"),
|
|
3473
|
-
public: z.boolean().describe("Whether publicly visible in the AI Catalog"),
|
|
3474
|
-
definition: z.string().describe("Flow YAML definition (validated via gitlab_design_flow)"),
|
|
3475
|
-
release: z.boolean().optional().describe("Whether to release the version immediately"),
|
|
3476
|
-
confirmed: z.boolean().optional().describe("Set true only after user has reviewed the YAML")
|
|
3477
|
-
},
|
|
3478
|
-
execute: async (args) => {
|
|
3479
|
-
if (!args.confirmed) {
|
|
3480
|
-
return [
|
|
3481
|
-
"STOP: Do not create the flow yet.",
|
|
3482
|
-
"",
|
|
3483
|
-
"Call gitlab_design_flow with action='get_schema' first to get the interactive workflow.",
|
|
3484
|
-
"Follow the multi-round design process, then call this tool with confirmed=true."
|
|
3485
|
-
].join("\n");
|
|
3486
|
-
}
|
|
3487
|
-
const validation = validateFlowYaml(args.definition);
|
|
3488
|
-
if (!validation.valid) {
|
|
3489
|
-
return `Flow YAML validation failed:
|
|
3490
|
-
${validation.errors.map((e, i) => ` ${i + 1}. ${e}`).join("\n")}
|
|
3491
|
-
|
|
3492
|
-
Fix the errors and try again.`;
|
|
3493
|
-
}
|
|
3494
|
-
const auth = authCache ?? readAuth();
|
|
3495
|
-
if (!auth) throw new Error("Not authenticated");
|
|
3496
|
-
const result = await createFlow(auth.instanceUrl, auth.token, args.project_id, {
|
|
3497
|
-
name: args.name,
|
|
3498
|
-
description: args.description,
|
|
3499
|
-
public: args.public,
|
|
3500
|
-
definition: args.definition,
|
|
3501
|
-
release: args.release
|
|
3502
|
-
});
|
|
3503
|
-
await refreshAgents();
|
|
3504
|
-
const json = JSON.stringify(result, null, 2);
|
|
3505
|
-
return `${json}
|
|
3506
|
-
|
|
3507
|
-
Flow created successfully. Ask the user if they want to enable it on the current project using gitlab_enable_project_flow.`;
|
|
3508
|
-
}
|
|
3509
|
-
}),
|
|
3510
|
-
gitlab_update_flow: (0, import_plugin.tool)({
|
|
3511
|
-
description: "Update an existing custom flow in the GitLab AI Catalog.\nOnly provided fields are updated; omitted fields remain unchanged.\nUse gitlab_design_flow with action='validate' to check YAML before updating.",
|
|
3512
|
-
args: {
|
|
3513
|
-
id: z.string().describe("Flow ID (numeric or full GID)"),
|
|
3514
|
-
name: z.string().optional().describe("New display name"),
|
|
3515
|
-
description: z.string().optional().describe("New description"),
|
|
3516
|
-
public: z.boolean().optional().describe("Whether publicly visible"),
|
|
3517
|
-
definition: z.string().optional().describe("New flow YAML definition"),
|
|
3518
|
-
release: z.boolean().optional().describe("Whether to release the latest version"),
|
|
3519
|
-
version_bump: z.enum(["MAJOR", "MINOR", "PATCH"]).optional().describe("Version bump type")
|
|
3520
|
-
},
|
|
3521
|
-
execute: async (args) => {
|
|
3522
|
-
if (args.definition) {
|
|
3523
|
-
const validation = validateFlowYaml(args.definition);
|
|
3524
|
-
if (!validation.valid) {
|
|
3525
|
-
return `Flow YAML validation failed:
|
|
3526
|
-
${validation.errors.map((e, i) => ` ${i + 1}. ${e}`).join("\n")}
|
|
3527
|
-
|
|
3528
|
-
Fix the errors and try again.`;
|
|
3529
|
-
}
|
|
3530
|
-
}
|
|
3531
|
-
const auth = authCache ?? readAuth();
|
|
3532
|
-
if (!auth) throw new Error("Not authenticated");
|
|
3533
|
-
const result = await updateFlow(auth.instanceUrl, auth.token, args.id, {
|
|
3534
|
-
name: args.name,
|
|
3535
|
-
description: args.description,
|
|
3536
|
-
public: args.public,
|
|
3537
|
-
definition: args.definition,
|
|
3538
|
-
release: args.release,
|
|
3539
|
-
versionBump: args.version_bump
|
|
3540
|
-
});
|
|
3541
|
-
await refreshAgents();
|
|
3542
|
-
return JSON.stringify(result, null, 2);
|
|
3543
|
-
}
|
|
3544
|
-
}),
|
|
3545
|
-
...makeAgentFlowTools(
|
|
3546
|
-
z,
|
|
3547
|
-
() => authCache,
|
|
3548
|
-
readAuth,
|
|
3549
|
-
(a) => {
|
|
3550
|
-
authCache = a;
|
|
3551
|
-
},
|
|
3552
|
-
refreshAgents
|
|
3553
|
-
)
|
|
3674
|
+
...makeFlowTools(ctx),
|
|
3675
|
+
...makeMcpTools(() => cachedAgents),
|
|
3676
|
+
...makeCatalogCrudTools(ctx),
|
|
3677
|
+
...makeCatalogItemTools(ctx)
|
|
3554
3678
|
}
|
|
3555
3679
|
};
|
|
3556
3680
|
};
|