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/index.js CHANGED
@@ -1,8 +1,11 @@
1
+ import {
2
+ gql
3
+ } from "./chunk-DPR6OUYG.js";
4
+
1
5
  // src/index.ts
2
- import { tool } from "@opencode-ai/plugin";
3
6
  import { GitLabModelCache } from "gitlab-ai-provider";
4
7
 
5
- // src/catalog.ts
8
+ // src/flow-execution.ts
6
9
  import { load as yamlLoad } from "js-yaml";
7
10
 
8
11
  // src/generated/foundational-flows.ts
@@ -1180,7 +1183,7 @@ flow:
1180
1183
  "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'
1181
1184
  };
1182
1185
 
1183
- // src/catalog.ts
1186
+ // src/flow-execution.ts
1184
1187
  function extractFlowInputs(flowConfig) {
1185
1188
  if (!flowConfig) return [];
1186
1189
  const flow = flowConfig.flow;
@@ -1236,16 +1239,41 @@ query FlowVersionDefinition($itemId: AiCatalogItemID!) {
1236
1239
  }
1237
1240
  }
1238
1241
  }`;
1239
- async function gql(instanceUrl, token, query, variables) {
1240
- const res = await fetch(`${instanceUrl.replace(/\/$/, "")}/api/graphql`, {
1241
- method: "POST",
1242
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
1243
- body: JSON.stringify({ query, variables })
1244
- });
1245
- const json = await res.json();
1246
- if (json.errors?.length) throw new Error(json.errors[0].message);
1247
- return json.data;
1248
- }
1242
+ var RESOLVE_ROOT_NAMESPACE_QUERY = `
1243
+ query resolveRootNamespace($projectPath: ID!) {
1244
+ project(fullPath: $projectPath) {
1245
+ id
1246
+ group {
1247
+ id
1248
+ rootNamespace { id }
1249
+ }
1250
+ }
1251
+ }`;
1252
+ var WORKFLOW_STATUS_QUERY = `
1253
+ query getWorkflowStatus($workflowId: AiDuoWorkflowsWorkflowID!) {
1254
+ duoWorkflowWorkflows(workflowId: $workflowId) {
1255
+ nodes {
1256
+ id
1257
+ status
1258
+ humanStatus
1259
+ createdAt
1260
+ updatedAt
1261
+ workflowDefinition
1262
+ lastExecutorLogsUrl
1263
+ latestCheckpoint {
1264
+ duoMessages {
1265
+ content
1266
+ correlationId
1267
+ role
1268
+ messageType
1269
+ status
1270
+ timestamp
1271
+ toolInfo
1272
+ }
1273
+ }
1274
+ }
1275
+ }
1276
+ }`;
1249
1277
  async function fetchFoundationalChatAgents(instanceUrl, token, projectId) {
1250
1278
  const agents = [];
1251
1279
  let after = null;
@@ -1330,59 +1358,18 @@ async function fetchCustomAgents(instanceUrl, token, projectId) {
1330
1358
  }
1331
1359
  return agents;
1332
1360
  }
1333
- var MCP_SERVERS_QUERY = `
1334
- query AiCatalogMcpServers($projectId: ProjectID!, $after: String) {
1335
- aiCatalogConfiguredItems(first: 50, projectId: $projectId, itemTypes: [AGENT], after: $after) {
1336
- pageInfo { hasNextPage endCursor }
1337
- nodes {
1338
- item {
1339
- id
1340
- name
1341
- latestVersion {
1342
- ... on AiCatalogAgentVersion {
1343
- mcpServers {
1344
- nodes { id name description url transport authType currentUserConnected }
1345
- }
1346
- }
1347
- }
1348
- }
1349
- }
1350
- }
1351
- }`;
1352
- async function fetchMcpServers(instanceUrl, token, projectId, agents) {
1361
+ async function fetchCatalogAgents(instanceUrl, token, projectId) {
1362
+ const { fetchMcpServers: fetchMcpServers2 } = await import("./mcp-servers-2HCDB7XM.js");
1353
1363
  try {
1354
- let after = null;
1355
- const serversByAgent = /* @__PURE__ */ new Map();
1356
- for (; ; ) {
1357
- const data = await gql(instanceUrl, token, MCP_SERVERS_QUERY, {
1358
- projectId,
1359
- ...after ? { after } : {}
1360
- });
1361
- const page = data?.aiCatalogConfiguredItems;
1362
- if (!page) break;
1363
- for (const node of page.nodes ?? []) {
1364
- const item = node.item;
1365
- const version = item?.latestVersion;
1366
- if (!version?.mcpServers?.nodes?.length) continue;
1367
- const servers = version.mcpServers.nodes.map((s) => ({
1368
- id: s.id,
1369
- name: s.name,
1370
- description: s.description ?? "",
1371
- url: s.url,
1372
- transport: s.transport,
1373
- authType: s.authType,
1374
- currentUserConnected: !!s.currentUserConnected
1375
- }));
1376
- serversByAgent.set(item.id, servers);
1377
- }
1378
- if (!page.pageInfo?.hasNextPage) break;
1379
- after = page.pageInfo.endCursor;
1380
- }
1381
- for (const agent of agents) {
1382
- const servers = serversByAgent.get(agent.identifier);
1383
- if (servers?.length) agent.mcpServers = servers;
1384
- }
1364
+ const [foundational, custom] = await Promise.all([
1365
+ fetchFoundationalChatAgents(instanceUrl, token, projectId),
1366
+ fetchCustomAgents(instanceUrl, token, projectId)
1367
+ ]);
1368
+ const agents = [...foundational, ...custom];
1369
+ await fetchMcpServers2(instanceUrl, token, projectId, agents);
1370
+ return agents;
1385
1371
  } catch {
1372
+ return [];
1386
1373
  }
1387
1374
  }
1388
1375
  async function getFlowDefinition(instanceUrl, token, opts) {
@@ -1414,16 +1401,6 @@ async function getFlowDefinition(instanceUrl, token, opts) {
1414
1401
  }
1415
1402
  return { config, name, ...apiError && !config ? { error: apiError } : {} };
1416
1403
  }
1417
- var RESOLVE_ROOT_NAMESPACE_QUERY = `
1418
- query resolveRootNamespace($projectPath: ID!) {
1419
- project(fullPath: $projectPath) {
1420
- id
1421
- group {
1422
- id
1423
- rootNamespace { id }
1424
- }
1425
- }
1426
- }`;
1427
1404
  async function resolveRootNamespaceId(instanceUrl, token, projectPath) {
1428
1405
  try {
1429
1406
  const data = await gql(instanceUrl, token, RESOLVE_ROOT_NAMESPACE_QUERY, {
@@ -1480,31 +1457,6 @@ async function executeFlow(instanceUrl, token, projectPath, consumerId, goal, op
1480
1457
  }
1481
1458
  return res.json();
1482
1459
  }
1483
- var WORKFLOW_STATUS_QUERY = `
1484
- query getWorkflowStatus($workflowId: AiDuoWorkflowsWorkflowID!) {
1485
- duoWorkflowWorkflows(workflowId: $workflowId) {
1486
- nodes {
1487
- id
1488
- status
1489
- humanStatus
1490
- createdAt
1491
- updatedAt
1492
- workflowDefinition
1493
- lastExecutorLogsUrl
1494
- latestCheckpoint {
1495
- duoMessages {
1496
- content
1497
- correlationId
1498
- role
1499
- messageType
1500
- status
1501
- timestamp
1502
- toolInfo
1503
- }
1504
- }
1505
- }
1506
- }
1507
- }`;
1508
1460
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
1509
1461
  async function getWorkflowStatus(instanceUrl, token, workflowId) {
1510
1462
  const gid = `gid://gitlab/Ai::DuoWorkflows::Workflow/${workflowId}`;
@@ -1524,19 +1476,8 @@ async function getWorkflowStatus(instanceUrl, token, workflowId) {
1524
1476
  if (!nodes?.length) throw new Error(`Workflow ${workflowId} not found`);
1525
1477
  return nodes[0];
1526
1478
  }
1527
- async function fetchCatalogAgents(instanceUrl, token, projectId) {
1528
- try {
1529
- const [foundational, custom] = await Promise.all([
1530
- fetchFoundationalChatAgents(instanceUrl, token, projectId),
1531
- fetchCustomAgents(instanceUrl, token, projectId)
1532
- ]);
1533
- const agents = [...foundational, ...custom];
1534
- await fetchMcpServers(instanceUrl, token, projectId, agents);
1535
- return agents;
1536
- } catch {
1537
- return [];
1538
- }
1539
- }
1479
+
1480
+ // src/catalog-items.ts
1540
1481
  var LIST_AI_CATALOG_ITEMS_QUERY = `
1541
1482
  query listAiCatalogItems(
1542
1483
  $itemTypes: [AiCatalogItemType!]
@@ -1671,6 +1612,80 @@ query resolveProjectIds($projectPath: ID!) {
1671
1612
  namespace { id }
1672
1613
  }
1673
1614
  }`;
1615
+ function normalizeItemGid(id) {
1616
+ if (id.startsWith("gid://")) return id;
1617
+ if (!/^\d+$/.test(id)) throw new Error(`Invalid catalog item ID: "${id}"`);
1618
+ return `gid://gitlab/Ai::Catalog::Item/${id}`;
1619
+ }
1620
+ async function resolveProjectGid(instanceUrl, token, projectPath) {
1621
+ const result = await gql(instanceUrl, token, RESOLVE_PROJECT_IDS_FOR_TOOLS_QUERY, {
1622
+ projectPath
1623
+ });
1624
+ return result.project.id;
1625
+ }
1626
+ async function listAiCatalogItems(instanceUrl, token, itemTypes, options) {
1627
+ const variables = {
1628
+ itemTypes,
1629
+ first: options?.first ?? 20
1630
+ };
1631
+ if (options?.search) variables.search = options.search;
1632
+ if (options?.after) variables.after = options.after;
1633
+ const result = await gql(instanceUrl, token, LIST_AI_CATALOG_ITEMS_QUERY, variables);
1634
+ return result.aiCatalogItems;
1635
+ }
1636
+ async function getAiCatalogItem(instanceUrl, token, itemId) {
1637
+ const gid = normalizeItemGid(itemId);
1638
+ const result = await gql(instanceUrl, token, GET_AI_CATALOG_ITEM_QUERY, { id: gid });
1639
+ return result.aiCatalogItem;
1640
+ }
1641
+ async function listProjectAiCatalogItems(instanceUrl, token, projectPath, itemTypes, options) {
1642
+ const projectGid = await resolveProjectGid(instanceUrl, token, projectPath);
1643
+ const variables = {
1644
+ projectId: projectGid,
1645
+ itemTypes,
1646
+ includeFoundationalConsumers: true,
1647
+ first: options?.first ?? 20
1648
+ };
1649
+ if (options?.after) variables.after = options.after;
1650
+ const result = await gql(instanceUrl, token, LIST_PROJECT_CONFIGURED_ITEMS_QUERY, variables);
1651
+ return result.aiCatalogConfiguredItems;
1652
+ }
1653
+ async function enableAiCatalogItemForProject(instanceUrl, token, projectPath, itemId) {
1654
+ const projectGid = await resolveProjectGid(instanceUrl, token, projectPath);
1655
+ const gid = normalizeItemGid(itemId);
1656
+ const result = await gql(instanceUrl, token, ENABLE_AI_CATALOG_ITEM_MUTATION, {
1657
+ input: { itemId: gid, target: { projectId: projectGid } }
1658
+ });
1659
+ if (result.aiCatalogItemConsumerCreate.errors.length > 0) {
1660
+ throw new Error(
1661
+ `Failed to enable item: ${result.aiCatalogItemConsumerCreate.errors.join(", ")}`
1662
+ );
1663
+ }
1664
+ return result.aiCatalogItemConsumerCreate;
1665
+ }
1666
+ async function disableAiCatalogItemForProject(instanceUrl, token, projectPath, itemId) {
1667
+ const projectGid = await resolveProjectGid(instanceUrl, token, projectPath);
1668
+ const gid = normalizeItemGid(itemId);
1669
+ const consumerResult = await gql(instanceUrl, token, FIND_ITEM_CONSUMER_FOR_DISABLE_QUERY, {
1670
+ projectId: projectGid,
1671
+ itemTypes: ["AGENT", "FLOW", "THIRD_PARTY_FLOW"]
1672
+ });
1673
+ const consumer = consumerResult.aiCatalogConfiguredItems.nodes.find(
1674
+ (n) => n.item.id === gid
1675
+ );
1676
+ if (!consumer?.id) throw new Error("Agent/flow is not enabled in this project");
1677
+ const result = await gql(instanceUrl, token, DISABLE_AI_CATALOG_ITEM_MUTATION, {
1678
+ input: { id: consumer.id }
1679
+ });
1680
+ if (result.aiCatalogItemConsumerDelete.errors.length > 0) {
1681
+ throw new Error(
1682
+ `Failed to disable item: ${result.aiCatalogItemConsumerDelete.errors.join(", ")}`
1683
+ );
1684
+ }
1685
+ return result.aiCatalogItemConsumerDelete;
1686
+ }
1687
+
1688
+ // src/catalog-crud.ts
1674
1689
  var CREATE_AGENT_MUTATION = `
1675
1690
  mutation AiCatalogAgentCreate($input: AiCatalogAgentCreateInput!) {
1676
1691
  aiCatalogAgentCreate(input: $input) {
@@ -1772,78 +1787,11 @@ mutation AiCatalogFlowUpdate($input: AiCatalogFlowUpdateInput!) {
1772
1787
  }
1773
1788
  }
1774
1789
  }`;
1775
- function normalizeItemGid(id) {
1790
+ function normalizeItemGid2(id) {
1776
1791
  if (id.startsWith("gid://")) return id;
1777
1792
  if (!/^\d+$/.test(id)) throw new Error(`Invalid catalog item ID: "${id}"`);
1778
1793
  return `gid://gitlab/Ai::Catalog::Item/${id}`;
1779
1794
  }
1780
- async function listAiCatalogItems(instanceUrl, token, itemTypes, options) {
1781
- const variables = {
1782
- itemTypes,
1783
- first: options?.first ?? 20
1784
- };
1785
- if (options?.search) variables.search = options.search;
1786
- if (options?.after) variables.after = options.after;
1787
- const result = await gql(instanceUrl, token, LIST_AI_CATALOG_ITEMS_QUERY, variables);
1788
- return result.aiCatalogItems;
1789
- }
1790
- async function getAiCatalogItem(instanceUrl, token, itemId) {
1791
- const gid = normalizeItemGid(itemId);
1792
- const result = await gql(instanceUrl, token, GET_AI_CATALOG_ITEM_QUERY, { id: gid });
1793
- return result.aiCatalogItem;
1794
- }
1795
- async function resolveProjectGid(instanceUrl, token, projectPath) {
1796
- const result = await gql(instanceUrl, token, RESOLVE_PROJECT_IDS_FOR_TOOLS_QUERY, {
1797
- projectPath
1798
- });
1799
- return result.project.id;
1800
- }
1801
- async function listProjectAiCatalogItems(instanceUrl, token, projectPath, itemTypes, options) {
1802
- const projectGid = await resolveProjectGid(instanceUrl, token, projectPath);
1803
- const variables = {
1804
- projectId: projectGid,
1805
- itemTypes,
1806
- includeFoundationalConsumers: true,
1807
- first: options?.first ?? 20
1808
- };
1809
- if (options?.after) variables.after = options.after;
1810
- const result = await gql(instanceUrl, token, LIST_PROJECT_CONFIGURED_ITEMS_QUERY, variables);
1811
- return result.aiCatalogConfiguredItems;
1812
- }
1813
- async function enableAiCatalogItemForProject(instanceUrl, token, projectPath, itemId) {
1814
- const projectGid = await resolveProjectGid(instanceUrl, token, projectPath);
1815
- const gid = normalizeItemGid(itemId);
1816
- const result = await gql(instanceUrl, token, ENABLE_AI_CATALOG_ITEM_MUTATION, {
1817
- input: { itemId: gid, target: { projectId: projectGid } }
1818
- });
1819
- if (result.aiCatalogItemConsumerCreate.errors.length > 0) {
1820
- throw new Error(
1821
- `Failed to enable item: ${result.aiCatalogItemConsumerCreate.errors.join(", ")}`
1822
- );
1823
- }
1824
- return result.aiCatalogItemConsumerCreate;
1825
- }
1826
- async function disableAiCatalogItemForProject(instanceUrl, token, projectPath, itemId) {
1827
- const projectGid = await resolveProjectGid(instanceUrl, token, projectPath);
1828
- const gid = normalizeItemGid(itemId);
1829
- const consumerResult = await gql(instanceUrl, token, FIND_ITEM_CONSUMER_FOR_DISABLE_QUERY, {
1830
- projectId: projectGid,
1831
- itemTypes: ["AGENT", "FLOW", "THIRD_PARTY_FLOW"]
1832
- });
1833
- const consumer = consumerResult.aiCatalogConfiguredItems.nodes.find(
1834
- (n) => n.item.id === gid
1835
- );
1836
- if (!consumer?.id) throw new Error("Agent/flow is not enabled in this project");
1837
- const result = await gql(instanceUrl, token, DISABLE_AI_CATALOG_ITEM_MUTATION, {
1838
- input: { id: consumer.id }
1839
- });
1840
- if (result.aiCatalogItemConsumerDelete.errors.length > 0) {
1841
- throw new Error(
1842
- `Failed to disable item: ${result.aiCatalogItemConsumerDelete.errors.join(", ")}`
1843
- );
1844
- }
1845
- return result.aiCatalogItemConsumerDelete;
1846
- }
1847
1795
  async function createAgent(instanceUrl, token, projectPath, params) {
1848
1796
  const projectGid = await resolveProjectGid(instanceUrl, token, projectPath);
1849
1797
  const input = {
@@ -1865,7 +1813,7 @@ async function createAgent(instanceUrl, token, projectPath, params) {
1865
1813
  return result.aiCatalogAgentCreate.item;
1866
1814
  }
1867
1815
  async function updateAgent(instanceUrl, token, itemId, params) {
1868
- const gid = normalizeItemGid(itemId);
1816
+ const gid = normalizeItemGid2(itemId);
1869
1817
  const input = { id: gid };
1870
1818
  if (params.name !== void 0) input.name = params.name;
1871
1819
  if (params.description !== void 0) input.description = params.description;
@@ -1904,7 +1852,7 @@ async function createFlow(instanceUrl, token, projectPath, params) {
1904
1852
  return result.aiCatalogFlowCreate.item;
1905
1853
  }
1906
1854
  async function updateFlow(instanceUrl, token, itemId, params) {
1907
- const gid = normalizeItemGid(itemId);
1855
+ const gid = normalizeItemGid2(itemId);
1908
1856
  const input = { id: gid };
1909
1857
  if (params.name !== void 0) input.name = params.name;
1910
1858
  if (params.description !== void 0) input.description = params.description;
@@ -1919,6 +1867,484 @@ async function updateFlow(instanceUrl, token, itemId, params) {
1919
1867
  return result.aiCatalogFlowUpdate.item;
1920
1868
  }
1921
1869
 
1870
+ // src/auth.ts
1871
+ import { readFileSync } from "fs";
1872
+ import { join } from "path";
1873
+ import os from "os";
1874
+ function readAuth() {
1875
+ try {
1876
+ const authPath = join(os.homedir(), ".local", "share", "opencode", "auth.json");
1877
+ const data = JSON.parse(readFileSync(authPath, "utf-8"));
1878
+ const gitlab = data?.gitlab;
1879
+ if (!gitlab) return null;
1880
+ const token = gitlab.type === "oauth" ? gitlab.access : gitlab.type === "api" ? gitlab.key : null;
1881
+ if (!token) return null;
1882
+ const instanceUrl = gitlab.enterpriseUrl ?? process.env.GITLAB_INSTANCE_URL ?? "https://gitlab.com";
1883
+ return { token, instanceUrl };
1884
+ } catch {
1885
+ return null;
1886
+ }
1887
+ }
1888
+
1889
+ // src/agents.ts
1890
+ function resolveModelId(entry) {
1891
+ const ref = entry.selectedModelRef ?? entry.discovery?.defaultModel?.ref;
1892
+ if (!ref) return "duo-workflow-default";
1893
+ return `duo-workflow-${ref.replace(/[/_]/g, "-")}`;
1894
+ }
1895
+
1896
+ // src/prompts.ts
1897
+ var FLOW_DISPATCH_GUIDELINES = [
1898
+ `## GitLab Flow Dispatch Guidelines`,
1899
+ ``,
1900
+ `CRITICAL: You must NEVER call gitlab_execute_project_flow or gitlab_get_flow_definition directly.`,
1901
+ `Flows are ALWAYS executed via the Task tool with subagent_type "general".`,
1902
+ `When the user's message contains flow dispatch instructions (starting with "IMPORTANT: You MUST"),`,
1903
+ `follow those instructions exactly \u2014 call the Task tool with the provided parameters.`,
1904
+ ``,
1905
+ `### Multiple Flows or Resources`,
1906
+ `When multiple flows need to run (multiple @mentions, or batch across resources), dispatch them`,
1907
+ `via a SINGLE "general" subagent. The general subagent can execute multiple tool calls in parallel,`,
1908
+ `so all flows fire simultaneously. Do NOT dispatch multiple Task calls \u2014 use ONE Task with a prompt`,
1909
+ `that lists all the flows to execute, so the subagent runs them concurrently.`,
1910
+ ``,
1911
+ `### Batch Operations (Multiple Resources)`,
1912
+ `If the user asks to run flows on multiple resources (e.g., "for each MR"), first list the`,
1913
+ `resources yourself using GitLab API tools, then dispatch ONE general subagent whose prompt`,
1914
+ `includes all flow executions (N flows x M resources) to run in parallel.`
1915
+ ].join("\n");
1916
+ var AGENT_CREATION_GUIDELINES = `## Creating Custom GitLab Agents
1917
+
1918
+ Before calling gitlab_create_agent, you MUST:
1919
+ 1. Call gitlab_list_builtin_tools and gitlab_list_project_mcp_servers.
1920
+ 2. Ask the user 4 questions using the question tool (one call, all 4 questions):
1921
+ - Agent name (suggest one, allow custom)
1922
+ - Visibility: Public or Private
1923
+ - Tools: show tools grouped by category as multi-select (Search, Issues, MRs, Epics, Files, Git, CI/CD, Security, Audit, Planning, Wiki, API)
1924
+ - MCP servers: multi-select from available servers
1925
+ 3. Show the generated system prompt and ask for confirmation.
1926
+ 4. Only then call gitlab_create_agent. Use full tool GIDs like "gid://gitlab/Ai::Catalog::BuiltInTool/1".
1927
+ 5. Ask if the user wants to enable it on the current project.`;
1928
+ var FLOW_SCHEMA_REFERENCE = `## Flow YAML Schema Reference
1929
+
1930
+ ### Top-level structure (all required unless noted):
1931
+ version: "v1" # Always "v1"
1932
+ environment: ambient # Always "ambient"
1933
+ components: [...] # Array of components (min 1)
1934
+ routers: [...] # Array of routers connecting components
1935
+ flow:
1936
+ entry_point: "component_name" # First component to run
1937
+ inputs: [...] # Optional: additional context inputs
1938
+ prompts: [...] # Optional: inline prompt definitions
1939
+
1940
+ ### Component types:
1941
+
1942
+ 1. DeterministicStepComponent \u2014 runs ONE tool, no LLM call:
1943
+ - name: "fetch_data" # alphanumeric + underscore only
1944
+ type: DeterministicStepComponent
1945
+ tool_name: "get_merge_request"
1946
+ inputs: # map tool parameters
1947
+ - { from: "context:goal", as: "merge_request_iid" }
1948
+ - { from: "context:project_id", as: "project_id" }
1949
+
1950
+ 2. OneOffComponent \u2014 single LLM call + tool execution:
1951
+ - name: "process_data"
1952
+ type: OneOffComponent
1953
+ prompt_id: "my_prompt" # references inline prompt
1954
+ prompt_version: null # null = use inline prompt from prompts section
1955
+ toolset: ["read_file", "edit_file"]
1956
+ inputs:
1957
+ - { from: "context:fetch_data.tool_responses", as: "data" }
1958
+ max_correction_attempts: 3 # retries on tool failure (default 3)
1959
+
1960
+ 3. AgentComponent \u2014 multi-turn LLM reasoning loop:
1961
+ - name: "analyze"
1962
+ type: AgentComponent
1963
+ prompt_id: "my_agent_prompt"
1964
+ prompt_version: null
1965
+ toolset: ["read_file", "grep"]
1966
+ inputs:
1967
+ - { from: "context:goal", as: "user_goal" }
1968
+ ui_log_events: ["on_agent_final_answer"]
1969
+
1970
+ ### IOKey syntax (inputs/from values):
1971
+ "context:goal" # The flow goal (user input)
1972
+ "context:project_id" # Project ID (auto-injected)
1973
+ "context:<component_name>.tool_responses" # Tool output from a component
1974
+ "context:<component_name>.final_answer" # Agent's final text answer
1975
+ "context:<component_name>.execution_result" # OneOff execution result
1976
+ { from: "context:x", as: "var_name" } # Rename for prompt template
1977
+ { from: "true", as: "flag", literal: true } # Literal value
1978
+
1979
+ ### Router patterns:
1980
+ Direct: { from: "step1", to: "step2" }
1981
+ To end: { from: "last_step", to: "end" }
1982
+ Conditional: { from: "step1", condition: { input: "context:step1.final_answer.decision", routes: { "yes": "step2", "no": "end" } } }
1983
+
1984
+ ### Inline prompts (in prompts section):
1985
+ - prompt_id: "my_prompt"
1986
+ name: "My Prompt"
1987
+ unit_primitives: ["duo_agent_platform"] # Always this value
1988
+ prompt_template:
1989
+ system: "You are a helpful assistant. {{var_name}} is available."
1990
+ user: "Process: {{data}}"
1991
+ placeholder: "history" # Optional, for AgentComponent conversation history
1992
+
1993
+ ### Tool names: use names from gitlab_list_builtin_tools (e.g., "read_file", "get_merge_request", "create_merge_request_note").`;
1994
+ var FLOW_EXAMPLE_LINEAR = `## Example: Simple linear flow (fetch MR \u2192 analyze \u2192 post comment)
1995
+ \`\`\`yaml
1996
+ version: "v1"
1997
+ environment: ambient
1998
+ components:
1999
+ - name: fetch_mr
2000
+ type: DeterministicStepComponent
2001
+ tool_name: build_review_merge_request_context
2002
+ inputs:
2003
+ - { from: "context:project_id", as: "project_id" }
2004
+ - { from: "context:goal", as: "merge_request_iid" }
2005
+ ui_log_events: ["on_tool_execution_success", "on_tool_execution_failed"]
2006
+ - name: analyze_and_comment
2007
+ type: OneOffComponent
2008
+ prompt_id: review_prompt
2009
+ prompt_version: null
2010
+ toolset: ["create_merge_request_note"]
2011
+ inputs:
2012
+ - { from: "context:fetch_mr.tool_responses", as: "mr_data" }
2013
+ - { from: "context:project_id", as: "project_id" }
2014
+ - { from: "context:goal", as: "merge_request_iid" }
2015
+ max_correction_attempts: 3
2016
+ ui_log_events: ["on_tool_execution_success"]
2017
+ routers:
2018
+ - { from: fetch_mr, to: analyze_and_comment }
2019
+ - { from: analyze_and_comment, to: end }
2020
+ flow:
2021
+ entry_point: fetch_mr
2022
+ prompts:
2023
+ - prompt_id: review_prompt
2024
+ name: MR Review Prompt
2025
+ unit_primitives: ["duo_agent_platform"]
2026
+ prompt_template:
2027
+ system: |
2028
+ You review merge requests. Analyze the MR data and post a concise review comment.
2029
+ Focus on code quality, potential bugs, and improvements.
2030
+ user: "Review MR !{{merge_request_iid}} in project {{project_id}}: {{mr_data}}"
2031
+ \`\`\``;
2032
+ var FLOW_EXAMPLE_CONDITIONAL = `## Example: Conditional flow (gather data \u2192 decide \u2192 branch)
2033
+ \`\`\`yaml
2034
+ version: "v1"
2035
+ environment: ambient
2036
+ components:
2037
+ - name: gather_context
2038
+ type: DeterministicStepComponent
2039
+ tool_name: get_vulnerability_details
2040
+ inputs:
2041
+ - { from: "context:goal", as: "vulnerability_id" }
2042
+ ui_log_events: ["on_tool_execution_success"]
2043
+ - name: evaluate
2044
+ type: AgentComponent
2045
+ prompt_id: eval_prompt
2046
+ prompt_version: null
2047
+ toolset: []
2048
+ inputs:
2049
+ - { from: "context:gather_context.tool_responses", as: "vuln_data" }
2050
+ ui_log_events: ["on_agent_final_answer"]
2051
+ - name: create_fix
2052
+ type: AgentComponent
2053
+ prompt_id: fix_prompt
2054
+ prompt_version: null
2055
+ toolset: ["read_file", "edit_file", "grep"]
2056
+ inputs:
2057
+ - { from: "context:gather_context.tool_responses", as: "vuln_data" }
2058
+ ui_log_events: ["on_agent_final_answer", "on_tool_execution_success"]
2059
+ routers:
2060
+ - { from: gather_context, to: evaluate }
2061
+ - from: evaluate
2062
+ condition:
2063
+ input: "context:evaluate.final_answer"
2064
+ routes:
2065
+ "fix_needed": create_fix
2066
+ "false_positive": end
2067
+ default_route: end
2068
+ - { from: create_fix, to: end }
2069
+ flow:
2070
+ entry_point: gather_context
2071
+ prompts:
2072
+ - prompt_id: eval_prompt
2073
+ name: Vulnerability Evaluator
2074
+ unit_primitives: ["duo_agent_platform"]
2075
+ prompt_template:
2076
+ system: |
2077
+ Evaluate if a vulnerability needs fixing. Respond with exactly "fix_needed" or "false_positive".
2078
+ user: "Evaluate: {{vuln_data}}"
2079
+ - prompt_id: fix_prompt
2080
+ name: Fix Generator
2081
+ unit_primitives: ["duo_agent_platform"]
2082
+ prompt_template:
2083
+ system: |
2084
+ You fix security vulnerabilities. Read the relevant code and apply the fix.
2085
+ user: "Fix this vulnerability: {{vuln_data}}"
2086
+ placeholder: history
2087
+ \`\`\``;
2088
+
2089
+ // src/hooks.ts
2090
+ function buildFlowSubagentPrompt(flow, projectPath, projectUrl) {
2091
+ return [
2092
+ `You execute the "${flow.name}" GitLab flow. Project: ${projectPath} (${projectUrl}).`,
2093
+ ``,
2094
+ `STEP 1: Call gitlab_get_flow_definition with consumer_id=${flow.consumerId}, foundational=${!!flow.foundational}.`,
2095
+ `Parse the YAML config:`,
2096
+ `- In "components", find { from: "context:goal", as: "<name>" }. The "as" value is what the goal parameter must contain:`,
2097
+ ` "merge_request_iid" -> just the number (e.g. 14)`,
2098
+ ` "pipeline_url"/"url"/"issue_url" -> full URL (e.g. ${projectUrl}/-/merge_requests/5)`,
2099
+ ` "vulnerability_id" -> just the ID number`,
2100
+ ` "goal" -> free-form text`,
2101
+ `- In "flow.inputs", find additional_context categories (skip "agent_platform_standard_context").`,
2102
+ ``,
2103
+ `STEP 2: Resolve the goal to the EXACT value the flow expects (from step 1).`,
2104
+ `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.`,
2105
+ `Use GitLab API tools if needed to look up IDs or construct URLs from the user's message.`,
2106
+ ``,
2107
+ `STEP 3: Gather additional_context values (if any from step 1) using available tools.`,
2108
+ ``,
2109
+ `STEP 4: Call gitlab_execute_project_flow with:`,
2110
+ ` project_id: "${projectPath}"`,
2111
+ ` consumer_id: ${flow.consumerId}`,
2112
+ ` goal: <resolved value from step 2>`,
2113
+ ` additional_context (if needed): [{"Category":"<cat>","Content":"{\\"field\\":\\"val\\"}"}]`,
2114
+ ``,
2115
+ `STEP 5: Call gitlab_get_workflow_status with the workflow_id. Report status and URL: ${projectUrl}/-/automate/agent-sessions/<id>`
2116
+ ].join("\n");
2117
+ }
2118
+ function makeChatMessageHook(getAuthCache, flowAgents, getProjectPath) {
2119
+ return async (_input, output) => {
2120
+ const projectPath = getProjectPath();
2121
+ const indicesToRemove = [];
2122
+ const flowMentions = [];
2123
+ for (let i = 0; i < output.parts.length; i++) {
2124
+ const part = output.parts[i];
2125
+ if (part.type !== "agent") continue;
2126
+ const flow = flowAgents.get(part.name);
2127
+ if (!flow || !flow.consumerId || !projectPath) continue;
2128
+ flowMentions.push({ idx: i, flow, displayName: part.name });
2129
+ if (i + 1 < output.parts.length) {
2130
+ const next = output.parts[i + 1];
2131
+ if (next.type === "text" && next.synthetic && next.text?.includes("call the task tool with subagent")) {
2132
+ indicesToRemove.push(i + 1);
2133
+ }
2134
+ }
2135
+ }
2136
+ if (flowMentions.length === 0) {
2137
+ return;
2138
+ }
2139
+ const authCache = getAuthCache();
2140
+ if (flowMentions.length === 1) {
2141
+ const { idx, flow } = flowMentions[0];
2142
+ const baseUrl2 = authCache?.instanceUrl?.replace(/\/$/, "") ?? "https://gitlab.com";
2143
+ const projectUrl2 = `${baseUrl2}/${projectPath}`;
2144
+ const rawText2 = output.parts.filter((p) => p.type === "text" && !p.synthetic).map((p) => p.text).join(" ").trim() || "Execute the flow";
2145
+ const subagentPrompt = buildFlowSubagentPrompt(flow, projectPath, projectUrl2) + `
2146
+
2147
+ User goal: "${rawText2}"`;
2148
+ const resultText = [
2149
+ `IMPORTANT: You MUST call the Task tool RIGHT NOW to dispatch a subagent. Do NOT execute these steps yourself.`,
2150
+ ``,
2151
+ `Call the Task tool with:`,
2152
+ ` subagent_type: "general"`,
2153
+ ` description: "Execute ${flow.name} flow"`,
2154
+ ` prompt: ${JSON.stringify(subagentPrompt)}`,
2155
+ ``,
2156
+ `Do not do anything else. Just call the Task tool with the above parameters.`
2157
+ ].join("\n");
2158
+ const original2 = output.parts[idx];
2159
+ output.parts[idx] = { ...original2, type: "text", text: resultText };
2160
+ delete output.parts[idx].name;
2161
+ delete output.parts[idx].source;
2162
+ for (const rmIdx of indicesToRemove.reverse()) {
2163
+ output.parts.splice(rmIdx, 1);
2164
+ }
2165
+ return;
2166
+ }
2167
+ const baseUrl = authCache?.instanceUrl?.replace(/\/$/, "") ?? "https://gitlab.com";
2168
+ const projectUrl = `${baseUrl}/${projectPath}`;
2169
+ const flowNames = new Set(flowMentions.map((m) => m.displayName));
2170
+ let rawText = output.parts.filter((p) => p.type === "text" && !p.synthetic).map((p) => p.text).join(" ").trim() || "Execute the flows";
2171
+ for (const name of flowNames) {
2172
+ rawText = rawText.replace(new RegExp(`@${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "g"), "").trim();
2173
+ }
2174
+ rawText = rawText.replace(/\s{2,}/g, " ").trim() || "Execute the flows";
2175
+ const flowList = flowMentions.map(
2176
+ ({ flow, displayName }, i) => `${i + 1}. "${displayName}" \u2014 consumer_id=${flow.consumerId}, foundational=${!!flow.foundational}`
2177
+ );
2178
+ const batchPrompt = [
2179
+ `Execute ${flowMentions.length} GitLab flows on project ${projectPath} (${projectUrl}).`,
2180
+ `User goal: "${rawText}"`,
2181
+ ``,
2182
+ `Flows to execute:`,
2183
+ ...flowList,
2184
+ ``,
2185
+ `EXECUTION PLAN:`,
2186
+ `1. Call gitlab_get_flow_definition for ALL flows listed above in a SINGLE response (${flowMentions.length} tool calls at once).`,
2187
+ ` Parse each YAML to find what "context:goal" maps to (the "as" field in components).`,
2188
+ ``,
2189
+ `2. If the user's goal involves multiple resources (e.g., "for each MR"), list them using GitLab API tools.`,
2190
+ ``,
2191
+ `3. Call gitlab_execute_project_flow for EVERY flow+resource combination in a SINGLE response.`,
2192
+ ` For each call, set the goal to the EXACT value the flow expects (e.g., just "14" for merge_request_iid).`,
2193
+ ` project_id: "${projectPath}"`,
2194
+ ` You MUST emit ALL execute calls in ONE response \u2014 do NOT wait for one to finish before calling the next.`,
2195
+ ``,
2196
+ `4. Collect all workflow_ids from step 3. Call gitlab_get_workflow_status for ALL of them in a SINGLE response.`,
2197
+ ``,
2198
+ `5. Present a summary table: flow name, resource, status, URL (${projectUrl}/-/automate/agent-sessions/<id>).`,
2199
+ ``,
2200
+ `CRITICAL: In steps 1, 3, and 4 you MUST make multiple tool calls in the SAME response for parallel execution.`
2201
+ ].join("\n");
2202
+ const combinedText = [
2203
+ `IMPORTANT: You MUST call the Task tool RIGHT NOW with subagent_type "general" to dispatch all flows in parallel.`,
2204
+ `Do NOT call flow tools yourself. Do NOT dispatch multiple Task calls \u2014 use ONE.`,
2205
+ ``,
2206
+ `Call the Task tool with:`,
2207
+ ` subagent_type: "general"`,
2208
+ ` description: "Execute ${flowMentions.length} flows in parallel"`,
2209
+ ` prompt: ${JSON.stringify(batchPrompt)}`,
2210
+ ``,
2211
+ `Do not do anything else. Just call the Task tool with the above parameters.`
2212
+ ].join("\n");
2213
+ const firstIdx = flowMentions[0].idx;
2214
+ const original = output.parts[firstIdx];
2215
+ output.parts[firstIdx] = { ...original, type: "text", text: combinedText };
2216
+ delete output.parts[firstIdx].name;
2217
+ delete output.parts[firstIdx].source;
2218
+ for (let i = flowMentions.length - 1; i >= 1; i--) {
2219
+ indicesToRemove.push(flowMentions[i].idx);
2220
+ }
2221
+ for (const idx of [...new Set(indicesToRemove)].sort((a, b) => b - a)) {
2222
+ output.parts.splice(idx, 1);
2223
+ }
2224
+ };
2225
+ }
2226
+ function makeChatParamsHook(gitlabAgentNames) {
2227
+ return async (input, _output) => {
2228
+ if (!gitlabAgentNames.has(input.agent)) return;
2229
+ const model = input.model;
2230
+ const modelId = model?.modelID ?? model?.id ?? "";
2231
+ const isDWS = modelId.includes("duo-workflow");
2232
+ if (!isDWS) {
2233
+ const name = model?.name ?? modelId ?? "unknown";
2234
+ throw new Error(
2235
+ `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.`
2236
+ );
2237
+ }
2238
+ };
2239
+ }
2240
+ function makeSystemTransformHook(flowAgents, getAuthCache) {
2241
+ return async (_input, output) => {
2242
+ if (flowAgents.size) {
2243
+ output.system.push(FLOW_DISPATCH_GUIDELINES);
2244
+ }
2245
+ if (getAuthCache()) {
2246
+ output.system.push(AGENT_CREATION_GUIDELINES);
2247
+ }
2248
+ };
2249
+ }
2250
+
2251
+ // src/tools/flow-tools.ts
2252
+ import { tool } from "@opencode-ai/plugin";
2253
+ var z = tool.schema;
2254
+ function makeFlowTools(ctx) {
2255
+ return {
2256
+ gitlab_execute_project_flow: tool({
2257
+ 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.",
2258
+ args: {
2259
+ project_id: z.string().describe('Project path (e.g., "gitlab-org/gitlab")'),
2260
+ consumer_id: z.number().describe("AI Catalog ItemConsumer numeric ID"),
2261
+ goal: z.string().describe("User prompt/goal for the flow, include relevant URLs"),
2262
+ additional_context: z.string().optional().describe(
2263
+ 'JSON array of flow inputs: [{"Category":"merge_request","Content":"{\\"url\\":\\"https://...\\"}"}]'
2264
+ ),
2265
+ issue_id: z.number().optional().describe("Issue IID for context"),
2266
+ merge_request_id: z.number().optional().describe("Merge request IID for context")
2267
+ },
2268
+ execute: async (args) => {
2269
+ const auth = ctx.ensureAuth();
2270
+ if (!auth) return "Error: GitLab authentication not available";
2271
+ let flowInputs;
2272
+ if (args.additional_context) {
2273
+ try {
2274
+ flowInputs = JSON.parse(args.additional_context);
2275
+ } catch {
2276
+ return "Error: additional_context must be a valid JSON array";
2277
+ }
2278
+ }
2279
+ try {
2280
+ const result = await executeFlow(
2281
+ auth.instanceUrl,
2282
+ auth.token,
2283
+ args.project_id,
2284
+ args.consumer_id,
2285
+ args.goal,
2286
+ {
2287
+ issueId: args.issue_id,
2288
+ mergeRequestId: args.merge_request_id,
2289
+ namespaceId: ctx.getNamespaceId(),
2290
+ flowInputs
2291
+ }
2292
+ );
2293
+ return JSON.stringify(result, null, 2);
2294
+ } catch (err) {
2295
+ return `Error executing flow: ${err.message}`;
2296
+ }
2297
+ }
2298
+ }),
2299
+ gitlab_get_flow_definition: tool({
2300
+ 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.",
2301
+ args: {
2302
+ consumer_id: z.number().describe("AI Catalog ItemConsumer numeric ID"),
2303
+ foundational: z.boolean().describe("true for GitLab foundational flows, false for custom flows")
2304
+ },
2305
+ execute: async (args) => {
2306
+ const auth = ctx.ensureAuth();
2307
+ if (!auth) return "Error: GitLab authentication not available";
2308
+ const flow = [...ctx.getFlowAgents().values()].find(
2309
+ (f) => f.consumerId === args.consumer_id
2310
+ );
2311
+ try {
2312
+ const result = await getFlowDefinition(auth.instanceUrl, auth.token, {
2313
+ consumerId: args.consumer_id,
2314
+ flowName: flow?.name,
2315
+ foundational: args.foundational,
2316
+ catalogItemVersionId: flow?.catalogItemVersionId,
2317
+ itemIdentifier: flow?.identifier,
2318
+ workflowDefinition: flow?.workflowDefinition
2319
+ });
2320
+ return JSON.stringify(result, null, 2);
2321
+ } catch (err) {
2322
+ return `Error getting flow definition: ${err.message}`;
2323
+ }
2324
+ }
2325
+ }),
2326
+ gitlab_get_workflow_status: tool({
2327
+ 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.",
2328
+ args: {
2329
+ workflow_id: z.number().describe("Workflow numeric ID (from gitlab_execute_project_flow result)")
2330
+ },
2331
+ execute: async (args) => {
2332
+ const auth = ctx.ensureAuth();
2333
+ if (!auth) return "Error: GitLab authentication not available";
2334
+ try {
2335
+ const result = await getWorkflowStatus(auth.instanceUrl, auth.token, args.workflow_id);
2336
+ return JSON.stringify(result, null, 2);
2337
+ } catch (err) {
2338
+ return `Error getting workflow status: ${err.message}`;
2339
+ }
2340
+ }
2341
+ })
2342
+ };
2343
+ }
2344
+
2345
+ // src/tools/catalog-crud-tools.ts
2346
+ import { tool as tool2 } from "@opencode-ai/plugin";
2347
+
1922
2348
  // src/flow-validator.ts
1923
2349
  import Ajv from "ajv";
1924
2350
  import yaml from "js-yaml";
@@ -2526,30 +2952,257 @@ function validateFlowYaml(yamlString) {
2526
2952
  return { valid: true, errors: [] };
2527
2953
  }
2528
2954
 
2529
- // src/agents.ts
2530
- function resolveModelId(entry) {
2531
- const ref = entry.selectedModelRef ?? entry.discovery?.defaultModel?.ref;
2532
- if (!ref) return "duo-workflow-default";
2533
- return `duo-workflow-${ref.replace(/[/_]/g, "-")}`;
2955
+ // src/tools/catalog-crud-tools.ts
2956
+ var z2 = tool2.schema;
2957
+ function makeCatalogCrudTools(ctx) {
2958
+ return {
2959
+ gitlab_create_agent: tool2({
2960
+ 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.",
2961
+ args: {
2962
+ project_id: z2.string().describe('Project path (e.g., "gitlab-org/gitlab")'),
2963
+ name: z2.string().describe("Display name for the agent"),
2964
+ description: z2.string().describe("Description of what the agent does"),
2965
+ public: z2.boolean().describe("Whether the agent is publicly visible in the AI Catalog"),
2966
+ system_prompt: z2.string().describe("System prompt that defines the agent's behavior"),
2967
+ user_prompt: z2.string().optional().describe("User prompt template (optional)"),
2968
+ tools: z2.array(z2.string()).optional().describe(
2969
+ '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.'
2970
+ ),
2971
+ mcp_tools: z2.array(z2.string()).optional().describe("Array of MCP tool names to enable"),
2972
+ mcp_servers: z2.array(z2.string()).optional().describe(
2973
+ '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.'
2974
+ ),
2975
+ release: z2.boolean().optional().describe("Whether to release the version immediately (default: false)"),
2976
+ confirmed: z2.boolean().optional().describe(
2977
+ "Set to true only after the user has reviewed and confirmed all parameters. Omit or set false on first call."
2978
+ )
2979
+ },
2980
+ execute: async (args) => {
2981
+ if (!args.confirmed) {
2982
+ return [
2983
+ "STOP: Do not create the agent yet. You must ask the user to confirm the configuration first.",
2984
+ "",
2985
+ "Follow these steps NOW:",
2986
+ "1. Call gitlab_list_builtin_tools and gitlab_list_project_mcp_servers to discover options.",
2987
+ "2. Use the question tool to ask the user ALL 4 of these (in one call):",
2988
+ " - Agent name (suggest one, allow custom input)",
2989
+ " - Visibility: Public or Private",
2990
+ " - Tools: group by category (Search, Issues, MRs, Epics, Files, Git, CI/CD, Security, Audit, Planning, Wiki, API) as multi-select",
2991
+ " - MCP servers: multi-select from available servers",
2992
+ "3. Generate a system prompt and show it to the user for approval.",
2993
+ "4. Call gitlab_create_agent again with confirmed=true after the user approves."
2994
+ ].join("\n");
2995
+ }
2996
+ const auth = ctx.ensureAuth();
2997
+ if (!auth) throw new Error("Not authenticated");
2998
+ const result = await createAgent(auth.instanceUrl, auth.token, args.project_id, {
2999
+ name: args.name,
3000
+ description: args.description,
3001
+ public: args.public,
3002
+ systemPrompt: args.system_prompt,
3003
+ userPrompt: args.user_prompt,
3004
+ tools: args.tools,
3005
+ mcpTools: args.mcp_tools,
3006
+ mcpServers: args.mcp_servers,
3007
+ release: args.release
3008
+ });
3009
+ await ctx.refreshAgents();
3010
+ return JSON.stringify(result, null, 2);
3011
+ }
3012
+ }),
3013
+ gitlab_update_agent: tool2({
3014
+ description: "Update an existing custom agent in the GitLab AI Catalog.\nOnly provided fields are updated; omitted fields remain unchanged.",
3015
+ args: {
3016
+ id: z2.string().describe("Agent ID (numeric or full GID)"),
3017
+ name: z2.string().optional().describe("New display name"),
3018
+ description: z2.string().optional().describe("New description"),
3019
+ public: z2.boolean().optional().describe("Whether publicly visible"),
3020
+ system_prompt: z2.string().optional().describe("New system prompt"),
3021
+ user_prompt: z2.string().optional().describe("New user prompt template"),
3022
+ tools: z2.array(z2.string()).optional().describe(
3023
+ 'New set of built-in tool Global IDs (full GIDs like "gid://gitlab/Ai::Catalog::BuiltInTool/1")'
3024
+ ),
3025
+ mcp_tools: z2.array(z2.string()).optional().describe("New set of MCP tool names"),
3026
+ mcp_servers: z2.array(z2.string()).optional().describe(
3027
+ 'New set of MCP server Global IDs (full GIDs like "gid://gitlab/Ai::Catalog::McpServer/1")'
3028
+ ),
3029
+ release: z2.boolean().optional().describe("Whether to release the latest version"),
3030
+ version_bump: z2.enum(["MAJOR", "MINOR", "PATCH"]).optional().describe("Version bump type")
3031
+ },
3032
+ execute: async (args) => {
3033
+ const auth = ctx.ensureAuth();
3034
+ if (!auth) throw new Error("Not authenticated");
3035
+ const result = await updateAgent(auth.instanceUrl, auth.token, args.id, {
3036
+ name: args.name,
3037
+ description: args.description,
3038
+ public: args.public,
3039
+ systemPrompt: args.system_prompt,
3040
+ userPrompt: args.user_prompt,
3041
+ tools: args.tools,
3042
+ mcpTools: args.mcp_tools,
3043
+ mcpServers: args.mcp_servers,
3044
+ release: args.release,
3045
+ versionBump: args.version_bump
3046
+ });
3047
+ await ctx.refreshAgents();
3048
+ return JSON.stringify(result, null, 2);
3049
+ }
3050
+ }),
3051
+ gitlab_list_builtin_tools: tool2({
3052
+ 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.",
3053
+ args: {},
3054
+ execute: async () => {
3055
+ const auth = ctx.ensureAuth();
3056
+ if (!auth) throw new Error("Not authenticated");
3057
+ const tools = await listBuiltInTools(auth.instanceUrl, auth.token);
3058
+ if (!tools.length) return "No built-in tools available.";
3059
+ return JSON.stringify(tools, null, 2);
3060
+ }
3061
+ }),
3062
+ gitlab_design_flow: tool2({
3063
+ 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.",
3064
+ args: {
3065
+ action: z2.enum(["get_schema", "validate"]).describe(
3066
+ '"get_schema" returns the schema reference and examples. "validate" validates a YAML definition.'
3067
+ ),
3068
+ definition: z2.string().optional().describe("YAML definition to validate (required when action=validate)")
3069
+ },
3070
+ execute: async (args) => {
3071
+ if (args.action === "validate") {
3072
+ if (!args.definition) return "Error: definition is required for validate action.";
3073
+ const result = validateFlowYaml(args.definition);
3074
+ if (result.valid) return "VALID: Flow definition passes schema validation.";
3075
+ return `INVALID: ${result.errors.length} error(s):
3076
+ ${result.errors.map((e, i) => ` ${i + 1}. ${e}`).join("\n")}`;
3077
+ }
3078
+ return [
3079
+ "Follow this multi-round workflow to design a flow:",
3080
+ "",
3081
+ "ROUND 1: Call gitlab_list_builtin_tools to discover available tool names for the flow.",
3082
+ " Then use the question tool to ask the user:",
3083
+ " - Flow name",
3084
+ " - Visibility: Public or Private",
3085
+ " - What the flow should do (step-by-step description)",
3086
+ " - What GitLab resource it operates on (MR, issue, pipeline, vulnerability, etc.)",
3087
+ "",
3088
+ "ROUND 2: Based on the user's answers, propose a component architecture in plain text:",
3089
+ " - List each step with its type (DeterministicStep, OneOff, or Agent)",
3090
+ " - Explain what each step does and what tools it uses",
3091
+ " - Show the routing (linear or conditional)",
3092
+ " Ask the user to confirm or adjust.",
3093
+ "",
3094
+ "ROUND 3: Generate the full YAML definition using the schema below.",
3095
+ " Call gitlab_design_flow with action='validate' to check it.",
3096
+ " Show the validated YAML to the user for final approval.",
3097
+ " Then call gitlab_create_flow with confirmed=true.",
3098
+ "",
3099
+ "=== FLOW YAML SCHEMA ===",
3100
+ FLOW_SCHEMA_REFERENCE,
3101
+ "",
3102
+ "=== EXAMPLE: Linear flow ===",
3103
+ FLOW_EXAMPLE_LINEAR,
3104
+ "",
3105
+ "=== EXAMPLE: Conditional flow ===",
3106
+ FLOW_EXAMPLE_CONDITIONAL
3107
+ ].join("\n");
3108
+ }
3109
+ }),
3110
+ gitlab_create_flow: tool2({
3111
+ 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.",
3112
+ args: {
3113
+ project_id: z2.string().describe('Project path (e.g., "gitlab-org/gitlab")'),
3114
+ name: z2.string().describe("Display name for the flow"),
3115
+ description: z2.string().describe("Description of what the flow does"),
3116
+ public: z2.boolean().describe("Whether publicly visible in the AI Catalog"),
3117
+ definition: z2.string().describe("Flow YAML definition (validated via gitlab_design_flow)"),
3118
+ release: z2.boolean().optional().describe("Whether to release the version immediately"),
3119
+ confirmed: z2.boolean().optional().describe("Set true only after user has reviewed the YAML")
3120
+ },
3121
+ execute: async (args) => {
3122
+ if (!args.confirmed) {
3123
+ return [
3124
+ "STOP: Do not create the flow yet.",
3125
+ "",
3126
+ "Call gitlab_design_flow with action='get_schema' first to get the interactive workflow.",
3127
+ "Follow the multi-round design process, then call this tool with confirmed=true."
3128
+ ].join("\n");
3129
+ }
3130
+ const validation = validateFlowYaml(args.definition);
3131
+ if (!validation.valid) {
3132
+ return `Flow YAML validation failed:
3133
+ ${validation.errors.map((e, i) => ` ${i + 1}. ${e}`).join("\n")}
3134
+
3135
+ Fix the errors and try again.`;
3136
+ }
3137
+ const auth = ctx.ensureAuth();
3138
+ if (!auth) throw new Error("Not authenticated");
3139
+ const result = await createFlow(auth.instanceUrl, auth.token, args.project_id, {
3140
+ name: args.name,
3141
+ description: args.description,
3142
+ public: args.public,
3143
+ definition: args.definition,
3144
+ release: args.release
3145
+ });
3146
+ await ctx.refreshAgents();
3147
+ const json = JSON.stringify(result, null, 2);
3148
+ return `${json}
3149
+
3150
+ Flow created successfully. Ask the user if they want to enable it on the current project using gitlab_enable_project_flow.`;
3151
+ }
3152
+ }),
3153
+ gitlab_update_flow: tool2({
3154
+ 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.",
3155
+ args: {
3156
+ id: z2.string().describe("Flow ID (numeric or full GID)"),
3157
+ name: z2.string().optional().describe("New display name"),
3158
+ description: z2.string().optional().describe("New description"),
3159
+ public: z2.boolean().optional().describe("Whether publicly visible"),
3160
+ definition: z2.string().optional().describe("New flow YAML definition"),
3161
+ release: z2.boolean().optional().describe("Whether to release the latest version"),
3162
+ version_bump: z2.enum(["MAJOR", "MINOR", "PATCH"]).optional().describe("Version bump type")
3163
+ },
3164
+ execute: async (args) => {
3165
+ if (args.definition) {
3166
+ const validation = validateFlowYaml(args.definition);
3167
+ if (!validation.valid) {
3168
+ return `Flow YAML validation failed:
3169
+ ${validation.errors.map((e, i) => ` ${i + 1}. ${e}`).join("\n")}
3170
+
3171
+ Fix the errors and try again.`;
3172
+ }
3173
+ }
3174
+ const auth = ctx.ensureAuth();
3175
+ if (!auth) throw new Error("Not authenticated");
3176
+ const result = await updateFlow(auth.instanceUrl, auth.token, args.id, {
3177
+ name: args.name,
3178
+ description: args.description,
3179
+ public: args.public,
3180
+ definition: args.definition,
3181
+ release: args.release,
3182
+ versionBump: args.version_bump
3183
+ });
3184
+ await ctx.refreshAgents();
3185
+ return JSON.stringify(result, null, 2);
3186
+ }
3187
+ })
3188
+ };
2534
3189
  }
2535
3190
 
2536
- // src/index.ts
2537
- import { readFileSync } from "fs";
2538
- import { join } from "path";
2539
- import os from "os";
2540
- var z = tool.schema;
2541
- function makeItemTools(z2, itemType, label, getAuth, ensureAuth, onChanged) {
3191
+ // src/tools/catalog-item-tools.ts
3192
+ import { tool as tool3 } from "@opencode-ai/plugin";
3193
+ var z3 = tool3.schema;
3194
+ function makeItemTools(itemType, label, getAuth, ensureAuth, onChanged) {
2542
3195
  const itemTypes = [itemType];
2543
3196
  const Label = label.charAt(0).toUpperCase() + label.slice(1);
2544
3197
  return {
2545
- [`gitlab_list_${label}s`]: tool({
3198
+ [`gitlab_list_${label}s`]: tool3({
2546
3199
  description: `List ${label}s in the GitLab AI Catalog.
2547
3200
  Returns ${label}s with name, description, visibility, foundational flag, and version info.
2548
3201
  Supports search and cursor-based pagination.`,
2549
3202
  args: {
2550
- search: z2.string().optional().describe(`Search query to filter ${label}s by name or description`),
2551
- first: z2.number().optional().describe("Number of items to return (default 20)"),
2552
- after: z2.string().optional().describe("Cursor for pagination (from pageInfo.endCursor)")
3203
+ search: z3.string().optional().describe(`Search query to filter ${label}s by name or description`),
3204
+ first: z3.number().optional().describe("Number of items to return (default 20)"),
3205
+ after: z3.string().optional().describe("Cursor for pagination (from pageInfo.endCursor)")
2553
3206
  },
2554
3207
  execute: async (args) => {
2555
3208
  const auth = ensureAuth();
@@ -2562,12 +3215,12 @@ Supports search and cursor-based pagination.`,
2562
3215
  }
2563
3216
  }
2564
3217
  }),
2565
- [`gitlab_get_${label}`]: tool({
3218
+ [`gitlab_get_${label}`]: tool3({
2566
3219
  description: `Get details of a specific ${label} from the AI Catalog.
2567
3220
  Returns full details including name, description, visibility, versions, creator, and permissions.
2568
3221
  Accepts either a full Global ID (gid://gitlab/Ai::Catalog::Item/123) or just the numeric ID.`,
2569
3222
  args: {
2570
- [`${label}_id`]: z2.string().describe(`${Label} ID: full GID or numeric ID`)
3223
+ [`${label}_id`]: z3.string().describe(`${Label} ID: full GID or numeric ID`)
2571
3224
  },
2572
3225
  execute: async (args) => {
2573
3226
  const auth = ensureAuth();
@@ -2580,14 +3233,14 @@ Accepts either a full Global ID (gid://gitlab/Ai::Catalog::Item/123) or just the
2580
3233
  }
2581
3234
  }
2582
3235
  }),
2583
- [`gitlab_list_project_${label}s`]: tool({
3236
+ [`gitlab_list_project_${label}s`]: tool3({
2584
3237
  description: `List ${label}s enabled for a specific project.
2585
3238
  Returns all configured ${label}s including foundational ones.
2586
3239
  Each result includes the ${label} details and its consumer configuration.`,
2587
3240
  args: {
2588
- project_id: z2.string().describe('Project path (e.g., "gitlab-org/gitlab")'),
2589
- first: z2.number().optional().describe("Number of items to return (default 20)"),
2590
- after: z2.string().optional().describe("Cursor for pagination")
3241
+ project_id: z3.string().describe('Project path (e.g., "gitlab-org/gitlab")'),
3242
+ first: z3.number().optional().describe("Number of items to return (default 20)"),
3243
+ after: z3.string().optional().describe("Cursor for pagination")
2591
3244
  },
2592
3245
  execute: async (args) => {
2593
3246
  const auth = ensureAuth();
@@ -2606,14 +3259,14 @@ Each result includes the ${label} details and its consumer configuration.`,
2606
3259
  }
2607
3260
  }
2608
3261
  }),
2609
- [`gitlab_enable_project_${label}`]: tool({
3262
+ [`gitlab_enable_project_${label}`]: tool3({
2610
3263
  description: `Enable a ${label} in a project.
2611
3264
  Requires Maintainer or Owner role.
2612
3265
  Enabling in a project also enables at the group level.
2613
3266
  Foundational ${label}s cannot be enabled this way (use admin settings).`,
2614
3267
  args: {
2615
- project_id: z2.string().describe('Project path (e.g., "gitlab-org/gitlab")'),
2616
- [`${label}_id`]: z2.string().describe(`${Label} ID: full GID or numeric ID`)
3268
+ project_id: z3.string().describe('Project path (e.g., "gitlab-org/gitlab")'),
3269
+ [`${label}_id`]: z3.string().describe(`${Label} ID: full GID or numeric ID`)
2617
3270
  },
2618
3271
  execute: async (args) => {
2619
3272
  const auth = ensureAuth();
@@ -2626,287 +3279,81 @@ Foundational ${label}s cannot be enabled this way (use admin settings).`,
2626
3279
  args[`${label}_id`]
2627
3280
  );
2628
3281
  await onChanged?.();
2629
- return JSON.stringify(result, null, 2) + "\n\nNote: Restart opencode for the @ menu to reflect changes.";
2630
- } catch (err) {
2631
- return `Error: ${err.message}`;
2632
- }
2633
- }
2634
- }),
2635
- [`gitlab_disable_project_${label}`]: tool({
2636
- description: `Disable a ${label} in a project.
2637
- Requires Maintainer or Owner role.
2638
- Resolves the consumer ID internally from the ${label} ID.`,
2639
- args: {
2640
- project_id: z2.string().describe('Project path (e.g., "gitlab-org/gitlab")'),
2641
- [`${label}_id`]: z2.string().describe(`${Label} ID: full GID or numeric ID`)
2642
- },
2643
- execute: async (args) => {
2644
- const auth = ensureAuth();
2645
- if (!auth) return "Error: GitLab authentication not available";
2646
- try {
2647
- const result = await disableAiCatalogItemForProject(
2648
- auth.instanceUrl,
2649
- auth.token,
2650
- args.project_id,
2651
- args[`${label}_id`]
2652
- );
2653
- await onChanged?.();
2654
- return JSON.stringify(result, null, 2) + "\n\nNote: Restart opencode for the @ menu to reflect changes.";
2655
- } catch (err) {
2656
- return `Error: ${err.message}`;
2657
- }
2658
- }
2659
- })
2660
- };
2661
- }
2662
- function makeAgentFlowTools(z2, getAuth, readAuthFn, setAuth, onChanged) {
2663
- const ensureAuth = () => {
2664
- let auth = getAuth();
2665
- if (!auth) {
2666
- auth = readAuthFn();
2667
- if (auth) setAuth(auth);
2668
- }
2669
- return auth;
2670
- };
2671
- return {
2672
- ...makeItemTools(z2, "AGENT", "agent", getAuth, ensureAuth, onChanged),
2673
- ...makeItemTools(z2, "FLOW", "flow", getAuth, ensureAuth, onChanged)
2674
- };
2675
- }
2676
- function readAuth() {
2677
- try {
2678
- const authPath = join(os.homedir(), ".local", "share", "opencode", "auth.json");
2679
- const data = JSON.parse(readFileSync(authPath, "utf-8"));
2680
- const gitlab = data?.gitlab;
2681
- if (!gitlab) return null;
2682
- const token = gitlab.type === "oauth" ? gitlab.access : gitlab.type === "api" ? gitlab.key : null;
2683
- if (!token) return null;
2684
- const instanceUrl = gitlab.enterpriseUrl ?? process.env.GITLAB_INSTANCE_URL ?? "https://gitlab.com";
2685
- return { token, instanceUrl };
2686
- } catch {
2687
- return null;
2688
- }
2689
- }
2690
- function buildFlowSubagentPrompt(flow, projectPath, projectUrl) {
2691
- return [
2692
- `You execute the "${flow.name}" GitLab flow. Project: ${projectPath} (${projectUrl}).`,
2693
- ``,
2694
- `STEP 1: Call gitlab_get_flow_definition with consumer_id=${flow.consumerId}, foundational=${!!flow.foundational}.`,
2695
- `Parse the YAML config:`,
2696
- `- In "components", find { from: "context:goal", as: "<name>" }. The "as" value is what the goal parameter must contain:`,
2697
- ` "merge_request_iid" -> just the number (e.g. 14)`,
2698
- ` "pipeline_url"/"url"/"issue_url" -> full URL (e.g. ${projectUrl}/-/merge_requests/5)`,
2699
- ` "vulnerability_id" -> just the ID number`,
2700
- ` "goal" -> free-form text`,
2701
- `- In "flow.inputs", find additional_context categories (skip "agent_platform_standard_context").`,
2702
- ``,
2703
- `STEP 2: Resolve the goal to the EXACT value the flow expects (from step 1).`,
2704
- `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.`,
2705
- `Use GitLab API tools if needed to look up IDs or construct URLs from the user's message.`,
2706
- ``,
2707
- `STEP 3: Gather additional_context values (if any from step 1) using available tools.`,
2708
- ``,
2709
- `STEP 4: Call gitlab_execute_project_flow with:`,
2710
- ` project_id: "${projectPath}"`,
2711
- ` consumer_id: ${flow.consumerId}`,
2712
- ` goal: <resolved value from step 2>`,
2713
- ` additional_context (if needed): [{"Category":"<cat>","Content":"{\\"field\\":\\"val\\"}"}]`,
2714
- ``,
2715
- `STEP 5: Call gitlab_get_workflow_status with the workflow_id. Report status and URL: ${projectUrl}/-/automate/agent-sessions/<id>`
2716
- ].join("\n");
2717
- }
2718
- var memo = /* @__PURE__ */ new Map();
2719
- var FLOW_DISPATCH_GUIDELINES = [
2720
- `## GitLab Flow Dispatch Guidelines`,
2721
- ``,
2722
- `CRITICAL: You must NEVER call gitlab_execute_project_flow or gitlab_get_flow_definition directly.`,
2723
- `Flows are ALWAYS executed via the Task tool with subagent_type "general".`,
2724
- `When the user's message contains flow dispatch instructions (starting with "IMPORTANT: You MUST"),`,
2725
- `follow those instructions exactly \u2014 call the Task tool with the provided parameters.`,
2726
- ``,
2727
- `### Multiple Flows or Resources`,
2728
- `When multiple flows need to run (multiple @mentions, or batch across resources), dispatch them`,
2729
- `via a SINGLE "general" subagent. The general subagent can execute multiple tool calls in parallel,`,
2730
- `so all flows fire simultaneously. Do NOT dispatch multiple Task calls \u2014 use ONE Task with a prompt`,
2731
- `that lists all the flows to execute, so the subagent runs them concurrently.`,
2732
- ``,
2733
- `### Batch Operations (Multiple Resources)`,
2734
- `If the user asks to run flows on multiple resources (e.g., "for each MR"), first list the`,
2735
- `resources yourself using GitLab API tools, then dispatch ONE general subagent whose prompt`,
2736
- `includes all flow executions (N flows x M resources) to run in parallel.`
2737
- ].join("\n");
2738
- var AGENT_CREATION_GUIDELINES = `## Creating Custom GitLab Agents
2739
-
2740
- Before calling gitlab_create_agent, you MUST:
2741
- 1. Call gitlab_list_builtin_tools and gitlab_list_project_mcp_servers.
2742
- 2. Ask the user 4 questions using the question tool (one call, all 4 questions):
2743
- - Agent name (suggest one, allow custom)
2744
- - Visibility: Public or Private
2745
- - Tools: show tools grouped by category as multi-select (Search, Issues, MRs, Epics, Files, Git, CI/CD, Security, Audit, Planning, Wiki, API)
2746
- - MCP servers: multi-select from available servers
2747
- 3. Show the generated system prompt and ask for confirmation.
2748
- 4. Only then call gitlab_create_agent. Use full tool GIDs like "gid://gitlab/Ai::Catalog::BuiltInTool/1".
2749
- 5. Ask if the user wants to enable it on the current project.`;
2750
- var FLOW_SCHEMA_REFERENCE = `## Flow YAML Schema Reference
2751
-
2752
- ### Top-level structure (all required unless noted):
2753
- version: "v1" # Always "v1"
2754
- environment: ambient # Always "ambient"
2755
- components: [...] # Array of components (min 1)
2756
- routers: [...] # Array of routers connecting components
2757
- flow:
2758
- entry_point: "component_name" # First component to run
2759
- inputs: [...] # Optional: additional context inputs
2760
- prompts: [...] # Optional: inline prompt definitions
2761
-
2762
- ### Component types:
2763
-
2764
- 1. DeterministicStepComponent \u2014 runs ONE tool, no LLM call:
2765
- - name: "fetch_data" # alphanumeric + underscore only
2766
- type: DeterministicStepComponent
2767
- tool_name: "get_merge_request"
2768
- inputs: # map tool parameters
2769
- - { from: "context:goal", as: "merge_request_iid" }
2770
- - { from: "context:project_id", as: "project_id" }
2771
-
2772
- 2. OneOffComponent \u2014 single LLM call + tool execution:
2773
- - name: "process_data"
2774
- type: OneOffComponent
2775
- prompt_id: "my_prompt" # references inline prompt
2776
- prompt_version: null # null = use inline prompt from prompts section
2777
- toolset: ["read_file", "edit_file"]
2778
- inputs:
2779
- - { from: "context:fetch_data.tool_responses", as: "data" }
2780
- max_correction_attempts: 3 # retries on tool failure (default 3)
2781
-
2782
- 3. AgentComponent \u2014 multi-turn LLM reasoning loop:
2783
- - name: "analyze"
2784
- type: AgentComponent
2785
- prompt_id: "my_agent_prompt"
2786
- prompt_version: null
2787
- toolset: ["read_file", "grep"]
2788
- inputs:
2789
- - { from: "context:goal", as: "user_goal" }
2790
- ui_log_events: ["on_agent_final_answer"]
2791
-
2792
- ### IOKey syntax (inputs/from values):
2793
- "context:goal" # The flow goal (user input)
2794
- "context:project_id" # Project ID (auto-injected)
2795
- "context:<component_name>.tool_responses" # Tool output from a component
2796
- "context:<component_name>.final_answer" # Agent's final text answer
2797
- "context:<component_name>.execution_result" # OneOff execution result
2798
- { from: "context:x", as: "var_name" } # Rename for prompt template
2799
- { from: "true", as: "flag", literal: true } # Literal value
2800
-
2801
- ### Router patterns:
2802
- Direct: { from: "step1", to: "step2" }
2803
- To end: { from: "last_step", to: "end" }
2804
- Conditional: { from: "step1", condition: { input: "context:step1.final_answer.decision", routes: { "yes": "step2", "no": "end" } } }
3282
+ return JSON.stringify(result, null, 2) + "\n\nNote: Restart opencode for the @ menu to reflect changes.";
3283
+ } catch (err) {
3284
+ return `Error: ${err.message}`;
3285
+ }
3286
+ }
3287
+ }),
3288
+ [`gitlab_disable_project_${label}`]: tool3({
3289
+ description: `Disable a ${label} in a project.
3290
+ Requires Maintainer or Owner role.
3291
+ Resolves the consumer ID internally from the ${label} ID.`,
3292
+ args: {
3293
+ project_id: z3.string().describe('Project path (e.g., "gitlab-org/gitlab")'),
3294
+ [`${label}_id`]: z3.string().describe(`${Label} ID: full GID or numeric ID`)
3295
+ },
3296
+ execute: async (args) => {
3297
+ const auth = ensureAuth();
3298
+ if (!auth) return "Error: GitLab authentication not available";
3299
+ try {
3300
+ const result = await disableAiCatalogItemForProject(
3301
+ auth.instanceUrl,
3302
+ auth.token,
3303
+ args.project_id,
3304
+ args[`${label}_id`]
3305
+ );
3306
+ await onChanged?.();
3307
+ return JSON.stringify(result, null, 2) + "\n\nNote: Restart opencode for the @ menu to reflect changes.";
3308
+ } catch (err) {
3309
+ return `Error: ${err.message}`;
3310
+ }
3311
+ }
3312
+ })
3313
+ };
3314
+ }
3315
+ function makeCatalogItemTools(ctx) {
3316
+ return {
3317
+ ...makeItemTools("AGENT", "agent", ctx.getAuth, ctx.ensureAuth, ctx.refreshAgents),
3318
+ ...makeItemTools("FLOW", "flow", ctx.getAuth, ctx.ensureAuth, ctx.refreshAgents)
3319
+ };
3320
+ }
2805
3321
 
2806
- ### Inline prompts (in prompts section):
2807
- - prompt_id: "my_prompt"
2808
- name: "My Prompt"
2809
- unit_primitives: ["duo_agent_platform"] # Always this value
2810
- prompt_template:
2811
- system: "You are a helpful assistant. {{var_name}} is available."
2812
- user: "Process: {{data}}"
2813
- placeholder: "history" # Optional, for AgentComponent conversation history
3322
+ // src/tools/mcp-tools.ts
3323
+ import { tool as tool4 } from "@opencode-ai/plugin";
3324
+ var z4 = tool4.schema;
3325
+ function makeMcpTools(getCachedAgents) {
3326
+ return {
3327
+ gitlab_list_project_mcp_servers: tool4({
3328
+ 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.",
3329
+ args: {
3330
+ project_id: z4.string().describe('Project path (e.g., "gitlab-org/gitlab")')
3331
+ },
3332
+ execute: async (_args) => {
3333
+ const serverMap = /* @__PURE__ */ new Map();
3334
+ for (const agent of getCachedAgents()) {
3335
+ if (!agent.mcpServers?.length) continue;
3336
+ for (const server of agent.mcpServers) {
3337
+ const existing = serverMap.get(server.id);
3338
+ if (existing) {
3339
+ if (!existing.usedBy.includes(agent.name)) existing.usedBy.push(agent.name);
3340
+ } else {
3341
+ serverMap.set(server.id, { ...server, usedBy: [agent.name] });
3342
+ }
3343
+ }
3344
+ }
3345
+ const servers = [...serverMap.values()];
3346
+ if (!servers.length) {
3347
+ return "No MCP servers found for agents enabled in this project.";
3348
+ }
3349
+ return JSON.stringify(servers, null, 2);
3350
+ }
3351
+ })
3352
+ };
3353
+ }
2814
3354
 
2815
- ### Tool names: use names from gitlab_list_builtin_tools (e.g., "read_file", "get_merge_request", "create_merge_request_note").`;
2816
- var FLOW_EXAMPLE_LINEAR = `## Example: Simple linear flow (fetch MR \u2192 analyze \u2192 post comment)
2817
- \`\`\`yaml
2818
- version: "v1"
2819
- environment: ambient
2820
- components:
2821
- - name: fetch_mr
2822
- type: DeterministicStepComponent
2823
- tool_name: build_review_merge_request_context
2824
- inputs:
2825
- - { from: "context:project_id", as: "project_id" }
2826
- - { from: "context:goal", as: "merge_request_iid" }
2827
- ui_log_events: ["on_tool_execution_success", "on_tool_execution_failed"]
2828
- - name: analyze_and_comment
2829
- type: OneOffComponent
2830
- prompt_id: review_prompt
2831
- prompt_version: null
2832
- toolset: ["create_merge_request_note"]
2833
- inputs:
2834
- - { from: "context:fetch_mr.tool_responses", as: "mr_data" }
2835
- - { from: "context:project_id", as: "project_id" }
2836
- - { from: "context:goal", as: "merge_request_iid" }
2837
- max_correction_attempts: 3
2838
- ui_log_events: ["on_tool_execution_success"]
2839
- routers:
2840
- - { from: fetch_mr, to: analyze_and_comment }
2841
- - { from: analyze_and_comment, to: end }
2842
- flow:
2843
- entry_point: fetch_mr
2844
- prompts:
2845
- - prompt_id: review_prompt
2846
- name: MR Review Prompt
2847
- unit_primitives: ["duo_agent_platform"]
2848
- prompt_template:
2849
- system: |
2850
- You review merge requests. Analyze the MR data and post a concise review comment.
2851
- Focus on code quality, potential bugs, and improvements.
2852
- user: "Review MR !{{merge_request_iid}} in project {{project_id}}: {{mr_data}}"
2853
- \`\`\``;
2854
- var FLOW_EXAMPLE_CONDITIONAL = `## Example: Conditional flow (gather data \u2192 decide \u2192 branch)
2855
- \`\`\`yaml
2856
- version: "v1"
2857
- environment: ambient
2858
- components:
2859
- - name: gather_context
2860
- type: DeterministicStepComponent
2861
- tool_name: get_vulnerability_details
2862
- inputs:
2863
- - { from: "context:goal", as: "vulnerability_id" }
2864
- ui_log_events: ["on_tool_execution_success"]
2865
- - name: evaluate
2866
- type: AgentComponent
2867
- prompt_id: eval_prompt
2868
- prompt_version: null
2869
- toolset: []
2870
- inputs:
2871
- - { from: "context:gather_context.tool_responses", as: "vuln_data" }
2872
- ui_log_events: ["on_agent_final_answer"]
2873
- - name: create_fix
2874
- type: AgentComponent
2875
- prompt_id: fix_prompt
2876
- prompt_version: null
2877
- toolset: ["read_file", "edit_file", "grep"]
2878
- inputs:
2879
- - { from: "context:gather_context.tool_responses", as: "vuln_data" }
2880
- ui_log_events: ["on_agent_final_answer", "on_tool_execution_success"]
2881
- routers:
2882
- - { from: gather_context, to: evaluate }
2883
- - from: evaluate
2884
- condition:
2885
- input: "context:evaluate.final_answer"
2886
- routes:
2887
- "fix_needed": create_fix
2888
- "false_positive": end
2889
- default_route: end
2890
- - { from: create_fix, to: end }
2891
- flow:
2892
- entry_point: gather_context
2893
- prompts:
2894
- - prompt_id: eval_prompt
2895
- name: Vulnerability Evaluator
2896
- unit_primitives: ["duo_agent_platform"]
2897
- prompt_template:
2898
- system: |
2899
- Evaluate if a vulnerability needs fixing. Respond with exactly "fix_needed" or "false_positive".
2900
- user: "Evaluate: {{vuln_data}}"
2901
- - prompt_id: fix_prompt
2902
- name: Fix Generator
2903
- unit_primitives: ["duo_agent_platform"]
2904
- prompt_template:
2905
- system: |
2906
- You fix security vulnerabilities. Read the relevant code and apply the fix.
2907
- user: "Fix this vulnerability: {{vuln_data}}"
2908
- placeholder: history
2909
- \`\`\``;
3355
+ // src/index.ts
3356
+ var memo = /* @__PURE__ */ new Map();
2910
3357
  var plugin = async (input) => {
2911
3358
  let authCache = null;
2912
3359
  let projectPath;
@@ -2916,6 +3363,13 @@ var plugin = async (input) => {
2916
3363
  let cachedAgents = [];
2917
3364
  let cfgRef = null;
2918
3365
  let baseModelIdRef;
3366
+ function ensureAuth() {
3367
+ if (!authCache) {
3368
+ const auth = readAuth();
3369
+ if (auth) authCache = auth;
3370
+ }
3371
+ return authCache;
3372
+ }
2919
3373
  async function load() {
2920
3374
  const auth = readAuth();
2921
3375
  if (!auth) return null;
@@ -2989,6 +3443,14 @@ var plugin = async (input) => {
2989
3443
  }
2990
3444
  }
2991
3445
  }
3446
+ const ctx = {
3447
+ getAuth: () => authCache,
3448
+ ensureAuth,
3449
+ getFlowAgents: () => flowAgents,
3450
+ getCachedAgents: () => cachedAgents,
3451
+ getNamespaceId: () => namespaceId,
3452
+ refreshAgents
3453
+ };
2992
3454
  return {
2993
3455
  async config(cfg) {
2994
3456
  const result = await load();
@@ -3032,491 +3494,18 @@ var plugin = async (input) => {
3032
3494
  }
3033
3495
  }
3034
3496
  },
3035
- "chat.message": async (_input, output) => {
3036
- const indicesToRemove = [];
3037
- const flowMentions = [];
3038
- for (let i = 0; i < output.parts.length; i++) {
3039
- const part = output.parts[i];
3040
- if (part.type !== "agent") continue;
3041
- const flow = flowAgents.get(part.name);
3042
- if (!flow || !flow.consumerId || !projectPath) continue;
3043
- flowMentions.push({ idx: i, flow, displayName: part.name });
3044
- if (i + 1 < output.parts.length) {
3045
- const next = output.parts[i + 1];
3046
- if (next.type === "text" && next.synthetic && next.text?.includes("call the task tool with subagent")) {
3047
- indicesToRemove.push(i + 1);
3048
- }
3049
- }
3050
- }
3051
- if (flowMentions.length === 0) {
3052
- return;
3053
- }
3054
- if (flowMentions.length === 1) {
3055
- const { idx, flow } = flowMentions[0];
3056
- const baseUrl2 = authCache?.instanceUrl?.replace(/\/$/, "") ?? "https://gitlab.com";
3057
- const projectUrl2 = `${baseUrl2}/${projectPath}`;
3058
- const rawText2 = output.parts.filter((p) => p.type === "text" && !p.synthetic).map((p) => p.text).join(" ").trim() || "Execute the flow";
3059
- const subagentPrompt = buildFlowSubagentPrompt(flow, projectPath, projectUrl2) + `
3060
-
3061
- User goal: "${rawText2}"`;
3062
- const resultText = [
3063
- `IMPORTANT: You MUST call the Task tool RIGHT NOW to dispatch a subagent. Do NOT execute these steps yourself.`,
3064
- ``,
3065
- `Call the Task tool with:`,
3066
- ` subagent_type: "general"`,
3067
- ` description: "Execute ${flow.name} flow"`,
3068
- ` prompt: ${JSON.stringify(subagentPrompt)}`,
3069
- ``,
3070
- `Do not do anything else. Just call the Task tool with the above parameters.`
3071
- ].join("\n");
3072
- const original2 = output.parts[idx];
3073
- output.parts[idx] = { ...original2, type: "text", text: resultText };
3074
- delete output.parts[idx].name;
3075
- delete output.parts[idx].source;
3076
- for (const rmIdx of indicesToRemove.reverse()) {
3077
- output.parts.splice(rmIdx, 1);
3078
- }
3079
- return;
3080
- }
3081
- const baseUrl = authCache?.instanceUrl?.replace(/\/$/, "") ?? "https://gitlab.com";
3082
- const projectUrl = `${baseUrl}/${projectPath}`;
3083
- const flowNames = new Set(flowMentions.map((m) => m.displayName));
3084
- let rawText = output.parts.filter((p) => p.type === "text" && !p.synthetic).map((p) => p.text).join(" ").trim() || "Execute the flows";
3085
- for (const name of flowNames) {
3086
- rawText = rawText.replace(new RegExp(`@${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "g"), "").trim();
3087
- }
3088
- rawText = rawText.replace(/\s{2,}/g, " ").trim() || "Execute the flows";
3089
- const flowList = flowMentions.map(
3090
- ({ flow, displayName }, i) => `${i + 1}. "${displayName}" \u2014 consumer_id=${flow.consumerId}, foundational=${!!flow.foundational}`
3091
- );
3092
- const batchPrompt = [
3093
- `Execute ${flowMentions.length} GitLab flows on project ${projectPath} (${projectUrl}).`,
3094
- `User goal: "${rawText}"`,
3095
- ``,
3096
- `Flows to execute:`,
3097
- ...flowList,
3098
- ``,
3099
- `EXECUTION PLAN:`,
3100
- `1. Call gitlab_get_flow_definition for ALL flows listed above in a SINGLE response (${flowMentions.length} tool calls at once).`,
3101
- ` Parse each YAML to find what "context:goal" maps to (the "as" field in components).`,
3102
- ``,
3103
- `2. If the user's goal involves multiple resources (e.g., "for each MR"), list them using GitLab API tools.`,
3104
- ``,
3105
- `3. Call gitlab_execute_project_flow for EVERY flow+resource combination in a SINGLE response.`,
3106
- ` For each call, set the goal to the EXACT value the flow expects (e.g., just "14" for merge_request_iid).`,
3107
- ` project_id: "${projectPath}"`,
3108
- ` You MUST emit ALL execute calls in ONE response \u2014 do NOT wait for one to finish before calling the next.`,
3109
- ``,
3110
- `4. Collect all workflow_ids from step 3. Call gitlab_get_workflow_status for ALL of them in a SINGLE response.`,
3111
- ``,
3112
- `5. Present a summary table: flow name, resource, status, URL (${projectUrl}/-/automate/agent-sessions/<id>).`,
3113
- ``,
3114
- `CRITICAL: In steps 1, 3, and 4 you MUST make multiple tool calls in the SAME response for parallel execution.`
3115
- ].join("\n");
3116
- const combinedText = [
3117
- `IMPORTANT: You MUST call the Task tool RIGHT NOW with subagent_type "general" to dispatch all flows in parallel.`,
3118
- `Do NOT call flow tools yourself. Do NOT dispatch multiple Task calls \u2014 use ONE.`,
3119
- ``,
3120
- `Call the Task tool with:`,
3121
- ` subagent_type: "general"`,
3122
- ` description: "Execute ${flowMentions.length} flows in parallel"`,
3123
- ` prompt: ${JSON.stringify(batchPrompt)}`,
3124
- ``,
3125
- `Do not do anything else. Just call the Task tool with the above parameters.`
3126
- ].join("\n");
3127
- const firstIdx = flowMentions[0].idx;
3128
- const original = output.parts[firstIdx];
3129
- output.parts[firstIdx] = { ...original, type: "text", text: combinedText };
3130
- delete output.parts[firstIdx].name;
3131
- delete output.parts[firstIdx].source;
3132
- for (let i = flowMentions.length - 1; i >= 1; i--) {
3133
- indicesToRemove.push(flowMentions[i].idx);
3134
- }
3135
- for (const idx of [...new Set(indicesToRemove)].sort((a, b) => b - a)) {
3136
- output.parts.splice(idx, 1);
3137
- }
3138
- },
3139
- "chat.params": async (input2, _output) => {
3140
- if (!gitlabAgentNames.has(input2.agent)) return;
3141
- const model = input2.model;
3142
- const modelId = model?.modelID ?? model?.id ?? "";
3143
- const isDWS = modelId.includes("duo-workflow");
3144
- if (!isDWS) {
3145
- const name = model?.name ?? modelId ?? "unknown";
3146
- throw new Error(
3147
- `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.`
3148
- );
3149
- }
3150
- },
3151
- "experimental.chat.system.transform": async (_input, output) => {
3152
- if (flowAgents.size) {
3153
- output.system.push(FLOW_DISPATCH_GUIDELINES);
3154
- }
3155
- if (authCache) {
3156
- output.system.push(AGENT_CREATION_GUIDELINES);
3157
- }
3158
- },
3497
+ "chat.message": makeChatMessageHook(
3498
+ () => authCache,
3499
+ flowAgents,
3500
+ () => projectPath
3501
+ ),
3502
+ "chat.params": makeChatParamsHook(gitlabAgentNames),
3503
+ "experimental.chat.system.transform": makeSystemTransformHook(flowAgents, () => authCache),
3159
3504
  tool: {
3160
- gitlab_execute_project_flow: tool({
3161
- 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.",
3162
- args: {
3163
- project_id: z.string().describe('Project path (e.g., "gitlab-org/gitlab")'),
3164
- consumer_id: z.number().describe("AI Catalog ItemConsumer numeric ID"),
3165
- goal: z.string().describe("User prompt/goal for the flow, include relevant URLs"),
3166
- additional_context: z.string().optional().describe(
3167
- 'JSON array of flow inputs: [{"Category":"merge_request","Content":"{\\"url\\":\\"https://...\\"}"}]'
3168
- ),
3169
- issue_id: z.number().optional().describe("Issue IID for context"),
3170
- merge_request_id: z.number().optional().describe("Merge request IID for context")
3171
- },
3172
- execute: async (args) => {
3173
- if (!authCache) {
3174
- const auth = readAuth();
3175
- if (!auth) return "Error: GitLab authentication not available";
3176
- authCache = auth;
3177
- }
3178
- let flowInputs;
3179
- if (args.additional_context) {
3180
- try {
3181
- flowInputs = JSON.parse(args.additional_context);
3182
- } catch {
3183
- return "Error: additional_context must be a valid JSON array";
3184
- }
3185
- }
3186
- try {
3187
- const result = await executeFlow(
3188
- authCache.instanceUrl,
3189
- authCache.token,
3190
- args.project_id,
3191
- args.consumer_id,
3192
- args.goal,
3193
- {
3194
- issueId: args.issue_id,
3195
- mergeRequestId: args.merge_request_id,
3196
- namespaceId,
3197
- flowInputs
3198
- }
3199
- );
3200
- return JSON.stringify(result, null, 2);
3201
- } catch (err) {
3202
- return `Error executing flow: ${err.message}`;
3203
- }
3204
- }
3205
- }),
3206
- gitlab_get_flow_definition: tool({
3207
- 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.",
3208
- args: {
3209
- consumer_id: z.number().describe("AI Catalog ItemConsumer numeric ID"),
3210
- foundational: z.boolean().describe("true for GitLab foundational flows, false for custom flows")
3211
- },
3212
- execute: async (args) => {
3213
- if (!authCache) {
3214
- const auth = readAuth();
3215
- if (!auth) return "Error: GitLab authentication not available";
3216
- authCache = auth;
3217
- }
3218
- const flow = [...flowAgents.values()].find((f) => f.consumerId === args.consumer_id);
3219
- try {
3220
- const result = await getFlowDefinition(authCache.instanceUrl, authCache.token, {
3221
- consumerId: args.consumer_id,
3222
- flowName: flow?.name,
3223
- foundational: args.foundational,
3224
- catalogItemVersionId: flow?.catalogItemVersionId,
3225
- itemIdentifier: flow?.identifier,
3226
- workflowDefinition: flow?.workflowDefinition
3227
- });
3228
- return JSON.stringify(result, null, 2);
3229
- } catch (err) {
3230
- return `Error getting flow definition: ${err.message}`;
3231
- }
3232
- }
3233
- }),
3234
- gitlab_get_workflow_status: tool({
3235
- 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.",
3236
- args: {
3237
- workflow_id: z.number().describe("Workflow numeric ID (from gitlab_execute_project_flow result)")
3238
- },
3239
- execute: async (args) => {
3240
- if (!authCache) {
3241
- const auth = readAuth();
3242
- if (!auth) return "Error: GitLab authentication not available";
3243
- authCache = auth;
3244
- }
3245
- try {
3246
- const result = await getWorkflowStatus(
3247
- authCache.instanceUrl,
3248
- authCache.token,
3249
- args.workflow_id
3250
- );
3251
- return JSON.stringify(result, null, 2);
3252
- } catch (err) {
3253
- return `Error getting workflow status: ${err.message}`;
3254
- }
3255
- }
3256
- }),
3257
- gitlab_list_project_mcp_servers: tool({
3258
- 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.",
3259
- args: {
3260
- project_id: z.string().describe('Project path (e.g., "gitlab-org/gitlab")')
3261
- },
3262
- execute: async (_args) => {
3263
- const serverMap = /* @__PURE__ */ new Map();
3264
- for (const agent of cachedAgents) {
3265
- if (!agent.mcpServers?.length) continue;
3266
- for (const server of agent.mcpServers) {
3267
- const existing = serverMap.get(server.id);
3268
- if (existing) {
3269
- if (!existing.usedBy.includes(agent.name)) existing.usedBy.push(agent.name);
3270
- } else {
3271
- serverMap.set(server.id, { ...server, usedBy: [agent.name] });
3272
- }
3273
- }
3274
- }
3275
- const servers = [...serverMap.values()];
3276
- if (!servers.length) {
3277
- return "No MCP servers found for agents enabled in this project.";
3278
- }
3279
- return JSON.stringify(servers, null, 2);
3280
- }
3281
- }),
3282
- gitlab_create_agent: tool({
3283
- 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.",
3284
- args: {
3285
- project_id: z.string().describe('Project path (e.g., "gitlab-org/gitlab")'),
3286
- name: z.string().describe("Display name for the agent"),
3287
- description: z.string().describe("Description of what the agent does"),
3288
- public: z.boolean().describe("Whether the agent is publicly visible in the AI Catalog"),
3289
- system_prompt: z.string().describe("System prompt that defines the agent's behavior"),
3290
- user_prompt: z.string().optional().describe("User prompt template (optional)"),
3291
- tools: z.array(z.string()).optional().describe(
3292
- '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.'
3293
- ),
3294
- mcp_tools: z.array(z.string()).optional().describe("Array of MCP tool names to enable"),
3295
- mcp_servers: z.array(z.string()).optional().describe(
3296
- '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.'
3297
- ),
3298
- release: z.boolean().optional().describe("Whether to release the version immediately (default: false)"),
3299
- confirmed: z.boolean().optional().describe(
3300
- "Set to true only after the user has reviewed and confirmed all parameters. Omit or set false on first call."
3301
- )
3302
- },
3303
- execute: async (args) => {
3304
- if (!args.confirmed) {
3305
- return [
3306
- "STOP: Do not create the agent yet. You must ask the user to confirm the configuration first.",
3307
- "",
3308
- "Follow these steps NOW:",
3309
- "1. Call gitlab_list_builtin_tools and gitlab_list_project_mcp_servers to discover options.",
3310
- "2. Use the question tool to ask the user ALL 4 of these (in one call):",
3311
- " - Agent name (suggest one, allow custom input)",
3312
- " - Visibility: Public or Private",
3313
- " - Tools: group by category (Search, Issues, MRs, Epics, Files, Git, CI/CD, Security, Audit, Planning, Wiki, API) as multi-select",
3314
- " - MCP servers: multi-select from available servers",
3315
- "3. Generate a system prompt and show it to the user for approval.",
3316
- "4. Call gitlab_create_agent again with confirmed=true after the user approves."
3317
- ].join("\n");
3318
- }
3319
- const auth = authCache ?? readAuth();
3320
- if (!auth) throw new Error("Not authenticated");
3321
- const result = await createAgent(auth.instanceUrl, auth.token, args.project_id, {
3322
- name: args.name,
3323
- description: args.description,
3324
- public: args.public,
3325
- systemPrompt: args.system_prompt,
3326
- userPrompt: args.user_prompt,
3327
- tools: args.tools,
3328
- mcpTools: args.mcp_tools,
3329
- mcpServers: args.mcp_servers,
3330
- release: args.release
3331
- });
3332
- await refreshAgents();
3333
- return JSON.stringify(result, null, 2);
3334
- }
3335
- }),
3336
- gitlab_update_agent: tool({
3337
- description: "Update an existing custom agent in the GitLab AI Catalog.\nOnly provided fields are updated; omitted fields remain unchanged.",
3338
- args: {
3339
- id: z.string().describe("Agent ID (numeric or full GID)"),
3340
- name: z.string().optional().describe("New display name"),
3341
- description: z.string().optional().describe("New description"),
3342
- public: z.boolean().optional().describe("Whether publicly visible"),
3343
- system_prompt: z.string().optional().describe("New system prompt"),
3344
- user_prompt: z.string().optional().describe("New user prompt template"),
3345
- tools: z.array(z.string()).optional().describe(
3346
- 'New set of built-in tool Global IDs (full GIDs like "gid://gitlab/Ai::Catalog::BuiltInTool/1")'
3347
- ),
3348
- mcp_tools: z.array(z.string()).optional().describe("New set of MCP tool names"),
3349
- mcp_servers: z.array(z.string()).optional().describe(
3350
- 'New set of MCP server Global IDs (full GIDs like "gid://gitlab/Ai::Catalog::McpServer/1")'
3351
- ),
3352
- release: z.boolean().optional().describe("Whether to release the latest version"),
3353
- version_bump: z.enum(["MAJOR", "MINOR", "PATCH"]).optional().describe("Version bump type")
3354
- },
3355
- execute: async (args) => {
3356
- const auth = authCache ?? readAuth();
3357
- if (!auth) throw new Error("Not authenticated");
3358
- const result = await updateAgent(auth.instanceUrl, auth.token, args.id, {
3359
- name: args.name,
3360
- description: args.description,
3361
- public: args.public,
3362
- systemPrompt: args.system_prompt,
3363
- userPrompt: args.user_prompt,
3364
- tools: args.tools,
3365
- mcpTools: args.mcp_tools,
3366
- mcpServers: args.mcp_servers,
3367
- release: args.release,
3368
- versionBump: args.version_bump
3369
- });
3370
- await refreshAgents();
3371
- return JSON.stringify(result, null, 2);
3372
- }
3373
- }),
3374
- gitlab_list_builtin_tools: tool({
3375
- 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.",
3376
- args: {},
3377
- execute: async () => {
3378
- const auth = authCache ?? readAuth();
3379
- if (!auth) throw new Error("Not authenticated");
3380
- const tools = await listBuiltInTools(auth.instanceUrl, auth.token);
3381
- if (!tools.length) return "No built-in tools available.";
3382
- return JSON.stringify(tools, null, 2);
3383
- }
3384
- }),
3385
- gitlab_design_flow: tool({
3386
- 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.",
3387
- args: {
3388
- action: z.enum(["get_schema", "validate"]).describe(
3389
- '"get_schema" returns the schema reference and examples. "validate" validates a YAML definition.'
3390
- ),
3391
- definition: z.string().optional().describe("YAML definition to validate (required when action=validate)")
3392
- },
3393
- execute: async (args) => {
3394
- if (args.action === "validate") {
3395
- if (!args.definition) return "Error: definition is required for validate action.";
3396
- const result = validateFlowYaml(args.definition);
3397
- if (result.valid) return "VALID: Flow definition passes schema validation.";
3398
- return `INVALID: ${result.errors.length} error(s):
3399
- ${result.errors.map((e, i) => ` ${i + 1}. ${e}`).join("\n")}`;
3400
- }
3401
- return [
3402
- "Follow this multi-round workflow to design a flow:",
3403
- "",
3404
- "ROUND 1: Call gitlab_list_builtin_tools to discover available tool names for the flow.",
3405
- " Then use the question tool to ask the user:",
3406
- " - Flow name",
3407
- " - Visibility: Public or Private",
3408
- " - What the flow should do (step-by-step description)",
3409
- " - What GitLab resource it operates on (MR, issue, pipeline, vulnerability, etc.)",
3410
- "",
3411
- "ROUND 2: Based on the user's answers, propose a component architecture in plain text:",
3412
- " - List each step with its type (DeterministicStep, OneOff, or Agent)",
3413
- " - Explain what each step does and what tools it uses",
3414
- " - Show the routing (linear or conditional)",
3415
- " Ask the user to confirm or adjust.",
3416
- "",
3417
- "ROUND 3: Generate the full YAML definition using the schema below.",
3418
- " Call gitlab_design_flow with action='validate' to check it.",
3419
- " Show the validated YAML to the user for final approval.",
3420
- " Then call gitlab_create_flow with confirmed=true.",
3421
- "",
3422
- "=== FLOW YAML SCHEMA ===",
3423
- FLOW_SCHEMA_REFERENCE,
3424
- "",
3425
- "=== EXAMPLE: Linear flow ===",
3426
- FLOW_EXAMPLE_LINEAR,
3427
- "",
3428
- "=== EXAMPLE: Conditional flow ===",
3429
- FLOW_EXAMPLE_CONDITIONAL
3430
- ].join("\n");
3431
- }
3432
- }),
3433
- gitlab_create_flow: tool({
3434
- 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.",
3435
- args: {
3436
- project_id: z.string().describe('Project path (e.g., "gitlab-org/gitlab")'),
3437
- name: z.string().describe("Display name for the flow"),
3438
- description: z.string().describe("Description of what the flow does"),
3439
- public: z.boolean().describe("Whether publicly visible in the AI Catalog"),
3440
- definition: z.string().describe("Flow YAML definition (validated via gitlab_design_flow)"),
3441
- release: z.boolean().optional().describe("Whether to release the version immediately"),
3442
- confirmed: z.boolean().optional().describe("Set true only after user has reviewed the YAML")
3443
- },
3444
- execute: async (args) => {
3445
- if (!args.confirmed) {
3446
- return [
3447
- "STOP: Do not create the flow yet.",
3448
- "",
3449
- "Call gitlab_design_flow with action='get_schema' first to get the interactive workflow.",
3450
- "Follow the multi-round design process, then call this tool with confirmed=true."
3451
- ].join("\n");
3452
- }
3453
- const validation = validateFlowYaml(args.definition);
3454
- if (!validation.valid) {
3455
- return `Flow YAML validation failed:
3456
- ${validation.errors.map((e, i) => ` ${i + 1}. ${e}`).join("\n")}
3457
-
3458
- Fix the errors and try again.`;
3459
- }
3460
- const auth = authCache ?? readAuth();
3461
- if (!auth) throw new Error("Not authenticated");
3462
- const result = await createFlow(auth.instanceUrl, auth.token, args.project_id, {
3463
- name: args.name,
3464
- description: args.description,
3465
- public: args.public,
3466
- definition: args.definition,
3467
- release: args.release
3468
- });
3469
- await refreshAgents();
3470
- const json = JSON.stringify(result, null, 2);
3471
- return `${json}
3472
-
3473
- Flow created successfully. Ask the user if they want to enable it on the current project using gitlab_enable_project_flow.`;
3474
- }
3475
- }),
3476
- gitlab_update_flow: tool({
3477
- 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.",
3478
- args: {
3479
- id: z.string().describe("Flow ID (numeric or full GID)"),
3480
- name: z.string().optional().describe("New display name"),
3481
- description: z.string().optional().describe("New description"),
3482
- public: z.boolean().optional().describe("Whether publicly visible"),
3483
- definition: z.string().optional().describe("New flow YAML definition"),
3484
- release: z.boolean().optional().describe("Whether to release the latest version"),
3485
- version_bump: z.enum(["MAJOR", "MINOR", "PATCH"]).optional().describe("Version bump type")
3486
- },
3487
- execute: async (args) => {
3488
- if (args.definition) {
3489
- const validation = validateFlowYaml(args.definition);
3490
- if (!validation.valid) {
3491
- return `Flow YAML validation failed:
3492
- ${validation.errors.map((e, i) => ` ${i + 1}. ${e}`).join("\n")}
3493
-
3494
- Fix the errors and try again.`;
3495
- }
3496
- }
3497
- const auth = authCache ?? readAuth();
3498
- if (!auth) throw new Error("Not authenticated");
3499
- const result = await updateFlow(auth.instanceUrl, auth.token, args.id, {
3500
- name: args.name,
3501
- description: args.description,
3502
- public: args.public,
3503
- definition: args.definition,
3504
- release: args.release,
3505
- versionBump: args.version_bump
3506
- });
3507
- await refreshAgents();
3508
- return JSON.stringify(result, null, 2);
3509
- }
3510
- }),
3511
- ...makeAgentFlowTools(
3512
- z,
3513
- () => authCache,
3514
- readAuth,
3515
- (a) => {
3516
- authCache = a;
3517
- },
3518
- refreshAgents
3519
- )
3505
+ ...makeFlowTools(ctx),
3506
+ ...makeMcpTools(() => cachedAgents),
3507
+ ...makeCatalogCrudTools(ctx),
3508
+ ...makeCatalogItemTools(ctx)
3520
3509
  }
3521
3510
  };
3522
3511
  };