opencode-gitlab-duo-agentic 0.1.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 ADDED
@@ -0,0 +1,2368 @@
1
+ // src/plugin/config.ts
2
+ import path2 from "path";
3
+ import fs2 from "fs";
4
+
5
+ // src/plugin/models.ts
6
+ import path from "path";
7
+ import fs from "fs";
8
+
9
+ // src/shared/model_entry.ts
10
+ function buildModelEntry(name) {
11
+ return {
12
+ name,
13
+ release_date: "",
14
+ attachment: false,
15
+ reasoning: false,
16
+ temperature: true,
17
+ tool_call: true,
18
+ limit: { context: 0, output: 0 },
19
+ modalities: { input: ["text"], output: ["text"] },
20
+ options: {}
21
+ };
22
+ }
23
+
24
+ // src/shared/constants.ts
25
+ var GITLAB_DUO_PROVIDER_ID = "gitlab-duo-agentic";
26
+ var GITLAB_DUO_DEFAULT_MODEL_ID = "duo-agentic";
27
+ var GITLAB_DUO_DEFAULT_MODEL_NAME = "Duo Agentic";
28
+ var GITLAB_DUO_PLUGIN_PACKAGE_NAME = "opencode-gitlab-duo-agentic";
29
+ var GITLAB_DUO_PROVIDER_NPM_ENTRY = GITLAB_DUO_PLUGIN_PACKAGE_NAME;
30
+
31
+ // src/plugin/models.ts
32
+ async function loadGitLabModels(options = {}) {
33
+ const modelsJsonPath = resolveModelsJsonPath(options.modelsPath);
34
+ if (modelsJsonPath) {
35
+ try {
36
+ const raw = await fs.promises.readFile(modelsJsonPath, "utf8");
37
+ const data = JSON.parse(raw);
38
+ if (data.models && Object.keys(data.models).length > 0) {
39
+ console.log(
40
+ `[gitlab-duo] Loaded ${Object.keys(data.models).length} model(s) from ${modelsJsonPath}`
41
+ );
42
+ return data.models;
43
+ }
44
+ } catch (error) {
45
+ console.warn(
46
+ `[gitlab-duo] Failed to read models.json at ${modelsJsonPath}:`,
47
+ error instanceof Error ? error.message : error
48
+ );
49
+ }
50
+ }
51
+ return {
52
+ [GITLAB_DUO_DEFAULT_MODEL_ID]: buildModelEntry(GITLAB_DUO_DEFAULT_MODEL_NAME)
53
+ };
54
+ }
55
+ function resolveModelsJsonPath(overridePath) {
56
+ const override = typeof overridePath === "string" && overridePath.trim() ? overridePath.trim() : process.env.GITLAB_DUO_MODELS_PATH;
57
+ if (override) {
58
+ const resolved = path.isAbsolute(override) ? override : path.resolve(process.cwd(), override);
59
+ if (fs.existsSync(resolved)) return resolved;
60
+ console.warn(`[gitlab-duo] models.json not found at override path ${resolved}`);
61
+ return null;
62
+ }
63
+ let current = process.cwd();
64
+ while (true) {
65
+ const candidate = path.join(current, "models.json");
66
+ if (fs.existsSync(candidate)) return candidate;
67
+ const parent = path.dirname(current);
68
+ if (parent === current) break;
69
+ current = parent;
70
+ }
71
+ return null;
72
+ }
73
+
74
+ // src/plugin/config.ts
75
+ async function configHook(input) {
76
+ input.provider ??= {};
77
+ const existing = input.provider[GITLAB_DUO_PROVIDER_ID];
78
+ const existingOptions = existing?.options ?? {};
79
+ const providerNpm = typeof existing?.npm === "string" && existing.npm.trim() ? existing.npm : GITLAB_DUO_PROVIDER_NPM_ENTRY;
80
+ const apiKey = typeof existingOptions.apiKey === "string" ? existingOptions.apiKey : process.env.GITLAB_TOKEN || "";
81
+ const instanceUrl = typeof existingOptions.instanceUrl === "string" ? existingOptions.instanceUrl : process.env.GITLAB_INSTANCE_URL || "https://gitlab.com";
82
+ const systemRules = typeof existingOptions.systemRules === "string" ? existingOptions.systemRules : "";
83
+ const systemRulesPath = typeof existingOptions.systemRulesPath === "string" ? existingOptions.systemRulesPath : "";
84
+ const modelsPath = typeof existingOptions.modelsPath === "string" ? existingOptions.modelsPath : void 0;
85
+ const mergedSystemRules = await mergeSystemRules(systemRules, systemRulesPath);
86
+ const sendSystemContext = typeof existingOptions.sendSystemContext === "boolean" ? existingOptions.sendSystemContext : true;
87
+ const enableMcp = typeof existingOptions.enableMcp === "boolean" ? existingOptions.enableMcp : true;
88
+ if (!apiKey) {
89
+ console.warn(
90
+ "[gitlab-duo] GITLAB_TOKEN is empty for the OpenCode process. Ensure it is exported in the same shell."
91
+ );
92
+ }
93
+ input.provider[GITLAB_DUO_PROVIDER_ID] = {
94
+ name: existing?.name ?? "GitLab Duo Agentic",
95
+ npm: providerNpm,
96
+ options: {
97
+ ...existingOptions,
98
+ instanceUrl,
99
+ apiKey,
100
+ sendSystemContext,
101
+ enableMcp,
102
+ systemRules: mergedSystemRules || void 0
103
+ },
104
+ models: await loadGitLabModels({ modelsPath })
105
+ };
106
+ }
107
+ async function mergeSystemRules(rules, rulesPath) {
108
+ const baseRules = rules.trim();
109
+ if (!rulesPath) return baseRules;
110
+ const resolvedPath = path2.isAbsolute(rulesPath) ? rulesPath : path2.resolve(process.cwd(), rulesPath);
111
+ try {
112
+ const fileRules = (await fs2.promises.readFile(resolvedPath, "utf8")).trim();
113
+ if (!fileRules) return baseRules;
114
+ return baseRules ? `${baseRules}
115
+
116
+ ${fileRules}` : fileRules;
117
+ } catch (error) {
118
+ console.warn(`[gitlab-duo] Failed to read systemRulesPath at ${resolvedPath}:`, error);
119
+ return baseRules;
120
+ }
121
+ }
122
+
123
+ // src/plugin/tools.ts
124
+ import { tool } from "@opencode-ai/plugin";
125
+ import path3 from "path";
126
+ import fs3 from "fs";
127
+ function createReadTools() {
128
+ return {
129
+ read_file: tool({
130
+ description: "Read the contents of a file. Paths are relative to the repository root.",
131
+ args: {
132
+ file_path: tool.schema.string().describe("The file path to read.")
133
+ },
134
+ async execute(args, ctx) {
135
+ const { resolvedPath, displayPath } = resolveReadPath(args.file_path, ctx);
136
+ await ctx.ask({
137
+ permission: "read",
138
+ patterns: [resolvedPath],
139
+ always: ["*"],
140
+ metadata: {}
141
+ });
142
+ try {
143
+ return await fs3.promises.readFile(resolvedPath, "utf8");
144
+ } catch (error) {
145
+ throw new Error(formatReadError(displayPath, error));
146
+ }
147
+ }
148
+ }),
149
+ read_files: tool({
150
+ description: "Read the contents of multiple files. Paths are relative to the repository root.",
151
+ args: {
152
+ file_paths: tool.schema.array(tool.schema.string()).describe("The file paths to read.")
153
+ },
154
+ async execute(args, ctx) {
155
+ const targets = (args.file_paths ?? []).map((filePath) => ({
156
+ inputPath: filePath,
157
+ ...resolveReadPath(filePath, ctx)
158
+ }));
159
+ await ctx.ask({
160
+ permission: "read",
161
+ patterns: targets.map((target) => target.resolvedPath),
162
+ always: ["*"],
163
+ metadata: {}
164
+ });
165
+ const results = await Promise.all(
166
+ targets.map(async (target) => {
167
+ try {
168
+ const content = await fs3.promises.readFile(target.resolvedPath, "utf8");
169
+ return [target.inputPath, { content }];
170
+ } catch (error) {
171
+ return [target.inputPath, { error: formatReadError(target.displayPath, error) }];
172
+ }
173
+ })
174
+ );
175
+ const output = {};
176
+ for (const [pathKey, result] of results) {
177
+ output[pathKey] = result;
178
+ }
179
+ return JSON.stringify(output);
180
+ }
181
+ })
182
+ };
183
+ }
184
+ function resolveReadPath(filePath, ctx) {
185
+ const displayPath = filePath;
186
+ const resolvedPath = path3.isAbsolute(filePath) ? filePath : path3.resolve(ctx.worktree, filePath);
187
+ const worktreePath = path3.resolve(ctx.worktree);
188
+ if (resolvedPath !== worktreePath && !resolvedPath.startsWith(worktreePath + path3.sep)) {
189
+ throw new Error(`File is outside the repository: "${displayPath}"`);
190
+ }
191
+ return { resolvedPath, displayPath };
192
+ }
193
+ function formatReadError(filePath, error) {
194
+ const fsError = error;
195
+ if (fsError?.code === "ENOENT") return `File not found: "${filePath}"`;
196
+ const message = error instanceof Error ? error.message : String(error);
197
+ return `Error reading file: ${message}`;
198
+ }
199
+
200
+ // src/plugin/gitlab-duo-agentic.ts
201
+ var GitLabDuoAgenticPlugin = async () => {
202
+ return {
203
+ config: configHook,
204
+ tool: createReadTools()
205
+ };
206
+ };
207
+
208
+ // src/provider/core/stream_adapter.ts
209
+ import { createRequire } from "module";
210
+ function resolveReadableStream() {
211
+ if (typeof ReadableStream !== "undefined") {
212
+ return ReadableStream;
213
+ }
214
+ const require2 = createRequire(import.meta.url);
215
+ const web = require2("node:stream/web");
216
+ return web.ReadableStream;
217
+ }
218
+ function asyncIteratorToReadableStream(iter) {
219
+ const iterator = iter[Symbol.asyncIterator]();
220
+ const Readable = resolveReadableStream();
221
+ return new Readable({
222
+ async pull(controller) {
223
+ try {
224
+ const { value, done } = await iterator.next();
225
+ if (done) {
226
+ controller.close();
227
+ return;
228
+ }
229
+ controller.enqueue(value);
230
+ } catch (error) {
231
+ controller.error(error);
232
+ }
233
+ },
234
+ async cancel() {
235
+ if (iterator.return) {
236
+ await iterator.return();
237
+ }
238
+ }
239
+ });
240
+ }
241
+
242
+ // src/provider/core/prompt_utils.ts
243
+ function asString(value) {
244
+ return typeof value === "string" ? value : void 0;
245
+ }
246
+ function isPlainObject(value) {
247
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
248
+ }
249
+ function asStringArray(value) {
250
+ if (!Array.isArray(value)) return [];
251
+ return value.filter((item) => typeof item === "string");
252
+ }
253
+ function extractLastUserText(prompt) {
254
+ const parts = getLastUserTextParts(prompt);
255
+ if (parts.length === 0) return null;
256
+ const texts = parts.filter((part) => !part.synthetic && !part.ignored).map((part) => stripSystemReminder(part.text ?? "")).filter((text) => text.trim().length > 0);
257
+ if (texts.length === 0) return null;
258
+ return texts.join("").trim();
259
+ }
260
+ function getLastUserTextParts(prompt) {
261
+ if (!Array.isArray(prompt)) return [];
262
+ for (let i = prompt.length - 1; i >= 0; i -= 1) {
263
+ const message = prompt[i];
264
+ if (message?.role !== "user" || !Array.isArray(message.content)) continue;
265
+ const textParts = message.content.filter((part) => part.type === "text");
266
+ if (textParts.length > 0) return textParts;
267
+ }
268
+ return [];
269
+ }
270
+ function stripSystemReminder(text) {
271
+ return text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "").trim();
272
+ }
273
+ function extractAgentReminders(prompt) {
274
+ const parts = getLastUserTextParts(prompt);
275
+ if (parts.length === 0) return [];
276
+ const reminders = [];
277
+ for (const part of parts) {
278
+ if (!part.text) continue;
279
+ if (part.synthetic) {
280
+ const text = part.text.trim();
281
+ if (text.length > 0) {
282
+ reminders.push(text);
283
+ }
284
+ continue;
285
+ }
286
+ const matches = part.text.match(/<system-reminder>[\s\S]*?<\/system-reminder>/g);
287
+ if (matches) {
288
+ reminders.push(...matches);
289
+ }
290
+ }
291
+ return normalizeModeReminders(reminders);
292
+ }
293
+ function normalizeModeReminders(reminders) {
294
+ const mode = detectLatestMode(reminders);
295
+ if (!mode) return reminders;
296
+ return reminders.filter((reminder) => {
297
+ const classification = classifyModeReminder(reminder);
298
+ if (classification === "other") return true;
299
+ return classification === mode;
300
+ });
301
+ }
302
+ function detectLatestMode(reminders) {
303
+ let mode = null;
304
+ for (const reminder of reminders) {
305
+ const explicit = /operational mode has changed from\s+([a-z_]+)\s+to\s+([a-z_]+)/i.exec(reminder);
306
+ if (explicit) {
307
+ const normalized = normalizeMode(explicit[2]);
308
+ if (normalized) mode = normalized;
309
+ continue;
310
+ }
311
+ const classification = classifyModeReminder(reminder);
312
+ if (classification !== "other") {
313
+ mode = classification;
314
+ }
315
+ }
316
+ return mode;
317
+ }
318
+ function normalizeMode(value) {
319
+ const normalized = value.trim().toLowerCase();
320
+ if (normalized === "plan") return "plan";
321
+ if (normalized === "build") return "build";
322
+ return null;
323
+ }
324
+ function classifyModeReminder(reminder) {
325
+ const text = reminder.toLowerCase();
326
+ if (text.includes("operational mode has changed from build to plan")) return "plan";
327
+ if (text.includes("operational mode has changed from plan to build")) return "build";
328
+ if (text.includes("you are no longer in read-only mode")) return "build";
329
+ if (text.includes("you are in read-only mode")) return "plan";
330
+ if (text.includes("your operational mode has changed from plan to build")) return "build";
331
+ if (text.includes("your operational mode has changed from build to plan")) return "plan";
332
+ if (text.includes("you are permitted to make file changes")) return "build";
333
+ return "other";
334
+ }
335
+ function extractSystemPrompt(prompt) {
336
+ if (!Array.isArray(prompt)) return null;
337
+ const parts = [];
338
+ for (const message of prompt) {
339
+ const msg = message;
340
+ if (msg.role === "system" && typeof msg.content === "string" && msg.content.trim()) {
341
+ parts.push(msg.content);
342
+ }
343
+ }
344
+ return parts.length > 0 ? parts.join("\n") : null;
345
+ }
346
+ function sanitizeSystemPrompt(prompt) {
347
+ let result = prompt;
348
+ result = result.replace(/^You are [Oo]pen[Cc]ode[,.].*$/gm, "");
349
+ result = result.replace(/^Your name is opencode\s*$/gm, "");
350
+ result = result.replace(
351
+ /If the user asks for help or wants to give feedback[\s\S]*?https:\/\/github\.com\/anomalyco\/opencode\s*/g,
352
+ ""
353
+ );
354
+ result = result.replace(
355
+ /When the user directly asks about OpenCode[\s\S]*?https:\/\/opencode\.ai\/docs\s*/g,
356
+ ""
357
+ );
358
+ result = result.replace(/https:\/\/github\.com\/anomalyco\/opencode\S*/g, "");
359
+ result = result.replace(/https:\/\/opencode\.ai\S*/g, "");
360
+ result = result.replace(/\bOpenCode\b/g, "GitLab Duo");
361
+ result = result.replace(/\bopencode\b/g, "GitLab Duo");
362
+ result = result.replace(/The exact model ID is GitLab Duo\//g, "The exact model ID is ");
363
+ result = result.replace(/\n{3,}/g, "\n\n");
364
+ return result.trim();
365
+ }
366
+ function extractToolResults(prompt) {
367
+ if (!Array.isArray(prompt)) return [];
368
+ const results = [];
369
+ for (const message of prompt) {
370
+ const content = message.content;
371
+ if (!Array.isArray(content)) continue;
372
+ for (const part of content) {
373
+ if (part.type === "tool-result") {
374
+ const toolCallId = String(part.toolCallId ?? "");
375
+ const toolName = String(part.toolName ?? "");
376
+ const outputField = part.output;
377
+ const resultField = part.result;
378
+ let output = "";
379
+ let error;
380
+ if (isPlainObject(outputField) && "type" in outputField) {
381
+ const outputType = String(outputField.type);
382
+ const outputValue = outputField.value;
383
+ if (outputType === "text" || outputType === "json") {
384
+ output = typeof outputValue === "string" ? outputValue : JSON.stringify(outputValue ?? "");
385
+ } else if (outputType === "error-text" || outputType === "error-json") {
386
+ error = typeof outputValue === "string" ? outputValue : JSON.stringify(outputValue ?? "");
387
+ } else if (outputType === "content" && Array.isArray(outputValue)) {
388
+ output = outputValue.filter((v) => v.type === "text").map((v) => String(v.text ?? "")).join("\n");
389
+ }
390
+ } else if (outputField !== void 0) {
391
+ output = String(outputField);
392
+ } else if (resultField !== void 0) {
393
+ output = typeof resultField === "string" ? resultField : JSON.stringify(resultField);
394
+ if (isPlainObject(resultField)) error = asString(resultField.error);
395
+ }
396
+ if (!error) {
397
+ error = asString(part.error) ?? asString(part.errorText);
398
+ }
399
+ results.push({ toolCallId, toolName, output, error });
400
+ }
401
+ if (part.type === "tool-error") {
402
+ const toolCallId = String(part.toolCallId ?? "");
403
+ const toolName = String(part.toolName ?? "");
404
+ const errorValue = part.error ?? part.errorText ?? part.message;
405
+ const error = asString(errorValue) ?? String(errorValue ?? "");
406
+ results.push({ toolCallId, toolName, output: "", error });
407
+ }
408
+ }
409
+ }
410
+ return results;
411
+ }
412
+
413
+ // src/provider/core/shell_quote.ts
414
+ function shellQuote(value) {
415
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
416
+ }
417
+
418
+ // src/provider/application/tool_mapping.ts
419
+ function mapDuoToolRequest(toolName, args) {
420
+ switch (toolName) {
421
+ case "list_dir": {
422
+ const directory = asString(args.directory) ?? ".";
423
+ return {
424
+ toolName: "bash",
425
+ args: {
426
+ command: `ls -la ${shellQuote(directory)}`,
427
+ description: "List directory contents",
428
+ workdir: "."
429
+ }
430
+ };
431
+ }
432
+ case "read_file": {
433
+ const filePath = asString(args.file_path) ?? asString(args.filepath) ?? asString(args.filePath) ?? asString(args.path);
434
+ if (!filePath) return { toolName, args };
435
+ const mappedArgs = { filePath };
436
+ if (typeof args.offset === "number") mappedArgs.offset = args.offset;
437
+ if (typeof args.limit === "number") mappedArgs.limit = args.limit;
438
+ return { toolName: "read", args: mappedArgs };
439
+ }
440
+ case "read_files": {
441
+ const filePaths = asStringArray(args.file_paths);
442
+ if (filePaths.length === 0) return { toolName, args };
443
+ return filePaths.map((fp) => ({ toolName: "read", args: { filePath: fp } }));
444
+ }
445
+ case "create_file_with_contents": {
446
+ const filePath = asString(args.file_path);
447
+ const content = asString(args.contents);
448
+ if (!filePath || content === void 0) return { toolName, args };
449
+ return { toolName: "write", args: { filePath, content } };
450
+ }
451
+ case "edit_file": {
452
+ const filePath = asString(args.file_path);
453
+ const oldString = asString(args.old_str);
454
+ const newString = asString(args.new_str);
455
+ if (!filePath || oldString === void 0 || newString === void 0) return { toolName, args };
456
+ return { toolName: "edit", args: { filePath, oldString, newString } };
457
+ }
458
+ case "find_files": {
459
+ const pattern = asString(args.name_pattern);
460
+ if (!pattern) return { toolName, args };
461
+ return { toolName: "glob", args: { pattern } };
462
+ }
463
+ case "grep": {
464
+ const pattern = asString(args.pattern);
465
+ if (!pattern) return { toolName, args };
466
+ const searchDirectory = asString(args.search_directory);
467
+ const caseInsensitive = Boolean(args.case_insensitive);
468
+ const normalizedPattern = caseInsensitive && !pattern.startsWith("(?i)") ? `(?i)${pattern}` : pattern;
469
+ const mappedArgs = { pattern: normalizedPattern };
470
+ if (searchDirectory) mappedArgs.path = searchDirectory;
471
+ return { toolName: "grep", args: mappedArgs };
472
+ }
473
+ case "mkdir": {
474
+ const directory = asString(args.directory_path);
475
+ if (!directory) return { toolName, args };
476
+ return {
477
+ toolName: "bash",
478
+ args: {
479
+ command: `mkdir -p ${shellQuote(directory)}`,
480
+ description: "Create directory",
481
+ workdir: "."
482
+ }
483
+ };
484
+ }
485
+ case "shell_command": {
486
+ const command = asString(args.command);
487
+ if (!command) return { toolName, args };
488
+ return {
489
+ toolName: "bash",
490
+ args: { command, description: "Run shell command", workdir: "." }
491
+ };
492
+ }
493
+ case "run_command": {
494
+ const program = asString(args.program);
495
+ if (program) {
496
+ const parts = [shellQuote(program)];
497
+ const flags = args.flags;
498
+ if (Array.isArray(flags)) parts.push(...flags.map((f) => shellQuote(String(f))));
499
+ const cmdArgs = args.arguments;
500
+ if (Array.isArray(cmdArgs)) parts.push(...cmdArgs.map((a) => shellQuote(String(a))));
501
+ return {
502
+ toolName: "bash",
503
+ args: { command: parts.join(" "), description: "Run command", workdir: "." }
504
+ };
505
+ }
506
+ const command = asString(args.command);
507
+ if (!command) return { toolName, args };
508
+ return {
509
+ toolName: "bash",
510
+ args: { command, description: "Run command", workdir: "." }
511
+ };
512
+ }
513
+ case "run_git_command": {
514
+ const command = asString(args.command);
515
+ if (!command) return { toolName, args };
516
+ const rawArgs = args.args;
517
+ const extraArgs = Array.isArray(rawArgs) ? rawArgs.map((value) => shellQuote(String(value))).join(" ") : asString(rawArgs);
518
+ const gitCommand = extraArgs ? `git ${shellQuote(command)} ${extraArgs}` : `git ${shellQuote(command)}`;
519
+ return {
520
+ toolName: "bash",
521
+ args: { command: gitCommand, description: "Run git command", workdir: "." }
522
+ };
523
+ }
524
+ default:
525
+ return { toolName, args };
526
+ }
527
+ }
528
+ var DUO_MCP_TOOLS = [
529
+ {
530
+ name: "list_dir",
531
+ description: "List directory contents relative to the repository root.",
532
+ schema: {
533
+ type: "object",
534
+ properties: {
535
+ directory: { type: "string", description: "Directory path relative to repo root." }
536
+ },
537
+ required: ["directory"]
538
+ }
539
+ },
540
+ {
541
+ name: "read_file",
542
+ description: "Read the contents of a file.",
543
+ schema: {
544
+ type: "object",
545
+ properties: {
546
+ file_path: { type: "string", description: "The file path to read." }
547
+ },
548
+ required: ["file_path"]
549
+ }
550
+ },
551
+ {
552
+ name: "read_files",
553
+ description: "Read multiple files.",
554
+ schema: {
555
+ type: "object",
556
+ properties: {
557
+ file_paths: {
558
+ type: "array",
559
+ items: { type: "string" },
560
+ description: "List of file paths to read."
561
+ }
562
+ },
563
+ required: ["file_paths"]
564
+ }
565
+ },
566
+ {
567
+ name: "create_file_with_contents",
568
+ description: "Create a file and write contents.",
569
+ schema: {
570
+ type: "object",
571
+ properties: {
572
+ file_path: { type: "string", description: "The file path to write." },
573
+ contents: { type: "string", description: "Contents to write." }
574
+ },
575
+ required: ["file_path", "contents"]
576
+ }
577
+ },
578
+ {
579
+ name: "find_files",
580
+ description: "Find files by name pattern.",
581
+ schema: {
582
+ type: "object",
583
+ properties: {
584
+ name_pattern: { type: "string", description: "Pattern to search for." }
585
+ },
586
+ required: ["name_pattern"]
587
+ }
588
+ },
589
+ {
590
+ name: "mkdir",
591
+ description: "Create a directory.",
592
+ schema: {
593
+ type: "object",
594
+ properties: {
595
+ directory_path: { type: "string", description: "Directory to create." }
596
+ },
597
+ required: ["directory_path"]
598
+ }
599
+ },
600
+ {
601
+ name: "edit_file",
602
+ description: "Edit a file by replacing a string.",
603
+ schema: {
604
+ type: "object",
605
+ properties: {
606
+ file_path: { type: "string", description: "Path of the file to edit." },
607
+ old_str: { type: "string", description: "String to replace." },
608
+ new_str: { type: "string", description: "Replacement string." }
609
+ },
610
+ required: ["file_path", "old_str", "new_str"]
611
+ }
612
+ },
613
+ {
614
+ name: "grep",
615
+ description: "Search for a pattern in files.",
616
+ schema: {
617
+ type: "object",
618
+ properties: {
619
+ pattern: { type: "string", description: "Search pattern." },
620
+ search_directory: { type: "string", description: "Directory to search." },
621
+ case_insensitive: { type: "boolean", description: "Case insensitive search." }
622
+ },
623
+ required: ["pattern"]
624
+ }
625
+ },
626
+ {
627
+ name: "shell_command",
628
+ description: "Execute a shell command.",
629
+ schema: {
630
+ type: "object",
631
+ properties: {
632
+ command: { type: "string", description: "Command to execute." }
633
+ },
634
+ required: ["command"]
635
+ }
636
+ },
637
+ {
638
+ name: "run_git_command",
639
+ description: "Run a git command in the repo.",
640
+ schema: {
641
+ type: "object",
642
+ properties: {
643
+ repository_url: { type: "string", description: "Git remote URL." },
644
+ command: { type: "string", description: "Git command (status, log, diff, ...)." },
645
+ args: { type: "string", description: "Arguments for the git command." }
646
+ },
647
+ required: ["repository_url", "command"]
648
+ }
649
+ }
650
+ ];
651
+ var BUILTIN_TOOL_NAMES = new Set(DUO_MCP_TOOLS.map((t) => t.name));
652
+ var OPENCODE_BUILTIN_TOOL_NAMES = /* @__PURE__ */ new Set([
653
+ "bash",
654
+ "edit",
655
+ "write",
656
+ "read",
657
+ "grep",
658
+ "glob",
659
+ "patch",
660
+ "skill",
661
+ "todowrite",
662
+ "todoread",
663
+ "webfetch",
664
+ "websearch",
665
+ "question",
666
+ "lsp",
667
+ "read_file",
668
+ "read_files"
669
+ ]);
670
+ function buildMcpTools(options) {
671
+ const tools = DUO_MCP_TOOLS.map((t) => ({
672
+ name: t.name,
673
+ description: t.description,
674
+ schema: t.schema,
675
+ isApproved: false
676
+ }));
677
+ if (options.tools) {
678
+ for (const t of options.tools) {
679
+ if (t.type !== "function") continue;
680
+ if (BUILTIN_TOOL_NAMES.has(t.name)) continue;
681
+ if (OPENCODE_BUILTIN_TOOL_NAMES.has(t.name)) continue;
682
+ tools.push({
683
+ name: t.name,
684
+ description: t.description,
685
+ schema: t.inputSchema,
686
+ isApproved: false
687
+ });
688
+ }
689
+ }
690
+ return tools;
691
+ }
692
+ function buildToolContext(tools) {
693
+ if (tools.length === 0) return null;
694
+ const content = `<tools>
695
+ ${tools.map((t) => {
696
+ const desc = t.description?.trim();
697
+ return desc ? `- ${t.name}: ${desc}` : `- ${t.name}`;
698
+ }).join("\n")}
699
+ </tools>
700
+ <rules>
701
+ - MUST use the tool-call simulation formats when requesting tools.
702
+ </rules>`;
703
+ return {
704
+ category: "tool_information",
705
+ content,
706
+ id: "available_tools",
707
+ metadata: {
708
+ title: "Available Tools",
709
+ enabled: true,
710
+ subType: "tools",
711
+ icon: "tool",
712
+ secondaryText: `${tools.length} tools`,
713
+ subTypeLabel: "Tooling"
714
+ }
715
+ };
716
+ }
717
+
718
+ // src/provider/core/token_usage.ts
719
+ var DEFAULT_CHARS_PER_TOKEN = 4;
720
+ var TokenUsageEstimator = class {
721
+ #inputChars = 0;
722
+ #outputChars = 0;
723
+ #charsPerToken;
724
+ constructor(charsPerToken = DEFAULT_CHARS_PER_TOKEN) {
725
+ this.#charsPerToken = charsPerToken;
726
+ }
727
+ /** Record characters sent to DWS (prompt, system context, tool results). */
728
+ addInputChars(text) {
729
+ this.#inputChars += text.length;
730
+ }
731
+ /** Record characters received from DWS (text chunks, tool call args). */
732
+ addOutputChars(text) {
733
+ this.#outputChars += text.length;
734
+ }
735
+ get inputTokens() {
736
+ return Math.ceil(this.#inputChars / this.#charsPerToken);
737
+ }
738
+ get outputTokens() {
739
+ return Math.ceil(this.#outputChars / this.#charsPerToken);
740
+ }
741
+ get totalTokens() {
742
+ return this.inputTokens + this.outputTokens;
743
+ }
744
+ /** Reset counters for a new turn. */
745
+ reset() {
746
+ this.#inputChars = 0;
747
+ this.#outputChars = 0;
748
+ }
749
+ };
750
+
751
+ // src/provider/application/model.ts
752
+ var GitLabDuoAgenticLanguageModel = class {
753
+ specificationVersion = "v2";
754
+ provider = GITLAB_DUO_PROVIDER_ID;
755
+ modelId;
756
+ supportedUrls = {};
757
+ #options;
758
+ #runtime;
759
+ #pendingToolRequests = /* @__PURE__ */ new Map();
760
+ #multiCallGroups = /* @__PURE__ */ new Map();
761
+ #sentToolCallIds = /* @__PURE__ */ new Set();
762
+ #lastSentPrompt = null;
763
+ #agentMode;
764
+ #agentModeReminder;
765
+ #usageEstimator = new TokenUsageEstimator();
766
+ constructor(modelId, options, runtime) {
767
+ this.modelId = modelId;
768
+ this.#options = options;
769
+ this.#runtime = runtime;
770
+ }
771
+ // ---------------------------------------------------------------------------
772
+ // LanguageModelV2 interface
773
+ // ---------------------------------------------------------------------------
774
+ async doGenerate(options) {
775
+ let text = "";
776
+ const stream = await this.doStream(options);
777
+ for await (const part of stream.stream) {
778
+ if (part.type === "text-delta") text += part.delta;
779
+ }
780
+ const content = [{ type: "text", text }];
781
+ const finishReason = "stop";
782
+ return {
783
+ content,
784
+ finishReason,
785
+ usage: {
786
+ inputTokens: this.#usageEstimator.inputTokens,
787
+ outputTokens: this.#usageEstimator.outputTokens,
788
+ totalTokens: this.#usageEstimator.totalTokens
789
+ },
790
+ warnings: []
791
+ };
792
+ }
793
+ async doStream(options) {
794
+ const workflowType = "chat";
795
+ const promptText = extractLastUserText(options.prompt);
796
+ const toolResults = extractToolResults(options.prompt);
797
+ this.#runtime.resetMapperState();
798
+ if (!this.#runtime.hasStarted) {
799
+ this.#sentToolCallIds.clear();
800
+ for (const r of toolResults) {
801
+ if (!this.#pendingToolRequests.has(r.toolCallId)) {
802
+ this.#sentToolCallIds.add(r.toolCallId);
803
+ }
804
+ }
805
+ this.#lastSentPrompt = null;
806
+ }
807
+ const freshToolResults = toolResults.filter((r) => !this.#sentToolCallIds.has(r.toolCallId));
808
+ const modelRef = this.modelId === GITLAB_DUO_DEFAULT_MODEL_ID ? void 0 : this.modelId;
809
+ this.#runtime.setSelectedModelIdentifier(modelRef);
810
+ await this.#runtime.ensureConnected(promptText || "", workflowType);
811
+ const mcpTools = this.#options.enableMcp === false ? [] : buildMcpTools(options);
812
+ const toolContext = buildToolContext(mcpTools);
813
+ const isNewUserMessage = promptText != null && promptText !== this.#lastSentPrompt;
814
+ let sentToolResults = false;
815
+ if (freshToolResults.length > 0) {
816
+ for (const result of freshToolResults) {
817
+ const hashIdx = result.toolCallId.indexOf("#");
818
+ if (hashIdx !== -1) {
819
+ const originalId = result.toolCallId.substring(0, hashIdx);
820
+ const group = this.#multiCallGroups.get(originalId);
821
+ if (!group) {
822
+ this.#sentToolCallIds.add(result.toolCallId);
823
+ continue;
824
+ }
825
+ this.#usageEstimator.addInputChars(result.output);
826
+ if (result.error) this.#usageEstimator.addInputChars(result.error);
827
+ group.collected.set(result.toolCallId, result.error ?? result.output);
828
+ this.#sentToolCallIds.add(result.toolCallId);
829
+ this.#pendingToolRequests.delete(result.toolCallId);
830
+ if (group.collected.size === group.subIds.length) {
831
+ const aggregated = group.subIds.map((id) => group.collected.get(id) ?? "").join("\n");
832
+ this.#runtime.sendToolResponse(
833
+ originalId,
834
+ { output: aggregated },
835
+ group.responseType
836
+ );
837
+ this.#multiCallGroups.delete(originalId);
838
+ this.#pendingToolRequests.delete(originalId);
839
+ sentToolResults = true;
840
+ }
841
+ continue;
842
+ }
843
+ const pending = this.#pendingToolRequests.get(result.toolCallId);
844
+ if (!pending) {
845
+ this.#sentToolCallIds.add(result.toolCallId);
846
+ continue;
847
+ }
848
+ this.#usageEstimator.addInputChars(result.output);
849
+ if (result.error) this.#usageEstimator.addInputChars(result.error);
850
+ this.#runtime.sendToolResponse(
851
+ result.toolCallId,
852
+ { output: result.output, error: result.error },
853
+ pending.responseType
854
+ );
855
+ sentToolResults = true;
856
+ this.#sentToolCallIds.add(result.toolCallId);
857
+ this.#pendingToolRequests.delete(result.toolCallId);
858
+ }
859
+ }
860
+ if (!sentToolResults && isNewUserMessage) {
861
+ const extraContext = [];
862
+ if (toolContext) extraContext.push(toolContext);
863
+ if (!this.#runtime.hasStarted) {
864
+ const systemPrompt = extractSystemPrompt(options.prompt);
865
+ if (systemPrompt) {
866
+ extraContext.push({
867
+ category: "agent_context",
868
+ content: sanitizeSystemPrompt(systemPrompt),
869
+ id: "agent_system_prompt",
870
+ metadata: {
871
+ title: "Agent System Prompt",
872
+ enabled: true,
873
+ subType: "system_prompt",
874
+ icon: "file-text",
875
+ secondaryText: "Full system prompt",
876
+ subTypeLabel: "System Prompt"
877
+ }
878
+ });
879
+ }
880
+ } else {
881
+ const promptContent = extractSystemPrompt(options.prompt);
882
+ if (promptContent) {
883
+ extraContext.push({
884
+ category: "agent_context",
885
+ content: sanitizeSystemPrompt(promptContent),
886
+ id: "agent_system_prompt",
887
+ metadata: {
888
+ title: "Agent System Prompt",
889
+ enabled: true,
890
+ subType: "system_prompt",
891
+ icon: "file-text",
892
+ secondaryText: "System prompt",
893
+ subTypeLabel: "System Prompt"
894
+ }
895
+ });
896
+ }
897
+ }
898
+ const agentReminders = extractAgentReminders(options.prompt);
899
+ const modeReminder = detectLatestModeReminder(agentReminders);
900
+ if (modeReminder) {
901
+ this.#agentMode = modeReminder.mode;
902
+ this.#agentModeReminder = modeReminder.reminder;
903
+ }
904
+ const remindersForContext = buildReminderContext(agentReminders, this.#agentModeReminder);
905
+ if (remindersForContext.length > 0) {
906
+ const reminderContent = sanitizeSystemPrompt(remindersForContext.join("\n\n"));
907
+ extraContext.push({
908
+ category: "agent_context",
909
+ content: reminderContent,
910
+ id: "agent_reminders",
911
+ metadata: {
912
+ title: "Agent Reminders",
913
+ enabled: true,
914
+ subType: "agent_reminders",
915
+ icon: "file-text",
916
+ secondaryText: "Agent mode instructions",
917
+ subTypeLabel: "Agent Reminders"
918
+ }
919
+ });
920
+ }
921
+ this.#runtime.sendStartRequest(
922
+ promptText,
923
+ workflowType,
924
+ mcpTools,
925
+ [],
926
+ extraContext
927
+ );
928
+ this.#lastSentPrompt = promptText;
929
+ this.#usageEstimator.addInputChars(promptText);
930
+ for (const ctx of extraContext) {
931
+ if (ctx.content) this.#usageEstimator.addInputChars(ctx.content);
932
+ }
933
+ }
934
+ const iterator = this.#mapEventsToStream(this.#runtime.getEventStream());
935
+ const stream = asyncIteratorToReadableStream(iterator);
936
+ return {
937
+ stream
938
+ };
939
+ }
940
+ // ---------------------------------------------------------------------------
941
+ // Event → stream mapping (2 paths: TEXT_CHUNK + TOOL_REQUEST)
942
+ // ---------------------------------------------------------------------------
943
+ async *#mapEventsToStream(events) {
944
+ const state = { textStarted: false };
945
+ const estimator = this.#usageEstimator;
946
+ yield { type: "stream-start", warnings: [] };
947
+ try {
948
+ for await (const event of events) {
949
+ if (event.type === "TEXT_CHUNK") {
950
+ if (event.content.length > 0) {
951
+ estimator.addOutputChars(event.content);
952
+ yield* this.#emitTextDelta(state, event.content);
953
+ }
954
+ continue;
955
+ }
956
+ if (event.type === "TOOL_COMPLETE") {
957
+ continue;
958
+ }
959
+ if (event.type === "TOOL_REQUEST") {
960
+ const args = event.args;
961
+ let mapped;
962
+ try {
963
+ mapped = mapDuoToolRequest(event.toolName, args);
964
+ } catch {
965
+ continue;
966
+ }
967
+ const responseType = event.responseType;
968
+ estimator.addOutputChars(JSON.stringify(args));
969
+ if (Array.isArray(mapped)) {
970
+ const subIds = mapped.map((_, i) => `${event.requestId}#${i}`);
971
+ this.#multiCallGroups.set(event.requestId, { subIds, collected: /* @__PURE__ */ new Map(), responseType });
972
+ this.#pendingToolRequests.set(event.requestId, { responseType });
973
+ for (const subId of subIds) {
974
+ this.#pendingToolRequests.set(subId, {});
975
+ }
976
+ yield* this.#emitMultiToolCalls(subIds, mapped);
977
+ return;
978
+ }
979
+ this.#pendingToolRequests.set(event.requestId, { responseType });
980
+ yield* this.#emitToolCall(event.requestId, mapped.toolName, mapped.args);
981
+ return;
982
+ }
983
+ if (event.type === "ERROR") {
984
+ const msg = event.message;
985
+ if (msg.includes("1013") || msg.includes("lock")) {
986
+ yield { type: "error", error: new Error("GitLab Duo workflow is locked (another session may still be active). Please try again in a few seconds.") };
987
+ } else {
988
+ yield { type: "error", error: new Error(`GitLab Duo: ${msg}`) };
989
+ }
990
+ return;
991
+ }
992
+ }
993
+ } catch (streamErr) {
994
+ yield { type: "error", error: streamErr instanceof Error ? streamErr : new Error(String(streamErr)) };
995
+ return;
996
+ }
997
+ yield { type: "finish", finishReason: "stop", usage: this.#currentUsage };
998
+ }
999
+ // ---------------------------------------------------------------------------
1000
+ // Stream part helpers
1001
+ // ---------------------------------------------------------------------------
1002
+ get #currentUsage() {
1003
+ return {
1004
+ inputTokens: this.#usageEstimator.inputTokens,
1005
+ outputTokens: this.#usageEstimator.outputTokens,
1006
+ totalTokens: this.#usageEstimator.totalTokens
1007
+ };
1008
+ }
1009
+ *#emitTextDelta(state, delta) {
1010
+ if (!state.textStarted) {
1011
+ state.textStarted = true;
1012
+ yield { type: "text-start", id: "txt-0" };
1013
+ }
1014
+ yield { type: "text-delta", id: "txt-0", delta };
1015
+ }
1016
+ *#emitToolCall(id, toolName, args) {
1017
+ const inputJson = JSON.stringify(args ?? {});
1018
+ yield { type: "tool-input-start", id, toolName };
1019
+ yield { type: "tool-input-delta", id, delta: inputJson };
1020
+ yield { type: "tool-input-end", id };
1021
+ yield { type: "tool-call", toolCallId: id, toolName, input: inputJson };
1022
+ yield { type: "finish", finishReason: "tool-calls", usage: this.#currentUsage };
1023
+ }
1024
+ *#emitMultiToolCalls(ids, calls) {
1025
+ for (let i = 0; i < calls.length; i++) {
1026
+ const inputJson = JSON.stringify(calls[i].args ?? {});
1027
+ yield { type: "tool-input-start", id: ids[i], toolName: calls[i].toolName };
1028
+ yield { type: "tool-input-delta", id: ids[i], delta: inputJson };
1029
+ yield { type: "tool-input-end", id: ids[i] };
1030
+ yield { type: "tool-call", toolCallId: ids[i], toolName: calls[i].toolName, input: inputJson };
1031
+ }
1032
+ yield { type: "finish", finishReason: "tool-calls", usage: this.#currentUsage };
1033
+ }
1034
+ };
1035
+ function buildReminderContext(reminders, modeReminder) {
1036
+ const nonModeReminders = reminders.filter((reminder) => classifyModeReminder2(reminder) === "other");
1037
+ if (!modeReminder) {
1038
+ return nonModeReminders;
1039
+ }
1040
+ return [...nonModeReminders, modeReminder];
1041
+ }
1042
+ function detectLatestModeReminder(reminders) {
1043
+ let latest;
1044
+ for (const reminder of reminders) {
1045
+ const classification = classifyModeReminder2(reminder);
1046
+ if (classification === "other") continue;
1047
+ latest = { mode: classification, reminder };
1048
+ }
1049
+ return latest;
1050
+ }
1051
+ function classifyModeReminder2(reminder) {
1052
+ const text = reminder.toLowerCase();
1053
+ if (text.includes("operational mode has changed from build to plan")) return "plan";
1054
+ if (text.includes("operational mode has changed from plan to build")) return "build";
1055
+ if (text.includes("you are no longer in read-only mode")) return "build";
1056
+ if (text.includes("you are now in read-only mode")) return "plan";
1057
+ if (text.includes("you are in read-only mode")) return "plan";
1058
+ if (text.includes("you are permitted to make file changes")) return "build";
1059
+ return "other";
1060
+ }
1061
+
1062
+ // src/provider/application/workflow_event_mapper.ts
1063
+ import crypto from "crypto";
1064
+
1065
+ // src/provider/core/ui_chat_log.ts
1066
+ import { z } from "zod";
1067
+ import { err, ok } from "neverthrow";
1068
+ var ToolInfoArgsSchema = z.record(z.unknown());
1069
+ var ToolResponseSchema = z.object({
1070
+ content: z.string(),
1071
+ additional_kwargs: z.record(z.unknown()),
1072
+ response_metadata: z.record(z.unknown()),
1073
+ type: z.string(),
1074
+ name: z.string(),
1075
+ id: z.string().nullable(),
1076
+ tool_call_id: z.string(),
1077
+ artifact: z.unknown(),
1078
+ status: z.string()
1079
+ });
1080
+ var ToolInfoSchema = z.object({
1081
+ name: z.string(),
1082
+ args: ToolInfoArgsSchema,
1083
+ tool_response: z.union([ToolResponseSchema, z.string()]).optional()
1084
+ });
1085
+ var BaseMessageSchema = z.object({
1086
+ message_sub_type: z.string().nullable(),
1087
+ content: z.string(),
1088
+ timestamp: z.string(),
1089
+ status: z.string().nullable(),
1090
+ correlation_id: z.string().nullable(),
1091
+ additional_context: z.unknown()
1092
+ });
1093
+ var WorkflowMessageSchema = BaseMessageSchema.extend({
1094
+ message_type: z.enum(["user", "agent"]),
1095
+ tool_info: z.null()
1096
+ });
1097
+ var WorkflowRequestSchema = BaseMessageSchema.extend({
1098
+ message_type: z.literal("request"),
1099
+ tool_info: ToolInfoSchema
1100
+ });
1101
+ var WorkflowToolSchema = BaseMessageSchema.extend({
1102
+ message_type: z.literal("tool"),
1103
+ tool_info: z.union([ToolInfoSchema, z.null()])
1104
+ });
1105
+ var ChatLogSchema = z.discriminatedUnion("message_type", [
1106
+ WorkflowMessageSchema,
1107
+ WorkflowRequestSchema,
1108
+ WorkflowToolSchema
1109
+ ]);
1110
+ function extractUiChatLog(message) {
1111
+ if (!message.checkpoint) return ok([]);
1112
+ let checkpoint;
1113
+ try {
1114
+ checkpoint = JSON.parse(message.checkpoint);
1115
+ } catch (error) {
1116
+ const cause = error instanceof Error ? error.message : String(error);
1117
+ return err(
1118
+ new Error(`Failed to parse workflow checkpoint. Checkpoint: ${message.checkpoint}. Cause: ${cause}`)
1119
+ );
1120
+ }
1121
+ if (!checkpoint.channel_values?.ui_chat_log || !Array.isArray(checkpoint.channel_values.ui_chat_log)) {
1122
+ return ok([]);
1123
+ }
1124
+ const validatedMessages = [];
1125
+ for (let i = 0; i < checkpoint.channel_values.ui_chat_log.length; i += 1) {
1126
+ const rawMessage = checkpoint.channel_values.ui_chat_log[i];
1127
+ const parseResult = ChatLogSchema.safeParse(rawMessage);
1128
+ if (!parseResult.success) {
1129
+ return err(
1130
+ new Error(
1131
+ `Failed to validate message at index ${i}: ${parseResult.error.message}. Raw message: ${JSON.stringify(
1132
+ rawMessage
1133
+ )}`
1134
+ )
1135
+ );
1136
+ }
1137
+ validatedMessages.push(parseResult.data);
1138
+ }
1139
+ return ok(validatedMessages);
1140
+ }
1141
+
1142
+ // src/provider/application/workflow_event_mapper.ts
1143
+ var WorkflowEventMapper = class {
1144
+ #lastMessageContent = "";
1145
+ #lastMessageId = "";
1146
+ resetStreamState() {
1147
+ this.#lastMessageContent = "";
1148
+ this.#lastMessageId = "";
1149
+ }
1150
+ #parseTimestamp(timestamp) {
1151
+ const parsed = Date.parse(timestamp);
1152
+ return Number.isNaN(parsed) ? Date.now() : parsed;
1153
+ }
1154
+ mapWorkflowEvent(duoEvent) {
1155
+ const events = [];
1156
+ const workflowMessagesResult = extractUiChatLog(duoEvent);
1157
+ if (workflowMessagesResult.isErr()) {
1158
+ return events;
1159
+ }
1160
+ const workflowMessages = workflowMessagesResult.value;
1161
+ if (workflowMessages.length === 0) return events;
1162
+ const latestMessage = workflowMessages[workflowMessages.length - 1];
1163
+ const latestMessageIndex = workflowMessages.length - 1;
1164
+ switch (latestMessage.message_type) {
1165
+ case "user":
1166
+ return events;
1167
+ case "agent": {
1168
+ const currentContent = latestMessage.content;
1169
+ const currentId = `${latestMessageIndex}`;
1170
+ const timestamp = this.#parseTimestamp(latestMessage.timestamp);
1171
+ if (currentId === this.#lastMessageId) {
1172
+ if (!currentContent.startsWith(this.#lastMessageContent)) {
1173
+ events.push({
1174
+ type: "TEXT_CHUNK",
1175
+ messageId: currentId,
1176
+ content: currentContent,
1177
+ timestamp
1178
+ });
1179
+ this.#lastMessageContent = currentContent;
1180
+ }
1181
+ const delta = currentContent.slice(this.#lastMessageContent.length);
1182
+ if (delta.length > 0) {
1183
+ events.push({
1184
+ type: "TEXT_CHUNK",
1185
+ messageId: currentId,
1186
+ content: delta,
1187
+ timestamp
1188
+ });
1189
+ this.#lastMessageContent = currentContent;
1190
+ }
1191
+ } else {
1192
+ events.push({
1193
+ type: "TEXT_CHUNK",
1194
+ messageId: currentId,
1195
+ content: currentContent,
1196
+ timestamp
1197
+ });
1198
+ this.#lastMessageContent = currentContent;
1199
+ this.#lastMessageId = currentId;
1200
+ }
1201
+ break;
1202
+ }
1203
+ case "request": {
1204
+ const requestId = latestMessage.correlation_id || crypto.randomUUID();
1205
+ events.push({
1206
+ type: "TOOL_REQUEST",
1207
+ requestId,
1208
+ toolName: latestMessage.tool_info.name,
1209
+ args: latestMessage.tool_info.args ?? {},
1210
+ timestamp: this.#parseTimestamp(latestMessage.timestamp)
1211
+ });
1212
+ break;
1213
+ }
1214
+ case "tool": {
1215
+ const toolId = `${latestMessageIndex}`;
1216
+ const timestamp = this.#parseTimestamp(latestMessage.timestamp);
1217
+ const toolResponse = latestMessage.tool_info?.tool_response;
1218
+ const output = typeof toolResponse === "string" ? toolResponse : toolResponse?.content ?? latestMessage.content;
1219
+ if (output.startsWith("Action error:")) {
1220
+ events.push({
1221
+ type: "TOOL_COMPLETE",
1222
+ toolId,
1223
+ result: "",
1224
+ error: output,
1225
+ timestamp
1226
+ });
1227
+ } else {
1228
+ events.push({
1229
+ type: "TOOL_COMPLETE",
1230
+ toolId,
1231
+ result: output,
1232
+ timestamp
1233
+ });
1234
+ }
1235
+ break;
1236
+ }
1237
+ default:
1238
+ break;
1239
+ }
1240
+ return events;
1241
+ }
1242
+ };
1243
+
1244
+ // src/provider/core/async_queue.ts
1245
+ var AsyncQueue = class {
1246
+ #items = [];
1247
+ #resolvers = [];
1248
+ #closed = false;
1249
+ push(item) {
1250
+ if (this.#closed) return;
1251
+ const resolver = this.#resolvers.shift();
1252
+ if (resolver) {
1253
+ resolver({ value: item, done: false });
1254
+ return;
1255
+ }
1256
+ this.#items.push(item);
1257
+ }
1258
+ close() {
1259
+ this.#closed = true;
1260
+ while (this.#resolvers.length > 0) {
1261
+ const resolver = this.#resolvers.shift();
1262
+ if (resolver) resolver({ value: void 0, done: true });
1263
+ }
1264
+ }
1265
+ async *iterate() {
1266
+ while (true) {
1267
+ if (this.#items.length > 0) {
1268
+ yield this.#items.shift();
1269
+ continue;
1270
+ }
1271
+ if (this.#closed) return;
1272
+ const next = await new Promise((resolve) => {
1273
+ this.#resolvers.push(resolve);
1274
+ });
1275
+ if (next.done) return;
1276
+ yield next.value;
1277
+ }
1278
+ }
1279
+ };
1280
+
1281
+ // src/provider/application/action_handler.ts
1282
+ function mapWorkflowActionToToolRequest(action) {
1283
+ const requestId = action.requestID;
1284
+ if (!requestId) return null;
1285
+ if (action.runMCPTool) {
1286
+ const rawArgs = action.runMCPTool.args;
1287
+ let parsedArgs;
1288
+ if (typeof rawArgs === "string") {
1289
+ try {
1290
+ parsedArgs = JSON.parse(rawArgs);
1291
+ } catch {
1292
+ parsedArgs = {};
1293
+ }
1294
+ } else {
1295
+ parsedArgs = rawArgs ?? {};
1296
+ }
1297
+ return { requestId, toolName: action.runMCPTool.name, args: parsedArgs };
1298
+ }
1299
+ if (action.runReadFile) {
1300
+ return {
1301
+ requestId,
1302
+ toolName: "read_file",
1303
+ args: {
1304
+ file_path: action.runReadFile.filepath,
1305
+ offset: action.runReadFile.offset,
1306
+ limit: action.runReadFile.limit
1307
+ }
1308
+ };
1309
+ }
1310
+ if (action.runReadFiles) {
1311
+ return {
1312
+ requestId,
1313
+ toolName: "read_files",
1314
+ args: { file_paths: action.runReadFiles.filepaths ?? [] }
1315
+ };
1316
+ }
1317
+ if (action.runWriteFile) {
1318
+ return {
1319
+ requestId,
1320
+ toolName: "create_file_with_contents",
1321
+ args: {
1322
+ file_path: action.runWriteFile.filepath,
1323
+ contents: action.runWriteFile.contents
1324
+ }
1325
+ };
1326
+ }
1327
+ if (action.runEditFile) {
1328
+ return {
1329
+ requestId,
1330
+ toolName: "edit_file",
1331
+ args: {
1332
+ file_path: action.runEditFile.filepath,
1333
+ old_str: action.runEditFile.oldString,
1334
+ new_str: action.runEditFile.newString
1335
+ }
1336
+ };
1337
+ }
1338
+ if (action.findFiles) {
1339
+ return {
1340
+ requestId,
1341
+ toolName: "find_files",
1342
+ args: { name_pattern: action.findFiles.name_pattern }
1343
+ };
1344
+ }
1345
+ if (action.listDirectory) {
1346
+ return {
1347
+ requestId,
1348
+ toolName: "list_dir",
1349
+ args: { directory: action.listDirectory.directory }
1350
+ };
1351
+ }
1352
+ if (action.grep) {
1353
+ const args = { pattern: action.grep.pattern };
1354
+ if (action.grep.search_directory) args.search_directory = action.grep.search_directory;
1355
+ if (action.grep.case_insensitive !== void 0) args.case_insensitive = action.grep.case_insensitive;
1356
+ return { requestId, toolName: "grep", args };
1357
+ }
1358
+ if (action.mkdir) {
1359
+ return {
1360
+ requestId,
1361
+ toolName: "mkdir",
1362
+ args: { directory_path: action.mkdir.directory_path }
1363
+ };
1364
+ }
1365
+ if (action.runShellCommand) {
1366
+ return {
1367
+ requestId,
1368
+ toolName: "shell_command",
1369
+ args: { command: action.runShellCommand.command }
1370
+ };
1371
+ }
1372
+ if (action.runCommand) {
1373
+ const parts = [shellQuote(action.runCommand.program)];
1374
+ if (action.runCommand.flags) parts.push(...action.runCommand.flags.map(shellQuote));
1375
+ if (action.runCommand.arguments) parts.push(...action.runCommand.arguments.map(shellQuote));
1376
+ return {
1377
+ requestId,
1378
+ toolName: "shell_command",
1379
+ args: { command: parts.join(" ") }
1380
+ };
1381
+ }
1382
+ if (action.runGitCommand) {
1383
+ return {
1384
+ requestId,
1385
+ toolName: "run_git_command",
1386
+ args: {
1387
+ repository_url: action.runGitCommand.repository_url ?? "",
1388
+ command: action.runGitCommand.command,
1389
+ args: action.runGitCommand.arguments
1390
+ }
1391
+ };
1392
+ }
1393
+ if (action.runHTTPRequest) {
1394
+ return {
1395
+ requestId,
1396
+ toolName: "gitlab_api_request",
1397
+ args: {
1398
+ method: action.runHTTPRequest.method,
1399
+ path: action.runHTTPRequest.path,
1400
+ body: action.runHTTPRequest.body
1401
+ },
1402
+ responseType: "http"
1403
+ };
1404
+ }
1405
+ return null;
1406
+ }
1407
+
1408
+ // src/provider/application/runtime.ts
1409
+ var GitLabAgenticRuntime = class {
1410
+ #options;
1411
+ #dependencies;
1412
+ #selectedModelIdentifier;
1413
+ #workflowId;
1414
+ #wsClient;
1415
+ #workflowToken;
1416
+ #queue;
1417
+ #stream;
1418
+ #mapper = new WorkflowEventMapper();
1419
+ #containerParams;
1420
+ #startRequestSent = false;
1421
+ constructor(options, dependencies) {
1422
+ this.#options = options;
1423
+ this.#dependencies = dependencies;
1424
+ }
1425
+ // ---------------------------------------------------------------------------
1426
+ // Public accessors
1427
+ // ---------------------------------------------------------------------------
1428
+ get hasStarted() {
1429
+ return this.#startRequestSent;
1430
+ }
1431
+ setSelectedModelIdentifier(ref) {
1432
+ if (ref === this.#selectedModelIdentifier) return;
1433
+ this.#selectedModelIdentifier = ref;
1434
+ this.#resetStreamState();
1435
+ }
1436
+ resetMapperState() {
1437
+ this.#mapper.resetStreamState();
1438
+ }
1439
+ // ---------------------------------------------------------------------------
1440
+ // Connection lifecycle
1441
+ // ---------------------------------------------------------------------------
1442
+ async ensureConnected(goal, workflowType) {
1443
+ if (this.#stream && this.#workflowId && this.#queue) {
1444
+ return;
1445
+ }
1446
+ if (!this.#containerParams) {
1447
+ this.#containerParams = await this.#resolveContainerParams();
1448
+ }
1449
+ if (!this.#workflowId) {
1450
+ this.#workflowId = await this.#createWorkflow(goal, workflowType);
1451
+ }
1452
+ const token = await this.#dependencies.workflowService.getWorkflowToken(
1453
+ this.#options.instanceUrl,
1454
+ this.#options.apiKey,
1455
+ workflowType
1456
+ );
1457
+ this.#workflowToken = token;
1458
+ const MAX_LOCK_RETRIES = 3;
1459
+ const LOCK_RETRY_DELAY_MS = 3e3;
1460
+ for (let attempt = 1; attempt <= MAX_LOCK_RETRIES; attempt++) {
1461
+ this.#queue = new AsyncQueue();
1462
+ try {
1463
+ await this.#connectWebSocket();
1464
+ return;
1465
+ } catch (err2) {
1466
+ const msg = err2 instanceof Error ? err2.message : String(err2);
1467
+ if ((msg.includes("1013") || msg.includes("lock")) && attempt < MAX_LOCK_RETRIES) {
1468
+ this.#resetStreamState();
1469
+ await this.#dependencies.clock.sleep(LOCK_RETRY_DELAY_MS);
1470
+ const retryToken = await this.#dependencies.workflowService.getWorkflowToken(
1471
+ this.#options.instanceUrl,
1472
+ this.#options.apiKey,
1473
+ workflowType
1474
+ );
1475
+ this.#workflowToken = retryToken;
1476
+ continue;
1477
+ }
1478
+ if (msg.includes("1013") || msg.includes("lock")) {
1479
+ throw new Error("GitLab Duo workflow is locked (another session may still be active). Please try again in a few seconds.");
1480
+ }
1481
+ throw new Error(`GitLab Duo connection failed: ${msg}`);
1482
+ }
1483
+ }
1484
+ }
1485
+ // ---------------------------------------------------------------------------
1486
+ // Messaging
1487
+ // ---------------------------------------------------------------------------
1488
+ sendStartRequest(goal, workflowType, mcpTools = [], preapprovedTools = [], extraContext = []) {
1489
+ if (!this.#stream || !this.#workflowId) throw new Error("Workflow client not initialized");
1490
+ const additionalContext = this.#options.sendSystemContext === false ? [] : this.#dependencies.systemContext.getSystemContextItems(this.#options.systemRules);
1491
+ additionalContext.push(...extraContext);
1492
+ const startRequest = {
1493
+ startRequest: {
1494
+ workflowID: this.#workflowId,
1495
+ clientVersion: "1.0",
1496
+ workflowDefinition: workflowType,
1497
+ goal,
1498
+ workflowMetadata: JSON.stringify({
1499
+ project_id: this.#containerParams?.projectId,
1500
+ namespace_id: this.#containerParams?.namespaceId
1501
+ }),
1502
+ additional_context: additionalContext.map((context) => ({
1503
+ ...context,
1504
+ metadata: context.metadata ? JSON.stringify(context.metadata) : void 0
1505
+ })),
1506
+ clientCapabilities: ["shell_command"],
1507
+ mcpTools,
1508
+ preapproved_tools: preapprovedTools
1509
+ }
1510
+ };
1511
+ this.#stream.write(startRequest);
1512
+ this.#startRequestSent = true;
1513
+ }
1514
+ sendToolResponse(requestId, response, responseType) {
1515
+ if (!this.#stream) throw new Error("Workflow client not initialized");
1516
+ if (responseType === "http") {
1517
+ const parsed = parseHttpToolOutput(response.output);
1518
+ const event2 = {
1519
+ actionResponse: {
1520
+ requestID: requestId,
1521
+ httpResponse: {
1522
+ status: parsed.status,
1523
+ headers: parsed.headers,
1524
+ response: parsed.body,
1525
+ error: response.error ?? ""
1526
+ }
1527
+ }
1528
+ };
1529
+ this.#stream.write(event2);
1530
+ return;
1531
+ }
1532
+ const event = {
1533
+ actionResponse: {
1534
+ requestID: requestId,
1535
+ plainTextResponse: {
1536
+ response: response.output,
1537
+ error: response.error ?? ""
1538
+ }
1539
+ }
1540
+ };
1541
+ this.#stream.write(event);
1542
+ }
1543
+ getEventStream() {
1544
+ if (!this.#queue) throw new Error("Workflow stream not initialized");
1545
+ return this.#queue.iterate();
1546
+ }
1547
+ // ---------------------------------------------------------------------------
1548
+ // Private: project / workflow resolution
1549
+ // ---------------------------------------------------------------------------
1550
+ async #resolveContainerParams() {
1551
+ const projectPath = await this.#dependencies.projectLookup.detectProjectPath(
1552
+ process.cwd(),
1553
+ this.#options.instanceUrl
1554
+ );
1555
+ if (!projectPath) {
1556
+ throw new Error(
1557
+ "Unable to detect GitLab project. Ensure you run OpenCode in a Git repository with a GitLab remote."
1558
+ );
1559
+ }
1560
+ try {
1561
+ const details = await this.#dependencies.projectLookup.fetchProjectDetailsWithFallback(
1562
+ this.#options.instanceUrl,
1563
+ this.#options.apiKey,
1564
+ projectPath
1565
+ );
1566
+ return {
1567
+ projectId: details.projectId,
1568
+ namespaceId: details.namespaceId
1569
+ };
1570
+ } catch {
1571
+ throw new Error(
1572
+ "Failed to fetch GitLab project details. Check that the remote URL is correct and the token has access."
1573
+ );
1574
+ }
1575
+ }
1576
+ async #createWorkflow(goal, workflowType) {
1577
+ try {
1578
+ return await this.#dependencies.workflowService.createWorkflow(
1579
+ this.#options.instanceUrl,
1580
+ this.#options.apiKey,
1581
+ goal,
1582
+ workflowType,
1583
+ this.#containerParams
1584
+ );
1585
+ } catch (error) {
1586
+ if (isWorkflowCreateError(error) && error.status === 400 && error.body.includes("No default namespace found")) {
1587
+ throw new Error(
1588
+ "No default namespace found. Ensure this repository has a GitLab remote so the namespace can be detected."
1589
+ );
1590
+ }
1591
+ throw error;
1592
+ }
1593
+ }
1594
+ // ---------------------------------------------------------------------------
1595
+ // Private: WebSocket stream binding
1596
+ // ---------------------------------------------------------------------------
1597
+ #bindStream(stream, queue) {
1598
+ const now = () => this.#dependencies.clock.now();
1599
+ const closeWithError = (message) => {
1600
+ queue.push({ type: "ERROR", message, timestamp: now() });
1601
+ queue.close();
1602
+ this.#resetStreamState();
1603
+ };
1604
+ const handleAction = async (action) => {
1605
+ if (action.newCheckpoint) {
1606
+ const duoEvent = {
1607
+ checkpoint: action.newCheckpoint.checkpoint,
1608
+ errors: action.newCheckpoint.errors || [],
1609
+ workflowGoal: action.newCheckpoint.goal,
1610
+ workflowStatus: action.newCheckpoint.status
1611
+ };
1612
+ const events = await this.#mapper.mapWorkflowEvent(duoEvent);
1613
+ for (const event of events) {
1614
+ queue.push(event);
1615
+ }
1616
+ return;
1617
+ }
1618
+ const toolRequest = mapWorkflowActionToToolRequest(action);
1619
+ if (toolRequest) {
1620
+ queue.push({
1621
+ type: "TOOL_REQUEST",
1622
+ ...toolRequest,
1623
+ timestamp: now()
1624
+ });
1625
+ return;
1626
+ }
1627
+ };
1628
+ stream.on("data", (action) => {
1629
+ void handleAction(action).catch((error) => {
1630
+ const message = error instanceof Error ? error.message : String(error);
1631
+ closeWithError(message);
1632
+ });
1633
+ });
1634
+ stream.on("error", (err2) => {
1635
+ closeWithError(err2.message);
1636
+ });
1637
+ stream.on("end", () => {
1638
+ queue.close();
1639
+ this.#resetStreamState();
1640
+ });
1641
+ }
1642
+ async #connectWebSocket() {
1643
+ if (!this.#queue) return;
1644
+ if (!this.#workflowToken) throw new Error("Workflow token unavailable");
1645
+ this.#wsClient = this.#dependencies.createWorkflowClient({
1646
+ gitlabInstanceUrl: new URL(this.#options.instanceUrl),
1647
+ token: this.#options.apiKey,
1648
+ headers: buildWorkflowHeaders(
1649
+ this.#workflowToken.duo_workflow_service.headers,
1650
+ this.#containerParams
1651
+ ),
1652
+ selectedModelIdentifier: this.#selectedModelIdentifier
1653
+ });
1654
+ const stream = await this.#wsClient.executeWorkflow();
1655
+ this.#stream = stream;
1656
+ this.#bindStream(stream, this.#queue);
1657
+ }
1658
+ #resetStreamState() {
1659
+ this.#stream = void 0;
1660
+ this.#queue = void 0;
1661
+ this.#startRequestSent = false;
1662
+ this.#wsClient?.dispose();
1663
+ this.#wsClient = void 0;
1664
+ }
1665
+ };
1666
+ function buildWorkflowHeaders(headers, containerParams) {
1667
+ const result = normalizeHeaders(headers);
1668
+ if (containerParams?.projectId) {
1669
+ result["x-gitlab-project-id"] = containerParams.projectId;
1670
+ }
1671
+ if (containerParams?.namespaceId) {
1672
+ result["x-gitlab-namespace-id"] = containerParams.namespaceId;
1673
+ }
1674
+ const featureSetting = process.env.GITLAB_AGENT_PLATFORM_FEATURE_SETTING_NAME;
1675
+ if (featureSetting) {
1676
+ result["x-gitlab-agent-platform-feature-setting-name"] = featureSetting;
1677
+ }
1678
+ return result;
1679
+ }
1680
+ function normalizeHeaders(headers) {
1681
+ const normalized = {};
1682
+ for (const [key, value] of Object.entries(headers || {})) {
1683
+ normalized[key.toLowerCase()] = value;
1684
+ }
1685
+ return normalized;
1686
+ }
1687
+ function isWorkflowCreateError(error) {
1688
+ if (!error || typeof error !== "object") return false;
1689
+ const value = error;
1690
+ return typeof value.status === "number" && typeof value.body === "string";
1691
+ }
1692
+ function parseHttpToolOutput(output) {
1693
+ const lines = output.trimEnd().split("\n");
1694
+ const lastLine = lines[lines.length - 1]?.trim() ?? "";
1695
+ const statusCode = parseInt(lastLine, 10);
1696
+ if (!Number.isNaN(statusCode) && statusCode >= 100 && statusCode < 600) {
1697
+ return {
1698
+ status: statusCode,
1699
+ headers: {},
1700
+ body: lines.slice(0, -1).join("\n")
1701
+ };
1702
+ }
1703
+ return { status: 0, headers: {}, body: output };
1704
+ }
1705
+
1706
+ // src/provider/adapters/default_runtime_dependencies.ts
1707
+ import { ProxyAgent } from "proxy-agent";
1708
+
1709
+ // src/provider/adapters/gitlab_utils.ts
1710
+ import fs4 from "fs/promises";
1711
+ import path4 from "path";
1712
+ async function detectProjectPath(cwd, instanceUrl) {
1713
+ let current = cwd;
1714
+ const instance = new URL(instanceUrl);
1715
+ const instanceHost = instance.host;
1716
+ const instanceBasePath = instance.pathname.replace(/\/$/, "");
1717
+ while (true) {
1718
+ try {
1719
+ const config = await readGitConfig(current);
1720
+ const url = extractGitRemoteUrl(config) || "";
1721
+ const remote = parseRemote(url);
1722
+ if (!remote) {
1723
+ return void 0;
1724
+ }
1725
+ if (remote.host !== instanceHost) {
1726
+ throw new Error(
1727
+ `GitLab remote host mismatch. Expected ${instanceHost}, got ${remote.host}.`
1728
+ );
1729
+ }
1730
+ return normalizeProjectPath(remote.path, instanceBasePath);
1731
+ } catch {
1732
+ const parent = path4.dirname(current);
1733
+ if (parent === current) return void 0;
1734
+ current = parent;
1735
+ }
1736
+ }
1737
+ }
1738
+ function extractGitRemoteUrl(config) {
1739
+ const lines = config.split("\n");
1740
+ let inOrigin = false;
1741
+ let originUrl;
1742
+ let firstUrl;
1743
+ for (const line of lines) {
1744
+ const trimmed = line.trim();
1745
+ const sectionMatch = /^\[remote\s+"([^"]+)"\]$/.exec(trimmed);
1746
+ if (sectionMatch) {
1747
+ inOrigin = sectionMatch[1] === "origin";
1748
+ continue;
1749
+ }
1750
+ const urlMatch = /^url\s*=\s*(.+)$/.exec(trimmed);
1751
+ if (urlMatch) {
1752
+ const value = urlMatch[1].trim();
1753
+ if (!firstUrl) firstUrl = value;
1754
+ if (inOrigin) originUrl = value;
1755
+ }
1756
+ }
1757
+ return originUrl ?? firstUrl;
1758
+ }
1759
+ function parseRemote(remoteUrl) {
1760
+ if (!remoteUrl) return void 0;
1761
+ if (remoteUrl.startsWith("http")) {
1762
+ try {
1763
+ const url = new URL(remoteUrl);
1764
+ return { host: url.host, path: url.pathname.replace(/^\//, "") };
1765
+ } catch {
1766
+ return void 0;
1767
+ }
1768
+ }
1769
+ if (remoteUrl.startsWith("git@")) {
1770
+ const match = /^git@([^:]+):(.+)$/.exec(remoteUrl);
1771
+ if (!match) return void 0;
1772
+ return { host: match[1], path: match[2] };
1773
+ }
1774
+ if (remoteUrl.startsWith("ssh://")) {
1775
+ try {
1776
+ const url = new URL(remoteUrl);
1777
+ return { host: url.host, path: url.pathname.replace(/^\//, "") };
1778
+ } catch {
1779
+ return void 0;
1780
+ }
1781
+ }
1782
+ return void 0;
1783
+ }
1784
+ function normalizeProjectPath(remotePath, instanceBasePath) {
1785
+ let pathValue = remotePath;
1786
+ if (instanceBasePath && instanceBasePath !== "/") {
1787
+ const base = instanceBasePath.replace(/^\//, "") + "/";
1788
+ if (pathValue.startsWith(base)) {
1789
+ pathValue = pathValue.slice(base.length);
1790
+ }
1791
+ }
1792
+ const cleaned = stripGitSuffix(pathValue);
1793
+ return cleaned.length > 0 ? cleaned : void 0;
1794
+ }
1795
+ function stripGitSuffix(pathname) {
1796
+ return pathname.endsWith(".git") ? pathname.slice(0, -4) : pathname;
1797
+ }
1798
+ function buildApiUrl(instanceUrl, apiPath) {
1799
+ const base = instanceUrl.endsWith("/") ? instanceUrl : `${instanceUrl}/`;
1800
+ return new URL(apiPath.replace(/^\//, ""), base).toString();
1801
+ }
1802
+ function buildAuthHeaders(apiKey) {
1803
+ return { authorization: `Bearer ${apiKey}` };
1804
+ }
1805
+ async function fetchProjectDetails(instanceUrl, apiKey, projectPath) {
1806
+ const url = buildApiUrl(instanceUrl, `api/v4/projects/${encodeURIComponent(projectPath)}`);
1807
+ const response = await fetch(url, {
1808
+ headers: buildAuthHeaders(apiKey)
1809
+ });
1810
+ if (!response.ok) {
1811
+ throw new Error(`Failed to fetch project details: ${response.status}`);
1812
+ }
1813
+ const data = await response.json();
1814
+ return {
1815
+ projectId: data.id ? String(data.id) : void 0,
1816
+ namespaceId: data.namespace?.id ? String(data.namespace.id) : void 0
1817
+ };
1818
+ }
1819
+ async function fetchProjectDetailsWithFallback(instanceUrl, apiKey, projectPath) {
1820
+ const candidates = getProjectPathCandidates(projectPath);
1821
+ for (const candidate of candidates) {
1822
+ try {
1823
+ return await fetchProjectDetails(instanceUrl, apiKey, candidate);
1824
+ } catch {
1825
+ continue;
1826
+ }
1827
+ }
1828
+ try {
1829
+ const name = projectPath.split("/").pop() || projectPath;
1830
+ const searchUrl = new URL(buildApiUrl(instanceUrl, "api/v4/projects"));
1831
+ searchUrl.searchParams.set("search", name);
1832
+ searchUrl.searchParams.set("simple", "true");
1833
+ searchUrl.searchParams.set("per_page", "100");
1834
+ searchUrl.searchParams.set("membership", "true");
1835
+ const response = await fetch(searchUrl.toString(), {
1836
+ headers: buildAuthHeaders(apiKey)
1837
+ });
1838
+ if (!response.ok) {
1839
+ throw new Error(`Failed to search projects: ${response.status}`);
1840
+ }
1841
+ const data = await response.json();
1842
+ const match = data.find((project) => project.path_with_namespace === projectPath);
1843
+ if (!match) {
1844
+ throw new Error("Project not found via search");
1845
+ }
1846
+ return {
1847
+ projectId: match.id ? String(match.id) : void 0,
1848
+ namespaceId: match.namespace?.id ? String(match.namespace.id) : void 0
1849
+ };
1850
+ } catch {
1851
+ throw new Error("Project not found via API");
1852
+ }
1853
+ }
1854
+ function getProjectPathCandidates(projectPath) {
1855
+ const candidates = /* @__PURE__ */ new Set();
1856
+ candidates.add(projectPath);
1857
+ const parts = projectPath.split("/");
1858
+ if (parts.length > 2) {
1859
+ const withoutFirst = parts.slice(1).join("/");
1860
+ candidates.add(withoutFirst);
1861
+ }
1862
+ return Array.from(candidates);
1863
+ }
1864
+ async function readGitConfig(cwd) {
1865
+ const gitPath = path4.join(cwd, ".git");
1866
+ const stat = await fs4.stat(gitPath);
1867
+ if (stat.isDirectory()) {
1868
+ return fs4.readFile(path4.join(gitPath, "config"), "utf8");
1869
+ }
1870
+ const file = await fs4.readFile(gitPath, "utf8");
1871
+ const match = /^gitdir:\s*(.+)$/m.exec(file);
1872
+ if (!match) throw new Error("Invalid .git file");
1873
+ const gitdir = match[1].trim();
1874
+ const resolved = path4.isAbsolute(gitdir) ? gitdir : path4.join(cwd, gitdir);
1875
+ return fs4.readFile(path4.join(resolved, "config"), "utf8");
1876
+ }
1877
+
1878
+ // src/provider/adapters/workflow_service.ts
1879
+ var WorkflowCreateError = class extends Error {
1880
+ status;
1881
+ body;
1882
+ constructor(status, body) {
1883
+ super(`Failed to create workflow: ${status} ${body}`);
1884
+ this.status = status;
1885
+ this.body = body;
1886
+ }
1887
+ };
1888
+ async function createWorkflow(instanceUrl, apiKey, goal, workflowDefinition, containerParams) {
1889
+ const url = buildApiUrl(instanceUrl, "/api/v4/ai/duo_workflows/workflows");
1890
+ const response = await fetch(url.toString(), {
1891
+ method: "POST",
1892
+ headers: {
1893
+ "content-type": "application/json",
1894
+ ...buildAuthHeaders(apiKey)
1895
+ },
1896
+ body: JSON.stringify({
1897
+ project_id: containerParams?.projectId,
1898
+ namespace_id: containerParams?.namespaceId,
1899
+ goal,
1900
+ workflow_definition: workflowDefinition,
1901
+ environment: "ide",
1902
+ allow_agent_to_request_user: true
1903
+ })
1904
+ });
1905
+ if (!response.ok) {
1906
+ const text = await response.text();
1907
+ throw new WorkflowCreateError(response.status, text);
1908
+ }
1909
+ const data = await response.json();
1910
+ if (!data.id) {
1911
+ throw new Error(`Workflow creation failed: ${data.error || data.message || "unknown"}`);
1912
+ }
1913
+ return data.id.toString();
1914
+ }
1915
+ async function getWorkflowToken(instanceUrl, apiKey, workflowDefinition) {
1916
+ const url = buildApiUrl(instanceUrl, "/api/v4/ai/duo_workflows/direct_access");
1917
+ const response = await fetch(url.toString(), {
1918
+ method: "POST",
1919
+ headers: {
1920
+ "content-type": "application/json",
1921
+ ...buildAuthHeaders(apiKey)
1922
+ },
1923
+ body: JSON.stringify({ workflow_definition: workflowDefinition })
1924
+ });
1925
+ if (!response.ok) {
1926
+ const text = await response.text();
1927
+ throw new Error(`Failed to fetch workflow token: ${response.status} ${text}`);
1928
+ }
1929
+ return await response.json();
1930
+ }
1931
+
1932
+ // src/provider/adapters/workflow_client.ts
1933
+ import WebSocket2 from "isomorphic-ws";
1934
+ import { v4 as uuid4 } from "uuid";
1935
+
1936
+ // src/provider/adapters/websocket_stream.ts
1937
+ import WebSocket from "isomorphic-ws";
1938
+ import { EventEmitter } from "events";
1939
+ var KEEPALIVE_PING_INTERVAL_MS = 45 * 1e3;
1940
+ var WebSocketWorkflowStream = class extends EventEmitter {
1941
+ #socket;
1942
+ #keepalivePingIntervalId;
1943
+ constructor(socket) {
1944
+ super();
1945
+ this.#socket = socket;
1946
+ this.#setupEventHandlers();
1947
+ }
1948
+ #setupEventHandlers() {
1949
+ this.#socket.on("message", (event) => {
1950
+ try {
1951
+ const data = event && typeof event === "object" && "data" in event ? event.data : event;
1952
+ let message;
1953
+ if (typeof data === "string") {
1954
+ message = data;
1955
+ } else if (Buffer.isBuffer(data)) {
1956
+ message = data.toString("utf8");
1957
+ } else if (data instanceof ArrayBuffer) {
1958
+ message = Buffer.from(data).toString("utf8");
1959
+ } else if (Array.isArray(data)) {
1960
+ message = Buffer.concat(data).toString("utf8");
1961
+ } else {
1962
+ return;
1963
+ }
1964
+ if (!message || message === "undefined") {
1965
+ return;
1966
+ }
1967
+ const parsed = JSON.parse(message);
1968
+ this.emit("data", parsed);
1969
+ } catch (err2) {
1970
+ this.emit("error", err2 instanceof Error ? err2 : new Error(String(err2)));
1971
+ }
1972
+ });
1973
+ this.#socket.on("open", () => {
1974
+ this.emit("open");
1975
+ });
1976
+ this.#socket.on("error", (event) => {
1977
+ if (event instanceof Error) {
1978
+ this.emit("error", event);
1979
+ return;
1980
+ }
1981
+ const serialized = safeStringifyErrorEvent(event);
1982
+ this.emit("error", new Error(serialized));
1983
+ });
1984
+ this.#socket.on("close", (code, reason) => {
1985
+ clearInterval(this.#keepalivePingIntervalId);
1986
+ if (code === 1e3) {
1987
+ this.emit("end");
1988
+ return;
1989
+ }
1990
+ const reasonString = reason?.toString("utf8");
1991
+ this.emit("error", new Error(`WebSocket closed abnormally: ${code} ${reasonString || ""}`));
1992
+ });
1993
+ this.#socket.on("pong", () => {
1994
+ });
1995
+ this.#startKeepalivePingInterval();
1996
+ }
1997
+ #startKeepalivePingInterval() {
1998
+ this.#keepalivePingIntervalId = setInterval(() => {
1999
+ if (this.#socket.readyState !== WebSocket.OPEN) return;
2000
+ const timestamp = Date.now().toString();
2001
+ this.#socket.ping(Buffer.from(timestamp), void 0, () => {
2002
+ });
2003
+ }, KEEPALIVE_PING_INTERVAL_MS);
2004
+ }
2005
+ write(data) {
2006
+ if (this.#socket.readyState !== WebSocket.OPEN) {
2007
+ return false;
2008
+ }
2009
+ this.#socket.send(JSON.stringify(data));
2010
+ return true;
2011
+ }
2012
+ end() {
2013
+ this.#socket.close(1e3);
2014
+ }
2015
+ };
2016
+ function safeStringifyErrorEvent(event) {
2017
+ const payload = {
2018
+ type: event.type,
2019
+ message: event.message,
2020
+ error: event.error ? String(event.error) : void 0,
2021
+ target: {
2022
+ readyState: event.target?.readyState,
2023
+ url: event.target?.url
2024
+ }
2025
+ };
2026
+ return JSON.stringify(payload);
2027
+ }
2028
+
2029
+ // src/provider/adapters/workflow_client.ts
2030
+ var WebSocketWorkflowClient = class {
2031
+ #connectionDetails;
2032
+ #selectedModelIdentifier;
2033
+ #socket = null;
2034
+ #stream = null;
2035
+ #correlationId = uuid4();
2036
+ constructor(connectionDetails) {
2037
+ this.#connectionDetails = connectionDetails;
2038
+ this.#selectedModelIdentifier = connectionDetails.selectedModelIdentifier;
2039
+ }
2040
+ async executeWorkflow() {
2041
+ const url = this.#buildWebSocketUrl();
2042
+ const headers = this.#createConnectionHeaders();
2043
+ const clientOptions = { headers };
2044
+ if (this.#connectionDetails.agent) {
2045
+ Object.assign(clientOptions, { agent: this.#connectionDetails.agent });
2046
+ }
2047
+ this.#socket = new WebSocket2(url, clientOptions);
2048
+ this.#stream = new WebSocketWorkflowStream(this.#socket);
2049
+ await new Promise((resolve, reject) => {
2050
+ const timeoutId = setTimeout(() => {
2051
+ reject(new Error("WebSocket connection timeout"));
2052
+ }, 15e3);
2053
+ const onOpen = () => {
2054
+ clearTimeout(timeoutId);
2055
+ this.#stream?.removeListener("error", onError);
2056
+ resolve();
2057
+ };
2058
+ const onError = (err2) => {
2059
+ clearTimeout(timeoutId);
2060
+ this.#stream?.removeListener("open", onOpen);
2061
+ reject(err2);
2062
+ };
2063
+ this.#stream?.once("open", onOpen);
2064
+ this.#stream?.once("error", onError);
2065
+ });
2066
+ return this.#stream;
2067
+ }
2068
+ dispose() {
2069
+ this.#stream?.end();
2070
+ this.#stream = null;
2071
+ this.#socket = null;
2072
+ }
2073
+ #buildWebSocketUrl() {
2074
+ const baseUrl = new URL(this.#connectionDetails.gitlabInstanceUrl);
2075
+ const basePath = baseUrl.pathname.endsWith("/") ? baseUrl.pathname : `${baseUrl.pathname}/`;
2076
+ const url = new URL(basePath + "api/v4/ai/duo_workflows/ws", baseUrl);
2077
+ url.protocol = baseUrl.protocol === "https:" ? "wss:" : "ws:";
2078
+ if (this.#selectedModelIdentifier) {
2079
+ url.searchParams.set("user_selected_model_identifier", this.#selectedModelIdentifier);
2080
+ }
2081
+ return url.toString();
2082
+ }
2083
+ #createConnectionHeaders() {
2084
+ const headers = { ...this.#connectionDetails.headers };
2085
+ headers["authorization"] = `Bearer ${this.#connectionDetails.token}`;
2086
+ headers["x-request-id"] = this.#correlationId;
2087
+ headers["x-gitlab-language-server-version"] = LANGUAGE_SERVER_VERSION;
2088
+ headers["x-gitlab-client-type"] = "node-websocket";
2089
+ headers["user-agent"] = buildUserAgent();
2090
+ headers["origin"] = this.#connectionDetails.gitlabInstanceUrl.origin;
2091
+ return headers;
2092
+ }
2093
+ };
2094
+ var LANGUAGE_SERVER_VERSION = "8.62.2";
2095
+ function buildUserAgent() {
2096
+ return `unknown/unknown unknown/unknown gitlab-language-server/${LANGUAGE_SERVER_VERSION}`;
2097
+ }
2098
+
2099
+ // src/provider/adapters/system_context.ts
2100
+ import os from "os";
2101
+ function getSystemContextItems(systemRules) {
2102
+ const platform = os.platform();
2103
+ const arch = os.arch();
2104
+ const items = [
2105
+ {
2106
+ category: "os_information",
2107
+ content: `<os><platform>${platform}</platform><architecture>${arch}</architecture></os>`,
2108
+ id: "os_information",
2109
+ metadata: {
2110
+ title: "Operating System",
2111
+ enabled: true,
2112
+ subType: "os",
2113
+ icon: "monitor",
2114
+ secondaryText: `${platform} ${arch}`,
2115
+ subTypeLabel: "System Information"
2116
+ }
2117
+ }
2118
+ ];
2119
+ const trimmedRules = systemRules?.trim();
2120
+ const combinedRules = trimmedRules ? `${trimmedRules}
2121
+
2122
+ ${SYSTEM_RULES}` : SYSTEM_RULES;
2123
+ if (combinedRules.trim()) {
2124
+ items.push({
2125
+ category: "user_rule",
2126
+ content: combinedRules,
2127
+ id: "user_rules",
2128
+ metadata: {
2129
+ title: "System Rules",
2130
+ enabled: true,
2131
+ subType: "user_rule",
2132
+ icon: "file-text",
2133
+ secondaryText: "User rules",
2134
+ subTypeLabel: "System rules"
2135
+ }
2136
+ });
2137
+ }
2138
+ return items;
2139
+ }
2140
+ var SYSTEM_RULES = `<system-reminder>
2141
+ You MUST follow ALL the rules in this block strictly.
2142
+
2143
+ <tool_orchestration>
2144
+ PARALLEL EXECUTION:
2145
+ - When gathering information, plan all needed searches upfront and execute
2146
+ them together using multiple tool calls in the same turn where possible.
2147
+ - Read multiple related files together rather than one at a time.
2148
+ - Patterns: grep + find_files together, read_file for multiple files together.
2149
+ - Temporary rule: do NOT use read_files; use read_file only (repeat calls as needed).
2150
+
2151
+ SEQUENTIAL EXECUTION (only when output depends on previous step):
2152
+ - Read a file BEFORE editing it (always).
2153
+ - Check dependencies BEFORE importing them.
2154
+ - Run tests AFTER making changes.
2155
+
2156
+ READ BEFORE WRITE:
2157
+ - Always read existing files before modifying them to understand context.
2158
+ - Check for existing patterns (naming, imports, error handling) and match them.
2159
+ - Verify the exact content to replace when using edit_file.
2160
+
2161
+ ERROR HANDLING:
2162
+ - If a tool fails, analyze the error before retrying.
2163
+ - If a shell command fails, check the error output and adapt.
2164
+ - Do not repeat the same failing operation without changes.
2165
+ </tool_orchestration>
2166
+
2167
+ <development_workflow>
2168
+ For software development tasks, follow this workflow:
2169
+
2170
+ 1. UNDERSTAND: Read relevant files, explore the codebase structure
2171
+ 2. PLAN: Break down the task into clear steps
2172
+ 3. IMPLEMENT: Make changes methodically, one step at a time
2173
+ 4. VERIFY: Run tests, type-checking, or build to validate changes
2174
+ 5. COMPLETE: Summarize what was accomplished
2175
+
2176
+ CODE QUALITY:
2177
+ - Match existing code style and patterns in the project
2178
+ - Write immediately executable code (no TODOs or placeholders)
2179
+ - Prefer editing existing files over creating new ones
2180
+ - Use the project's established error handling patterns
2181
+ </development_workflow>
2182
+
2183
+ <communication>
2184
+ - Be concise and direct. Responses appear in a chat panel.
2185
+ - Focus on practical solutions over theoretical discussion.
2186
+ - When unable to complete a request, explain the limitation briefly and
2187
+ provide alternatives.
2188
+ - Use active language: "Analyzing...", "Searching..." instead of "Let me..."
2189
+ </communication>
2190
+ </system-reminder>`;
2191
+
2192
+ // src/provider/application/di_container.ts
2193
+ var DIContainer = class {
2194
+ #registrations = /* @__PURE__ */ new Map();
2195
+ #resolving = /* @__PURE__ */ new Set();
2196
+ registerSingleton(token, factory) {
2197
+ this.#registrations.set(token, {
2198
+ scope: "singleton",
2199
+ factory,
2200
+ initialized: false
2201
+ });
2202
+ }
2203
+ registerTransient(token, factory) {
2204
+ this.#registrations.set(token, {
2205
+ scope: "transient",
2206
+ factory,
2207
+ initialized: false
2208
+ });
2209
+ }
2210
+ resolve(token) {
2211
+ const registration = this.#registrations.get(token);
2212
+ if (!registration) {
2213
+ throw new Error(`Missing DI registration for token: ${token}`);
2214
+ }
2215
+ if (registration.scope === "singleton") {
2216
+ if (!registration.initialized) {
2217
+ registration.instance = this.#build(token, registration.factory);
2218
+ registration.initialized = true;
2219
+ }
2220
+ return registration.instance;
2221
+ }
2222
+ return this.#build(token, registration.factory);
2223
+ }
2224
+ #build(token, factory) {
2225
+ if (this.#resolving.has(token)) {
2226
+ throw new Error(`Circular DI dependency detected for token: ${token}`);
2227
+ }
2228
+ this.#resolving.add(token);
2229
+ try {
2230
+ return factory(this);
2231
+ } finally {
2232
+ this.#resolving.delete(token);
2233
+ }
2234
+ }
2235
+ };
2236
+
2237
+ // src/provider/adapters/default_runtime_dependencies.ts
2238
+ var RUNTIME_TOKENS = {
2239
+ workflowService: "runtime.workflowService",
2240
+ workflowClientFactory: "runtime.workflowClientFactory",
2241
+ projectLookup: "runtime.projectLookup",
2242
+ systemContext: "runtime.systemContext",
2243
+ clock: "runtime.clock",
2244
+ runtimeDependencies: "runtime.dependencies"
2245
+ };
2246
+ function createRuntimeContainer() {
2247
+ const container = new DIContainer();
2248
+ registerDefaultRuntimeDependencies(container);
2249
+ return container;
2250
+ }
2251
+ function registerDefaultRuntimeDependencies(container) {
2252
+ container.registerSingleton(
2253
+ RUNTIME_TOKENS.workflowService,
2254
+ () => ({
2255
+ createWorkflow,
2256
+ getWorkflowToken
2257
+ })
2258
+ );
2259
+ container.registerSingleton(
2260
+ RUNTIME_TOKENS.workflowClientFactory,
2261
+ () => ((config) => new WebSocketWorkflowClient({
2262
+ ...config,
2263
+ ...resolveWorkflowClientOptions()
2264
+ }))
2265
+ );
2266
+ container.registerSingleton(
2267
+ RUNTIME_TOKENS.projectLookup,
2268
+ () => ({
2269
+ detectProjectPath,
2270
+ fetchProjectDetailsWithFallback
2271
+ })
2272
+ );
2273
+ container.registerSingleton(
2274
+ RUNTIME_TOKENS.systemContext,
2275
+ () => ({
2276
+ getSystemContextItems
2277
+ })
2278
+ );
2279
+ container.registerSingleton(
2280
+ RUNTIME_TOKENS.clock,
2281
+ () => ({
2282
+ now: () => Date.now(),
2283
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
2284
+ })
2285
+ );
2286
+ container.registerSingleton(
2287
+ RUNTIME_TOKENS.runtimeDependencies,
2288
+ (c) => ({
2289
+ workflowService: c.resolve(
2290
+ RUNTIME_TOKENS.workflowService
2291
+ ),
2292
+ createWorkflowClient: c.resolve(
2293
+ RUNTIME_TOKENS.workflowClientFactory
2294
+ ),
2295
+ projectLookup: c.resolve(
2296
+ RUNTIME_TOKENS.projectLookup
2297
+ ),
2298
+ systemContext: c.resolve(
2299
+ RUNTIME_TOKENS.systemContext
2300
+ ),
2301
+ clock: c.resolve(RUNTIME_TOKENS.clock)
2302
+ })
2303
+ );
2304
+ }
2305
+ function resolveWorkflowClientOptions() {
2306
+ if (process.env.HTTPS_PROXY || process.env.HTTP_PROXY) {
2307
+ return { agent: new ProxyAgent() };
2308
+ }
2309
+ return {};
2310
+ }
2311
+
2312
+ // src/provider/interfaces/provider.ts
2313
+ import { createRequire as createRequire2 } from "module";
2314
+ var REQUIRED_MODULES = [
2315
+ "isomorphic-ws",
2316
+ "uuid",
2317
+ "zod",
2318
+ "neverthrow",
2319
+ "proxy-agent"
2320
+ ];
2321
+ function assertDependencies() {
2322
+ const require2 = createRequire2(import.meta.url);
2323
+ const missing = [];
2324
+ for (const name of REQUIRED_MODULES) {
2325
+ try {
2326
+ require2.resolve(name);
2327
+ } catch {
2328
+ missing.push(name);
2329
+ }
2330
+ }
2331
+ if (missing.length > 0) {
2332
+ throw new Error(
2333
+ "Missing provider dependencies: " + missing.join(", ") + ". Run `npm install` in the package directory."
2334
+ );
2335
+ }
2336
+ }
2337
+ function assertInstanceUrl(value) {
2338
+ try {
2339
+ new URL(value);
2340
+ } catch {
2341
+ throw new Error(`Invalid instanceUrl: "${value}"`);
2342
+ }
2343
+ }
2344
+ function createGitLabDuoAgentic(options) {
2345
+ assertDependencies();
2346
+ assertInstanceUrl(options.instanceUrl);
2347
+ const container = createRuntimeContainer();
2348
+ const dependencies = container.resolve(
2349
+ RUNTIME_TOKENS.runtimeDependencies
2350
+ );
2351
+ const sharedRuntime = new GitLabAgenticRuntime(options, dependencies);
2352
+ return {
2353
+ languageModel(modelId) {
2354
+ return new GitLabDuoAgenticLanguageModel(modelId, options, sharedRuntime);
2355
+ },
2356
+ textEmbeddingModel() {
2357
+ throw new Error("GitLab Duo Agentic does not support text embedding models");
2358
+ },
2359
+ imageModel() {
2360
+ throw new Error("GitLab Duo Agentic does not support image models");
2361
+ }
2362
+ };
2363
+ }
2364
+ export {
2365
+ GitLabDuoAgenticPlugin,
2366
+ createGitLabDuoAgentic,
2367
+ GitLabDuoAgenticPlugin as default
2368
+ };