opencode-gitlab-duo-agentic 0.1.23 → 0.2.1
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 +541 -96
- 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 {
|
|
@@ -135,8 +136,8 @@ async function resolveRootNamespaceId(client, namespaceId) {
|
|
|
135
136
|
}
|
|
136
137
|
async function readGitConfig(cwd) {
|
|
137
138
|
const gitPath = path.join(cwd, ".git");
|
|
138
|
-
const
|
|
139
|
-
if (
|
|
139
|
+
const stat2 = await fs.stat(gitPath);
|
|
140
|
+
if (stat2.isDirectory()) {
|
|
140
141
|
return fs.readFile(path.join(gitPath, "config"), "utf8");
|
|
141
142
|
}
|
|
142
143
|
const content = await fs.readFile(gitPath, "utf8");
|
|
@@ -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);
|
|
@@ -318,8 +343,236 @@ function toModelsConfig(available) {
|
|
|
318
343
|
return out;
|
|
319
344
|
}
|
|
320
345
|
|
|
346
|
+
// src/workflow/opencode-tools.ts
|
|
347
|
+
var SUPPORTED_TOOLS = /* @__PURE__ */ new Set([
|
|
348
|
+
"bash",
|
|
349
|
+
"read",
|
|
350
|
+
"edit",
|
|
351
|
+
"write",
|
|
352
|
+
"glob",
|
|
353
|
+
"grep"
|
|
354
|
+
]);
|
|
355
|
+
async function fetchOpencodeTools(client, provider, model) {
|
|
356
|
+
try {
|
|
357
|
+
const response = await client.tool.list({ provider, model });
|
|
358
|
+
const tools = response.data ?? [];
|
|
359
|
+
return tools.filter((t) => SUPPORTED_TOOLS.has(t.id)).map((t) => ({
|
|
360
|
+
name: t.id,
|
|
361
|
+
description: t.description,
|
|
362
|
+
inputSchema: typeof t.parameters === "string" ? t.parameters : JSON.stringify(t.parameters)
|
|
363
|
+
}));
|
|
364
|
+
} catch {
|
|
365
|
+
return [];
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function buildFallbackTools() {
|
|
369
|
+
return [
|
|
370
|
+
{
|
|
371
|
+
name: "bash",
|
|
372
|
+
description: "Execute a shell command. Use the `command` parameter for the command to run, `description` for a short summary, optional `timeout` in milliseconds, and optional `workdir` to set the working directory.",
|
|
373
|
+
inputSchema: JSON.stringify({
|
|
374
|
+
type: "object",
|
|
375
|
+
properties: {
|
|
376
|
+
command: { type: "string", description: "The command to execute" },
|
|
377
|
+
description: { type: "string", description: "Clear, concise description of what this command does in 5-10 words" },
|
|
378
|
+
timeout: { type: "number", description: "Optional timeout in milliseconds" },
|
|
379
|
+
workdir: { type: "string", description: "The working directory to run the command in" }
|
|
380
|
+
},
|
|
381
|
+
required: ["command", "description"]
|
|
382
|
+
})
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
name: "read",
|
|
386
|
+
description: "Read a file or directory from the local filesystem. Returns content with line numbers prefixed. Supports offset and limit for pagination. Can read images and PDFs as attachments.",
|
|
387
|
+
inputSchema: JSON.stringify({
|
|
388
|
+
type: "object",
|
|
389
|
+
properties: {
|
|
390
|
+
filePath: { type: "string", description: "The absolute path to the file or directory to read" },
|
|
391
|
+
offset: { type: "number", description: "The line number to start reading from (1-indexed)" },
|
|
392
|
+
limit: { type: "number", description: "The maximum number of lines to read (defaults to 2000)" }
|
|
393
|
+
},
|
|
394
|
+
required: ["filePath"]
|
|
395
|
+
})
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
name: "edit",
|
|
399
|
+
description: "Performs exact string replacements in files. The oldString must match the file contents exactly. Provide surrounding context to make the match unique.",
|
|
400
|
+
inputSchema: JSON.stringify({
|
|
401
|
+
type: "object",
|
|
402
|
+
properties: {
|
|
403
|
+
filePath: { type: "string", description: "The absolute path to the file to modify" },
|
|
404
|
+
oldString: { type: "string", description: "The text to replace" },
|
|
405
|
+
newString: { type: "string", description: "The text to replace it with" },
|
|
406
|
+
replaceAll: { type: "boolean", description: "Replace all occurrences (default false)" }
|
|
407
|
+
},
|
|
408
|
+
required: ["filePath", "oldString", "newString"]
|
|
409
|
+
})
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
name: "write",
|
|
413
|
+
description: "Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Parent directories are created automatically.",
|
|
414
|
+
inputSchema: JSON.stringify({
|
|
415
|
+
type: "object",
|
|
416
|
+
properties: {
|
|
417
|
+
filePath: { type: "string", description: "The absolute path to the file to write" },
|
|
418
|
+
content: { type: "string", description: "The content to write to the file" }
|
|
419
|
+
},
|
|
420
|
+
required: ["filePath", "content"]
|
|
421
|
+
})
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
name: "glob",
|
|
425
|
+
description: "Fast file pattern matching. Supports glob patterns like '**/*.js' or 'src/**/*.ts'. Returns matching file paths sorted by modification time.",
|
|
426
|
+
inputSchema: JSON.stringify({
|
|
427
|
+
type: "object",
|
|
428
|
+
properties: {
|
|
429
|
+
pattern: { type: "string", description: "The glob pattern to match files against" },
|
|
430
|
+
path: { type: "string", description: "The directory to search in" }
|
|
431
|
+
},
|
|
432
|
+
required: ["pattern"]
|
|
433
|
+
})
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
name: "grep",
|
|
437
|
+
description: "Fast content search using regular expressions. Returns file paths and line numbers with matches. Supports filtering files by pattern with the include parameter.",
|
|
438
|
+
inputSchema: JSON.stringify({
|
|
439
|
+
type: "object",
|
|
440
|
+
properties: {
|
|
441
|
+
pattern: { type: "string", description: "The regex pattern to search for in file contents" },
|
|
442
|
+
path: { type: "string", description: "The directory to search in" },
|
|
443
|
+
include: { type: "string", description: "File pattern to include in the search (e.g. '*.js', '*.{ts,tsx}')" }
|
|
444
|
+
},
|
|
445
|
+
required: ["pattern"]
|
|
446
|
+
})
|
|
447
|
+
}
|
|
448
|
+
];
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// src/workflow/flow-config.ts
|
|
452
|
+
function buildFlowConfig(systemPrompt) {
|
|
453
|
+
return {
|
|
454
|
+
version: "v1",
|
|
455
|
+
environment: "chat-partial",
|
|
456
|
+
components: [
|
|
457
|
+
{
|
|
458
|
+
name: "opencode_agent",
|
|
459
|
+
type: "AgentComponent",
|
|
460
|
+
// Empty toolset: we rely entirely on MCP tools (OpenCode tools)
|
|
461
|
+
// registered via the mcpTools field in startRequest
|
|
462
|
+
toolset: []
|
|
463
|
+
}
|
|
464
|
+
],
|
|
465
|
+
prompts: [
|
|
466
|
+
{
|
|
467
|
+
name: "opencode_prompt",
|
|
468
|
+
prompt_id: "opencode_prompt",
|
|
469
|
+
model: {
|
|
470
|
+
params: {
|
|
471
|
+
model_class_provider: "anthropic",
|
|
472
|
+
max_tokens: 32768
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
unit_primitives: ["duo_chat"],
|
|
476
|
+
prompt_template: {
|
|
477
|
+
system: systemPrompt,
|
|
478
|
+
user: "{{goal}}",
|
|
479
|
+
placeholder: "history"
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
]
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
var OPENCODE_SYSTEM_PROMPT = `You are OpenCode, an AI coding assistant integrated with GitLab Duo.
|
|
486
|
+
|
|
487
|
+
<core_mission>
|
|
488
|
+
Your primary role is collaborative programming - working alongside the user to accomplish coding objectives using the tools available to you.
|
|
489
|
+
</core_mission>
|
|
490
|
+
|
|
491
|
+
<communication_guidelines>
|
|
492
|
+
- Provide clear and concise responses. Brevity and clarity are critical.
|
|
493
|
+
- Focus on clean, practical solutions that help users make progress.
|
|
494
|
+
- Keep responses brief and to the point. One-word answers are fine when they suffice.
|
|
495
|
+
- Use active, present-tense language: 'Analyzing...', 'Processing...' instead of 'Let me...', 'I will...'
|
|
496
|
+
- When unable to complete requests, explain the limitation concisely and provide alternatives.
|
|
497
|
+
- When users correct you, acknowledge briefly and apply the correction immediately.
|
|
498
|
+
</communication_guidelines>
|
|
499
|
+
|
|
500
|
+
<tool_usage>
|
|
501
|
+
You have access to file system and shell tools. Use them effectively:
|
|
502
|
+
|
|
503
|
+
- Use the 'read' tool to read files before editing them.
|
|
504
|
+
- Use the 'edit' tool for precise string replacements in existing files.
|
|
505
|
+
- Use the 'write' tool to create new files or overwrite existing ones.
|
|
506
|
+
- Use the 'glob' tool to find files by name patterns.
|
|
507
|
+
- Use the 'grep' tool to search file contents with regex.
|
|
508
|
+
- Use the 'bash' tool for shell commands (git, npm, docker, tests, builds, etc.).
|
|
509
|
+
|
|
510
|
+
Tool orchestration:
|
|
511
|
+
- Execute multiple independent tool operations in parallel when possible.
|
|
512
|
+
- Read files before modifying them to understand context.
|
|
513
|
+
- Only use sequential execution when one operation depends on another's output.
|
|
514
|
+
- Plan your information needs upfront and execute searches together.
|
|
515
|
+
</tool_usage>
|
|
516
|
+
|
|
517
|
+
<code_analysis>
|
|
518
|
+
Before writing any code:
|
|
519
|
+
1. Read existing files to understand context and preserve important logic.
|
|
520
|
+
2. Check dependencies exist before importing.
|
|
521
|
+
3. Match existing patterns: import style, naming conventions, component structure, error handling.
|
|
522
|
+
</code_analysis>
|
|
523
|
+
|
|
524
|
+
<code_standards>
|
|
525
|
+
- Write high-quality, general purpose solutions that work for all valid inputs.
|
|
526
|
+
- Make code immediately executable. No placeholders like "TODO: implement this".
|
|
527
|
+
- Match existing patterns in the codebase.
|
|
528
|
+
- Follow the project's established error handling approach.
|
|
529
|
+
- Verify changes work as expected before completing the task.
|
|
530
|
+
</code_standards>
|
|
531
|
+
|
|
532
|
+
<file_guidelines>
|
|
533
|
+
- ALWAYS prefer editing existing files over creating new ones.
|
|
534
|
+
- NEVER create documentation files unless explicitly requested.
|
|
535
|
+
- Use extended thinking for complex tasks rather than creating temporary files.
|
|
536
|
+
</file_guidelines>
|
|
537
|
+
|
|
538
|
+
<git_guidelines>
|
|
539
|
+
When working with git:
|
|
540
|
+
- Only create commits when explicitly requested by the user.
|
|
541
|
+
- NEVER run destructive git commands (push --force, hard reset) unless explicitly requested.
|
|
542
|
+
- NEVER skip hooks unless explicitly requested.
|
|
543
|
+
- Draft concise commit messages that focus on the "why" rather than the "what".
|
|
544
|
+
- Do not push to remote unless explicitly asked.
|
|
545
|
+
</git_guidelines>`;
|
|
546
|
+
|
|
547
|
+
// src/workflow/tools-config-store.ts
|
|
548
|
+
var stored;
|
|
549
|
+
function setToolsConfig(config) {
|
|
550
|
+
stored = config;
|
|
551
|
+
}
|
|
552
|
+
function getToolsConfig() {
|
|
553
|
+
return stored;
|
|
554
|
+
}
|
|
555
|
+
|
|
321
556
|
// src/plugin/hooks.ts
|
|
557
|
+
async function initializeToolsConfig(input) {
|
|
558
|
+
let mcpTools = await fetchOpencodeTools(
|
|
559
|
+
input.client,
|
|
560
|
+
"gitlab",
|
|
561
|
+
"duo-chat-sonnet-4-5"
|
|
562
|
+
);
|
|
563
|
+
if (mcpTools.length === 0) {
|
|
564
|
+
mcpTools = buildFallbackTools();
|
|
565
|
+
}
|
|
566
|
+
const flowConfig = buildFlowConfig(OPENCODE_SYSTEM_PROMPT);
|
|
567
|
+
setToolsConfig({
|
|
568
|
+
mcpTools,
|
|
569
|
+
flowConfig,
|
|
570
|
+
flowConfigSchemaVersion: "v1"
|
|
571
|
+
});
|
|
572
|
+
}
|
|
322
573
|
async function createPluginHooks(input) {
|
|
574
|
+
initializeToolsConfig(input).catch(() => {
|
|
575
|
+
});
|
|
323
576
|
return {
|
|
324
577
|
config: async (config) => applyRuntimeConfig(config, input.directory),
|
|
325
578
|
"chat.message": async ({ sessionID }, { parts }) => {
|
|
@@ -354,7 +607,8 @@ async function createPluginHooks(input) {
|
|
|
354
607
|
}
|
|
355
608
|
var UTILITY_AGENTS = /* @__PURE__ */ new Set(["title", "compaction"]);
|
|
356
609
|
function isUtilityAgent(agent) {
|
|
357
|
-
|
|
610
|
+
const name = typeof agent === "string" ? agent : agent.name;
|
|
611
|
+
return UTILITY_AGENTS.has(name);
|
|
358
612
|
}
|
|
359
613
|
function isGitLabProvider(model) {
|
|
360
614
|
if (model.api?.npm === "opencode-gitlab-duo-agentic") return true;
|
|
@@ -371,6 +625,25 @@ import { randomUUID as randomUUID2 } from "crypto";
|
|
|
371
625
|
// src/workflow/session.ts
|
|
372
626
|
import { randomUUID } from "crypto";
|
|
373
627
|
|
|
628
|
+
// src/utils/async-queue.ts
|
|
629
|
+
var AsyncQueue = class {
|
|
630
|
+
#values = [];
|
|
631
|
+
#waiters = [];
|
|
632
|
+
push(value) {
|
|
633
|
+
const waiter = this.#waiters.shift();
|
|
634
|
+
if (waiter) {
|
|
635
|
+
waiter(value);
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
this.#values.push(value);
|
|
639
|
+
}
|
|
640
|
+
shift() {
|
|
641
|
+
const value = this.#values.shift();
|
|
642
|
+
if (value !== void 0) return Promise.resolve(value);
|
|
643
|
+
return new Promise((resolve2) => this.#waiters.push(resolve2));
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
|
|
374
647
|
// src/workflow/checkpoint.ts
|
|
375
648
|
function createCheckpointState() {
|
|
376
649
|
return {
|
|
@@ -470,6 +743,23 @@ var WORKFLOW_STATUS = {
|
|
|
470
743
|
PLAN_APPROVAL_REQUIRED: "PLAN_APPROVAL_REQUIRED",
|
|
471
744
|
TOOL_CALL_APPROVAL_REQUIRED: "TOOL_CALL_APPROVAL_REQUIRED"
|
|
472
745
|
};
|
|
746
|
+
function isCheckpointAction(action) {
|
|
747
|
+
return "newCheckpoint" in action && action.newCheckpoint != null;
|
|
748
|
+
}
|
|
749
|
+
function isMcpToolAction(action) {
|
|
750
|
+
return "runMCPTool" in action && action.runMCPTool != null;
|
|
751
|
+
}
|
|
752
|
+
var TURN_COMPLETE_STATUSES = /* @__PURE__ */ new Set([
|
|
753
|
+
WORKFLOW_STATUS.INPUT_REQUIRED,
|
|
754
|
+
WORKFLOW_STATUS.FINISHED,
|
|
755
|
+
WORKFLOW_STATUS.FAILED,
|
|
756
|
+
WORKFLOW_STATUS.STOPPED,
|
|
757
|
+
WORKFLOW_STATUS.PLAN_APPROVAL_REQUIRED,
|
|
758
|
+
WORKFLOW_STATUS.TOOL_CALL_APPROVAL_REQUIRED
|
|
759
|
+
]);
|
|
760
|
+
function isTurnComplete(status) {
|
|
761
|
+
return TURN_COMPLETE_STATUSES.has(status);
|
|
762
|
+
}
|
|
473
763
|
|
|
474
764
|
// src/workflow/websocket-client.ts
|
|
475
765
|
import WebSocket from "isomorphic-ws";
|
|
@@ -484,7 +774,7 @@ var WorkflowWebSocketClient = class {
|
|
|
484
774
|
async connect(url, headers) {
|
|
485
775
|
const socket = new WebSocket(url, { headers });
|
|
486
776
|
this.#socket = socket;
|
|
487
|
-
await new Promise((
|
|
777
|
+
await new Promise((resolve2, reject) => {
|
|
488
778
|
const timeout = setTimeout(() => {
|
|
489
779
|
cleanup();
|
|
490
780
|
socket.close(1e3);
|
|
@@ -497,7 +787,7 @@ var WorkflowWebSocketClient = class {
|
|
|
497
787
|
};
|
|
498
788
|
const onOpen = () => {
|
|
499
789
|
cleanup();
|
|
500
|
-
|
|
790
|
+
resolve2();
|
|
501
791
|
};
|
|
502
792
|
const onError = (error) => {
|
|
503
793
|
cleanup();
|
|
@@ -562,20 +852,164 @@ function decodeSocketMessage(data) {
|
|
|
562
852
|
return void 0;
|
|
563
853
|
}
|
|
564
854
|
|
|
855
|
+
// src/workflow/tool-executor.ts
|
|
856
|
+
import { readFile, readdir, writeFile, mkdir, stat } from "fs/promises";
|
|
857
|
+
import { exec } from "child_process";
|
|
858
|
+
import { resolve, dirname } from "path";
|
|
859
|
+
var ToolExecutor = class {
|
|
860
|
+
#cwd;
|
|
861
|
+
constructor(cwd) {
|
|
862
|
+
this.#cwd = cwd;
|
|
863
|
+
}
|
|
864
|
+
async execute(name, argsJson) {
|
|
865
|
+
const args = argsJson ? JSON.parse(argsJson) : {};
|
|
866
|
+
switch (name) {
|
|
867
|
+
case "bash":
|
|
868
|
+
return this.#bash(args);
|
|
869
|
+
case "read":
|
|
870
|
+
return this.#read(args);
|
|
871
|
+
case "edit":
|
|
872
|
+
return this.#edit(args);
|
|
873
|
+
case "write":
|
|
874
|
+
return this.#write(args);
|
|
875
|
+
case "glob":
|
|
876
|
+
return this.#glob(args);
|
|
877
|
+
case "grep":
|
|
878
|
+
return this.#grep(args);
|
|
879
|
+
default:
|
|
880
|
+
return { response: "", error: `Unknown tool: ${name}` };
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
async #bash(args) {
|
|
884
|
+
const cwd = args.workdir ? resolve(this.#cwd, args.workdir) : this.#cwd;
|
|
885
|
+
const timeout = args.timeout ?? 12e4;
|
|
886
|
+
return new Promise((resolve2) => {
|
|
887
|
+
exec(args.command, { cwd, timeout, maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
888
|
+
if (error) {
|
|
889
|
+
const output2 = [stdout, stderr].filter(Boolean).join("\n");
|
|
890
|
+
resolve2({ response: output2, error: error.message });
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
const output = [stdout, stderr].filter(Boolean).join("\n");
|
|
894
|
+
resolve2({ response: output });
|
|
895
|
+
});
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
async #read(args) {
|
|
899
|
+
try {
|
|
900
|
+
const filepath = resolve(this.#cwd, args.filePath);
|
|
901
|
+
const info = await stat(filepath);
|
|
902
|
+
if (info.isDirectory()) {
|
|
903
|
+
const entries = await readdir(filepath, { withFileTypes: true });
|
|
904
|
+
const lines = entries.map((e) => e.isDirectory() ? `${e.name}/` : e.name);
|
|
905
|
+
return { response: lines.join("\n") };
|
|
906
|
+
}
|
|
907
|
+
const content = await readFile(filepath, "utf-8");
|
|
908
|
+
const allLines = content.split("\n");
|
|
909
|
+
const offset = Math.max((args.offset ?? 1) - 1, 0);
|
|
910
|
+
const limit = args.limit ?? 2e3;
|
|
911
|
+
const slice = allLines.slice(offset, offset + limit);
|
|
912
|
+
const numbered = slice.map((line, i) => `${offset + i + 1}: ${line}`);
|
|
913
|
+
return { response: numbered.join("\n") };
|
|
914
|
+
} catch (err) {
|
|
915
|
+
return { response: "", error: err instanceof Error ? err.message : String(err) };
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
async #edit(args) {
|
|
919
|
+
try {
|
|
920
|
+
const filepath = resolve(this.#cwd, args.filePath);
|
|
921
|
+
let content = await readFile(filepath, "utf-8");
|
|
922
|
+
if (args.replaceAll) {
|
|
923
|
+
if (!content.includes(args.oldString)) {
|
|
924
|
+
return { response: "", error: "oldString not found in content" };
|
|
925
|
+
}
|
|
926
|
+
content = content.replaceAll(args.oldString, args.newString);
|
|
927
|
+
} else {
|
|
928
|
+
const idx = content.indexOf(args.oldString);
|
|
929
|
+
if (idx === -1) {
|
|
930
|
+
return { response: "", error: "oldString not found in content" };
|
|
931
|
+
}
|
|
932
|
+
const secondIdx = content.indexOf(args.oldString, idx + 1);
|
|
933
|
+
if (secondIdx !== -1) {
|
|
934
|
+
return { response: "", error: "Found multiple matches for oldString. Provide more surrounding lines to identify the correct match." };
|
|
935
|
+
}
|
|
936
|
+
content = content.slice(0, idx) + args.newString + content.slice(idx + args.oldString.length);
|
|
937
|
+
}
|
|
938
|
+
await writeFile(filepath, content, "utf-8");
|
|
939
|
+
return { response: `Successfully edited ${args.filePath}` };
|
|
940
|
+
} catch (err) {
|
|
941
|
+
return { response: "", error: err instanceof Error ? err.message : String(err) };
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
async #write(args) {
|
|
945
|
+
try {
|
|
946
|
+
const filepath = resolve(this.#cwd, args.filePath);
|
|
947
|
+
await mkdir(dirname(filepath), { recursive: true });
|
|
948
|
+
await writeFile(filepath, args.content, "utf-8");
|
|
949
|
+
return { response: `Successfully wrote ${args.filePath}` };
|
|
950
|
+
} catch (err) {
|
|
951
|
+
return { response: "", error: err instanceof Error ? err.message : String(err) };
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
async #glob(args) {
|
|
955
|
+
const cwd = args.path ? resolve(this.#cwd, args.path) : this.#cwd;
|
|
956
|
+
return new Promise((resolve2) => {
|
|
957
|
+
const command = process.platform === "win32" ? `dir /s /b "${args.pattern}"` : `find . -path "./${args.pattern}" -not -path "*/node_modules/*" -not -path "*/.git/*" 2>/dev/null | head -200`;
|
|
958
|
+
exec(command, { cwd, timeout: 3e4 }, (error, stdout) => {
|
|
959
|
+
if (error && !stdout) {
|
|
960
|
+
exec(`ls -1 ${args.pattern} 2>/dev/null | head -200`, { cwd, timeout: 3e4 }, (_err, out) => {
|
|
961
|
+
resolve2({ response: out.trim() || "No matches found" });
|
|
962
|
+
});
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
const lines = stdout.trim().split("\n").filter(Boolean);
|
|
966
|
+
resolve2({ response: lines.length > 0 ? lines.join("\n") : "No matches found" });
|
|
967
|
+
});
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
async #grep(args) {
|
|
971
|
+
const cwd = args.path ? resolve(this.#cwd, args.path) : this.#cwd;
|
|
972
|
+
return new Promise((resolve2) => {
|
|
973
|
+
const includeFlag = args.include ? `--glob '${args.include}'` : "";
|
|
974
|
+
const rgCommand = `rg --line-number --no-heading ${includeFlag} '${args.pattern.replace(/'/g, "'\\''")}' . 2>/dev/null | head -500`;
|
|
975
|
+
exec(rgCommand, { cwd, timeout: 3e4, maxBuffer: 5 * 1024 * 1024 }, (error, stdout) => {
|
|
976
|
+
if (stdout.trim()) {
|
|
977
|
+
resolve2({ response: stdout.trim() });
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
const grepInclude = args.include ? `--include='${args.include}'` : "";
|
|
981
|
+
const grepCommand = `grep -rn ${grepInclude} '${args.pattern.replace(/'/g, "'\\''")}' . 2>/dev/null | head -500`;
|
|
982
|
+
exec(grepCommand, { cwd, timeout: 3e4, maxBuffer: 5 * 1024 * 1024 }, (_err, out) => {
|
|
983
|
+
resolve2({ response: out.trim() || "No matches found" });
|
|
984
|
+
});
|
|
985
|
+
});
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
};
|
|
989
|
+
|
|
565
990
|
// src/workflow/session.ts
|
|
566
991
|
var WorkflowSession = class {
|
|
567
992
|
#client;
|
|
568
993
|
#tokenService;
|
|
569
994
|
#modelId;
|
|
995
|
+
#cwd;
|
|
570
996
|
#workflowId;
|
|
571
997
|
#projectPath;
|
|
572
998
|
#rootNamespaceId;
|
|
573
999
|
#checkpoint = createCheckpointState();
|
|
574
|
-
#
|
|
575
|
-
|
|
1000
|
+
#toolsConfig;
|
|
1001
|
+
#toolExecutor;
|
|
1002
|
+
/** Mutex: serialises concurrent calls to runTurn so only one runs at a time. */
|
|
1003
|
+
#turnLock = Promise.resolve();
|
|
1004
|
+
constructor(client, modelId, cwd) {
|
|
576
1005
|
this.#client = client;
|
|
577
1006
|
this.#tokenService = new WorkflowTokenService(client);
|
|
578
1007
|
this.#modelId = modelId;
|
|
1008
|
+
this.#cwd = cwd;
|
|
1009
|
+
this.#toolExecutor = new ToolExecutor(cwd);
|
|
1010
|
+
}
|
|
1011
|
+
setToolsConfig(config) {
|
|
1012
|
+
this.#toolsConfig = config;
|
|
579
1013
|
}
|
|
580
1014
|
get workflowId() {
|
|
581
1015
|
return this.#workflowId;
|
|
@@ -586,10 +1020,10 @@ var WorkflowSession = class {
|
|
|
586
1020
|
this.#tokenService.clear();
|
|
587
1021
|
}
|
|
588
1022
|
async *runTurn(goal, abortSignal) {
|
|
589
|
-
await this.#
|
|
590
|
-
let
|
|
591
|
-
this.#
|
|
592
|
-
|
|
1023
|
+
await this.#turnLock;
|
|
1024
|
+
let resolve2;
|
|
1025
|
+
this.#turnLock = new Promise((r) => {
|
|
1026
|
+
resolve2 = r;
|
|
593
1027
|
});
|
|
594
1028
|
const queue = new AsyncQueue();
|
|
595
1029
|
const socket = new WorkflowWebSocketClient({
|
|
@@ -617,6 +1051,8 @@ var WorkflowSession = class {
|
|
|
617
1051
|
"x-gitlab-client-type": "node-websocket"
|
|
618
1052
|
});
|
|
619
1053
|
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
1054
|
+
const mcpTools = this.#toolsConfig?.mcpTools ?? [];
|
|
1055
|
+
const preapprovedTools = mcpTools.map((t) => t.name);
|
|
620
1056
|
const sent = socket.send({
|
|
621
1057
|
startRequest: {
|
|
622
1058
|
workflowID: this.#workflowId,
|
|
@@ -627,9 +1063,13 @@ var WorkflowSession = class {
|
|
|
627
1063
|
extended_logging: access?.workflow_metadata?.extended_logging ?? false
|
|
628
1064
|
}),
|
|
629
1065
|
clientCapabilities: [],
|
|
630
|
-
mcpTools
|
|
1066
|
+
mcpTools,
|
|
631
1067
|
additional_context: [],
|
|
632
|
-
preapproved_tools:
|
|
1068
|
+
preapproved_tools: preapprovedTools,
|
|
1069
|
+
...this.#toolsConfig?.flowConfig ? {
|
|
1070
|
+
flowConfig: this.#toolsConfig.flowConfig,
|
|
1071
|
+
flowConfigSchemaVersion: this.#toolsConfig.flowConfigSchemaVersion ?? "v1"
|
|
1072
|
+
} : {}
|
|
633
1073
|
}
|
|
634
1074
|
});
|
|
635
1075
|
if (!sent) throw new Error("failed to send workflow startRequest");
|
|
@@ -654,6 +1094,20 @@ var WorkflowSession = class {
|
|
|
654
1094
|
continue;
|
|
655
1095
|
}
|
|
656
1096
|
if (!event.action.requestID) continue;
|
|
1097
|
+
if (isMcpToolAction(event.action)) {
|
|
1098
|
+
const { name, args } = event.action.runMCPTool;
|
|
1099
|
+
const result = await this.#toolExecutor.execute(name, args);
|
|
1100
|
+
socket.send({
|
|
1101
|
+
actionResponse: {
|
|
1102
|
+
requestID: event.action.requestID,
|
|
1103
|
+
plainTextResponse: {
|
|
1104
|
+
response: result.response,
|
|
1105
|
+
error: result.error ?? ""
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
657
1111
|
socket.send({
|
|
658
1112
|
actionResponse: {
|
|
659
1113
|
requestID: event.action.requestID,
|
|
@@ -667,7 +1121,7 @@ var WorkflowSession = class {
|
|
|
667
1121
|
} finally {
|
|
668
1122
|
abortSignal?.removeEventListener("abort", onAbort);
|
|
669
1123
|
socket.close();
|
|
670
|
-
|
|
1124
|
+
resolve2();
|
|
671
1125
|
}
|
|
672
1126
|
}
|
|
673
1127
|
async #createWorkflow(goal) {
|
|
@@ -690,7 +1144,7 @@ var WorkflowSession = class {
|
|
|
690
1144
|
}
|
|
691
1145
|
async #loadProjectContext() {
|
|
692
1146
|
if (this.#projectPath !== void 0) return;
|
|
693
|
-
const projectPath = await detectProjectPath(
|
|
1147
|
+
const projectPath = await detectProjectPath(this.#cwd, this.#client.instanceUrl);
|
|
694
1148
|
this.#projectPath = projectPath;
|
|
695
1149
|
if (!projectPath) return;
|
|
696
1150
|
try {
|
|
@@ -701,12 +1155,6 @@ var WorkflowSession = class {
|
|
|
701
1155
|
}
|
|
702
1156
|
}
|
|
703
1157
|
};
|
|
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
1158
|
function buildWebSocketUrl(instanceUrl, modelId) {
|
|
711
1159
|
const base = new URL(instanceUrl.endsWith("/") ? instanceUrl : `${instanceUrl}/`);
|
|
712
1160
|
const url = new URL("api/v4/ai/duo_workflows/ws", base);
|
|
@@ -715,35 +1163,73 @@ function buildWebSocketUrl(instanceUrl, modelId) {
|
|
|
715
1163
|
if (modelId) url.searchParams.set("user_selected_model_identifier", modelId);
|
|
716
1164
|
return url.toString();
|
|
717
1165
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
1166
|
+
|
|
1167
|
+
// src/provider/prompt.ts
|
|
1168
|
+
var SYSTEM_REMINDER_RE = /<system-reminder>[\s\S]*?<\/system-reminder>/g;
|
|
1169
|
+
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>$/;
|
|
1170
|
+
function extractGoal(prompt) {
|
|
1171
|
+
for (let i = prompt.length - 1; i >= 0; i--) {
|
|
1172
|
+
const message = prompt[i];
|
|
1173
|
+
if (message.role !== "user") continue;
|
|
1174
|
+
const content = Array.isArray(message.content) ? message.content : [];
|
|
1175
|
+
const text2 = content.filter((part) => part.type === "text").map((part) => stripSystemReminders(part.text)).filter(Boolean).join("\n").trim();
|
|
1176
|
+
if (text2) return text2;
|
|
728
1177
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
1178
|
+
return "";
|
|
1179
|
+
}
|
|
1180
|
+
function stripSystemReminders(value) {
|
|
1181
|
+
return value.replace(SYSTEM_REMINDER_RE, (block) => {
|
|
1182
|
+
const wrapped = WRAPPED_USER_RE.exec(block);
|
|
1183
|
+
return wrapped?.[1]?.trim() ?? "";
|
|
1184
|
+
}).trim();
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// src/provider/session-context.ts
|
|
1188
|
+
function readSessionID(options) {
|
|
1189
|
+
const providerBlock = readProviderBlock(options);
|
|
1190
|
+
if (typeof providerBlock?.workflowSessionID === "string" && providerBlock.workflowSessionID.trim()) {
|
|
1191
|
+
return providerBlock.workflowSessionID.trim();
|
|
733
1192
|
}
|
|
734
|
-
};
|
|
1193
|
+
const headers = options.headers ?? {};
|
|
1194
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
1195
|
+
if (key.toLowerCase() === "x-opencode-session" && value?.trim()) return value.trim();
|
|
1196
|
+
}
|
|
1197
|
+
return void 0;
|
|
1198
|
+
}
|
|
1199
|
+
function readProviderBlock(options) {
|
|
1200
|
+
const block = options.providerOptions?.[PROVIDER_ID];
|
|
1201
|
+
if (block && typeof block === "object" && !Array.isArray(block)) {
|
|
1202
|
+
return block;
|
|
1203
|
+
}
|
|
1204
|
+
return void 0;
|
|
1205
|
+
}
|
|
735
1206
|
|
|
736
1207
|
// src/provider/duo-workflow-model.ts
|
|
737
1208
|
var sessions = /* @__PURE__ */ new Map();
|
|
1209
|
+
var UNKNOWN_USAGE = {
|
|
1210
|
+
inputTokens: void 0,
|
|
1211
|
+
outputTokens: void 0,
|
|
1212
|
+
totalTokens: void 0
|
|
1213
|
+
};
|
|
738
1214
|
var DuoWorkflowModel = class {
|
|
739
1215
|
specificationVersion = "v2";
|
|
740
1216
|
provider = PROVIDER_ID;
|
|
741
1217
|
modelId;
|
|
742
1218
|
supportedUrls = {};
|
|
743
1219
|
#client;
|
|
744
|
-
|
|
1220
|
+
#cwd;
|
|
1221
|
+
#toolsConfig;
|
|
1222
|
+
constructor(modelId, client, cwd) {
|
|
745
1223
|
this.modelId = modelId;
|
|
746
1224
|
this.#client = client;
|
|
1225
|
+
this.#cwd = cwd ?? process.cwd();
|
|
1226
|
+
}
|
|
1227
|
+
/** Set the tools configuration for new and existing sessions. */
|
|
1228
|
+
setToolsConfig(config) {
|
|
1229
|
+
this.#toolsConfig = config;
|
|
1230
|
+
for (const session of sessions.values()) {
|
|
1231
|
+
session.setToolsConfig(config);
|
|
1232
|
+
}
|
|
747
1233
|
}
|
|
748
1234
|
async doGenerate(options) {
|
|
749
1235
|
const sessionID = readSessionID(options);
|
|
@@ -763,11 +1249,7 @@ var DuoWorkflowModel = class {
|
|
|
763
1249
|
}
|
|
764
1250
|
],
|
|
765
1251
|
finishReason: "stop",
|
|
766
|
-
usage:
|
|
767
|
-
inputTokens: void 0,
|
|
768
|
-
outputTokens: void 0,
|
|
769
|
-
totalTokens: void 0
|
|
770
|
-
},
|
|
1252
|
+
usage: UNKNOWN_USAGE,
|
|
771
1253
|
warnings: []
|
|
772
1254
|
};
|
|
773
1255
|
}
|
|
@@ -812,11 +1294,7 @@ var DuoWorkflowModel = class {
|
|
|
812
1294
|
controller.enqueue({
|
|
813
1295
|
type: "finish",
|
|
814
1296
|
finishReason: "stop",
|
|
815
|
-
usage:
|
|
816
|
-
inputTokens: void 0,
|
|
817
|
-
outputTokens: void 0,
|
|
818
|
-
totalTokens: void 0
|
|
819
|
-
}
|
|
1297
|
+
usage: UNKNOWN_USAGE
|
|
820
1298
|
});
|
|
821
1299
|
controller.close();
|
|
822
1300
|
} catch (error) {
|
|
@@ -827,11 +1305,7 @@ var DuoWorkflowModel = class {
|
|
|
827
1305
|
controller.enqueue({
|
|
828
1306
|
type: "finish",
|
|
829
1307
|
finishReason: "error",
|
|
830
|
-
usage:
|
|
831
|
-
inputTokens: void 0,
|
|
832
|
-
outputTokens: void 0,
|
|
833
|
-
totalTokens: void 0
|
|
834
|
-
}
|
|
1308
|
+
usage: UNKNOWN_USAGE
|
|
835
1309
|
});
|
|
836
1310
|
controller.close();
|
|
837
1311
|
}
|
|
@@ -845,57 +1319,28 @@ var DuoWorkflowModel = class {
|
|
|
845
1319
|
}
|
|
846
1320
|
};
|
|
847
1321
|
}
|
|
1322
|
+
/** Remove a cached session, freeing its resources. */
|
|
1323
|
+
disposeSession(sessionID) {
|
|
1324
|
+
return sessions.delete(sessionKey(this.#client.instanceUrl, this.modelId, sessionID));
|
|
1325
|
+
}
|
|
848
1326
|
#resolveSession(sessionID) {
|
|
849
|
-
const key =
|
|
1327
|
+
const key = sessionKey(this.#client.instanceUrl, this.modelId, sessionID);
|
|
850
1328
|
const existing = sessions.get(key);
|
|
851
1329
|
if (existing) return existing;
|
|
852
|
-
const created = new WorkflowSession(this.#client, this.modelId);
|
|
1330
|
+
const created = new WorkflowSession(this.#client, this.modelId, this.#cwd);
|
|
1331
|
+
const config = this.#toolsConfig ?? getToolsConfig();
|
|
1332
|
+
if (config) created.setToolsConfig(config);
|
|
853
1333
|
sessions.set(key, created);
|
|
854
1334
|
return created;
|
|
855
1335
|
}
|
|
856
1336
|
};
|
|
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;
|
|
1337
|
+
function sessionKey(instanceUrl, modelId, sessionID) {
|
|
1338
|
+
return `${instanceUrl}::${modelId}::${sessionID}`;
|
|
892
1339
|
}
|
|
893
1340
|
|
|
894
1341
|
// src/provider/index.ts
|
|
895
1342
|
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 };
|
|
1343
|
+
const client = resolveCredentials(input);
|
|
899
1344
|
return {
|
|
900
1345
|
languageModel(modelId) {
|
|
901
1346
|
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.2.1",
|
|
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": {
|