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.
- package/dist/src/config.js +16 -1
- package/dist/src/server.js +2 -1
- package/dist/src/setup.js +156 -11
- package/package.json +1 -1
package/dist/src/config.js
CHANGED
|
@@ -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),
|
package/dist/src/server.js
CHANGED
|
@@ -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("
|
|
147
|
-
|
|
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(`⚠️
|
|
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(`✅
|
|
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
|
|
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(`🎉
|
|
203
|
-
process.stderr.write("
|
|
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
|
|
207
|
-
|
|
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
|
}
|