opencode-gitlab-duo-agentic 0.2.2 → 0.2.4

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 +159 -10
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -391,10 +391,10 @@ function isGitLabProvider(model) {
391
391
  import { NoSuchModelError } from "@ai-sdk/provider";
392
392
 
393
393
  // src/provider/duo-workflow-model.ts
394
- import { randomUUID as randomUUID2 } from "crypto";
394
+ import { randomUUID as randomUUID3 } from "crypto";
395
395
 
396
396
  // src/workflow/session.ts
397
- import { randomUUID } from "crypto";
397
+ import { randomUUID as randomUUID2 } from "crypto";
398
398
 
399
399
  // src/utils/async-queue.ts
400
400
  var AsyncQueue = class {
@@ -416,9 +416,11 @@ var AsyncQueue = class {
416
416
  };
417
417
 
418
418
  // src/workflow/checkpoint.ts
419
+ import { randomUUID } from "crypto";
419
420
  function createCheckpointState() {
420
421
  return {
421
- uiChatLog: []
422
+ uiChatLog: [],
423
+ processedRequestIndices: /* @__PURE__ */ new Set()
422
424
  };
423
425
  }
424
426
  function extractAgentTextDeltas(checkpoint, state) {
@@ -443,6 +445,23 @@ function extractAgentTextDeltas(checkpoint, state) {
443
445
  state.uiChatLog = next;
444
446
  return out;
445
447
  }
448
+ function extractToolRequests(checkpoint, state) {
449
+ const next = parseCheckpoint(checkpoint);
450
+ const requests = [];
451
+ for (let i = 0; i < next.length; i++) {
452
+ const item = next[i];
453
+ if (item.message_type !== "request") continue;
454
+ if (!item.tool_info) continue;
455
+ if (state.processedRequestIndices.has(i)) continue;
456
+ state.processedRequestIndices.add(i);
457
+ requests.push({
458
+ requestId: item.correlation_id ?? randomUUID(),
459
+ toolName: item.tool_info.name,
460
+ args: item.tool_info.args ?? {}
461
+ });
462
+ }
463
+ return requests;
464
+ }
446
465
  function parseCheckpoint(raw) {
447
466
  if (!raw) return [];
448
467
  try {
@@ -624,6 +643,16 @@ function decodeSocketMessage(data) {
624
643
  import { readFile, readdir, writeFile, mkdir as fsMkdir, stat } from "fs/promises";
625
644
  import { exec } from "child_process";
626
645
  import { resolve, dirname } from "path";
646
+ function asString(value) {
647
+ return typeof value === "string" ? value : void 0;
648
+ }
649
+ function asNumber(value) {
650
+ return typeof value === "number" ? value : void 0;
651
+ }
652
+ function asStringArray(value) {
653
+ if (!Array.isArray(value)) return [];
654
+ return value.filter((v) => typeof v === "string");
655
+ }
627
656
  var ToolExecutor = class {
628
657
  #cwd;
629
658
  #client;
@@ -631,9 +660,104 @@ var ToolExecutor = class {
631
660
  this.#cwd = cwd;
632
661
  this.#client = client;
633
662
  }
663
+ /**
664
+ * Execute a tool request using Duo tool names and arg formats.
665
+ * This handles checkpoint-based tool requests where tool_info contains
666
+ * Duo-native names (read_file, edit_file, etc.) and Duo-native arg keys
667
+ * (file_path, old_str, new_str, etc.).
668
+ */
669
+ async executeDuoTool(toolName, args) {
670
+ try {
671
+ switch (toolName) {
672
+ case "read_file": {
673
+ const filePath = asString(args.file_path) ?? asString(args.filepath) ?? asString(args.filePath) ?? asString(args.path);
674
+ if (!filePath) return { response: "", error: "read_file: missing file_path" };
675
+ return this.#read({ filePath, offset: asNumber(args.offset), limit: asNumber(args.limit) });
676
+ }
677
+ case "read_files": {
678
+ const paths = asStringArray(args.file_paths);
679
+ if (paths.length === 0) return { response: "", error: "read_files: missing file_paths" };
680
+ return this.#readMultiple(paths);
681
+ }
682
+ case "create_file_with_contents": {
683
+ const filePath = asString(args.file_path);
684
+ const content = asString(args.contents);
685
+ if (!filePath || content === void 0) return { response: "", error: "create_file: missing file_path or contents" };
686
+ return this.#write({ filePath, content });
687
+ }
688
+ case "edit_file": {
689
+ const filePath = asString(args.file_path);
690
+ const oldString = asString(args.old_str);
691
+ const newString = asString(args.new_str);
692
+ if (!filePath || oldString === void 0 || newString === void 0) {
693
+ return { response: "", error: "edit_file: missing file_path, old_str or new_str" };
694
+ }
695
+ return this.#edit({ filePath, oldString, newString });
696
+ }
697
+ case "list_dir": {
698
+ const directory = asString(args.directory) ?? ".";
699
+ return this.#read({ filePath: directory });
700
+ }
701
+ case "find_files": {
702
+ const pattern = asString(args.name_pattern);
703
+ if (!pattern) return { response: "", error: "find_files: missing name_pattern" };
704
+ return this.#glob({ pattern });
705
+ }
706
+ case "grep": {
707
+ const pattern = asString(args.pattern);
708
+ if (!pattern) return { response: "", error: "grep: missing pattern" };
709
+ return this.#grep({
710
+ pattern,
711
+ path: asString(args.search_directory),
712
+ caseInsensitive: Boolean(args.case_insensitive)
713
+ });
714
+ }
715
+ case "mkdir": {
716
+ const dir = asString(args.directory_path);
717
+ if (!dir) return { response: "", error: "mkdir: missing directory_path" };
718
+ return this.#mkdir(dir);
719
+ }
720
+ case "shell_command": {
721
+ const command = asString(args.command);
722
+ if (!command) return { response: "", error: "shell_command: missing command" };
723
+ return this.#bash({ command });
724
+ }
725
+ case "run_command": {
726
+ const program = asString(args.program);
727
+ if (!program) return { response: "", error: "run_command: missing program" };
728
+ const parts = [program];
729
+ const flags = args.flags;
730
+ if (Array.isArray(flags)) parts.push(...flags.map(String));
731
+ const cmdArgs = args.arguments;
732
+ if (Array.isArray(cmdArgs)) parts.push(...cmdArgs.map(String));
733
+ return this.#bash({ command: parts.join(" ") });
734
+ }
735
+ case "run_git_command": {
736
+ const command = asString(args.command);
737
+ if (!command) return { response: "", error: "run_git_command: missing command" };
738
+ const extra = Array.isArray(args.args) ? args.args.map(String).join(" ") : asString(args.args);
739
+ const gitCmd = extra ? `git ${command} ${extra}` : `git ${command}`;
740
+ return this.#bash({ command: gitCmd });
741
+ }
742
+ case "gitlab_api_request": {
743
+ const method = asString(args.method) ?? "GET";
744
+ const path3 = asString(args.path);
745
+ if (!path3) return { response: "", error: "gitlab_api_request: missing path" };
746
+ return this.#httpRequest({ method, path: path3, body: asString(args.body) });
747
+ }
748
+ default: {
749
+ console.error(`[tool-executor] unknown Duo tool: ${toolName}, args: ${JSON.stringify(args).slice(0, 300)}`);
750
+ return { response: "", error: `Unknown Duo tool: ${toolName}` };
751
+ }
752
+ }
753
+ } catch (err) {
754
+ console.error(`[tool-executor] executeDuoTool error:`, err);
755
+ return { response: "", error: err instanceof Error ? err.message : String(err) };
756
+ }
757
+ }
634
758
  /**
635
759
  * Route a Duo WorkflowToolAction to the appropriate local handler.
636
- * This is the main bridge entry point.
760
+ * This handles standalone WebSocket actions (non-checkpoint path).
637
761
  */
638
762
  async executeAction(action) {
639
763
  try {
@@ -695,8 +819,11 @@ var ToolExecutor = class {
695
819
  if (action.runHTTPRequest) {
696
820
  return this.#httpRequest(action.runHTTPRequest);
697
821
  }
698
- return { response: "", error: "Unknown tool action" };
822
+ const keys = Object.keys(action).filter((k) => k !== "requestID");
823
+ console.error(`[tool-executor] unhandled action, keys: ${JSON.stringify(keys)}, raw: ${JSON.stringify(action).slice(0, 500)}`);
824
+ return { response: "", error: `Unknown tool action (keys: ${keys.join(", ")})` };
699
825
  } catch (err) {
826
+ console.error(`[tool-executor] execution error:`, err);
700
827
  return { response: "", error: err instanceof Error ? err.message : String(err) };
701
828
  }
702
829
  }
@@ -946,7 +1073,7 @@ var WorkflowSession = class {
946
1073
  await socket.connect(url, {
947
1074
  authorization: `Bearer ${this.#client.token}`,
948
1075
  origin: new URL(this.#client.instanceUrl).origin,
949
- "x-request-id": randomUUID(),
1076
+ "x-request-id": randomUUID2(),
950
1077
  "x-gitlab-client-type": "node-websocket"
951
1078
  });
952
1079
  abortSignal?.addEventListener("abort", onAbort, { once: true });
@@ -961,7 +1088,7 @@ var WorkflowSession = class {
961
1088
  workflowMetadata: JSON.stringify({
962
1089
  extended_logging: access?.workflow_metadata?.extended_logging ?? false
963
1090
  }),
964
- clientCapabilities: [],
1091
+ clientCapabilities: ["shell_command"],
965
1092
  mcpTools,
966
1093
  additional_context: [],
967
1094
  preapproved_tools: preapprovedTools,
@@ -981,20 +1108,42 @@ var WorkflowSession = class {
981
1108
  throw new Error(`workflow websocket closed abnormally (${event.code}): ${event.reason}`);
982
1109
  }
983
1110
  if (isCheckpointAction(event.action)) {
984
- const deltas = extractAgentTextDeltas(event.action.newCheckpoint.checkpoint, this.#checkpoint);
1111
+ const ckpt = event.action.newCheckpoint.checkpoint;
1112
+ const deltas = extractAgentTextDeltas(ckpt, this.#checkpoint);
985
1113
  for (const delta of deltas) {
986
1114
  yield {
987
1115
  type: "text-delta",
988
1116
  value: delta
989
1117
  };
990
1118
  }
1119
+ const toolRequests = extractToolRequests(ckpt, this.#checkpoint);
1120
+ for (const req of toolRequests) {
1121
+ console.error(`[duo-workflow] checkpoint tool request: ${req.toolName} requestId=${req.requestId} args=${JSON.stringify(req.args).slice(0, 200)}`);
1122
+ const result2 = await this.#toolExecutor.executeDuoTool(req.toolName, req.args);
1123
+ console.error(`[duo-workflow] checkpoint tool result: ${result2.response.length} bytes, error=${result2.error ?? "none"}`);
1124
+ socket.send({
1125
+ actionResponse: {
1126
+ requestID: req.requestId,
1127
+ plainTextResponse: {
1128
+ response: result2.response,
1129
+ error: result2.error ?? ""
1130
+ }
1131
+ }
1132
+ });
1133
+ }
991
1134
  if (isTurnComplete(event.action.newCheckpoint.status)) {
992
1135
  socket.close();
993
1136
  }
994
1137
  continue;
995
1138
  }
996
- if (!event.action.requestID) continue;
1139
+ const actionKeys = Object.keys(event.action).filter((k) => k !== "requestID");
1140
+ console.error(`[duo-workflow] tool action received: keys=${JSON.stringify(actionKeys)} requestID=${event.action.requestID ?? "MISSING"}`);
1141
+ if (!event.action.requestID) {
1142
+ console.error("[duo-workflow] skipping action without requestID");
1143
+ continue;
1144
+ }
997
1145
  const result = await this.#toolExecutor.executeAction(event.action);
1146
+ console.error(`[duo-workflow] tool result: response=${result.response.length} bytes, error=${result.error ?? "none"}`);
998
1147
  socket.send({
999
1148
  actionResponse: {
1000
1149
  requestID: event.action.requestID,
@@ -1150,7 +1299,7 @@ var DuoWorkflowModel = class {
1150
1299
  const goal = extractGoal(options.prompt);
1151
1300
  if (!goal) throw new Error("missing user message content");
1152
1301
  const session = this.#resolveSession(sessionID);
1153
- const textId = randomUUID2();
1302
+ const textId = randomUUID3();
1154
1303
  return {
1155
1304
  stream: new ReadableStream({
1156
1305
  start: async (controller) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-gitlab-duo-agentic",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "OpenCode plugin and provider for GitLab Duo Agentic workflows",
5
5
  "license": "MIT",
6
6
  "type": "module",