opencode-gitlab-duo-agentic 0.1.7 → 0.1.8

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