opencode-gitlab-duo-agentic 0.1.22 → 0.1.24

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 (2) hide show
  1. package/dist/index.js +135 -86
  2. package/package.json +1 -4
package/dist/index.js CHANGED
@@ -17,6 +17,7 @@ import crypto from "crypto";
17
17
  import fs2 from "fs/promises";
18
18
  import os from "os";
19
19
  import path2 from "path";
20
+ import { z } from "zod";
20
21
 
21
22
  // src/gitlab/client.ts
22
23
  var GitLabApiError = class extends Error {
@@ -190,6 +191,16 @@ function normalizeProjectPath(remotePath, instanceBasePath) {
190
191
  }
191
192
 
192
193
  // src/gitlab/models.ts
194
+ var CachePayloadSchema = z.object({
195
+ cachedAt: z.string(),
196
+ instanceUrl: z.string(),
197
+ models: z.array(
198
+ z.object({
199
+ id: z.string(),
200
+ name: z.string()
201
+ })
202
+ ).min(1)
203
+ });
193
204
  var QUERY = `query lsp_aiChatAvailableModels($rootNamespaceId: GroupID!) {
194
205
  aiChatAvailableModels(rootNamespaceId: $rootNamespaceId) {
195
206
  defaultModel { name ref }
@@ -251,8 +262,7 @@ function isStale(payload) {
251
262
  async function readCache(cachePath) {
252
263
  try {
253
264
  const raw = await fs2.readFile(cachePath, "utf8");
254
- const parsed = JSON.parse(raw);
255
- if (!parsed.models?.length) return null;
265
+ const parsed = CachePayloadSchema.parse(JSON.parse(raw));
256
266
  return parsed;
257
267
  } catch {
258
268
  return null;
@@ -286,13 +296,28 @@ function normalizeInstanceUrl(value) {
286
296
  }
287
297
  }
288
298
 
299
+ // src/gitlab/resolve-credentials.ts
300
+ function resolveCredentials(options = {}) {
301
+ const instanceUrl = normalizeInstanceUrl(options.instanceUrl ?? envInstanceUrl());
302
+ const token = firstNonEmptyString(options.apiKey, options.token) ?? envToken() ?? "";
303
+ return { instanceUrl, token };
304
+ }
305
+ function envToken() {
306
+ return process.env.GITLAB_TOKEN ?? process.env.GITLAB_OAUTH_TOKEN;
307
+ }
308
+ function firstNonEmptyString(...values) {
309
+ for (const v of values) {
310
+ if (typeof v === "string" && v.trim().length > 0) return v.trim();
311
+ }
312
+ return void 0;
313
+ }
314
+
289
315
  // src/plugin/config.ts
290
316
  async function applyRuntimeConfig(config, directory) {
291
317
  config.provider ??= {};
292
318
  const current = config.provider[PROVIDER_ID] ?? {};
293
319
  const options = current.options ?? {};
294
- const instanceUrl = normalizeInstanceUrl(options.instanceUrl ?? envInstanceUrl());
295
- const token = (typeof options.apiKey === "string" ? options.apiKey : void 0) ?? process.env.GITLAB_TOKEN ?? process.env.GITLAB_OAUTH_TOKEN ?? "";
320
+ const { instanceUrl, token } = resolveCredentials(options);
296
321
  const available = await loadAvailableModels(instanceUrl, token, directory);
297
322
  const modelIds = available.map((m) => m.id);
298
323
  const models = toModelsConfig(available);
@@ -336,6 +361,7 @@ async function createPluginHooks(input) {
336
361
  },
337
362
  "chat.params": async (context, output) => {
338
363
  if (!isGitLabProvider(context.model)) return;
364
+ if (isUtilityAgent(context.agent)) return;
339
365
  output.options = {
340
366
  ...output.options,
341
367
  workflowSessionID: context.sessionID
@@ -343,6 +369,7 @@ async function createPluginHooks(input) {
343
369
  },
344
370
  "chat.headers": async (context, output) => {
345
371
  if (!isGitLabProvider(context.model)) return;
372
+ if (isUtilityAgent(context.agent)) return;
346
373
  output.headers = {
347
374
  ...output.headers,
348
375
  "x-opencode-session": context.sessionID
@@ -350,6 +377,11 @@ async function createPluginHooks(input) {
350
377
  }
351
378
  };
352
379
  }
380
+ var UTILITY_AGENTS = /* @__PURE__ */ new Set(["title", "compaction"]);
381
+ function isUtilityAgent(agent) {
382
+ const name = typeof agent === "string" ? agent : agent.name;
383
+ return UTILITY_AGENTS.has(name);
384
+ }
353
385
  function isGitLabProvider(model) {
354
386
  if (model.api?.npm === "opencode-gitlab-duo-agentic") return true;
355
387
  if (model.providerID === "gitlab" && model.api?.npm !== "@gitlab/gitlab-ai-provider") return true;
@@ -365,6 +397,25 @@ import { randomUUID as randomUUID2 } from "crypto";
365
397
  // src/workflow/session.ts
366
398
  import { randomUUID } from "crypto";
367
399
 
400
+ // src/utils/async-queue.ts
401
+ var AsyncQueue = class {
402
+ #values = [];
403
+ #waiters = [];
404
+ push(value) {
405
+ const waiter = this.#waiters.shift();
406
+ if (waiter) {
407
+ waiter(value);
408
+ return;
409
+ }
410
+ this.#values.push(value);
411
+ }
412
+ shift() {
413
+ const value = this.#values.shift();
414
+ if (value !== void 0) return Promise.resolve(value);
415
+ return new Promise((resolve) => this.#waiters.push(resolve));
416
+ }
417
+ };
418
+
368
419
  // src/workflow/checkpoint.ts
369
420
  function createCheckpointState() {
370
421
  return {
@@ -464,6 +515,20 @@ var WORKFLOW_STATUS = {
464
515
  PLAN_APPROVAL_REQUIRED: "PLAN_APPROVAL_REQUIRED",
465
516
  TOOL_CALL_APPROVAL_REQUIRED: "TOOL_CALL_APPROVAL_REQUIRED"
466
517
  };
518
+ function isCheckpointAction(action) {
519
+ return "newCheckpoint" in action && action.newCheckpoint != null;
520
+ }
521
+ var TURN_COMPLETE_STATUSES = /* @__PURE__ */ new Set([
522
+ WORKFLOW_STATUS.INPUT_REQUIRED,
523
+ WORKFLOW_STATUS.FINISHED,
524
+ WORKFLOW_STATUS.FAILED,
525
+ WORKFLOW_STATUS.STOPPED,
526
+ WORKFLOW_STATUS.PLAN_APPROVAL_REQUIRED,
527
+ WORKFLOW_STATUS.TOOL_CALL_APPROVAL_REQUIRED
528
+ ]);
529
+ function isTurnComplete(status) {
530
+ return TURN_COMPLETE_STATUSES.has(status);
531
+ }
467
532
 
468
533
  // src/workflow/websocket-client.ts
469
534
  import WebSocket from "isomorphic-ws";
@@ -561,15 +626,18 @@ var WorkflowSession = class {
561
626
  #client;
562
627
  #tokenService;
563
628
  #modelId;
629
+ #cwd;
564
630
  #workflowId;
565
631
  #projectPath;
566
632
  #rootNamespaceId;
567
633
  #checkpoint = createCheckpointState();
568
- #pending = Promise.resolve();
569
- constructor(client, modelId) {
634
+ /** Mutex: serialises concurrent calls to runTurn so only one runs at a time. */
635
+ #turnLock = Promise.resolve();
636
+ constructor(client, modelId, cwd) {
570
637
  this.#client = client;
571
638
  this.#tokenService = new WorkflowTokenService(client);
572
639
  this.#modelId = modelId;
640
+ this.#cwd = cwd;
573
641
  }
574
642
  get workflowId() {
575
643
  return this.#workflowId;
@@ -580,9 +648,9 @@ var WorkflowSession = class {
580
648
  this.#tokenService.clear();
581
649
  }
582
650
  async *runTurn(goal, abortSignal) {
583
- await this.#pending;
651
+ await this.#turnLock;
584
652
  let resolve;
585
- this.#pending = new Promise((r) => {
653
+ this.#turnLock = new Promise((r) => {
586
654
  resolve = r;
587
655
  });
588
656
  const queue = new AsyncQueue();
@@ -684,7 +752,7 @@ var WorkflowSession = class {
684
752
  }
685
753
  async #loadProjectContext() {
686
754
  if (this.#projectPath !== void 0) return;
687
- const projectPath = await detectProjectPath(process.cwd(), this.#client.instanceUrl);
755
+ const projectPath = await detectProjectPath(this.#cwd, this.#client.instanceUrl);
688
756
  this.#projectPath = projectPath;
689
757
  if (!projectPath) return;
690
758
  try {
@@ -695,12 +763,6 @@ var WorkflowSession = class {
695
763
  }
696
764
  }
697
765
  };
698
- function isCheckpointAction(action) {
699
- return Boolean(action.newCheckpoint);
700
- }
701
- function isTurnComplete(status) {
702
- return status === WORKFLOW_STATUS.INPUT_REQUIRED || status === WORKFLOW_STATUS.FINISHED || status === WORKFLOW_STATUS.FAILED || status === WORKFLOW_STATUS.STOPPED || status === WORKFLOW_STATUS.PLAN_APPROVAL_REQUIRED || status === WORKFLOW_STATUS.TOOL_CALL_APPROVAL_REQUIRED;
703
- }
704
766
  function buildWebSocketUrl(instanceUrl, modelId) {
705
767
  const base = new URL(instanceUrl.endsWith("/") ? instanceUrl : `${instanceUrl}/`);
706
768
  const url = new URL("api/v4/ai/duo_workflows/ws", base);
@@ -709,35 +771,65 @@ function buildWebSocketUrl(instanceUrl, modelId) {
709
771
  if (modelId) url.searchParams.set("user_selected_model_identifier", modelId);
710
772
  return url.toString();
711
773
  }
712
- var AsyncQueue = class {
713
- #values = [];
714
- #waiters = [];
715
- push(value) {
716
- const waiter = this.#waiters.shift();
717
- if (waiter) {
718
- waiter(value);
719
- return;
720
- }
721
- this.#values.push(value);
774
+
775
+ // src/provider/prompt.ts
776
+ var SYSTEM_REMINDER_RE = /<system-reminder>[\s\S]*?<\/system-reminder>/g;
777
+ var WRAPPED_USER_RE = /^<system-reminder>\s*The user sent the following message:\s*\n([\s\S]*?)\n\s*Please address this message and continue with your tasks\.\s*<\/system-reminder>$/;
778
+ function extractGoal(prompt) {
779
+ for (let i = prompt.length - 1; i >= 0; i--) {
780
+ const message = prompt[i];
781
+ if (message.role !== "user") continue;
782
+ const content = Array.isArray(message.content) ? message.content : [];
783
+ const text2 = content.filter((part) => part.type === "text").map((part) => stripSystemReminders(part.text)).filter(Boolean).join("\n").trim();
784
+ if (text2) return text2;
722
785
  }
723
- shift() {
724
- const value = this.#values.shift();
725
- if (value !== void 0) return Promise.resolve(value);
726
- return new Promise((resolve) => this.#waiters.push(resolve));
786
+ return "";
787
+ }
788
+ function stripSystemReminders(value) {
789
+ return value.replace(SYSTEM_REMINDER_RE, (block) => {
790
+ const wrapped = WRAPPED_USER_RE.exec(block);
791
+ return wrapped?.[1]?.trim() ?? "";
792
+ }).trim();
793
+ }
794
+
795
+ // src/provider/session-context.ts
796
+ function readSessionID(options) {
797
+ const providerBlock = readProviderBlock(options);
798
+ if (typeof providerBlock?.workflowSessionID === "string" && providerBlock.workflowSessionID.trim()) {
799
+ return providerBlock.workflowSessionID.trim();
727
800
  }
728
- };
801
+ const headers = options.headers ?? {};
802
+ for (const [key, value] of Object.entries(headers)) {
803
+ if (key.toLowerCase() === "x-opencode-session" && value?.trim()) return value.trim();
804
+ }
805
+ return void 0;
806
+ }
807
+ function readProviderBlock(options) {
808
+ const block = options.providerOptions?.[PROVIDER_ID];
809
+ if (block && typeof block === "object" && !Array.isArray(block)) {
810
+ return block;
811
+ }
812
+ return void 0;
813
+ }
729
814
 
730
815
  // src/provider/duo-workflow-model.ts
731
816
  var sessions = /* @__PURE__ */ new Map();
817
+ var UNKNOWN_USAGE = {
818
+ inputTokens: void 0,
819
+ outputTokens: void 0,
820
+ totalTokens: void 0
821
+ };
732
822
  var DuoWorkflowModel = class {
733
823
  specificationVersion = "v2";
734
824
  provider = PROVIDER_ID;
735
825
  modelId;
736
826
  supportedUrls = {};
737
827
  #client;
738
- constructor(modelId, client) {
828
+ #cwd;
829
+ constructor(modelId, client, cwd) {
739
830
  this.modelId = modelId;
740
831
  this.#client = client;
832
+ this.#cwd = cwd ?? process.cwd();
741
833
  }
742
834
  async doGenerate(options) {
743
835
  const sessionID = readSessionID(options);
@@ -757,11 +849,7 @@ var DuoWorkflowModel = class {
757
849
  }
758
850
  ],
759
851
  finishReason: "stop",
760
- usage: {
761
- inputTokens: void 0,
762
- outputTokens: void 0,
763
- totalTokens: void 0
764
- },
852
+ usage: UNKNOWN_USAGE,
765
853
  warnings: []
766
854
  };
767
855
  }
@@ -806,11 +894,7 @@ var DuoWorkflowModel = class {
806
894
  controller.enqueue({
807
895
  type: "finish",
808
896
  finishReason: "stop",
809
- usage: {
810
- inputTokens: void 0,
811
- outputTokens: void 0,
812
- totalTokens: void 0
813
- }
897
+ usage: UNKNOWN_USAGE
814
898
  });
815
899
  controller.close();
816
900
  } catch (error) {
@@ -821,11 +905,7 @@ var DuoWorkflowModel = class {
821
905
  controller.enqueue({
822
906
  type: "finish",
823
907
  finishReason: "error",
824
- usage: {
825
- inputTokens: void 0,
826
- outputTokens: void 0,
827
- totalTokens: void 0
828
- }
908
+ usage: UNKNOWN_USAGE
829
909
  });
830
910
  controller.close();
831
911
  }
@@ -839,57 +919,26 @@ var DuoWorkflowModel = class {
839
919
  }
840
920
  };
841
921
  }
922
+ /** Remove a cached session, freeing its resources. */
923
+ disposeSession(sessionID) {
924
+ return sessions.delete(sessionKey(this.#client.instanceUrl, this.modelId, sessionID));
925
+ }
842
926
  #resolveSession(sessionID) {
843
- const key = `${this.#client.instanceUrl}::${this.modelId}::${sessionID}`;
927
+ const key = sessionKey(this.#client.instanceUrl, this.modelId, sessionID);
844
928
  const existing = sessions.get(key);
845
929
  if (existing) return existing;
846
- const created = new WorkflowSession(this.#client, this.modelId);
930
+ const created = new WorkflowSession(this.#client, this.modelId, this.#cwd);
847
931
  sessions.set(key, created);
848
932
  return created;
849
933
  }
850
934
  };
851
- function extractGoal(prompt) {
852
- for (let i = prompt.length - 1; i >= 0; i--) {
853
- const message = prompt[i];
854
- if (message.role !== "user") continue;
855
- const content = Array.isArray(message.content) ? message.content : [];
856
- const text2 = content.filter((part) => part.type === "text").map((part) => stripSystemReminders(part.text)).filter(Boolean).join("\n").trim();
857
- if (text2) return text2;
858
- }
859
- return "";
860
- }
861
- var SYSTEM_REMINDER_RE = /<system-reminder>[\s\S]*?<\/system-reminder>/g;
862
- var WRAPPED_USER_RE = /^<system-reminder>\s*The user sent the following message:\s*\n([\s\S]*?)\n\s*Please address this message and continue with your tasks\.\s*<\/system-reminder>$/;
863
- function stripSystemReminders(value) {
864
- return value.replace(SYSTEM_REMINDER_RE, (block) => {
865
- const wrapped = WRAPPED_USER_RE.exec(block);
866
- return wrapped?.[1]?.trim() ?? "";
867
- }).trim();
868
- }
869
- function readSessionID(options) {
870
- const providerBlock = readProviderBlock(options);
871
- if (typeof providerBlock?.workflowSessionID === "string" && providerBlock.workflowSessionID.trim()) {
872
- return providerBlock.workflowSessionID.trim();
873
- }
874
- const headers = options.headers ?? {};
875
- for (const [key, value] of Object.entries(headers)) {
876
- if (key.toLowerCase() === "x-opencode-session" && value?.trim()) return value.trim();
877
- }
878
- return void 0;
879
- }
880
- function readProviderBlock(options) {
881
- const block = options.providerOptions?.[PROVIDER_ID];
882
- if (block && typeof block === "object" && !Array.isArray(block)) {
883
- return block;
884
- }
885
- return void 0;
935
+ function sessionKey(instanceUrl, modelId, sessionID) {
936
+ return `${instanceUrl}::${modelId}::${sessionID}`;
886
937
  }
887
938
 
888
939
  // src/provider/index.ts
889
940
  function createFallbackProvider(input = {}) {
890
- const instanceUrl = normalizeInstanceUrl(input.instanceUrl ?? envInstanceUrl());
891
- const token = text(input.apiKey) ?? text(input.token) ?? process.env.GITLAB_TOKEN ?? process.env.GITLAB_OAUTH_TOKEN ?? "";
892
- const client = { instanceUrl, token };
941
+ const client = resolveCredentials(input);
893
942
  return {
894
943
  languageModel(modelId) {
895
944
  return new DuoWorkflowModel(modelId, client);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-gitlab-duo-agentic",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "description": "OpenCode plugin and provider for GitLab Duo Agentic workflows",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -43,9 +43,6 @@
43
43
  "@ai-sdk/provider": "2.0.1",
44
44
  "@opencode-ai/plugin": "^1.2.6",
45
45
  "isomorphic-ws": "^5.0.0",
46
- "neverthrow": "^8.2.0",
47
- "proxy-agent": "^6.5.0",
48
- "uuid": "^11.0.5",
49
46
  "zod": "^3.25.76"
50
47
  },
51
48
  "devDependencies": {