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.
- package/dist/index.js +130 -87
- 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 =
|
|
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
|
-
|
|
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
|
-
|
|
575
|
-
|
|
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.#
|
|
651
|
+
await this.#turnLock;
|
|
590
652
|
let resolve;
|
|
591
|
-
this.#
|
|
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(
|
|
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
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
858
|
-
|
|
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
|
|
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.
|
|
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": {
|