opencode-gitlab-duo-agentic 0.2.5 → 0.2.7

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 +379 -386
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -135,8 +135,8 @@ async function resolveRootNamespaceId(client, namespaceId) {
135
135
  }
136
136
  async function readGitConfig(cwd) {
137
137
  const gitPath = path.join(cwd, ".git");
138
- const stat2 = await fs.stat(gitPath);
139
- if (stat2.isDirectory()) {
138
+ const stat = await fs.stat(gitPath);
139
+ if (stat.isDirectory()) {
140
140
  return fs.readFile(path.join(gitPath, "config"), "utf8");
141
141
  }
142
142
  const content = await fs.readFile(gitPath, "utf8");
@@ -415,7 +415,7 @@ var AsyncQueue = class {
415
415
  const value = this.#values.shift();
416
416
  if (value !== void 0) return Promise.resolve(value);
417
417
  if (this.#closed) return Promise.resolve(null);
418
- return new Promise((resolve2) => this.#waiters.push(resolve2));
418
+ return new Promise((resolve) => this.#waiters.push(resolve));
419
419
  }
420
420
  close() {
421
421
  this.#closed = true;
@@ -572,7 +572,7 @@ var WorkflowWebSocketClient = class {
572
572
  async connect(url, headers) {
573
573
  const socket = new WebSocket(url, { headers });
574
574
  this.#socket = socket;
575
- await new Promise((resolve2, reject) => {
575
+ await new Promise((resolve, reject) => {
576
576
  const timeout = setTimeout(() => {
577
577
  cleanup();
578
578
  socket.close(1e3);
@@ -585,7 +585,7 @@ var WorkflowWebSocketClient = class {
585
585
  };
586
586
  const onOpen = () => {
587
587
  cleanup();
588
- resolve2();
588
+ resolve();
589
589
  };
590
590
  const onError = (error) => {
591
591
  cleanup();
@@ -650,375 +650,131 @@ function decodeSocketMessage(data) {
650
650
  return void 0;
651
651
  }
652
652
 
653
- // src/workflow/tool-executor.ts
654
- import { readFile, readdir, writeFile, mkdir as fsMkdir, stat } from "fs/promises";
655
- import { exec } from "child_process";
656
- import { resolve, dirname } from "path";
657
- function asString(value) {
658
- return typeof value === "string" ? value : void 0;
659
- }
660
- function asNumber(value) {
661
- return typeof value === "number" ? value : void 0;
662
- }
663
- function asStringArray(value) {
664
- if (!Array.isArray(value)) return [];
665
- return value.filter((v) => typeof v === "string");
666
- }
667
- var ToolExecutor = class {
668
- #cwd;
669
- #client;
670
- constructor(cwd, client) {
671
- this.#cwd = cwd;
672
- this.#client = client;
673
- }
674
- /**
675
- * Execute a tool request using Duo tool names and arg formats.
676
- * This handles checkpoint-based tool requests where tool_info contains
677
- * Duo-native names (read_file, edit_file, etc.) and Duo-native arg keys
678
- * (file_path, old_str, new_str, etc.).
679
- */
680
- async executeDuoTool(toolName, args) {
681
- try {
682
- switch (toolName) {
683
- case "read_file": {
684
- const filePath = asString(args.file_path) ?? asString(args.filepath) ?? asString(args.filePath) ?? asString(args.path);
685
- if (!filePath) return { response: "", error: "read_file: missing file_path" };
686
- return this.#read({ filePath, offset: asNumber(args.offset), limit: asNumber(args.limit) });
687
- }
688
- case "read_files": {
689
- const paths = asStringArray(args.file_paths);
690
- if (paths.length === 0) return { response: "", error: "read_files: missing file_paths" };
691
- return this.#readMultiple(paths);
692
- }
693
- case "create_file_with_contents": {
694
- const filePath = asString(args.file_path);
695
- const content = asString(args.contents);
696
- if (!filePath || content === void 0) return { response: "", error: "create_file: missing file_path or contents" };
697
- return this.#write({ filePath, content });
698
- }
699
- case "edit_file": {
700
- const filePath = asString(args.file_path);
701
- const oldString = asString(args.old_str);
702
- const newString = asString(args.new_str);
703
- if (!filePath || oldString === void 0 || newString === void 0) {
704
- return { response: "", error: "edit_file: missing file_path, old_str or new_str" };
705
- }
706
- return this.#edit({ filePath, oldString, newString });
707
- }
708
- case "list_dir": {
709
- const directory = asString(args.directory) ?? ".";
710
- return this.#read({ filePath: directory });
711
- }
712
- case "find_files": {
713
- const pattern = asString(args.name_pattern);
714
- if (!pattern) return { response: "", error: "find_files: missing name_pattern" };
715
- return this.#glob({ pattern });
716
- }
717
- case "grep": {
718
- const pattern = asString(args.pattern);
719
- if (!pattern) return { response: "", error: "grep: missing pattern" };
720
- return this.#grep({
721
- pattern,
722
- path: asString(args.search_directory),
723
- caseInsensitive: Boolean(args.case_insensitive)
724
- });
725
- }
726
- case "mkdir": {
727
- const dir = asString(args.directory_path);
728
- if (!dir) return { response: "", error: "mkdir: missing directory_path" };
729
- return this.#mkdir(dir);
730
- }
731
- case "shell_command": {
732
- const command = asString(args.command);
733
- if (!command) return { response: "", error: "shell_command: missing command" };
734
- return this.#bash({ command });
735
- }
736
- case "run_command": {
737
- const program = asString(args.program);
738
- if (!program) return { response: "", error: "run_command: missing program" };
739
- const parts = [program];
740
- const flags = args.flags;
741
- if (Array.isArray(flags)) parts.push(...flags.map(String));
742
- const cmdArgs = args.arguments;
743
- if (Array.isArray(cmdArgs)) parts.push(...cmdArgs.map(String));
744
- return this.#bash({ command: parts.join(" ") });
745
- }
746
- case "run_git_command": {
747
- const command = asString(args.command);
748
- if (!command) return { response: "", error: "run_git_command: missing command" };
749
- const extra = Array.isArray(args.args) ? args.args.map(String).join(" ") : asString(args.args);
750
- const gitCmd = extra ? `git ${command} ${extra}` : `git ${command}`;
751
- return this.#bash({ command: gitCmd });
752
- }
753
- case "gitlab_api_request": {
754
- const method = asString(args.method) ?? "GET";
755
- const path3 = asString(args.path);
756
- if (!path3) return { response: "", error: "gitlab_api_request: missing path" };
757
- return this.#httpRequest({ method, path: path3, body: asString(args.body) });
758
- }
759
- default: {
760
- console.error(`[tool-executor] unknown Duo tool: ${toolName}, args: ${JSON.stringify(args).slice(0, 300)}`);
761
- return { response: "", error: `Unknown Duo tool: ${toolName}` };
762
- }
653
+ // src/workflow/action-mapper.ts
654
+ function mapActionToToolRequest(action) {
655
+ const requestId = action.requestID;
656
+ if (!requestId) return null;
657
+ if (action.runMCPTool) {
658
+ let parsedArgs;
659
+ if (typeof action.runMCPTool.args === "string") {
660
+ try {
661
+ parsedArgs = JSON.parse(action.runMCPTool.args);
662
+ } catch {
663
+ parsedArgs = {};
763
664
  }
764
- } catch (err) {
765
- console.error(`[tool-executor] executeDuoTool error:`, err);
766
- return { response: "", error: err instanceof Error ? err.message : String(err) };
665
+ } else {
666
+ parsedArgs = {};
767
667
  }
668
+ return { requestId, toolName: action.runMCPTool.name, args: parsedArgs };
768
669
  }
769
- /**
770
- * Route a Duo WorkflowToolAction to the appropriate local handler.
771
- * This handles standalone WebSocket actions (non-checkpoint path).
772
- */
773
- async executeAction(action) {
774
- try {
775
- if (action.runReadFile) {
776
- return this.#read({
777
- filePath: action.runReadFile.filepath,
778
- offset: action.runReadFile.offset,
779
- limit: action.runReadFile.limit
780
- });
781
- }
782
- if (action.runReadFiles) {
783
- return this.#readMultiple(action.runReadFiles.filepaths);
784
- }
785
- if (action.runWriteFile) {
786
- return this.#write({
787
- filePath: action.runWriteFile.filepath,
788
- content: action.runWriteFile.contents
789
- });
790
- }
791
- if (action.runEditFile) {
792
- return this.#edit({
793
- filePath: action.runEditFile.filepath,
794
- oldString: action.runEditFile.oldString,
795
- newString: action.runEditFile.newString
796
- });
797
- }
798
- if (action.runShellCommand) {
799
- return this.#bash({ command: action.runShellCommand.command });
800
- }
801
- if (action.runCommand) {
802
- const parts = [action.runCommand.program];
803
- if (action.runCommand.flags) parts.push(...action.runCommand.flags);
804
- if (action.runCommand.arguments) parts.push(...action.runCommand.arguments);
805
- return this.#bash({ command: parts.join(" ") });
806
- }
807
- if (action.runGitCommand) {
808
- const cmd = action.runGitCommand.arguments ? `git ${action.runGitCommand.command} ${action.runGitCommand.arguments}` : `git ${action.runGitCommand.command}`;
809
- return this.#bash({ command: cmd });
810
- }
811
- if (action.listDirectory) {
812
- return this.#read({ filePath: action.listDirectory.directory });
813
- }
814
- if (action.grep) {
815
- return this.#grep({
816
- pattern: action.grep.pattern,
817
- path: action.grep.search_directory,
818
- caseInsensitive: action.grep.case_insensitive
819
- });
820
- }
821
- if (action.findFiles) {
822
- return this.#glob({ pattern: action.findFiles.name_pattern });
823
- }
824
- if (action.mkdir) {
825
- return this.#mkdir(action.mkdir.directory_path);
826
- }
827
- if (action.runMCPTool) {
828
- return this.#executeMcpTool(action.runMCPTool.name, action.runMCPTool.args);
829
- }
830
- if (action.runHTTPRequest) {
831
- return this.#httpRequest(action.runHTTPRequest);
670
+ if (action.runReadFile) {
671
+ return {
672
+ requestId,
673
+ toolName: "read_file",
674
+ args: {
675
+ file_path: action.runReadFile.filepath,
676
+ offset: action.runReadFile.offset,
677
+ limit: action.runReadFile.limit
832
678
  }
833
- const keys = Object.keys(action).filter((k) => k !== "requestID");
834
- console.error(`[tool-executor] unhandled action, keys: ${JSON.stringify(keys)}, raw: ${JSON.stringify(action).slice(0, 500)}`);
835
- return { response: "", error: `Unknown tool action (keys: ${keys.join(", ")})` };
836
- } catch (err) {
837
- console.error(`[tool-executor] execution error:`, err);
838
- return { response: "", error: err instanceof Error ? err.message : String(err) };
839
- }
840
- }
841
- /**
842
- * Execute an MCP tool by name (for runMCPTool actions).
843
- * Routes to the appropriate handler based on tool name.
844
- */
845
- async #executeMcpTool(name, argsJson) {
846
- const args = argsJson ? JSON.parse(argsJson) : {};
847
- switch (name) {
848
- case "bash":
849
- return this.#bash(args);
850
- case "read":
851
- return this.#read(args);
852
- case "edit":
853
- return this.#edit(args);
854
- case "write":
855
- return this.#write(args);
856
- case "glob":
857
- return this.#glob(args);
858
- case "grep":
859
- return this.#grep(args);
860
- default:
861
- return { response: "", error: `Unknown MCP tool: ${name}` };
862
- }
679
+ };
863
680
  }
864
- // ── Handlers ──────────────────────────────────────────────────────
865
- async #bash(args) {
866
- const cwd = args.workdir ? resolve(this.#cwd, args.workdir) : this.#cwd;
867
- const timeout = args.timeout ?? 12e4;
868
- return new Promise((res) => {
869
- exec(args.command, { cwd, timeout, maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
870
- const output = [stdout, stderr].filter(Boolean).join("\n");
871
- if (error) {
872
- res({ response: output, error: error.message });
873
- return;
874
- }
875
- res({ response: output });
876
- });
877
- });
681
+ if (action.runReadFiles) {
682
+ return {
683
+ requestId,
684
+ toolName: "read_files",
685
+ args: { file_paths: action.runReadFiles.filepaths ?? [] }
686
+ };
878
687
  }
879
- async #read(args) {
880
- try {
881
- const filepath = resolve(this.#cwd, args.filePath);
882
- const info = await stat(filepath);
883
- if (info.isDirectory()) {
884
- const entries = await readdir(filepath, { withFileTypes: true });
885
- const lines = entries.map((e) => e.isDirectory() ? `${e.name}/` : e.name);
886
- return { response: lines.join("\n") };
688
+ if (action.runWriteFile) {
689
+ return {
690
+ requestId,
691
+ toolName: "create_file_with_contents",
692
+ args: {
693
+ file_path: action.runWriteFile.filepath,
694
+ contents: action.runWriteFile.contents
887
695
  }
888
- const content = await readFile(filepath, "utf-8");
889
- const allLines = content.split("\n");
890
- const offset = Math.max((args.offset ?? 1) - 1, 0);
891
- const limit = args.limit ?? 2e3;
892
- const slice = allLines.slice(offset, offset + limit);
893
- const numbered = slice.map((line, i) => `${offset + i + 1}: ${line}`);
894
- return { response: numbered.join("\n") };
895
- } catch (err) {
896
- return { response: "", error: err instanceof Error ? err.message : String(err) };
897
- }
696
+ };
898
697
  }
899
- async #readMultiple(filepaths) {
900
- const results = [];
901
- for (const fp of filepaths) {
902
- const result = await this.#read({ filePath: fp });
903
- if (result.error) {
904
- results.push(`--- ${fp} ---
905
- ERROR: ${result.error}`);
906
- } else {
907
- results.push(`--- ${fp} ---
908
- ${result.response}`);
698
+ if (action.runEditFile) {
699
+ return {
700
+ requestId,
701
+ toolName: "edit_file",
702
+ args: {
703
+ file_path: action.runEditFile.filepath,
704
+ old_str: action.runEditFile.oldString,
705
+ new_str: action.runEditFile.newString
909
706
  }
910
- }
911
- return { response: results.join("\n\n") };
707
+ };
912
708
  }
913
- async #edit(args) {
914
- try {
915
- const filepath = resolve(this.#cwd, args.filePath);
916
- let content = await readFile(filepath, "utf-8");
917
- if (args.replaceAll) {
918
- if (!content.includes(args.oldString)) {
919
- return { response: "", error: "oldString not found in content" };
920
- }
921
- content = content.replaceAll(args.oldString, args.newString);
922
- } else {
923
- const idx = content.indexOf(args.oldString);
924
- if (idx === -1) {
925
- return { response: "", error: "oldString not found in content" };
926
- }
927
- const secondIdx = content.indexOf(args.oldString, idx + 1);
928
- if (secondIdx !== -1) {
929
- return { response: "", error: "Found multiple matches for oldString. Provide more surrounding lines to identify the correct match." };
930
- }
931
- content = content.slice(0, idx) + args.newString + content.slice(idx + args.oldString.length);
932
- }
933
- await writeFile(filepath, content, "utf-8");
934
- return { response: `Successfully edited ${args.filePath}` };
935
- } catch (err) {
936
- return { response: "", error: err instanceof Error ? err.message : String(err) };
937
- }
709
+ if (action.findFiles) {
710
+ return {
711
+ requestId,
712
+ toolName: "find_files",
713
+ args: { name_pattern: action.findFiles.name_pattern }
714
+ };
938
715
  }
939
- async #write(args) {
940
- try {
941
- const filepath = resolve(this.#cwd, args.filePath);
942
- await fsMkdir(dirname(filepath), { recursive: true });
943
- await writeFile(filepath, args.content, "utf-8");
944
- return { response: `Successfully wrote ${args.filePath}` };
945
- } catch (err) {
946
- return { response: "", error: err instanceof Error ? err.message : String(err) };
947
- }
716
+ if (action.listDirectory) {
717
+ return {
718
+ requestId,
719
+ toolName: "list_dir",
720
+ args: { directory: action.listDirectory.directory }
721
+ };
948
722
  }
949
- async #glob(args) {
950
- const cwd = args.path ? resolve(this.#cwd, args.path) : this.#cwd;
951
- return new Promise((res) => {
952
- 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`;
953
- exec(command, { cwd, timeout: 3e4 }, (error, stdout) => {
954
- if (error && !stdout) {
955
- exec(`ls -1 ${args.pattern} 2>/dev/null | head -200`, { cwd, timeout: 3e4 }, (_err, out) => {
956
- res({ response: out.trim() || "No matches found" });
957
- });
958
- return;
959
- }
960
- const lines = stdout.trim().split("\n").filter(Boolean);
961
- res({ response: lines.length > 0 ? lines.join("\n") : "No matches found" });
962
- });
963
- });
723
+ if (action.grep) {
724
+ const args = { pattern: action.grep.pattern };
725
+ if (action.grep.search_directory) args.search_directory = action.grep.search_directory;
726
+ if (action.grep.case_insensitive !== void 0) args.case_insensitive = action.grep.case_insensitive;
727
+ return { requestId, toolName: "grep", args };
964
728
  }
965
- async #grep(args) {
966
- const cwd = args.path ? resolve(this.#cwd, args.path) : this.#cwd;
967
- const caseFlag = args.caseInsensitive ? "--ignore-case" : "";
968
- return new Promise((res) => {
969
- const includeFlag = args.include ? `--glob '${args.include}'` : "";
970
- const escapedPattern = args.pattern.replace(/'/g, "'\\''");
971
- const rgCommand = `rg --line-number --no-heading ${caseFlag} ${includeFlag} '${escapedPattern}' . 2>/dev/null | head -500`;
972
- exec(rgCommand, { cwd, timeout: 3e4, maxBuffer: 5 * 1024 * 1024 }, (_error, stdout) => {
973
- if (stdout.trim()) {
974
- res({ response: stdout.trim() });
975
- return;
976
- }
977
- const grepCaseFlag = args.caseInsensitive ? "-i" : "";
978
- const grepInclude = args.include ? `--include='${args.include}'` : "";
979
- const grepCommand = `grep -rn ${grepCaseFlag} ${grepInclude} '${escapedPattern}' . 2>/dev/null | head -500`;
980
- exec(grepCommand, { cwd, timeout: 3e4, maxBuffer: 5 * 1024 * 1024 }, (_err, out) => {
981
- res({ response: out.trim() || "No matches found" });
982
- });
983
- });
984
- });
729
+ if (action.mkdir) {
730
+ return {
731
+ requestId,
732
+ toolName: "mkdir",
733
+ args: { directory_path: action.mkdir.directory_path }
734
+ };
985
735
  }
986
- async #mkdir(directoryPath) {
987
- try {
988
- const filepath = resolve(this.#cwd, directoryPath);
989
- await fsMkdir(filepath, { recursive: true });
990
- return { response: `Created directory ${directoryPath}` };
991
- } catch (err) {
992
- return { response: "", error: err instanceof Error ? err.message : String(err) };
993
- }
736
+ if (action.runShellCommand) {
737
+ return {
738
+ requestId,
739
+ toolName: "shell_command",
740
+ args: { command: action.runShellCommand.command }
741
+ };
994
742
  }
995
- async #httpRequest(args) {
996
- if (!this.#client) {
997
- return { response: "", error: "HTTP requests require GitLab client credentials" };
998
- }
999
- try {
1000
- const url = `${this.#client.instanceUrl}/api/v4/${args.path}`;
1001
- const headers = {
1002
- authorization: `Bearer ${this.#client.token}`
1003
- };
1004
- if (args.body) {
1005
- headers["content-type"] = "application/json";
743
+ if (action.runCommand) {
744
+ return {
745
+ requestId,
746
+ toolName: "run_command",
747
+ args: {
748
+ program: action.runCommand.program,
749
+ flags: action.runCommand.flags,
750
+ arguments: action.runCommand.arguments
1006
751
  }
1007
- const response = await fetch(url, {
1008
- method: args.method,
1009
- headers,
1010
- body: args.body ?? void 0
1011
- });
1012
- const text2 = await response.text();
1013
- if (!response.ok) {
1014
- return { response: text2, error: `HTTP ${response.status}: ${response.statusText}` };
752
+ };
753
+ }
754
+ if (action.runGitCommand) {
755
+ return {
756
+ requestId,
757
+ toolName: "run_git_command",
758
+ args: {
759
+ repository_url: action.runGitCommand.repository_url ?? "",
760
+ command: action.runGitCommand.command,
761
+ args: action.runGitCommand.arguments
1015
762
  }
1016
- return { response: text2 };
1017
- } catch (err) {
1018
- return { response: "", error: err instanceof Error ? err.message : String(err) };
1019
- }
763
+ };
1020
764
  }
1021
- };
765
+ if (action.runHTTPRequest) {
766
+ return {
767
+ requestId,
768
+ toolName: "gitlab_api_request",
769
+ args: {
770
+ method: action.runHTTPRequest.method,
771
+ path: action.runHTTPRequest.path,
772
+ body: action.runHTTPRequest.body
773
+ }
774
+ };
775
+ }
776
+ return null;
777
+ }
1022
778
 
1023
779
  // src/workflow/session.ts
1024
780
  var WorkflowSession = class {
@@ -1031,7 +787,6 @@ var WorkflowSession = class {
1031
787
  #rootNamespaceId;
1032
788
  #checkpoint = createCheckpointState();
1033
789
  #toolsConfig;
1034
- #toolExecutor;
1035
790
  #socket;
1036
791
  #queue;
1037
792
  #startRequestSent = false;
@@ -1040,7 +795,6 @@ var WorkflowSession = class {
1040
795
  this.#tokenService = new WorkflowTokenService(client);
1041
796
  this.#modelId = modelId;
1042
797
  this.#cwd = cwd;
1043
- this.#toolExecutor = new ToolExecutor(cwd, client);
1044
798
  }
1045
799
  /**
1046
800
  * Opt-in: override the server-side system prompt and/or register MCP tools.
@@ -1089,7 +843,7 @@ var WorkflowSession = class {
1089
843
  // ---------------------------------------------------------------------------
1090
844
  // Messaging
1091
845
  // ---------------------------------------------------------------------------
1092
- sendStartRequest(goal) {
846
+ sendStartRequest(goal, additionalContext = []) {
1093
847
  if (!this.#socket || !this.#workflowId) throw new Error("Not connected");
1094
848
  const mcpTools = this.#toolsConfig?.mcpTools ?? [];
1095
849
  const preapprovedTools = mcpTools.map((t) => t.name);
@@ -1104,7 +858,7 @@ var WorkflowSession = class {
1104
858
  }),
1105
859
  clientCapabilities: ["shell_command"],
1106
860
  mcpTools,
1107
- additional_context: [],
861
+ additional_context: additionalContext,
1108
862
  preapproved_tools: preapprovedTools,
1109
863
  ...this.#toolsConfig?.flowConfig ? {
1110
864
  flowConfig: this.#toolsConfig.flowConfig,
@@ -1171,24 +925,18 @@ var WorkflowSession = class {
1171
925
  }
1172
926
  return;
1173
927
  }
1174
- if (action.requestID) {
1175
- console.error(`[duo-workflow] ws tool action: keys=${JSON.stringify(Object.keys(action).filter((k) => k !== "requestID"))}`);
1176
- this.#executeStandaloneAction(action);
928
+ const toolAction = action;
929
+ const mapped = mapActionToToolRequest(toolAction);
930
+ if (mapped) {
931
+ console.error(`[duo-workflow] ws tool request: ${mapped.toolName} requestId=${mapped.requestId}`);
932
+ queue.push({
933
+ type: "tool-request",
934
+ requestId: mapped.requestId,
935
+ toolName: mapped.toolName,
936
+ args: mapped.args
937
+ });
1177
938
  }
1178
939
  }
1179
- async #executeStandaloneAction(action) {
1180
- if (!action.requestID || !this.#socket) return;
1181
- const result = await this.#toolExecutor.executeAction(action);
1182
- this.#socket.send({
1183
- actionResponse: {
1184
- requestID: action.requestID,
1185
- plainTextResponse: {
1186
- response: result.response,
1187
- error: result.error ?? ""
1188
- }
1189
- }
1190
- });
1191
- }
1192
940
  // ---------------------------------------------------------------------------
1193
941
  // Private: connection management
1194
942
  // ---------------------------------------------------------------------------
@@ -1265,16 +1013,28 @@ function extractToolResults(prompt) {
1265
1013
  if (!Array.isArray(content)) continue;
1266
1014
  for (const part of content) {
1267
1015
  const p = part;
1268
- if (p.type !== "tool-result") continue;
1269
- const toolCallId = String(p.toolCallId ?? "");
1270
- const toolName = String(p.toolName ?? "");
1271
- if (!toolCallId) continue;
1272
- const { output, error } = parseToolResultOutput(p);
1273
- results.push({ toolCallId, toolName, output, error });
1016
+ if (p.type === "tool-result") {
1017
+ const toolCallId = String(p.toolCallId ?? "");
1018
+ const toolName = String(p.toolName ?? "");
1019
+ if (!toolCallId) continue;
1020
+ const { output, error } = parseToolResultOutput(p);
1021
+ const finalError = error ?? asString(p.error) ?? asString(p.errorText);
1022
+ results.push({ toolCallId, toolName, output, error: finalError });
1023
+ }
1024
+ if (p.type === "tool-error") {
1025
+ const toolCallId = String(p.toolCallId ?? "");
1026
+ const toolName = String(p.toolName ?? "");
1027
+ const errorValue = p.error ?? p.errorText ?? p.message;
1028
+ const error = asString(errorValue) ?? String(errorValue ?? "");
1029
+ results.push({ toolCallId, toolName, output: "", error });
1030
+ }
1274
1031
  }
1275
1032
  }
1276
1033
  return results;
1277
1034
  }
1035
+ function asString(value) {
1036
+ return typeof value === "string" ? value : void 0;
1037
+ }
1278
1038
  function parseToolResultOutput(part) {
1279
1039
  const outputField = part.output;
1280
1040
  const resultField = part.result;
@@ -1296,10 +1056,67 @@ function parseToolResultOutput(part) {
1296
1056
  return { output: typeof outputField === "string" ? outputField : JSON.stringify(outputField) };
1297
1057
  }
1298
1058
  if (resultField !== void 0) {
1299
- return { output: typeof resultField === "string" ? resultField : JSON.stringify(resultField) };
1059
+ const output = typeof resultField === "string" ? resultField : JSON.stringify(resultField);
1060
+ const error = isPlainObject(resultField) ? asString(resultField.error) : void 0;
1061
+ return { output, error };
1300
1062
  }
1301
1063
  return { output: "" };
1302
1064
  }
1065
+ function extractSystemPrompt(prompt) {
1066
+ if (!Array.isArray(prompt)) return null;
1067
+ const parts = [];
1068
+ for (const message of prompt) {
1069
+ const msg = message;
1070
+ if (msg.role === "system" && typeof msg.content === "string" && msg.content.trim()) {
1071
+ parts.push(msg.content);
1072
+ }
1073
+ }
1074
+ return parts.length > 0 ? parts.join("\n") : null;
1075
+ }
1076
+ function sanitizeSystemPrompt(prompt) {
1077
+ let result = prompt;
1078
+ result = result.replace(/^You are [Oo]pen[Cc]ode[,.].*$/gm, "");
1079
+ result = result.replace(/^Your name is opencode\s*$/gm, "");
1080
+ result = result.replace(
1081
+ /If the user asks for help or wants to give feedback[\s\S]*?https:\/\/github\.com\/anomalyco\/opencode\s*/g,
1082
+ ""
1083
+ );
1084
+ result = result.replace(
1085
+ /When the user directly asks about OpenCode[\s\S]*?https:\/\/opencode\.ai\/docs\s*/g,
1086
+ ""
1087
+ );
1088
+ result = result.replace(/https:\/\/github\.com\/anomalyco\/opencode\S*/g, "");
1089
+ result = result.replace(/https:\/\/opencode\.ai\S*/g, "");
1090
+ result = result.replace(/\bOpenCode\b/g, "GitLab Duo");
1091
+ result = result.replace(/\bopencode\b/g, "GitLab Duo");
1092
+ result = result.replace(/The exact model ID is GitLab Duo\//g, "The exact model ID is ");
1093
+ result = result.replace(/\n{3,}/g, "\n\n");
1094
+ return result.trim();
1095
+ }
1096
+ function extractAgentReminders(prompt) {
1097
+ if (!Array.isArray(prompt)) return [];
1098
+ let textParts = [];
1099
+ for (let i = prompt.length - 1; i >= 0; i--) {
1100
+ const message = prompt[i];
1101
+ if (message?.role !== "user" || !Array.isArray(message.content)) continue;
1102
+ textParts = message.content.filter((p) => p.type === "text");
1103
+ if (textParts.length > 0) break;
1104
+ }
1105
+ if (textParts.length === 0) return [];
1106
+ const reminders = [];
1107
+ for (const part of textParts) {
1108
+ if (!part.text) continue;
1109
+ const text2 = String(part.text);
1110
+ if (part.synthetic) {
1111
+ const trimmed = text2.trim();
1112
+ if (trimmed.length > 0) reminders.push(trimmed);
1113
+ continue;
1114
+ }
1115
+ const matches = text2.match(/<system-reminder>[\s\S]*?<\/system-reminder>/g);
1116
+ if (matches) reminders.push(...matches);
1117
+ }
1118
+ return reminders;
1119
+ }
1303
1120
  function isPlainObject(value) {
1304
1121
  return typeof value === "object" && value !== null && !Array.isArray(value);
1305
1122
  }
@@ -1323,7 +1140,7 @@ function mapDuoToolRequest(toolName, args) {
1323
1140
  return { toolName: "read", args: mapped };
1324
1141
  }
1325
1142
  case "read_files": {
1326
- const filePaths = asStringArray2(args.file_paths);
1143
+ const filePaths = asStringArray(args.file_paths);
1327
1144
  if (filePaths.length === 0) return { toolName, args };
1328
1145
  return filePaths.map((fp) => ({ toolName: "read", args: { filePath: fp } }));
1329
1146
  }
@@ -1385,6 +1202,30 @@ function mapDuoToolRequest(toolName, args) {
1385
1202
  const gitCmd = extraArgs ? `git ${shellQuote(command)} ${extraArgs}` : `git ${shellQuote(command)}`;
1386
1203
  return { toolName: "bash", args: { command: gitCmd, description: "Run git command", workdir: "." } };
1387
1204
  }
1205
+ case "gitlab_api_request": {
1206
+ const method = asString2(args.method) ?? "GET";
1207
+ const apiPath = asString2(args.path);
1208
+ if (!apiPath) return { toolName, args };
1209
+ const body = asString2(args.body);
1210
+ const curlParts = [
1211
+ "curl",
1212
+ "-s",
1213
+ "-X",
1214
+ method,
1215
+ "-H",
1216
+ "'Authorization: Bearer $GITLAB_TOKEN'",
1217
+ "-H",
1218
+ "'Content-Type: application/json'"
1219
+ ];
1220
+ if (body) {
1221
+ curlParts.push("-d", shellQuote(body));
1222
+ }
1223
+ curlParts.push(shellQuote(`$GITLAB_INSTANCE_URL/api/v4/${apiPath}`));
1224
+ return {
1225
+ toolName: "bash",
1226
+ args: { command: curlParts.join(" "), description: `GitLab API: ${method} ${apiPath}`, workdir: "." }
1227
+ };
1228
+ }
1388
1229
  default:
1389
1230
  return { toolName, args };
1390
1231
  }
@@ -1392,7 +1233,7 @@ function mapDuoToolRequest(toolName, args) {
1392
1233
  function asString2(value) {
1393
1234
  return typeof value === "string" ? value : void 0;
1394
1235
  }
1395
- function asStringArray2(value) {
1236
+ function asStringArray(value) {
1396
1237
  if (!Array.isArray(value)) return [];
1397
1238
  return value.filter((v) => typeof v === "string");
1398
1239
  }
@@ -1421,6 +1262,85 @@ function readProviderBlock(options) {
1421
1262
  return void 0;
1422
1263
  }
1423
1264
 
1265
+ // src/provider/system-context.ts
1266
+ import os2 from "os";
1267
+ function buildSystemContext() {
1268
+ const platform = os2.platform();
1269
+ const arch = os2.arch();
1270
+ return [
1271
+ {
1272
+ category: "os_information",
1273
+ content: `<os><platform>${platform}</platform><architecture>${arch}</architecture></os>`,
1274
+ id: "os_information",
1275
+ metadata: JSON.stringify({
1276
+ title: "Operating System",
1277
+ enabled: true,
1278
+ subType: "os"
1279
+ })
1280
+ },
1281
+ {
1282
+ category: "user_rule",
1283
+ content: SYSTEM_RULES,
1284
+ id: "user_rules",
1285
+ metadata: JSON.stringify({
1286
+ title: "System Rules",
1287
+ enabled: true,
1288
+ subType: "user_rule"
1289
+ })
1290
+ }
1291
+ ];
1292
+ }
1293
+ var SYSTEM_RULES = `<system-reminder>
1294
+ You MUST follow ALL the rules in this block strictly.
1295
+
1296
+ <tool_orchestration>
1297
+ PARALLEL EXECUTION:
1298
+ - When gathering information, plan all needed searches upfront and execute
1299
+ them together using multiple tool calls in the same turn where possible.
1300
+ - Read multiple related files together rather than one at a time.
1301
+ - Patterns: grep + find_files together, read_file for multiple files together.
1302
+
1303
+ SEQUENTIAL EXECUTION (only when output depends on previous step):
1304
+ - Read a file BEFORE editing it (always).
1305
+ - Check dependencies BEFORE importing them.
1306
+ - Run tests AFTER making changes.
1307
+
1308
+ READ BEFORE WRITE:
1309
+ - Always read existing files before modifying them to understand context.
1310
+ - Check for existing patterns (naming, imports, error handling) and match them.
1311
+ - Verify the exact content to replace when using edit_file.
1312
+
1313
+ ERROR HANDLING:
1314
+ - If a tool fails, analyze the error before retrying.
1315
+ - If a shell command fails, check the error output and adapt.
1316
+ - Do not repeat the same failing operation without changes.
1317
+ </tool_orchestration>
1318
+
1319
+ <development_workflow>
1320
+ For software development tasks, follow this workflow:
1321
+
1322
+ 1. UNDERSTAND: Read relevant files, explore the codebase structure
1323
+ 2. PLAN: Break down the task into clear steps
1324
+ 3. IMPLEMENT: Make changes methodically, one step at a time
1325
+ 4. VERIFY: Run tests, type-checking, or build to validate changes
1326
+ 5. COMPLETE: Summarize what was accomplished
1327
+
1328
+ CODE QUALITY:
1329
+ - Match existing code style and patterns in the project
1330
+ - Write immediately executable code (no TODOs or placeholders)
1331
+ - Prefer editing existing files over creating new ones
1332
+ - Use the project's established error handling patterns
1333
+ </development_workflow>
1334
+
1335
+ <communication>
1336
+ - Be concise and direct. Responses appear in a chat panel.
1337
+ - Focus on practical solutions over theoretical discussion.
1338
+ - When unable to complete a request, explain the limitation briefly and
1339
+ provide alternatives.
1340
+ - Use active language: "Analyzing...", "Searching..." instead of "Let me..."
1341
+ </communication>
1342
+ </system-reminder>`;
1343
+
1424
1344
  // src/provider/duo-workflow-model.ts
1425
1345
  var sessions = /* @__PURE__ */ new Map();
1426
1346
  var UNKNOWN_USAGE = {
@@ -1442,6 +1362,8 @@ var DuoWorkflowModel = class {
1442
1362
  #sentToolCallIds = /* @__PURE__ */ new Set();
1443
1363
  #lastSentGoal = null;
1444
1364
  #stateSessionId;
1365
+ #agentMode;
1366
+ #agentModeReminder;
1445
1367
  constructor(modelId, client, cwd) {
1446
1368
  this.modelId = modelId;
1447
1369
  this.#client = client;
@@ -1481,6 +1403,8 @@ var DuoWorkflowModel = class {
1481
1403
  this.#multiCallGroups.clear();
1482
1404
  this.#sentToolCallIds.clear();
1483
1405
  this.#lastSentGoal = null;
1406
+ this.#agentMode = void 0;
1407
+ this.#agentModeReminder = void 0;
1484
1408
  this.#stateSessionId = sessionID;
1485
1409
  }
1486
1410
  const model = this;
@@ -1491,6 +1415,15 @@ var DuoWorkflowModel = class {
1491
1415
  const onAbort = () => session.abort();
1492
1416
  options.abortSignal?.addEventListener("abort", onAbort, { once: true });
1493
1417
  try {
1418
+ if (!session.hasStarted) {
1419
+ model.#sentToolCallIds.clear();
1420
+ for (const r of toolResults) {
1421
+ if (!model.#pendingToolRequests.has(r.toolCallId)) {
1422
+ model.#sentToolCallIds.add(r.toolCallId);
1423
+ }
1424
+ }
1425
+ model.#lastSentGoal = null;
1426
+ }
1494
1427
  const freshResults = toolResults.filter(
1495
1428
  (r) => !model.#sentToolCallIds.has(r.toolCallId)
1496
1429
  );
@@ -1530,7 +1463,41 @@ var DuoWorkflowModel = class {
1530
1463
  if (!sentToolResults && isNewGoal) {
1531
1464
  await session.ensureConnected(goal);
1532
1465
  if (!session.hasStarted) {
1533
- session.sendStartRequest(goal);
1466
+ const extraContext = [];
1467
+ extraContext.push(...buildSystemContext());
1468
+ const systemPrompt = extractSystemPrompt(options.prompt);
1469
+ if (systemPrompt) {
1470
+ extraContext.push({
1471
+ category: "agent_context",
1472
+ content: sanitizeSystemPrompt(systemPrompt),
1473
+ id: "agent_system_prompt",
1474
+ metadata: JSON.stringify({
1475
+ title: "Agent System Prompt",
1476
+ enabled: true,
1477
+ subType: "system_prompt"
1478
+ })
1479
+ });
1480
+ }
1481
+ const agentReminders = extractAgentReminders(options.prompt);
1482
+ const modeReminder = detectLatestModeReminder(agentReminders);
1483
+ if (modeReminder) {
1484
+ model.#agentMode = modeReminder.mode;
1485
+ model.#agentModeReminder = modeReminder.reminder;
1486
+ }
1487
+ const remindersForContext = buildReminderContext(agentReminders, model.#agentModeReminder);
1488
+ if (remindersForContext.length > 0) {
1489
+ extraContext.push({
1490
+ category: "agent_context",
1491
+ content: sanitizeSystemPrompt(remindersForContext.join("\n\n")),
1492
+ id: "agent_reminders",
1493
+ metadata: JSON.stringify({
1494
+ title: "Agent Reminders",
1495
+ enabled: true,
1496
+ subType: "agent_reminders"
1497
+ })
1498
+ });
1499
+ }
1500
+ session.sendStartRequest(goal, extraContext);
1534
1501
  }
1535
1502
  model.#lastSentGoal = goal;
1536
1503
  }
@@ -1638,6 +1605,32 @@ var DuoWorkflowModel = class {
1638
1605
  function sessionKey(instanceUrl, modelId, sessionID) {
1639
1606
  return `${instanceUrl}::${modelId}::${sessionID}`;
1640
1607
  }
1608
+ function detectLatestModeReminder(reminders) {
1609
+ let latest;
1610
+ for (const reminder of reminders) {
1611
+ const classification = classifyModeReminder(reminder);
1612
+ if (classification === "other") continue;
1613
+ latest = { mode: classification, reminder };
1614
+ }
1615
+ return latest;
1616
+ }
1617
+ function buildReminderContext(reminders, modeReminder) {
1618
+ const nonModeReminders = reminders.filter(
1619
+ (r) => classifyModeReminder(r) === "other"
1620
+ );
1621
+ if (!modeReminder) return nonModeReminders;
1622
+ return [...nonModeReminders, modeReminder];
1623
+ }
1624
+ function classifyModeReminder(reminder) {
1625
+ const text2 = reminder.toLowerCase();
1626
+ if (text2.includes("operational mode has changed from build to plan")) return "plan";
1627
+ if (text2.includes("operational mode has changed from plan to build")) return "build";
1628
+ if (text2.includes("you are no longer in read-only mode")) return "build";
1629
+ if (text2.includes("you are now in read-only mode")) return "plan";
1630
+ if (text2.includes("you are in read-only mode")) return "plan";
1631
+ if (text2.includes("you are permitted to make file changes")) return "build";
1632
+ return "other";
1633
+ }
1641
1634
 
1642
1635
  // src/provider/index.ts
1643
1636
  function createFallbackProvider(input = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-gitlab-duo-agentic",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "OpenCode plugin and provider for GitLab Duo Agentic workflows",
5
5
  "license": "MIT",
6
6
  "type": "module",