opencode-gitlab-duo-agentic 0.1.1 → 0.1.3

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.
Files changed (3) hide show
  1. package/README.md +31 -17
  2. package/dist/index.js +518 -202
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,10 +1,16 @@
1
1
  # opencode-gitlab-duo-agentic
2
2
 
3
- OpenCode plugin and provider for GitLab Duo Agentic workflows, published as a single npm package.
3
+ OpenCode plugin for GitLab Duo Agentic. It registers the provider, discovers models from the GitLab API, and exposes file-reading tools.
4
4
 
5
- ## Install
5
+ ## Setup
6
6
 
7
- Add the plugin to your OpenCode config:
7
+ 1. Export your GitLab token:
8
+
9
+ ```bash
10
+ export GITLAB_TOKEN=glpat-...
11
+ ```
12
+
13
+ 2. Add the plugin to `opencode.json`:
8
14
 
9
15
  ```json
10
16
  {
@@ -13,32 +19,40 @@ Add the plugin to your OpenCode config:
13
19
  }
14
20
  ```
15
21
 
16
- The plugin registers the provider `gitlab-duo-agentic` automatically and resolves it from the same module entrypoint as the plugin.
22
+ 3. Run `opencode`. The provider, models, and tools are registered automatically.
17
23
 
18
- ## Required environment variables
24
+ `GITLAB_INSTANCE_URL` defaults to `https://gitlab.com`. Set it only for self-managed GitLab.
19
25
 
20
- ```bash
21
- export GITLAB_TOKEN=glpat-...
22
- export GITLAB_INSTANCE_URL=https://gitlab.com
23
- ```
26
+ ## Provider options
27
+
28
+ Override defaults in `opencode.json` under `provider.gitlab-duo-agentic.options`.
24
29
 
25
- `GITLAB_INSTANCE_URL` is optional and defaults to `https://gitlab.com`.
30
+ | Option | Type | Default | Description |
31
+ |--------|------|---------|-------------|
32
+ | `instanceUrl` | string | `GITLAB_INSTANCE_URL` or `https://gitlab.com` | GitLab instance URL |
33
+ | `apiKey` | string | `GITLAB_TOKEN` | Personal access token |
34
+ | `sendSystemContext` | boolean | `true` | Send system context to Duo |
35
+ | `enableMcp` | boolean | `true` | Enable MCP tools |
36
+ | `systemRules` | string | `""` | Inline system rules |
37
+ | `systemRulesPath` | string | `""` | Path to a system rules file |
26
38
 
27
- ## Optional model configuration
39
+ ## Model discovery
28
40
 
29
- If you generate `models.json` (using `src/scripts/fetch_models.ts`), the plugin resolves it by:
41
+ Models are discovered in this order:
30
42
 
31
- 1. `options.modelsPath` in provider config
32
- 2. `GITLAB_DUO_MODELS_PATH`
33
- 3. `models.json` found by walking upward from `process.cwd()`
43
+ 1. Local cache (TTL: 24h)
44
+ 2. Live fetch from GitLab GraphQL API
45
+ 3. Stale cache (if live fetch fails)
46
+ 4. `models.json` on disk
47
+ 5. Default `duo-agentic` model
34
48
 
35
- If no file is found, it falls back to a default `duo-agentic` model.
49
+ Cache is stored in `~/.cache/opencode/` (or `XDG_CACHE_HOME`). Override TTL with `GITLAB_DUO_MODELS_CACHE_TTL` (seconds).
36
50
 
37
51
  ## Development
38
52
 
39
53
  ```bash
40
54
  npm install
41
- npm run typecheck
42
55
  npm run build
56
+ npm run typecheck
43
57
  npm run pack:check
44
58
  ```
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  // src/plugin/config.ts
2
- import path2 from "path";
3
- import fs2 from "fs";
2
+ import path4 from "path";
3
+ import fs4 from "fs";
4
4
 
5
5
  // src/plugin/models.ts
6
- import path from "path";
7
- import fs from "fs";
6
+ import path3 from "path";
7
+ import fs3 from "fs";
8
8
 
9
9
  // src/shared/model_entry.ts
10
10
  function buildModelEntry(name) {
@@ -28,12 +28,435 @@ var GITLAB_DUO_DEFAULT_MODEL_NAME = "Duo Agentic";
28
28
  var GITLAB_DUO_PLUGIN_PACKAGE_NAME = "opencode-gitlab-duo-agentic";
29
29
  var GITLAB_DUO_PROVIDER_NPM_ENTRY = GITLAB_DUO_PLUGIN_PACKAGE_NAME;
30
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
+
31
413
  // src/plugin/models.ts
32
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
+ }
33
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) {
34
457
  if (modelsJsonPath) {
35
458
  try {
36
- const raw = await fs.promises.readFile(modelsJsonPath, "utf8");
459
+ const raw = await fs3.promises.readFile(modelsJsonPath, "utf8");
37
460
  const data = JSON.parse(raw);
38
461
  if (data.models && Object.keys(data.models).length > 0) {
39
462
  console.log(
@@ -48,28 +471,36 @@ async function loadGitLabModels(options = {}) {
48
471
  );
49
472
  }
50
473
  }
51
- return {
52
- [GITLAB_DUO_DEFAULT_MODEL_ID]: buildModelEntry(GITLAB_DUO_DEFAULT_MODEL_NAME)
53
- };
474
+ return null;
54
475
  }
55
476
  function resolveModelsJsonPath(overridePath) {
56
477
  const override = typeof overridePath === "string" && overridePath.trim() ? overridePath.trim() : process.env.GITLAB_DUO_MODELS_PATH;
57
478
  if (override) {
58
- const resolved = path.isAbsolute(override) ? override : path.resolve(process.cwd(), override);
59
- if (fs.existsSync(resolved)) return resolved;
479
+ const resolved = path3.isAbsolute(override) ? override : path3.resolve(process.cwd(), override);
480
+ if (fs3.existsSync(resolved)) return resolved;
60
481
  console.warn(`[gitlab-duo] models.json not found at override path ${resolved}`);
61
482
  return null;
62
483
  }
63
484
  let current = process.cwd();
64
485
  while (true) {
65
- const candidate = path.join(current, "models.json");
66
- if (fs.existsSync(candidate)) return candidate;
67
- const parent = path.dirname(current);
486
+ const candidate = path3.join(current, "models.json");
487
+ if (fs3.existsSync(candidate)) return candidate;
488
+ const parent = path3.dirname(current);
68
489
  if (parent === current) break;
69
490
  current = parent;
70
491
  }
71
492
  return null;
72
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
+ }
73
504
 
74
505
  // src/plugin/config.ts
75
506
  async function configHook(input) {
@@ -101,15 +532,15 @@ async function configHook(input) {
101
532
  enableMcp,
102
533
  systemRules: mergedSystemRules || void 0
103
534
  },
104
- models: await loadGitLabModels({ modelsPath })
535
+ models: await loadGitLabModels({ modelsPath, instanceUrl, apiKey })
105
536
  };
106
537
  }
107
538
  async function mergeSystemRules(rules, rulesPath) {
108
539
  const baseRules = rules.trim();
109
540
  if (!rulesPath) return baseRules;
110
- const resolvedPath = path2.isAbsolute(rulesPath) ? rulesPath : path2.resolve(process.cwd(), rulesPath);
541
+ const resolvedPath = path4.isAbsolute(rulesPath) ? rulesPath : path4.resolve(process.cwd(), rulesPath);
111
542
  try {
112
- const fileRules = (await fs2.promises.readFile(resolvedPath, "utf8")).trim();
543
+ const fileRules = (await fs4.promises.readFile(resolvedPath, "utf8")).trim();
113
544
  if (!fileRules) return baseRules;
114
545
  return baseRules ? `${baseRules}
115
546
 
@@ -122,8 +553,8 @@ ${fileRules}` : fileRules;
122
553
 
123
554
  // src/plugin/tools.ts
124
555
  import { tool } from "@opencode-ai/plugin";
125
- import path3 from "path";
126
- import fs3 from "fs";
556
+ import path5 from "path";
557
+ import fs5 from "fs";
127
558
  function createReadTools() {
128
559
  return {
129
560
  read_file: tool({
@@ -140,7 +571,7 @@ function createReadTools() {
140
571
  metadata: {}
141
572
  });
142
573
  try {
143
- return await fs3.promises.readFile(resolvedPath, "utf8");
574
+ return await fs5.promises.readFile(resolvedPath, "utf8");
144
575
  } catch (error) {
145
576
  throw new Error(formatReadError(displayPath, error));
146
577
  }
@@ -165,7 +596,7 @@ function createReadTools() {
165
596
  const results = await Promise.all(
166
597
  targets.map(async (target) => {
167
598
  try {
168
- const content = await fs3.promises.readFile(target.resolvedPath, "utf8");
599
+ const content = await fs5.promises.readFile(target.resolvedPath, "utf8");
169
600
  return [target.inputPath, { content }];
170
601
  } catch (error) {
171
602
  return [target.inputPath, { error: formatReadError(target.displayPath, error) }];
@@ -183,9 +614,9 @@ function createReadTools() {
183
614
  }
184
615
  function resolveReadPath(filePath, ctx) {
185
616
  const displayPath = filePath;
186
- const resolvedPath = path3.isAbsolute(filePath) ? filePath : path3.resolve(ctx.worktree, filePath);
187
- const worktreePath = path3.resolve(ctx.worktree);
188
- if (resolvedPath !== worktreePath && !resolvedPath.startsWith(worktreePath + path3.sep)) {
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)) {
189
620
  throw new Error(`File is outside the repository: "${displayPath}"`);
190
621
  }
191
622
  return { resolvedPath, displayPath };
@@ -794,6 +1225,14 @@ var GitLabDuoAgenticLanguageModel = class {
794
1225
  const workflowType = "chat";
795
1226
  const promptText = extractLastUserText(options.prompt);
796
1227
  const toolResults = extractToolResults(options.prompt);
1228
+ console.warn("[duo-debug] doStream called", {
1229
+ promptText: promptText?.slice(0, 80),
1230
+ toolResultCount: toolResults.length,
1231
+ hasStarted: this.#runtime.hasStarted,
1232
+ lastSentPrompt: this.#lastSentPrompt?.slice(0, 80) ?? null,
1233
+ pendingToolRequests: this.#pendingToolRequests.size,
1234
+ sentToolCallIds: this.#sentToolCallIds.size
1235
+ });
797
1236
  this.#runtime.resetMapperState();
798
1237
  if (!this.#runtime.hasStarted) {
799
1238
  this.#sentToolCallIds.clear();
@@ -803,14 +1242,18 @@ var GitLabDuoAgenticLanguageModel = class {
803
1242
  }
804
1243
  }
805
1244
  this.#lastSentPrompt = null;
1245
+ console.warn("[duo-debug] hasStarted=false => reset sentToolCallIds, lastSentPrompt=null");
806
1246
  }
807
1247
  const freshToolResults = toolResults.filter((r) => !this.#sentToolCallIds.has(r.toolCallId));
1248
+ console.warn("[duo-debug] freshToolResults:", freshToolResults.length);
808
1249
  const modelRef = this.modelId === GITLAB_DUO_DEFAULT_MODEL_ID ? void 0 : this.modelId;
809
1250
  this.#runtime.setSelectedModelIdentifier(modelRef);
810
1251
  await this.#runtime.ensureConnected(promptText || "", workflowType);
1252
+ console.warn("[duo-debug] ensureConnected returned OK");
811
1253
  const mcpTools = this.#options.enableMcp === false ? [] : buildMcpTools(options);
812
1254
  const toolContext = buildToolContext(mcpTools);
813
1255
  const isNewUserMessage = promptText != null && promptText !== this.#lastSentPrompt;
1256
+ console.warn("[duo-debug] isNewUserMessage:", isNewUserMessage, "promptText != null:", promptText != null, "promptText !== lastSentPrompt:", promptText !== this.#lastSentPrompt);
814
1257
  let sentToolResults = false;
815
1258
  if (freshToolResults.length > 0) {
816
1259
  for (const result of freshToolResults) {
@@ -857,6 +1300,7 @@ var GitLabDuoAgenticLanguageModel = class {
857
1300
  this.#pendingToolRequests.delete(result.toolCallId);
858
1301
  }
859
1302
  }
1303
+ console.warn("[duo-debug] startRequest gate: !sentToolResults:", !sentToolResults, "isNewUserMessage:", isNewUserMessage, "=> will send:", !sentToolResults && isNewUserMessage);
860
1304
  if (!sentToolResults && isNewUserMessage) {
861
1305
  const extraContext = [];
862
1306
  if (toolContext) extraContext.push(toolContext);
@@ -918,6 +1362,7 @@ var GitLabDuoAgenticLanguageModel = class {
918
1362
  }
919
1363
  });
920
1364
  }
1365
+ console.warn("[duo-debug] >>> calling sendStartRequest", { goal: promptText?.slice(0, 80), hasStarted: this.#runtime.hasStarted, extraContextCount: extraContext.length });
921
1366
  this.#runtime.sendStartRequest(
922
1367
  promptText,
923
1368
  workflowType,
@@ -926,11 +1371,15 @@ var GitLabDuoAgenticLanguageModel = class {
926
1371
  extraContext
927
1372
  );
928
1373
  this.#lastSentPrompt = promptText;
1374
+ console.warn("[duo-debug] >>> sendStartRequest completed, lastSentPrompt set");
929
1375
  this.#usageEstimator.addInputChars(promptText);
930
1376
  for (const ctx of extraContext) {
931
1377
  if (ctx.content) this.#usageEstimator.addInputChars(ctx.content);
932
1378
  }
1379
+ } else {
1380
+ console.warn("[duo-debug] SKIPPED sendStartRequest (sentToolResults:", sentToolResults, "isNewUserMessage:", isNewUserMessage, ")");
933
1381
  }
1382
+ console.warn("[duo-debug] creating event stream iterator");
934
1383
  const iterator = this.#mapEventsToStream(this.#runtime.getEventStream());
935
1384
  const stream = asyncIteratorToReadableStream(iterator);
936
1385
  return {
@@ -943,9 +1392,13 @@ var GitLabDuoAgenticLanguageModel = class {
943
1392
  async *#mapEventsToStream(events) {
944
1393
  const state = { textStarted: false };
945
1394
  const estimator = this.#usageEstimator;
1395
+ let eventCount = 0;
1396
+ console.warn("[duo-debug] #mapEventsToStream: starting iteration");
946
1397
  yield { type: "stream-start", warnings: [] };
947
1398
  try {
948
1399
  for await (const event of events) {
1400
+ eventCount++;
1401
+ console.warn("[duo-debug] #mapEventsToStream event #" + eventCount + ":", event.type, event.type === "TEXT_CHUNK" ? "(len=" + event.content.length + ")" : "");
949
1402
  if (event.type === "TEXT_CHUNK") {
950
1403
  if (event.content.length > 0) {
951
1404
  estimator.addOutputChars(event.content);
@@ -991,9 +1444,11 @@ var GitLabDuoAgenticLanguageModel = class {
991
1444
  }
992
1445
  }
993
1446
  } catch (streamErr) {
1447
+ console.warn("[duo-debug] #mapEventsToStream: caught error after", eventCount, "events:", streamErr instanceof Error ? streamErr.message : String(streamErr));
994
1448
  yield { type: "error", error: streamErr instanceof Error ? streamErr : new Error(String(streamErr)) };
995
1449
  return;
996
1450
  }
1451
+ console.warn("[duo-debug] #mapEventsToStream: loop ended normally after", eventCount, "events => yielding finish:stop");
997
1452
  yield { type: "finish", finishReason: "stop", usage: this.#currentUsage };
998
1453
  }
999
1454
  // ---------------------------------------------------------------------------
@@ -1060,7 +1515,7 @@ function classifyModeReminder2(reminder) {
1060
1515
  }
1061
1516
 
1062
1517
  // src/provider/application/workflow_event_mapper.ts
1063
- import crypto from "crypto";
1518
+ import crypto2 from "crypto";
1064
1519
 
1065
1520
  // src/provider/core/ui_chat_log.ts
1066
1521
  import { z } from "zod";
@@ -1201,7 +1656,7 @@ var WorkflowEventMapper = class {
1201
1656
  break;
1202
1657
  }
1203
1658
  case "request": {
1204
- const requestId = latestMessage.correlation_id || crypto.randomUUID();
1659
+ const requestId = latestMessage.correlation_id || crypto2.randomUUID();
1205
1660
  events.push({
1206
1661
  type: "TOOL_REQUEST",
1207
1662
  requestId,
@@ -1440,14 +1895,26 @@ var GitLabAgenticRuntime = class {
1440
1895
  // Connection lifecycle
1441
1896
  // ---------------------------------------------------------------------------
1442
1897
  async ensureConnected(goal, workflowType) {
1898
+ console.warn("[duo-debug] ensureConnected called", {
1899
+ hasStream: !!this.#stream,
1900
+ hasWorkflowId: !!this.#workflowId,
1901
+ workflowId: this.#workflowId?.slice(0, 12),
1902
+ hasQueue: !!this.#queue,
1903
+ startRequestSent: this.#startRequestSent
1904
+ });
1443
1905
  if (this.#stream && this.#workflowId && this.#queue) {
1906
+ console.warn("[duo-debug] ensureConnected: short-circuit (already connected)");
1444
1907
  return;
1445
1908
  }
1446
1909
  if (!this.#containerParams) {
1447
1910
  this.#containerParams = await this.#resolveContainerParams();
1448
1911
  }
1449
1912
  if (!this.#workflowId) {
1913
+ console.warn("[duo-debug] ensureConnected: creating new workflow");
1450
1914
  this.#workflowId = await this.#createWorkflow(goal, workflowType);
1915
+ console.warn("[duo-debug] ensureConnected: workflow created:", this.#workflowId?.slice(0, 12));
1916
+ } else {
1917
+ console.warn("[duo-debug] ensureConnected: reusing existing workflowId:", this.#workflowId?.slice(0, 12));
1451
1918
  }
1452
1919
  const token = await this.#dependencies.workflowService.getWorkflowToken(
1453
1920
  this.#options.instanceUrl,
@@ -1455,15 +1922,19 @@ var GitLabAgenticRuntime = class {
1455
1922
  workflowType
1456
1923
  );
1457
1924
  this.#workflowToken = token;
1925
+ console.warn("[duo-debug] ensureConnected: got workflow token");
1458
1926
  const MAX_LOCK_RETRIES = 3;
1459
1927
  const LOCK_RETRY_DELAY_MS = 3e3;
1460
1928
  for (let attempt = 1; attempt <= MAX_LOCK_RETRIES; attempt++) {
1461
1929
  this.#queue = new AsyncQueue();
1462
1930
  try {
1931
+ console.warn("[duo-debug] ensureConnected: connecting WebSocket (attempt", attempt, ")");
1463
1932
  await this.#connectWebSocket();
1933
+ console.warn("[duo-debug] ensureConnected: WebSocket connected OK");
1464
1934
  return;
1465
1935
  } catch (err2) {
1466
1936
  const msg = err2 instanceof Error ? err2.message : String(err2);
1937
+ console.warn("[duo-debug] ensureConnected: WebSocket error:", msg);
1467
1938
  if ((msg.includes("1013") || msg.includes("lock")) && attempt < MAX_LOCK_RETRIES) {
1468
1939
  this.#resetStreamState();
1469
1940
  await this.#dependencies.clock.sleep(LOCK_RETRY_DELAY_MS);
@@ -1508,7 +1979,13 @@ var GitLabAgenticRuntime = class {
1508
1979
  preapproved_tools: preapprovedTools
1509
1980
  }
1510
1981
  };
1511
- this.#stream.write(startRequest);
1982
+ console.warn("[duo-debug] sendStartRequest: writing to stream", {
1983
+ workflowId: this.#workflowId?.slice(0, 12),
1984
+ goal: goal?.slice(0, 80),
1985
+ contextCount: additionalContext.length
1986
+ });
1987
+ const writeResult = this.#stream.write(startRequest);
1988
+ console.warn("[duo-debug] sendStartRequest: write() returned:", writeResult);
1512
1989
  this.#startRequestSent = true;
1513
1990
  }
1514
1991
  sendToolResponse(requestId, response, responseType) {
@@ -1597,12 +2074,14 @@ var GitLabAgenticRuntime = class {
1597
2074
  #bindStream(stream, queue) {
1598
2075
  const now = () => this.#dependencies.clock.now();
1599
2076
  const closeWithError = (message) => {
2077
+ console.warn("[duo-debug] stream closeWithError:", message);
1600
2078
  queue.push({ type: "ERROR", message, timestamp: now() });
1601
2079
  queue.close();
1602
2080
  this.#resetStreamState();
1603
2081
  };
1604
2082
  const handleAction = async (action) => {
1605
2083
  if (action.newCheckpoint) {
2084
+ console.warn("[duo-debug] stream data: newCheckpoint status=", action.newCheckpoint.status, "goal=", action.newCheckpoint.goal?.slice(0, 40));
1606
2085
  const duoEvent = {
1607
2086
  checkpoint: action.newCheckpoint.checkpoint,
1608
2087
  errors: action.newCheckpoint.errors || [],
@@ -1610,6 +2089,7 @@ var GitLabAgenticRuntime = class {
1610
2089
  workflowStatus: action.newCheckpoint.status
1611
2090
  };
1612
2091
  const events = await this.#mapper.mapWorkflowEvent(duoEvent);
2092
+ console.warn("[duo-debug] mapper produced", events.length, "events:", events.map((e) => e.type));
1613
2093
  for (const event of events) {
1614
2094
  queue.push(event);
1615
2095
  }
@@ -1617,6 +2097,7 @@ var GitLabAgenticRuntime = class {
1617
2097
  }
1618
2098
  const toolRequest = mapWorkflowActionToToolRequest(action);
1619
2099
  if (toolRequest) {
2100
+ console.warn("[duo-debug] stream data: toolRequest", toolRequest.toolName);
1620
2101
  queue.push({
1621
2102
  type: "TOOL_REQUEST",
1622
2103
  ...toolRequest,
@@ -1624,6 +2105,7 @@ var GitLabAgenticRuntime = class {
1624
2105
  });
1625
2106
  return;
1626
2107
  }
2108
+ console.warn("[duo-debug] stream data: unhandled action", Object.keys(action));
1627
2109
  };
1628
2110
  stream.on("data", (action) => {
1629
2111
  void handleAction(action).catch((error) => {
@@ -1632,9 +2114,11 @@ var GitLabAgenticRuntime = class {
1632
2114
  });
1633
2115
  });
1634
2116
  stream.on("error", (err2) => {
2117
+ console.warn("[duo-debug] stream error:", err2.message);
1635
2118
  closeWithError(err2.message);
1636
2119
  });
1637
2120
  stream.on("end", () => {
2121
+ console.warn("[duo-debug] stream end => closing queue + resetStreamState");
1638
2122
  queue.close();
1639
2123
  this.#resetStreamState();
1640
2124
  });
@@ -1656,6 +2140,7 @@ var GitLabAgenticRuntime = class {
1656
2140
  this.#bindStream(stream, this.#queue);
1657
2141
  }
1658
2142
  #resetStreamState() {
2143
+ console.warn("[duo-debug] #resetStreamState called", { hadStream: !!this.#stream, hadQueue: !!this.#queue, workflowId: this.#workflowId?.slice(0, 12) });
1659
2144
  this.#stream = void 0;
1660
2145
  this.#queue = void 0;
1661
2146
  this.#startRequestSent = false;
@@ -1706,175 +2191,6 @@ function parseHttpToolOutput(output) {
1706
2191
  // src/provider/adapters/default_runtime_dependencies.ts
1707
2192
  import { ProxyAgent } from "proxy-agent";
1708
2193
 
1709
- // src/provider/adapters/gitlab_utils.ts
1710
- import fs4 from "fs/promises";
1711
- import path4 from "path";
1712
- async function detectProjectPath(cwd, instanceUrl) {
1713
- let current = cwd;
1714
- const instance = new URL(instanceUrl);
1715
- const instanceHost = instance.host;
1716
- const instanceBasePath = instance.pathname.replace(/\/$/, "");
1717
- while (true) {
1718
- try {
1719
- const config = await readGitConfig(current);
1720
- const url = extractGitRemoteUrl(config) || "";
1721
- const remote = parseRemote(url);
1722
- if (!remote) {
1723
- return void 0;
1724
- }
1725
- if (remote.host !== instanceHost) {
1726
- throw new Error(
1727
- `GitLab remote host mismatch. Expected ${instanceHost}, got ${remote.host}.`
1728
- );
1729
- }
1730
- return normalizeProjectPath(remote.path, instanceBasePath);
1731
- } catch {
1732
- const parent = path4.dirname(current);
1733
- if (parent === current) return void 0;
1734
- current = parent;
1735
- }
1736
- }
1737
- }
1738
- function extractGitRemoteUrl(config) {
1739
- const lines = config.split("\n");
1740
- let inOrigin = false;
1741
- let originUrl;
1742
- let firstUrl;
1743
- for (const line of lines) {
1744
- const trimmed = line.trim();
1745
- const sectionMatch = /^\[remote\s+"([^"]+)"\]$/.exec(trimmed);
1746
- if (sectionMatch) {
1747
- inOrigin = sectionMatch[1] === "origin";
1748
- continue;
1749
- }
1750
- const urlMatch = /^url\s*=\s*(.+)$/.exec(trimmed);
1751
- if (urlMatch) {
1752
- const value = urlMatch[1].trim();
1753
- if (!firstUrl) firstUrl = value;
1754
- if (inOrigin) originUrl = value;
1755
- }
1756
- }
1757
- return originUrl ?? firstUrl;
1758
- }
1759
- function parseRemote(remoteUrl) {
1760
- if (!remoteUrl) return void 0;
1761
- if (remoteUrl.startsWith("http")) {
1762
- try {
1763
- const url = new URL(remoteUrl);
1764
- return { host: url.host, path: url.pathname.replace(/^\//, "") };
1765
- } catch {
1766
- return void 0;
1767
- }
1768
- }
1769
- if (remoteUrl.startsWith("git@")) {
1770
- const match = /^git@([^:]+):(.+)$/.exec(remoteUrl);
1771
- if (!match) return void 0;
1772
- return { host: match[1], path: match[2] };
1773
- }
1774
- if (remoteUrl.startsWith("ssh://")) {
1775
- try {
1776
- const url = new URL(remoteUrl);
1777
- return { host: url.host, path: url.pathname.replace(/^\//, "") };
1778
- } catch {
1779
- return void 0;
1780
- }
1781
- }
1782
- return void 0;
1783
- }
1784
- function normalizeProjectPath(remotePath, instanceBasePath) {
1785
- let pathValue = remotePath;
1786
- if (instanceBasePath && instanceBasePath !== "/") {
1787
- const base = instanceBasePath.replace(/^\//, "") + "/";
1788
- if (pathValue.startsWith(base)) {
1789
- pathValue = pathValue.slice(base.length);
1790
- }
1791
- }
1792
- const cleaned = stripGitSuffix(pathValue);
1793
- return cleaned.length > 0 ? cleaned : void 0;
1794
- }
1795
- function stripGitSuffix(pathname) {
1796
- return pathname.endsWith(".git") ? pathname.slice(0, -4) : pathname;
1797
- }
1798
- function buildApiUrl(instanceUrl, apiPath) {
1799
- const base = instanceUrl.endsWith("/") ? instanceUrl : `${instanceUrl}/`;
1800
- return new URL(apiPath.replace(/^\//, ""), base).toString();
1801
- }
1802
- function buildAuthHeaders(apiKey) {
1803
- return { authorization: `Bearer ${apiKey}` };
1804
- }
1805
- async function fetchProjectDetails(instanceUrl, apiKey, projectPath) {
1806
- const url = buildApiUrl(instanceUrl, `api/v4/projects/${encodeURIComponent(projectPath)}`);
1807
- const response = await fetch(url, {
1808
- headers: buildAuthHeaders(apiKey)
1809
- });
1810
- if (!response.ok) {
1811
- throw new Error(`Failed to fetch project details: ${response.status}`);
1812
- }
1813
- const data = await response.json();
1814
- return {
1815
- projectId: data.id ? String(data.id) : void 0,
1816
- namespaceId: data.namespace?.id ? String(data.namespace.id) : void 0
1817
- };
1818
- }
1819
- async function fetchProjectDetailsWithFallback(instanceUrl, apiKey, projectPath) {
1820
- const candidates = getProjectPathCandidates(projectPath);
1821
- for (const candidate of candidates) {
1822
- try {
1823
- return await fetchProjectDetails(instanceUrl, apiKey, candidate);
1824
- } catch {
1825
- continue;
1826
- }
1827
- }
1828
- try {
1829
- const name = projectPath.split("/").pop() || projectPath;
1830
- const searchUrl = new URL(buildApiUrl(instanceUrl, "api/v4/projects"));
1831
- searchUrl.searchParams.set("search", name);
1832
- searchUrl.searchParams.set("simple", "true");
1833
- searchUrl.searchParams.set("per_page", "100");
1834
- searchUrl.searchParams.set("membership", "true");
1835
- const response = await fetch(searchUrl.toString(), {
1836
- headers: buildAuthHeaders(apiKey)
1837
- });
1838
- if (!response.ok) {
1839
- throw new Error(`Failed to search projects: ${response.status}`);
1840
- }
1841
- const data = await response.json();
1842
- const match = data.find((project) => project.path_with_namespace === projectPath);
1843
- if (!match) {
1844
- throw new Error("Project not found via search");
1845
- }
1846
- return {
1847
- projectId: match.id ? String(match.id) : void 0,
1848
- namespaceId: match.namespace?.id ? String(match.namespace.id) : void 0
1849
- };
1850
- } catch {
1851
- throw new Error("Project not found via API");
1852
- }
1853
- }
1854
- function getProjectPathCandidates(projectPath) {
1855
- const candidates = /* @__PURE__ */ new Set();
1856
- candidates.add(projectPath);
1857
- const parts = projectPath.split("/");
1858
- if (parts.length > 2) {
1859
- const withoutFirst = parts.slice(1).join("/");
1860
- candidates.add(withoutFirst);
1861
- }
1862
- return Array.from(candidates);
1863
- }
1864
- async function readGitConfig(cwd) {
1865
- const gitPath = path4.join(cwd, ".git");
1866
- const stat = await fs4.stat(gitPath);
1867
- if (stat.isDirectory()) {
1868
- return fs4.readFile(path4.join(gitPath, "config"), "utf8");
1869
- }
1870
- const file = await fs4.readFile(gitPath, "utf8");
1871
- const match = /^gitdir:\s*(.+)$/m.exec(file);
1872
- if (!match) throw new Error("Invalid .git file");
1873
- const gitdir = match[1].trim();
1874
- const resolved = path4.isAbsolute(gitdir) ? gitdir : path4.join(cwd, gitdir);
1875
- return fs4.readFile(path4.join(resolved, "config"), "utf8");
1876
- }
1877
-
1878
2194
  // src/provider/adapters/workflow_service.ts
1879
2195
  var WorkflowCreateError = class extends Error {
1880
2196
  status;
@@ -2097,10 +2413,10 @@ function buildUserAgent() {
2097
2413
  }
2098
2414
 
2099
2415
  // src/provider/adapters/system_context.ts
2100
- import os from "os";
2416
+ import os2 from "os";
2101
2417
  function getSystemContextItems(systemRules) {
2102
- const platform = os.platform();
2103
- const arch = os.arch();
2418
+ const platform = os2.platform();
2419
+ const arch = os2.arch();
2104
2420
  const items = [
2105
2421
  {
2106
2422
  category: "os_information",
@@ -2334,7 +2650,7 @@ function assertDependencies() {
2334
2650
  );
2335
2651
  }
2336
2652
  }
2337
- function resolveInstanceUrl(value) {
2653
+ function resolveInstanceUrl2(value) {
2338
2654
  if (typeof value === "string" && value.trim()) {
2339
2655
  const normalized = value.trim();
2340
2656
  assertInstanceUrl(normalized);
@@ -2356,15 +2672,15 @@ function assertInstanceUrl(value) {
2356
2672
  );
2357
2673
  }
2358
2674
  }
2359
- function resolveApiKey(value) {
2675
+ function resolveApiKey2(value) {
2360
2676
  if (typeof value === "string" && value.trim()) return value.trim();
2361
2677
  return process.env.GITLAB_TOKEN || "";
2362
2678
  }
2363
2679
  function createGitLabDuoAgentic(options = {}) {
2364
2680
  assertDependencies();
2365
2681
  const resolvedOptions = {
2366
- instanceUrl: resolveInstanceUrl(options.instanceUrl),
2367
- apiKey: resolveApiKey(options.apiKey),
2682
+ instanceUrl: resolveInstanceUrl2(options.instanceUrl),
2683
+ apiKey: resolveApiKey2(options.apiKey),
2368
2684
  sendSystemContext: typeof options.sendSystemContext === "boolean" ? options.sendSystemContext : true,
2369
2685
  enableMcp: typeof options.enableMcp === "boolean" ? options.enableMcp : true,
2370
2686
  systemRules: typeof options.systemRules === "string" ? options.systemRules : void 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-gitlab-duo-agentic",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "OpenCode plugin and provider for GitLab Duo Agentic workflows",
5
5
  "license": "MIT",
6
6
  "type": "module",