opencode-gitlab-dap 1.4.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3123,6 +3123,7 @@ components:
3123
3123
  prompt_version: "^1.0.0"
3124
3124
  response_schema_id: "fix_pipeline_decide_approach"
3125
3125
  response_schema_version: "^1.0.0"
3126
+ response_schema_tracking: true
3126
3127
  inputs:
3127
3128
  - from: "context:fix_pipeline_context.final_answer"
3128
3129
  as: "fix_pipeline_context_results"
@@ -3467,6 +3468,7 @@ components:
3467
3468
  prompt_version: "^1.0.0"
3468
3469
  response_schema_id: "fix_pipeline_next_decide_approach"
3469
3470
  response_schema_version: "^1.0.0"
3471
+ response_schema_tracking: true
3470
3472
  inputs:
3471
3473
  - from: "context:fix_pipeline_context.final_answer"
3472
3474
  as: "fix_pipeline_context_results"
@@ -3911,6 +3913,61 @@ async function fetchCustomAgents(instanceUrl, token, projectId) {
3911
3913
  }
3912
3914
  return agents;
3913
3915
  }
3916
+ var MCP_SERVERS_QUERY = `
3917
+ query AiCatalogMcpServers($projectId: ProjectID!, $after: String) {
3918
+ aiCatalogConfiguredItems(first: 50, projectId: $projectId, itemTypes: [AGENT], after: $after) {
3919
+ pageInfo { hasNextPage endCursor }
3920
+ nodes {
3921
+ item {
3922
+ id
3923
+ name
3924
+ latestVersion {
3925
+ ... on AiCatalogAgentVersion {
3926
+ mcpServers {
3927
+ nodes { id name description url transport authType currentUserConnected }
3928
+ }
3929
+ }
3930
+ }
3931
+ }
3932
+ }
3933
+ }
3934
+ }`;
3935
+ async function fetchMcpServers(instanceUrl, token, projectId, agents) {
3936
+ try {
3937
+ let after = null;
3938
+ const serversByAgent = /* @__PURE__ */ new Map();
3939
+ for (; ; ) {
3940
+ const data = await gql(instanceUrl, token, MCP_SERVERS_QUERY, {
3941
+ projectId,
3942
+ ...after ? { after } : {}
3943
+ });
3944
+ const page = data?.aiCatalogConfiguredItems;
3945
+ if (!page) break;
3946
+ for (const node of page.nodes ?? []) {
3947
+ const item = node.item;
3948
+ const version = item?.latestVersion;
3949
+ if (!version?.mcpServers?.nodes?.length) continue;
3950
+ const servers = version.mcpServers.nodes.map((s) => ({
3951
+ id: s.id,
3952
+ name: s.name,
3953
+ description: s.description ?? "",
3954
+ url: s.url,
3955
+ transport: s.transport,
3956
+ authType: s.authType,
3957
+ currentUserConnected: !!s.currentUserConnected
3958
+ }));
3959
+ serversByAgent.set(item.id, servers);
3960
+ }
3961
+ if (!page.pageInfo?.hasNextPage) break;
3962
+ after = page.pageInfo.endCursor;
3963
+ }
3964
+ for (const agent of agents) {
3965
+ const servers = serversByAgent.get(agent.identifier);
3966
+ if (servers?.length) agent.mcpServers = servers;
3967
+ }
3968
+ } catch {
3969
+ }
3970
+ }
3914
3971
  async function getFlowDefinition(instanceUrl, token, opts) {
3915
3972
  const name = opts.flowName ?? `consumer-${opts.consumerId}`;
3916
3973
  let config = null;
@@ -4056,7 +4113,9 @@ async function fetchCatalogAgents(instanceUrl, token, projectId) {
4056
4113
  fetchFoundationalChatAgents(instanceUrl, token, projectId),
4057
4114
  fetchCustomAgents(instanceUrl, token, projectId)
4058
4115
  ]);
4059
- return [...foundational, ...custom];
4116
+ const agents = [...foundational, ...custom];
4117
+ await fetchMcpServers(instanceUrl, token, projectId, agents);
4118
+ return agents;
4060
4119
  } catch {
4061
4120
  return [];
4062
4121
  }
@@ -4195,6 +4254,67 @@ query resolveProjectIds($projectPath: ID!) {
4195
4254
  namespace { id }
4196
4255
  }
4197
4256
  }`;
4257
+ var CREATE_AGENT_MUTATION = `
4258
+ mutation AiCatalogAgentCreate($input: AiCatalogAgentCreateInput!) {
4259
+ aiCatalogAgentCreate(input: $input) {
4260
+ errors
4261
+ item {
4262
+ id
4263
+ name
4264
+ description
4265
+ itemType
4266
+ public
4267
+ project { id, nameWithNamespace, webUrl }
4268
+ latestVersion {
4269
+ id
4270
+ humanVersionName
4271
+ released
4272
+ ... on AiCatalogAgentVersion {
4273
+ systemPrompt
4274
+ userPrompt
4275
+ tools { nodes { id, name, description } }
4276
+ mcpServers { nodes { id, name, url } }
4277
+ }
4278
+ }
4279
+ }
4280
+ }
4281
+ }`;
4282
+ var UPDATE_AGENT_MUTATION = `
4283
+ mutation AiCatalogAgentUpdate($input: AiCatalogAgentUpdateInput!) {
4284
+ aiCatalogAgentUpdate(input: $input) {
4285
+ errors
4286
+ item {
4287
+ id
4288
+ name
4289
+ description
4290
+ itemType
4291
+ public
4292
+ project { id, nameWithNamespace, webUrl }
4293
+ latestVersion {
4294
+ id
4295
+ humanVersionName
4296
+ released
4297
+ ... on AiCatalogAgentVersion {
4298
+ systemPrompt
4299
+ userPrompt
4300
+ tools { nodes { id, name, description } }
4301
+ mcpServers { nodes { id, name, url } }
4302
+ }
4303
+ }
4304
+ }
4305
+ }
4306
+ }`;
4307
+ var LIST_BUILTIN_TOOLS_QUERY = `
4308
+ query AiCatalogBuiltInTools {
4309
+ aiCatalogBuiltInTools(first: 1000) {
4310
+ nodes {
4311
+ id
4312
+ name
4313
+ title
4314
+ description
4315
+ }
4316
+ }
4317
+ }`;
4198
4318
  function normalizeItemGid(id) {
4199
4319
  if (id.startsWith("gid://")) return id;
4200
4320
  if (!/^\d+$/.test(id)) throw new Error(`Invalid catalog item ID: "${id}"`);
@@ -4267,6 +4387,49 @@ async function disableAiCatalogItemForProject(instanceUrl, token, projectPath, i
4267
4387
  }
4268
4388
  return result.aiCatalogItemConsumerDelete;
4269
4389
  }
4390
+ async function createAgent(instanceUrl, token, projectPath, params) {
4391
+ const projectGid = await resolveProjectGid(instanceUrl, token, projectPath);
4392
+ const input = {
4393
+ projectId: projectGid,
4394
+ name: params.name,
4395
+ description: params.description,
4396
+ public: params.public,
4397
+ systemPrompt: params.systemPrompt
4398
+ };
4399
+ if (params.userPrompt) input.userPrompt = params.userPrompt;
4400
+ if (params.tools?.length) input.tools = params.tools;
4401
+ if (params.mcpTools?.length) input.mcpTools = params.mcpTools;
4402
+ if (params.mcpServers?.length) input.mcpServers = params.mcpServers;
4403
+ if (params.release !== void 0) input.release = params.release;
4404
+ const result = await gql(instanceUrl, token, CREATE_AGENT_MUTATION, { input });
4405
+ if (result.aiCatalogAgentCreate.errors.length > 0) {
4406
+ throw new Error(`Failed to create agent: ${result.aiCatalogAgentCreate.errors.join(", ")}`);
4407
+ }
4408
+ return result.aiCatalogAgentCreate.item;
4409
+ }
4410
+ async function updateAgent(instanceUrl, token, itemId, params) {
4411
+ const gid = normalizeItemGid(itemId);
4412
+ const input = { id: gid };
4413
+ if (params.name !== void 0) input.name = params.name;
4414
+ if (params.description !== void 0) input.description = params.description;
4415
+ if (params.public !== void 0) input.public = params.public;
4416
+ if (params.systemPrompt !== void 0) input.systemPrompt = params.systemPrompt;
4417
+ if (params.userPrompt !== void 0) input.userPrompt = params.userPrompt;
4418
+ if (params.tools !== void 0) input.tools = params.tools;
4419
+ if (params.mcpTools !== void 0) input.mcpTools = params.mcpTools;
4420
+ if (params.mcpServers !== void 0) input.mcpServers = params.mcpServers;
4421
+ if (params.release !== void 0) input.release = params.release;
4422
+ if (params.versionBump) input.versionBump = params.versionBump;
4423
+ const result = await gql(instanceUrl, token, UPDATE_AGENT_MUTATION, { input });
4424
+ if (result.aiCatalogAgentUpdate.errors.length > 0) {
4425
+ throw new Error(`Failed to update agent: ${result.aiCatalogAgentUpdate.errors.join(", ")}`);
4426
+ }
4427
+ return result.aiCatalogAgentUpdate.item;
4428
+ }
4429
+ async function listBuiltInTools(instanceUrl, token) {
4430
+ const result = await gql(instanceUrl, token, LIST_BUILTIN_TOOLS_QUERY, {});
4431
+ return result.aiCatalogBuiltInTools?.nodes ?? [];
4432
+ }
4270
4433
 
4271
4434
  // src/agents.ts
4272
4435
  function resolveModelId(entry) {
@@ -4429,12 +4592,73 @@ function readAuth() {
4429
4592
  return null;
4430
4593
  }
4431
4594
  }
4595
+ function buildFlowSubagentPrompt(flow, projectPath, projectUrl) {
4596
+ return [
4597
+ `You execute the "${flow.name}" GitLab flow. Project: ${projectPath} (${projectUrl}).`,
4598
+ ``,
4599
+ `STEP 1: Call gitlab_get_flow_definition with consumer_id=${flow.consumerId}, foundational=${!!flow.foundational}.`,
4600
+ `Parse the YAML config:`,
4601
+ `- In "components", find { from: "context:goal", as: "<name>" }. The "as" value is what the goal parameter must contain:`,
4602
+ ` "merge_request_iid" -> just the number (e.g. 14)`,
4603
+ ` "pipeline_url"/"url"/"issue_url" -> full URL (e.g. ${projectUrl}/-/merge_requests/5)`,
4604
+ ` "vulnerability_id" -> just the ID number`,
4605
+ ` "goal" -> free-form text`,
4606
+ `- In "flow.inputs", find additional_context categories (skip "agent_platform_standard_context").`,
4607
+ ``,
4608
+ `STEP 2: Resolve the goal to the EXACT value the flow expects (from step 1).`,
4609
+ `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.`,
4610
+ `Use GitLab API tools if needed to look up IDs or construct URLs from the user's message.`,
4611
+ ``,
4612
+ `STEP 3: Gather additional_context values (if any from step 1) using available tools.`,
4613
+ ``,
4614
+ `STEP 4: Call gitlab_execute_project_flow with:`,
4615
+ ` project_id: "${projectPath}"`,
4616
+ ` consumer_id: ${flow.consumerId}`,
4617
+ ` goal: <resolved value from step 2>`,
4618
+ ` additional_context (if needed): [{"Category":"<cat>","Content":"{\\"field\\":\\"val\\"}"}]`,
4619
+ ``,
4620
+ `STEP 5: Call gitlab_get_workflow_status with the workflow_id. Report status and URL: ${projectUrl}/-/automate/agent-sessions/<id>`
4621
+ ].join("\n");
4622
+ }
4432
4623
  var memo = /* @__PURE__ */ new Map();
4624
+ var FLOW_DISPATCH_GUIDELINES = [
4625
+ `## GitLab Flow Dispatch Guidelines`,
4626
+ ``,
4627
+ `CRITICAL: You must NEVER call gitlab_execute_project_flow or gitlab_get_flow_definition directly.`,
4628
+ `Flows are ALWAYS executed via the Task tool with subagent_type "general".`,
4629
+ `When the user's message contains flow dispatch instructions (starting with "IMPORTANT: You MUST"),`,
4630
+ `follow those instructions exactly \u2014 call the Task tool with the provided parameters.`,
4631
+ ``,
4632
+ `### Multiple Flows or Resources`,
4633
+ `When multiple flows need to run (multiple @mentions, or batch across resources), dispatch them`,
4634
+ `via a SINGLE "general" subagent. The general subagent can execute multiple tool calls in parallel,`,
4635
+ `so all flows fire simultaneously. Do NOT dispatch multiple Task calls \u2014 use ONE Task with a prompt`,
4636
+ `that lists all the flows to execute, so the subagent runs them concurrently.`,
4637
+ ``,
4638
+ `### Batch Operations (Multiple Resources)`,
4639
+ `If the user asks to run flows on multiple resources (e.g., "for each MR"), first list the`,
4640
+ `resources yourself using GitLab API tools, then dispatch ONE general subagent whose prompt`,
4641
+ `includes all flow executions (N flows x M resources) to run in parallel.`
4642
+ ].join("\n");
4643
+ var AGENT_CREATION_GUIDELINES = `## Creating Custom GitLab Agents
4644
+
4645
+ Before calling gitlab_create_agent, you MUST:
4646
+ 1. Call gitlab_list_builtin_tools and gitlab_list_project_mcp_servers.
4647
+ 2. Ask the user 4 questions using the question tool (one call, all 4 questions):
4648
+ - Agent name (suggest one, allow custom)
4649
+ - Visibility: Public or Private
4650
+ - Tools: show tools grouped by category as multi-select (Search, Issues, MRs, Epics, Files, Git, CI/CD, Security, Audit, Planning, Wiki, API)
4651
+ - MCP servers: multi-select from available servers
4652
+ 3. Show the generated system prompt and ask for confirmation.
4653
+ 4. Only then call gitlab_create_agent. Use full tool GIDs like "gid://gitlab/Ai::Catalog::BuiltInTool/1".
4654
+ 5. Ask if the user wants to enable it on the current project.`;
4433
4655
  var plugin = async (input) => {
4434
4656
  let authCache = null;
4435
4657
  let projectPath;
4436
4658
  let namespaceId;
4437
4659
  const flowAgents = /* @__PURE__ */ new Map();
4660
+ const gitlabAgentNames = /* @__PURE__ */ new Set();
4661
+ let cachedAgents = [];
4438
4662
  let cfgRef = null;
4439
4663
  let baseModelIdRef;
4440
4664
  async function load2() {
@@ -4469,6 +4693,7 @@ var plugin = async (input) => {
4469
4693
  })
4470
4694
  ]);
4471
4695
  flowAgents.clear();
4696
+ gitlabAgentNames.clear();
4472
4697
  const currentNames = /* @__PURE__ */ new Set();
4473
4698
  for (const agent of result.agents) {
4474
4699
  const isFlow = agent.itemType === "FLOW";
@@ -4477,12 +4702,17 @@ var plugin = async (input) => {
4477
4702
  currentNames.add(dName);
4478
4703
  if (isFlow && agent.consumerId && projectPath) {
4479
4704
  flowAgents.set(dName, agent);
4705
+ const rBaseUrl = authCache?.instanceUrl?.replace(/\/$/, "") ?? "https://gitlab.com";
4706
+ const rProjectUrl = `${rBaseUrl}/${projectPath}`;
4480
4707
  cfgRef.agent[dName] = {
4481
4708
  name: dName,
4482
4709
  description: `[GitLab Flow] ${agent.description}`,
4483
- mode: "subagent"
4710
+ mode: "subagent",
4711
+ prompt: buildFlowSubagentPrompt(agent, projectPath, rProjectUrl),
4712
+ permission: { "*": "allow" }
4484
4713
  };
4485
4714
  } else {
4715
+ gitlabAgentNames.add(dName);
4486
4716
  cfgRef.agent[dName] = {
4487
4717
  name: dName,
4488
4718
  description: agent.foundational ? "[GitLab Foundational Agent]" : "[GitLab Custom Agent]",
@@ -4508,23 +4738,29 @@ var plugin = async (input) => {
4508
4738
  async config(cfg) {
4509
4739
  const result = await load2();
4510
4740
  if (!result?.agents.length) return;
4741
+ cachedAgents = result.agents;
4511
4742
  const baseModelId = resolveModelId(result.entry);
4512
4743
  cfg.agent ??= {};
4513
4744
  cfgRef = cfg;
4514
4745
  baseModelIdRef = baseModelId;
4746
+ const cfgBaseUrl = authCache?.instanceUrl?.replace(/\/$/, "") ?? "https://gitlab.com";
4515
4747
  for (const agent of result.agents) {
4516
4748
  const isFlow = agent.itemType === "FLOW";
4517
4749
  const icon = agent.foundational ? " \u{1F98A}" : "";
4518
4750
  if (isFlow && agent.consumerId && projectPath) {
4519
4751
  const displayName = `${agent.name}${icon}`;
4520
4752
  flowAgents.set(displayName, agent);
4753
+ const pUrl = `${cfgBaseUrl}/${projectPath}`;
4521
4754
  cfg.agent[displayName] = {
4522
4755
  name: displayName,
4523
4756
  description: `[GitLab Flow] ${agent.description}`,
4524
- mode: "subagent"
4757
+ mode: "subagent",
4758
+ prompt: buildFlowSubagentPrompt(agent, projectPath, pUrl),
4759
+ permission: { "*": "allow" }
4525
4760
  };
4526
4761
  } else {
4527
4762
  const displayName = `${agent.name}${icon}`;
4763
+ gitlabAgentNames.add(displayName);
4528
4764
  cfg.agent[displayName] = {
4529
4765
  name: displayName,
4530
4766
  description: agent.foundational ? "[GitLab Foundational Agent]" : "[GitLab Custom Agent]",
@@ -4543,86 +4779,128 @@ var plugin = async (input) => {
4543
4779
  },
4544
4780
  "chat.message": async (_input, output) => {
4545
4781
  const indicesToRemove = [];
4546
- const replacements = [];
4782
+ const flowMentions = [];
4547
4783
  for (let i = 0; i < output.parts.length; i++) {
4548
4784
  const part = output.parts[i];
4549
4785
  if (part.type !== "agent") continue;
4550
4786
  const flow = flowAgents.get(part.name);
4551
4787
  if (!flow || !flow.consumerId || !projectPath) continue;
4552
- const rawText = output.parts.filter((p) => p.type === "text" && !p.synthetic).map((p) => p.text).join(" ").trim() || "Execute the flow";
4553
- const baseUrl = authCache?.instanceUrl?.replace(/\/$/, "") ?? "https://gitlab.com";
4554
- const projectUrl = `${baseUrl}/${projectPath}`;
4555
- const subagentPrompt = [
4556
- `Execute the "${flow.name}" GitLab flow on project ${projectPath} (${projectUrl}).`,
4557
- `User goal: "${rawText}"`,
4558
- ``,
4559
- `You have access to ALL available tools: GitLab API tools (gitlab_list_merge_requests, gitlab_get_merge_request, gitlab_list_pipelines, gitlab_get_pipeline, gitlab_list_issues, gitlab_get_issue, gitlab_search, etc.), MCP servers, file tools, and any other tools in your environment. Use whatever tools you need to gather the required flow inputs.`,
4560
- ``,
4561
- `STEP 1 - FETCH AND DISPLAY FLOW DEFINITION:`,
4562
- `Call gitlab_get_flow_definition with consumer_id: ${flow.consumerId} and foundational: ${!!flow.foundational}.`,
4563
- `The response has a "config" field containing YAML. Parse it and find the "flow:" \u2192 "inputs:" section.`,
4564
- `Each input has a "category" and "input_schema" with field names and types.`,
4565
- `Print ALL inputs you found. Skip "agent_platform_standard_context" (auto-injected by server).`,
4566
- `If the config has no "inputs" or "inputs: []", say "No required inputs" and skip to step 3.`,
4567
- ``,
4568
- `STEP 2 - GATHER INPUT VALUES:`,
4569
- `For each required input from step 1, be creative and resourceful in finding the real values:`,
4570
- `- Use ANY available tools to find the data: GitLab tools, MCP servers, search, file reads, etc.`,
4571
- `- Look at the user's goal for hints (MR numbers, pipeline IDs, branch names, URLs)`,
4572
- `- For URL fields: construct full GitLab URLs using base ${projectUrl}`,
4573
- `- For branch fields: look up from MRs, pipelines, or project default branch`,
4574
- `- For IDs: extract from the user's message or look up via GitLab API`,
4575
- `- If an input cannot be determined, ask yourself what makes sense given the context`,
4576
- `Print each resolved input value before proceeding.`,
4577
- ``,
4578
- `STEP 3 - EXECUTE THE FLOW:`,
4579
- `Call gitlab_execute_project_flow with:`,
4580
- `- project_id: "${projectPath}"`,
4581
- `- consumer_id: ${flow.consumerId}`,
4582
- `- goal: the user's goal with any relevant URLs/context appended`,
4583
- `- additional_context: JSON array of inputs from step 2, each entry: {"Category":"<category>","Content":"{\\"field\\":\\"value\\"}"}`,
4584
- ` Example: [{"Category":"merge_request","Content":"{\\"url\\":\\"${projectUrl}/-/merge_requests/12\\"}"}]`,
4585
- ``,
4586
- `STEP 4 - CONFIRM FLOW STARTED:`,
4587
- `Call gitlab_get_workflow_status with the returned workflow_id.`,
4588
- `The tool blocks automatically while status is CREATED (up to 2 minutes).`,
4589
- `Once it returns, report:`,
4590
- `- The workflow status (RUNNING, FINISHED, FAILED, etc.)`,
4591
- `- The workflow URL: ${projectUrl}/-/automate/agent-sessions/<workflow_id>`,
4592
- `- A brief summary of what was executed`,
4593
- `Then stop. Do NOT keep polling. The user can follow the flow execution in the GitLab UI.`
4594
- ].join("\n");
4788
+ flowMentions.push({ idx: i, flow, displayName: part.name });
4789
+ if (i + 1 < output.parts.length) {
4790
+ const next = output.parts[i + 1];
4791
+ if (next.type === "text" && next.synthetic && next.text?.includes("call the task tool with subagent")) {
4792
+ indicesToRemove.push(i + 1);
4793
+ }
4794
+ }
4795
+ }
4796
+ if (flowMentions.length === 0) {
4797
+ return;
4798
+ }
4799
+ if (flowMentions.length === 1) {
4800
+ const { idx, flow } = flowMentions[0];
4801
+ const baseUrl2 = authCache?.instanceUrl?.replace(/\/$/, "") ?? "https://gitlab.com";
4802
+ const projectUrl2 = `${baseUrl2}/${projectPath}`;
4803
+ const rawText2 = output.parts.filter((p) => p.type === "text" && !p.synthetic).map((p) => p.text).join(" ").trim() || "Execute the flow";
4804
+ const subagentPrompt = buildFlowSubagentPrompt(flow, projectPath, projectUrl2) + `
4805
+
4806
+ User goal: "${rawText2}"`;
4595
4807
  const resultText = [
4596
4808
  `IMPORTANT: You MUST call the Task tool RIGHT NOW to dispatch a subagent. Do NOT execute these steps yourself.`,
4597
4809
  ``,
4598
4810
  `Call the Task tool with:`,
4811
+ ` subagent_type: "general"`,
4599
4812
  ` description: "Execute ${flow.name} flow"`,
4600
4813
  ` prompt: ${JSON.stringify(subagentPrompt)}`,
4601
4814
  ``,
4602
4815
  `Do not do anything else. Just call the Task tool with the above parameters.`
4603
4816
  ].join("\n");
4604
- replacements.push({ idx: i, text: resultText });
4605
- if (i + 1 < output.parts.length) {
4606
- const next = output.parts[i + 1];
4607
- if (next.type === "text" && next.synthetic && next.text?.includes("call the task tool with subagent")) {
4608
- indicesToRemove.push(i + 1);
4609
- }
4610
- }
4611
- }
4612
- for (const { idx, text } of replacements) {
4613
- const original = output.parts[idx];
4614
- output.parts[idx] = {
4615
- ...original,
4616
- type: "text",
4617
- text
4618
- };
4817
+ const original2 = output.parts[idx];
4818
+ output.parts[idx] = { ...original2, type: "text", text: resultText };
4619
4819
  delete output.parts[idx].name;
4620
4820
  delete output.parts[idx].source;
4821
+ for (const rmIdx of indicesToRemove.reverse()) {
4822
+ output.parts.splice(rmIdx, 1);
4823
+ }
4824
+ return;
4825
+ }
4826
+ const baseUrl = authCache?.instanceUrl?.replace(/\/$/, "") ?? "https://gitlab.com";
4827
+ const projectUrl = `${baseUrl}/${projectPath}`;
4828
+ const flowNames = new Set(flowMentions.map((m) => m.displayName));
4829
+ let rawText = output.parts.filter((p) => p.type === "text" && !p.synthetic).map((p) => p.text).join(" ").trim() || "Execute the flows";
4830
+ for (const name of flowNames) {
4831
+ rawText = rawText.replace(new RegExp(`@${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "g"), "").trim();
4832
+ }
4833
+ rawText = rawText.replace(/\s{2,}/g, " ").trim() || "Execute the flows";
4834
+ const flowList = flowMentions.map(
4835
+ ({ flow, displayName }, i) => `${i + 1}. "${displayName}" \u2014 consumer_id=${flow.consumerId}, foundational=${!!flow.foundational}`
4836
+ );
4837
+ const batchPrompt = [
4838
+ `Execute ${flowMentions.length} GitLab flows on project ${projectPath} (${projectUrl}).`,
4839
+ `User goal: "${rawText}"`,
4840
+ ``,
4841
+ `Flows to execute:`,
4842
+ ...flowList,
4843
+ ``,
4844
+ `EXECUTION PLAN:`,
4845
+ `1. Call gitlab_get_flow_definition for ALL flows listed above in a SINGLE response (${flowMentions.length} tool calls at once).`,
4846
+ ` Parse each YAML to find what "context:goal" maps to (the "as" field in components).`,
4847
+ ``,
4848
+ `2. If the user's goal involves multiple resources (e.g., "for each MR"), list them using GitLab API tools.`,
4849
+ ``,
4850
+ `3. Call gitlab_execute_project_flow for EVERY flow+resource combination in a SINGLE response.`,
4851
+ ` For each call, set the goal to the EXACT value the flow expects (e.g., just "14" for merge_request_iid).`,
4852
+ ` project_id: "${projectPath}"`,
4853
+ ` You MUST emit ALL execute calls in ONE response \u2014 do NOT wait for one to finish before calling the next.`,
4854
+ ``,
4855
+ `4. Collect all workflow_ids from step 3. Call gitlab_get_workflow_status for ALL of them in a SINGLE response.`,
4856
+ ``,
4857
+ `5. Present a summary table: flow name, resource, status, URL (${projectUrl}/-/automate/agent-sessions/<id>).`,
4858
+ ``,
4859
+ `CRITICAL: In steps 1, 3, and 4 you MUST make multiple tool calls in the SAME response for parallel execution.`
4860
+ ].join("\n");
4861
+ const combinedText = [
4862
+ `IMPORTANT: You MUST call the Task tool RIGHT NOW with subagent_type "general" to dispatch all flows in parallel.`,
4863
+ `Do NOT call flow tools yourself. Do NOT dispatch multiple Task calls \u2014 use ONE.`,
4864
+ ``,
4865
+ `Call the Task tool with:`,
4866
+ ` subagent_type: "general"`,
4867
+ ` description: "Execute ${flowMentions.length} flows in parallel"`,
4868
+ ` prompt: ${JSON.stringify(batchPrompt)}`,
4869
+ ``,
4870
+ `Do not do anything else. Just call the Task tool with the above parameters.`
4871
+ ].join("\n");
4872
+ const firstIdx = flowMentions[0].idx;
4873
+ const original = output.parts[firstIdx];
4874
+ output.parts[firstIdx] = { ...original, type: "text", text: combinedText };
4875
+ delete output.parts[firstIdx].name;
4876
+ delete output.parts[firstIdx].source;
4877
+ for (let i = flowMentions.length - 1; i >= 1; i--) {
4878
+ indicesToRemove.push(flowMentions[i].idx);
4621
4879
  }
4622
- for (const idx of indicesToRemove.reverse()) {
4880
+ for (const idx of [...new Set(indicesToRemove)].sort((a, b) => b - a)) {
4623
4881
  output.parts.splice(idx, 1);
4624
4882
  }
4625
4883
  },
4884
+ "chat.params": async (input2, _output) => {
4885
+ if (!gitlabAgentNames.has(input2.agent)) return;
4886
+ const model = input2.model;
4887
+ const modelId = model?.modelID ?? model?.id ?? "";
4888
+ const isDWS = modelId.includes("duo-workflow");
4889
+ if (!isDWS) {
4890
+ const name = model?.name ?? modelId ?? "unknown";
4891
+ throw new Error(
4892
+ `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.`
4893
+ );
4894
+ }
4895
+ },
4896
+ "experimental.chat.system.transform": async (_input, output) => {
4897
+ if (flowAgents.size) {
4898
+ output.system.push(FLOW_DISPATCH_GUIDELINES);
4899
+ }
4900
+ if (authCache) {
4901
+ output.system.push(AGENT_CREATION_GUIDELINES);
4902
+ }
4903
+ },
4626
4904
  tool: {
4627
4905
  gitlab_execute_project_flow: tool({
4628
4906
  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.",
@@ -4721,6 +4999,134 @@ var plugin = async (input) => {
4721
4999
  }
4722
5000
  }
4723
5001
  }),
5002
+ gitlab_list_project_mcp_servers: tool({
5003
+ 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.",
5004
+ args: {
5005
+ project_id: z.string().describe('Project path (e.g., "gitlab-org/gitlab")')
5006
+ },
5007
+ execute: async (_args) => {
5008
+ const serverMap = /* @__PURE__ */ new Map();
5009
+ for (const agent of cachedAgents) {
5010
+ if (!agent.mcpServers?.length) continue;
5011
+ for (const server of agent.mcpServers) {
5012
+ const existing = serverMap.get(server.id);
5013
+ if (existing) {
5014
+ if (!existing.usedBy.includes(agent.name)) existing.usedBy.push(agent.name);
5015
+ } else {
5016
+ serverMap.set(server.id, { ...server, usedBy: [agent.name] });
5017
+ }
5018
+ }
5019
+ }
5020
+ const servers = [...serverMap.values()];
5021
+ if (!servers.length) {
5022
+ return "No MCP servers found for agents enabled in this project.";
5023
+ }
5024
+ return JSON.stringify(servers, null, 2);
5025
+ }
5026
+ }),
5027
+ gitlab_create_agent: tool({
5028
+ 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.",
5029
+ args: {
5030
+ project_id: z.string().describe('Project path (e.g., "gitlab-org/gitlab")'),
5031
+ name: z.string().describe("Display name for the agent"),
5032
+ description: z.string().describe("Description of what the agent does"),
5033
+ public: z.boolean().describe("Whether the agent is publicly visible in the AI Catalog"),
5034
+ system_prompt: z.string().describe("System prompt that defines the agent's behavior"),
5035
+ user_prompt: z.string().optional().describe("User prompt template (optional)"),
5036
+ tools: z.array(z.string()).optional().describe(
5037
+ '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.'
5038
+ ),
5039
+ mcp_tools: z.array(z.string()).optional().describe("Array of MCP tool names to enable"),
5040
+ mcp_servers: z.array(z.string()).optional().describe(
5041
+ '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.'
5042
+ ),
5043
+ release: z.boolean().optional().describe("Whether to release the version immediately (default: false)"),
5044
+ confirmed: z.boolean().optional().describe(
5045
+ "Set to true only after the user has reviewed and confirmed all parameters. Omit or set false on first call."
5046
+ )
5047
+ },
5048
+ execute: async (args) => {
5049
+ if (!args.confirmed) {
5050
+ return [
5051
+ "STOP: Do not create the agent yet. You must ask the user to confirm the configuration first.",
5052
+ "",
5053
+ "Follow these steps NOW:",
5054
+ "1. Call gitlab_list_builtin_tools and gitlab_list_project_mcp_servers to discover options.",
5055
+ "2. Use the question tool to ask the user ALL 4 of these (in one call):",
5056
+ " - Agent name (suggest one, allow custom input)",
5057
+ " - Visibility: Public or Private",
5058
+ " - Tools: group by category (Search, Issues, MRs, Epics, Files, Git, CI/CD, Security, Audit, Planning, Wiki, API) as multi-select",
5059
+ " - MCP servers: multi-select from available servers",
5060
+ "3. Generate a system prompt and show it to the user for approval.",
5061
+ "4. Call gitlab_create_agent again with confirmed=true after the user approves."
5062
+ ].join("\n");
5063
+ }
5064
+ const auth = authCache ?? readAuth();
5065
+ if (!auth) throw new Error("Not authenticated");
5066
+ const result = await createAgent(auth.instanceUrl, auth.token, args.project_id, {
5067
+ name: args.name,
5068
+ description: args.description,
5069
+ public: args.public,
5070
+ systemPrompt: args.system_prompt,
5071
+ userPrompt: args.user_prompt,
5072
+ tools: args.tools,
5073
+ mcpTools: args.mcp_tools,
5074
+ mcpServers: args.mcp_servers,
5075
+ release: args.release
5076
+ });
5077
+ await refreshAgents();
5078
+ return JSON.stringify(result, null, 2);
5079
+ }
5080
+ }),
5081
+ gitlab_update_agent: tool({
5082
+ description: "Update an existing custom agent in the GitLab AI Catalog.\nOnly provided fields are updated; omitted fields remain unchanged.",
5083
+ args: {
5084
+ id: z.string().describe("Agent ID (numeric or full GID)"),
5085
+ name: z.string().optional().describe("New display name"),
5086
+ description: z.string().optional().describe("New description"),
5087
+ public: z.boolean().optional().describe("Whether publicly visible"),
5088
+ system_prompt: z.string().optional().describe("New system prompt"),
5089
+ user_prompt: z.string().optional().describe("New user prompt template"),
5090
+ tools: z.array(z.string()).optional().describe(
5091
+ 'New set of built-in tool Global IDs (full GIDs like "gid://gitlab/Ai::Catalog::BuiltInTool/1")'
5092
+ ),
5093
+ mcp_tools: z.array(z.string()).optional().describe("New set of MCP tool names"),
5094
+ mcp_servers: z.array(z.string()).optional().describe(
5095
+ 'New set of MCP server Global IDs (full GIDs like "gid://gitlab/Ai::Catalog::McpServer/1")'
5096
+ ),
5097
+ release: z.boolean().optional().describe("Whether to release the latest version"),
5098
+ version_bump: z.enum(["MAJOR", "MINOR", "PATCH"]).optional().describe("Version bump type")
5099
+ },
5100
+ execute: async (args) => {
5101
+ const auth = authCache ?? readAuth();
5102
+ if (!auth) throw new Error("Not authenticated");
5103
+ const result = await updateAgent(auth.instanceUrl, auth.token, args.id, {
5104
+ name: args.name,
5105
+ description: args.description,
5106
+ public: args.public,
5107
+ systemPrompt: args.system_prompt,
5108
+ userPrompt: args.user_prompt,
5109
+ tools: args.tools,
5110
+ mcpTools: args.mcp_tools,
5111
+ mcpServers: args.mcp_servers,
5112
+ release: args.release,
5113
+ versionBump: args.version_bump
5114
+ });
5115
+ await refreshAgents();
5116
+ return JSON.stringify(result, null, 2);
5117
+ }
5118
+ }),
5119
+ gitlab_list_builtin_tools: tool({
5120
+ 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.",
5121
+ args: {},
5122
+ execute: async () => {
5123
+ const auth = authCache ?? readAuth();
5124
+ if (!auth) throw new Error("Not authenticated");
5125
+ const tools = await listBuiltInTools(auth.instanceUrl, auth.token);
5126
+ if (!tools.length) return "No built-in tools available.";
5127
+ return JSON.stringify(tools, null, 2);
5128
+ }
5129
+ }),
4724
5130
  ...makeAgentFlowTools(
4725
5131
  z,
4726
5132
  () => authCache,