opencode-gitlab-duo-agentic 0.1.23 → 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 +130 -87
  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);
@@ -354,7 +379,8 @@ async function createPluginHooks(input) {
354
379
  }
355
380
  var UTILITY_AGENTS = /* @__PURE__ */ new Set(["title", "compaction"]);
356
381
  function isUtilityAgent(agent) {
357
- return UTILITY_AGENTS.has(agent.name);
382
+ const name = typeof agent === "string" ? agent : agent.name;
383
+ return UTILITY_AGENTS.has(name);
358
384
  }
359
385
  function isGitLabProvider(model) {
360
386
  if (model.api?.npm === "opencode-gitlab-duo-agentic") return true;
@@ -371,6 +397,25 @@ import { randomUUID as randomUUID2 } from "crypto";
371
397
  // src/workflow/session.ts
372
398
  import { randomUUID } from "crypto";
373
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
+
374
419
  // src/workflow/checkpoint.ts
375
420
  function createCheckpointState() {
376
421
  return {
@@ -470,6 +515,20 @@ var WORKFLOW_STATUS = {
470
515
  PLAN_APPROVAL_REQUIRED: "PLAN_APPROVAL_REQUIRED",
471
516
  TOOL_CALL_APPROVAL_REQUIRED: "TOOL_CALL_APPROVAL_REQUIRED"
472
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
+ }
473
532
 
474
533
  // src/workflow/websocket-client.ts
475
534
  import WebSocket from "isomorphic-ws";
@@ -567,15 +626,18 @@ var WorkflowSession = class {
567
626
  #client;
568
627
  #tokenService;
569
628
  #modelId;
629
+ #cwd;
570
630
  #workflowId;
571
631
  #projectPath;
572
632
  #rootNamespaceId;
573
633
  #checkpoint = createCheckpointState();
574
- #pending = Promise.resolve();
575
- 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) {
576
637
  this.#client = client;
577
638
  this.#tokenService = new WorkflowTokenService(client);
578
639
  this.#modelId = modelId;
640
+ this.#cwd = cwd;
579
641
  }
580
642
  get workflowId() {
581
643
  return this.#workflowId;
@@ -586,9 +648,9 @@ var WorkflowSession = class {
586
648
  this.#tokenService.clear();
587
649
  }
588
650
  async *runTurn(goal, abortSignal) {
589
- await this.#pending;
651
+ await this.#turnLock;
590
652
  let resolve;
591
- this.#pending = new Promise((r) => {
653
+ this.#turnLock = new Promise((r) => {
592
654
  resolve = r;
593
655
  });
594
656
  const queue = new AsyncQueue();
@@ -690,7 +752,7 @@ var WorkflowSession = class {
690
752
  }
691
753
  async #loadProjectContext() {
692
754
  if (this.#projectPath !== void 0) return;
693
- const projectPath = await detectProjectPath(process.cwd(), this.#client.instanceUrl);
755
+ const projectPath = await detectProjectPath(this.#cwd, this.#client.instanceUrl);
694
756
  this.#projectPath = projectPath;
695
757
  if (!projectPath) return;
696
758
  try {
@@ -701,12 +763,6 @@ var WorkflowSession = class {
701
763
  }
702
764
  }
703
765
  };
704
- function isCheckpointAction(action) {
705
- return Boolean(action.newCheckpoint);
706
- }
707
- function isTurnComplete(status) {
708
- 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;
709
- }
710
766
  function buildWebSocketUrl(instanceUrl, modelId) {
711
767
  const base = new URL(instanceUrl.endsWith("/") ? instanceUrl : `${instanceUrl}/`);
712
768
  const url = new URL("api/v4/ai/duo_workflows/ws", base);
@@ -715,35 +771,65 @@ function buildWebSocketUrl(instanceUrl, modelId) {
715
771
  if (modelId) url.searchParams.set("user_selected_model_identifier", modelId);
716
772
  return url.toString();
717
773
  }
718
- var AsyncQueue = class {
719
- #values = [];
720
- #waiters = [];
721
- push(value) {
722
- const waiter = this.#waiters.shift();
723
- if (waiter) {
724
- waiter(value);
725
- return;
726
- }
727
- 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;
728
785
  }
729
- shift() {
730
- const value = this.#values.shift();
731
- if (value !== void 0) return Promise.resolve(value);
732
- 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();
733
800
  }
734
- };
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
+ }
735
814
 
736
815
  // src/provider/duo-workflow-model.ts
737
816
  var sessions = /* @__PURE__ */ new Map();
817
+ var UNKNOWN_USAGE = {
818
+ inputTokens: void 0,
819
+ outputTokens: void 0,
820
+ totalTokens: void 0
821
+ };
738
822
  var DuoWorkflowModel = class {
739
823
  specificationVersion = "v2";
740
824
  provider = PROVIDER_ID;
741
825
  modelId;
742
826
  supportedUrls = {};
743
827
  #client;
744
- constructor(modelId, client) {
828
+ #cwd;
829
+ constructor(modelId, client, cwd) {
745
830
  this.modelId = modelId;
746
831
  this.#client = client;
832
+ this.#cwd = cwd ?? process.cwd();
747
833
  }
748
834
  async doGenerate(options) {
749
835
  const sessionID = readSessionID(options);
@@ -763,11 +849,7 @@ var DuoWorkflowModel = class {
763
849
  }
764
850
  ],
765
851
  finishReason: "stop",
766
- usage: {
767
- inputTokens: void 0,
768
- outputTokens: void 0,
769
- totalTokens: void 0
770
- },
852
+ usage: UNKNOWN_USAGE,
771
853
  warnings: []
772
854
  };
773
855
  }
@@ -812,11 +894,7 @@ var DuoWorkflowModel = class {
812
894
  controller.enqueue({
813
895
  type: "finish",
814
896
  finishReason: "stop",
815
- usage: {
816
- inputTokens: void 0,
817
- outputTokens: void 0,
818
- totalTokens: void 0
819
- }
897
+ usage: UNKNOWN_USAGE
820
898
  });
821
899
  controller.close();
822
900
  } catch (error) {
@@ -827,11 +905,7 @@ var DuoWorkflowModel = class {
827
905
  controller.enqueue({
828
906
  type: "finish",
829
907
  finishReason: "error",
830
- usage: {
831
- inputTokens: void 0,
832
- outputTokens: void 0,
833
- totalTokens: void 0
834
- }
908
+ usage: UNKNOWN_USAGE
835
909
  });
836
910
  controller.close();
837
911
  }
@@ -845,57 +919,26 @@ var DuoWorkflowModel = class {
845
919
  }
846
920
  };
847
921
  }
922
+ /** Remove a cached session, freeing its resources. */
923
+ disposeSession(sessionID) {
924
+ return sessions.delete(sessionKey(this.#client.instanceUrl, this.modelId, sessionID));
925
+ }
848
926
  #resolveSession(sessionID) {
849
- const key = `${this.#client.instanceUrl}::${this.modelId}::${sessionID}`;
927
+ const key = sessionKey(this.#client.instanceUrl, this.modelId, sessionID);
850
928
  const existing = sessions.get(key);
851
929
  if (existing) return existing;
852
- const created = new WorkflowSession(this.#client, this.modelId);
930
+ const created = new WorkflowSession(this.#client, this.modelId, this.#cwd);
853
931
  sessions.set(key, created);
854
932
  return created;
855
933
  }
856
934
  };
857
- function extractGoal(prompt) {
858
- for (let i = prompt.length - 1; i >= 0; i--) {
859
- const message = prompt[i];
860
- if (message.role !== "user") continue;
861
- const content = Array.isArray(message.content) ? message.content : [];
862
- const text2 = content.filter((part) => part.type === "text").map((part) => stripSystemReminders(part.text)).filter(Boolean).join("\n").trim();
863
- if (text2) return text2;
864
- }
865
- return "";
866
- }
867
- var SYSTEM_REMINDER_RE = /<system-reminder>[\s\S]*?<\/system-reminder>/g;
868
- 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>$/;
869
- function stripSystemReminders(value) {
870
- return value.replace(SYSTEM_REMINDER_RE, (block) => {
871
- const wrapped = WRAPPED_USER_RE.exec(block);
872
- return wrapped?.[1]?.trim() ?? "";
873
- }).trim();
874
- }
875
- function readSessionID(options) {
876
- const providerBlock = readProviderBlock(options);
877
- if (typeof providerBlock?.workflowSessionID === "string" && providerBlock.workflowSessionID.trim()) {
878
- return providerBlock.workflowSessionID.trim();
879
- }
880
- const headers = options.headers ?? {};
881
- for (const [key, value] of Object.entries(headers)) {
882
- if (key.toLowerCase() === "x-opencode-session" && value?.trim()) return value.trim();
883
- }
884
- return void 0;
885
- }
886
- function readProviderBlock(options) {
887
- const block = options.providerOptions?.[PROVIDER_ID];
888
- if (block && typeof block === "object" && !Array.isArray(block)) {
889
- return block;
890
- }
891
- return void 0;
935
+ function sessionKey(instanceUrl, modelId, sessionID) {
936
+ return `${instanceUrl}::${modelId}::${sessionID}`;
892
937
  }
893
938
 
894
939
  // src/provider/index.ts
895
940
  function createFallbackProvider(input = {}) {
896
- const instanceUrl = normalizeInstanceUrl(input.instanceUrl ?? envInstanceUrl());
897
- const token = text(input.apiKey) ?? text(input.token) ?? process.env.GITLAB_TOKEN ?? process.env.GITLAB_OAUTH_TOKEN ?? "";
898
- const client = { instanceUrl, token };
941
+ const client = resolveCredentials(input);
899
942
  return {
900
943
  languageModel(modelId) {
901
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.23",
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": {