gitlab-duo-mcp-bridge 0.2.0 → 0.3.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.
@@ -30,13 +30,28 @@ function isTruthy(value) {
30
30
  return false;
31
31
  return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
32
32
  }
33
+ export function resolveModel(model) {
34
+ if (!model)
35
+ return undefined;
36
+ const MODEL_MAPPING = {
37
+ sonnet: "claude_sonnet_4_6",
38
+ haiku: "claude_haiku_4_5_20251001",
39
+ opus: "claude_opus_4_5_20251101",
40
+ "gpt5-codex": "gpt_5_codex",
41
+ gpt5: "gpt_5",
42
+ "gpt5-mini": "gpt_5_mini",
43
+ gemini: "gemini_2_5_flash_vertex",
44
+ };
45
+ const key = model.trim().toLowerCase();
46
+ return MODEL_MAPPING[key] ?? model;
47
+ }
33
48
  export function loadConfig(env = process.env) {
34
49
  return {
35
50
  command: trimmedOrUndefined(env.DUO_CLI_COMMAND) ?? "glab",
36
51
  baseArgs: splitArgs(env.DUO_CLI_BASE_ARGS, ["duo", "cli", "run"]),
37
52
  goalFlag: trimmedOrUndefined(env.DUO_CLI_GOAL_FLAG) ?? "--goal",
38
53
  modelFlag: trimmedOrUndefined(env.DUO_CLI_MODEL_FLAG) ?? "--model",
39
- model: trimmedOrUndefined(env.GITLAB_DUO_MODEL),
54
+ model: resolveModel(trimmedOrUndefined(env.GITLAB_DUO_MODEL)),
40
55
  extraArgs: splitArgs(env.DUO_CLI_EXTRA_ARGS, []),
41
56
  timeoutMs: parseIntOr(env.DUO_TIMEOUT_MS, 120_000),
42
57
  cwd: trimmedOrUndefined(env.DUO_CLI_CWD),
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
6
  import { z } from "zod";
7
+ import { resolveModel } from "./config.js";
7
8
  import { buildReviewGoal } from "./goal.js";
8
9
  import { runDuo } from "./duoRunner.js";
9
10
  import { buildPointerGoal, writeGoalFile, } from "./goalFile.js";
@@ -119,7 +120,7 @@ export async function handleReview(config, input) {
119
120
  baseArgs: config.baseArgs,
120
121
  goalFlag: config.goalFlag,
121
122
  modelFlag: config.modelFlag,
122
- model: input.model ?? config.model,
123
+ model: resolveModel(input.model) ?? config.model,
123
124
  extraArgs: config.extraArgs,
124
125
  timeoutMs: input.timeoutMs ?? config.timeoutMs,
125
126
  cwd: effectiveCwd,
package/dist/src/setup.js CHANGED
@@ -1,6 +1,11 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import os from "node:os";
4
+ import readline from "node:readline/promises";
5
+ import { stdin as input, stdout as output } from "node:process";
6
+ import { exec } from "node:child_process";
7
+ import { promisify } from "node:util";
8
+ const execAsync = promisify(exec);
4
9
  export async function runSetup() {
5
10
  const home = os.homedir();
6
11
  const platform = os.platform();
@@ -143,18 +148,102 @@ export async function runSetup() {
143
148
  path: path.join(home, ".continue", "mcpServers", "gitlab-duo.json"),
144
149
  key: "raw",
145
150
  });
146
- process.stderr.write("Iniciando la configuración automática de gitlab-duo-mcp-bridge...\n\n");
147
- let configuredCount = 0;
151
+ process.stderr.write("Starting setup of gitlab-duo-mcp-bridge...\n\n");
152
+ const rl = readline.createInterface({ input, output });
153
+ process.stderr.write("Where would you like to configure gitlab-duo-mcp-bridge?\n");
154
+ process.stderr.write(" 1. Globally (system-wide for detected IDEs & MCP clients)\n");
155
+ process.stderr.write(" 2. Locally (in the current project directory)\n\n");
156
+ let choice = "1";
157
+ try {
158
+ const choiceAnswer = await rl.question("Enter choice (1 or 2) [1]: ");
159
+ if (choiceAnswer.trim() === "2" || choiceAnswer.trim().toLowerCase() === "locally") {
160
+ choice = "2";
161
+ }
162
+ }
163
+ catch (err) {
164
+ // Gracefully fallback to choice "1" on any error
165
+ }
166
+ if (choice === "2") {
167
+ rl.close();
168
+ process.stderr.write("\nConfiguring locally in the current project directory...\n\n");
169
+ const localPath = path.join(process.cwd(), "mcp.json");
170
+ try {
171
+ let config = {};
172
+ try {
173
+ const content = await fs.readFile(localPath, "utf8");
174
+ if (content.trim() !== "") {
175
+ config = JSON.parse(content);
176
+ }
177
+ }
178
+ catch (err) {
179
+ if (err.code !== "ENOENT") {
180
+ process.stderr.write(`⚠️ Could not read or parse existing mcp.json at ${localPath}: ${err.message}\n`);
181
+ }
182
+ }
183
+ if (!config.mcpServers) {
184
+ config.mcpServers = {};
185
+ }
186
+ config.mcpServers["gitlab-duo"] = newConfigEntry;
187
+ await fs.writeFile(localPath, JSON.stringify(config, null, 2), "utf8");
188
+ process.stderr.write(`✅ Successfully configured locally: mcp.json\n └─ Path: ${localPath}\n\n`);
189
+ process.stderr.write("🎉 Configuration complete!\n");
190
+ process.stderr.write("Remember to restart or reload your AI client/agent to detect the 'duo_review' tool.\n");
191
+ await checkGitLabConnection();
192
+ printMiniGuide();
193
+ }
194
+ catch (err) {
195
+ process.stderr.write(`❌ Error configuring locally at ${localPath}: ${err.message}\n`);
196
+ }
197
+ return;
198
+ }
199
+ process.stderr.write("\nScanning for active MCP client configuration directories...\n\n");
200
+ const detectedTargets = [];
148
201
  for (const target of targets) {
149
202
  const parentDir = path.dirname(target.path);
150
203
  try {
151
- // Check if parent directory exists
152
204
  await fs.access(parentDir);
205
+ detectedTargets.push(target);
153
206
  }
154
207
  catch {
155
208
  // Parent directory does not exist, so client is likely not installed or used. Skip it.
156
- continue;
157
209
  }
210
+ }
211
+ if (detectedTargets.length === 0) {
212
+ rl.close();
213
+ process.stderr.write("⚠️ No active MCP client configuration directories were found.\n");
214
+ process.stderr.write("If you use Claude Desktop, Cursor, Cline, Roo Code, Windsurf, Zed, or Continue, please make sure they are installed and have been opened at least once.\n");
215
+ return;
216
+ }
217
+ process.stderr.write("We detected the following MCP client(s):\n");
218
+ detectedTargets.forEach((target, index) => {
219
+ process.stderr.write(` ${index + 1}. ${target.name}\n`);
220
+ });
221
+ process.stderr.write("\n");
222
+ let selectedTargets = detectedTargets;
223
+ try {
224
+ const answer = await rl.question("Do you want to configure all detected clients? (Y/n): ");
225
+ const parsedAnswer = answer.trim().toLowerCase();
226
+ if (parsedAnswer !== "" && parsedAnswer !== "y" && parsedAnswer !== "yes") {
227
+ selectedTargets = [];
228
+ for (const target of detectedTargets) {
229
+ const individualAnswer = await rl.question(`Configure ${target.name}? (y/N): `);
230
+ if (individualAnswer.trim().toLowerCase().startsWith("y")) {
231
+ selectedTargets.push(target);
232
+ }
233
+ }
234
+ }
235
+ }
236
+ finally {
237
+ rl.close();
238
+ }
239
+ if (selectedTargets.length === 0) {
240
+ process.stderr.write("\n⚠️ No clients were selected for configuration.\n");
241
+ return;
242
+ }
243
+ process.stderr.write("\n");
244
+ let configuredCount = 0;
245
+ for (const target of selectedTargets) {
246
+ const parentDir = path.dirname(target.path);
158
247
  try {
159
248
  let config = {};
160
249
  if (target.key === "raw") {
@@ -176,7 +265,7 @@ export async function runSetup() {
176
265
  }
177
266
  catch (err) {
178
267
  if (err.code !== "ENOENT") {
179
- process.stderr.write(`⚠️ No se pudo leer o parsear ${target.name} en ${target.path}: ${err.message}\n`);
268
+ process.stderr.write(`⚠️ Could not read or parse ${target.name} at ${target.path}: ${err.message}\n`);
180
269
  continue;
181
270
  }
182
271
  }
@@ -190,20 +279,76 @@ export async function runSetup() {
190
279
  await fs.mkdir(parentDir, { recursive: true });
191
280
  // Write config back
192
281
  await fs.writeFile(target.path, JSON.stringify(config, null, 2), "utf8");
193
- process.stderr.write(`✅ Configurado con éxito: ${target.name}\n └─ Ruta: ${target.path}\n`);
282
+ process.stderr.write(`✅ Successfully configured: ${target.name}\n └─ Path: ${target.path}\n`);
194
283
  configuredCount++;
195
284
  }
196
285
  catch (err) {
197
- process.stderr.write(`❌ Error al configurar ${target.name} en ${target.path}: ${err.message}\n`);
286
+ process.stderr.write(`❌ Error configuring ${target.name} at ${target.path}: ${err.message}\n`);
198
287
  }
199
288
  }
200
289
  process.stderr.write("\n");
201
290
  if (configuredCount > 0) {
202
- process.stderr.write(`🎉 ¡Configuración completa! Se configuraron ${configuredCount} cliente(s) MCP.\n`);
203
- process.stderr.write("Recuerda reiniciar o recargar tu cliente de IA para que detecte la herramienta 'duo_review'.\n");
291
+ process.stderr.write(`🎉 Configuration complete! Successfully configured ${configuredCount} MCP client(s).\n`);
292
+ process.stderr.write("Remember to restart or reload your AI client to detect the 'duo_review' tool.\n");
293
+ await checkGitLabConnection();
294
+ printMiniGuide();
204
295
  }
205
296
  else {
206
- process.stderr.write("⚠️ No se encontraron directorios de configuración de clientes MCP activos.\n");
207
- process.stderr.write("Si usas Claude Desktop, Cursor, Cline, Roo Code, Windsurf, Zed o Continue, asegúrate de haberlos instalado e iniciado al menos una vez.\n");
297
+ process.stderr.write("⚠️ No clients were configured.\n");
298
+ }
299
+ }
300
+ async function checkGitLabConnection() {
301
+ process.stderr.write("\nChecking GitLab CLI installation and connection...\n");
302
+ try {
303
+ const { stdout } = await execAsync("glab auth status");
304
+ process.stderr.write("✅ GitLab CLI and connection verified successfully!\n");
305
+ if (stdout) {
306
+ process.stderr.write(stdout
307
+ .trim()
308
+ .split("\n")
309
+ .map((line) => ` ${line}`)
310
+ .join("\n") + "\n\n");
311
+ }
208
312
  }
313
+ catch (err) {
314
+ process.stderr.write("⚠️ Connection check failed or GitLab CLI not fully authenticated:\n");
315
+ if (err.code === "ENOENT" || (err.message && err.message.includes("not found"))) {
316
+ process.stderr.write(" Could not find the 'glab' executable. Please make sure GitLab CLI is installed and added to your PATH.\n\n");
317
+ }
318
+ else {
319
+ const errorOutput = err.stderr || err.stdout || err.message || String(err);
320
+ process.stderr.write(errorOutput
321
+ .trim()
322
+ .split("\n")
323
+ .map((line) => ` ${line}`)
324
+ .join("\n") + "\n\n");
325
+ }
326
+ }
327
+ }
328
+ function printMiniGuide() {
329
+ process.stderr.write(`📋 QUICK COMMAND & USAGE GUIDE
330
+ =============================
331
+
332
+ 1. How to ask your AI agent to run a review:
333
+ - "Review my current uncommitted changes with @duo_review"
334
+ - "Run @duo_review on src/auth.ts and src/db.ts"
335
+ - "Use @duo_review with model: sonnet to review this project"
336
+
337
+ 2. AI Model Abstractions (Friendly Names):
338
+ You can use simple, friendly model names instead of complex GitLab identifiers!
339
+ The bridge automatically maps them for you:
340
+
341
+ • sonnet -> claude_sonnet_4_6
342
+ • haiku -> claude_haiku_4_5_20251001
343
+ • opus -> claude_opus_4_5_20251101
344
+ • gpt5 -> gpt_5
345
+ • gpt5-mini -> gpt_5_mini
346
+ • gpt5-codex -> gpt_5_codex
347
+ • gemini -> gemini_2_5_flash_vertex
348
+
349
+ 3. How to use friendly models:
350
+ - In your agent's config or env: set GITLAB_DUO_MODEL="sonnet"
351
+ - In your prompt: "Review with @duo_review using model: gemini"
352
+
353
+ =============================\n\n`);
209
354
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitlab-duo-mcp-bridge",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "MCP server that wraps the GitLab Duo CLI as a clean, fault-tolerant duo_review tool for AI coding agents.",
5
5
  "type": "module",
6
6
  "bin": {