oh-my-opencode 0.1.26 → 0.1.27

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 CHANGED
@@ -784,8 +784,11 @@ ${CONTEXT_REMINDER}
784
784
  event: eventHandler
785
785
  };
786
786
  }
787
- // src/hooks/session-recovery.ts
788
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
787
+ // src/hooks/session-recovery/storage.ts
788
+ import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
789
+ import { join as join2 } from "path";
790
+
791
+ // src/hooks/session-recovery/constants.ts
789
792
  import { join } from "path";
790
793
 
791
794
  // node_modules/xdg-basedir/index.js
@@ -807,141 +810,36 @@ if (xdgConfig) {
807
810
  xdgConfigDirectories.unshift(xdgConfig);
808
811
  }
809
812
 
810
- // src/hooks/session-recovery.ts
813
+ // src/hooks/session-recovery/constants.ts
811
814
  var OPENCODE_STORAGE = join(xdgData ?? "", "opencode", "storage");
812
815
  var MESSAGE_STORAGE = join(OPENCODE_STORAGE, "message");
813
816
  var PART_STORAGE = join(OPENCODE_STORAGE, "part");
814
- function getErrorMessage(error) {
815
- if (!error)
816
- return "";
817
- if (typeof error === "string")
818
- return error.toLowerCase();
819
- const errorObj = error;
820
- return (errorObj.data?.message || errorObj.message || "").toLowerCase();
821
- }
822
- function detectErrorType(error) {
823
- const message = getErrorMessage(error);
824
- if (message.includes("tool_use") && message.includes("tool_result")) {
825
- return "tool_result_missing";
826
- }
827
- if (message.includes("thinking") && (message.includes("first block") || message.includes("must start with") || message.includes("preceeding"))) {
828
- return "thinking_block_order";
829
- }
830
- if (message.includes("thinking is disabled") && message.includes("cannot contain")) {
831
- return "thinking_disabled_violation";
832
- }
833
- if (message.includes("non-empty content") || message.includes("must have non-empty content")) {
834
- return "empty_content_message";
835
- }
836
- return null;
837
- }
838
- function extractToolUseIds(parts) {
839
- return parts.filter((p) => p.type === "tool_use" && !!p.id).map((p) => p.id);
840
- }
841
- async function recoverToolResultMissing(client, sessionID, failedAssistantMsg) {
842
- const parts = failedAssistantMsg.parts || [];
843
- const toolUseIds = extractToolUseIds(parts);
844
- if (toolUseIds.length === 0) {
845
- return false;
846
- }
847
- const toolResultParts = toolUseIds.map((id) => ({
848
- type: "tool_result",
849
- tool_use_id: id,
850
- content: "Operation cancelled by user (ESC pressed)"
851
- }));
852
- try {
853
- await client.session.prompt({
854
- path: { id: sessionID },
855
- body: { parts: toolResultParts }
856
- });
857
- return true;
858
- } catch {
859
- return false;
860
- }
861
- }
862
- async function recoverThinkingBlockOrder(client, sessionID, failedAssistantMsg, directory) {
863
- const messageID = failedAssistantMsg.info?.id;
864
- if (!messageID) {
865
- return false;
866
- }
867
- const existingParts = failedAssistantMsg.parts || [];
868
- const patchedParts = [{ type: "thinking", thinking: "" }, ...existingParts];
869
- try {
870
- await client.message?.update?.({
871
- path: { id: messageID },
872
- body: { parts: patchedParts }
873
- });
874
- return true;
875
- } catch {}
876
- try {
877
- await client.session.patch?.({
878
- path: { id: sessionID },
879
- body: {
880
- messageID,
881
- parts: patchedParts
882
- }
883
- });
884
- return true;
885
- } catch {}
886
- return await fallbackRevertStrategy(client, sessionID, failedAssistantMsg, directory);
887
- }
888
- async function recoverThinkingDisabledViolation(client, sessionID, failedAssistantMsg) {
889
- const messageID = failedAssistantMsg.info?.id;
890
- if (!messageID) {
891
- return false;
892
- }
893
- const existingParts = failedAssistantMsg.parts || [];
894
- const strippedParts = existingParts.filter((p) => p.type !== "thinking" && p.type !== "redacted_thinking");
895
- if (strippedParts.length === 0) {
896
- return false;
897
- }
898
- try {
899
- await client.message?.update?.({
900
- path: { id: messageID },
901
- body: { parts: strippedParts }
902
- });
903
- return true;
904
- } catch {}
905
- try {
906
- await client.session.patch?.({
907
- path: { id: sessionID },
908
- body: {
909
- messageID,
910
- parts: strippedParts
911
- }
912
- });
913
- return true;
914
- } catch {}
915
- return false;
916
- }
917
817
  var THINKING_TYPES = new Set(["thinking", "redacted_thinking", "reasoning"]);
918
818
  var META_TYPES = new Set(["step-start", "step-finish"]);
819
+ var CONTENT_TYPES = new Set(["text", "tool", "tool_use", "tool_result"]);
820
+
821
+ // src/hooks/session-recovery/storage.ts
919
822
  function generatePartId() {
920
823
  const timestamp = Date.now().toString(16);
921
824
  const random = Math.random().toString(36).substring(2, 10);
922
825
  return `prt_${timestamp}${random}`;
923
826
  }
924
827
  function getMessageDir(sessionID) {
925
- const projectHash = readdirSync(MESSAGE_STORAGE).find((dir) => {
926
- const sessionDir = join(MESSAGE_STORAGE, dir);
927
- try {
928
- return readdirSync(sessionDir).some((f) => f.includes(sessionID.replace("ses_", "")));
929
- } catch {
930
- return false;
931
- }
932
- });
933
- if (projectHash) {
934
- return join(MESSAGE_STORAGE, projectHash, sessionID);
828
+ if (!existsSync(MESSAGE_STORAGE))
829
+ return "";
830
+ const directPath = join2(MESSAGE_STORAGE, sessionID);
831
+ if (existsSync(directPath)) {
832
+ return directPath;
935
833
  }
936
834
  for (const dir of readdirSync(MESSAGE_STORAGE)) {
937
- const sessionPath = join(MESSAGE_STORAGE, dir, sessionID);
835
+ const sessionPath = join2(MESSAGE_STORAGE, dir, sessionID);
938
836
  if (existsSync(sessionPath)) {
939
837
  return sessionPath;
940
838
  }
941
839
  }
942
840
  return "";
943
841
  }
944
- function readMessagesFromStorage(sessionID) {
842
+ function readMessages(sessionID) {
945
843
  const messageDir = getMessageDir(sessionID);
946
844
  if (!messageDir || !existsSync(messageDir))
947
845
  return [];
@@ -950,7 +848,7 @@ function readMessagesFromStorage(sessionID) {
950
848
  if (!file.endsWith(".json"))
951
849
  continue;
952
850
  try {
953
- const content = readFileSync(join(messageDir, file), "utf-8");
851
+ const content = readFileSync(join2(messageDir, file), "utf-8");
954
852
  messages.push(JSON.parse(content));
955
853
  } catch {
956
854
  continue;
@@ -958,8 +856,8 @@ function readMessagesFromStorage(sessionID) {
958
856
  }
959
857
  return messages.sort((a, b) => a.id.localeCompare(b.id));
960
858
  }
961
- function readPartsFromStorage(messageID) {
962
- const partDir = join(PART_STORAGE, messageID);
859
+ function readParts(messageID) {
860
+ const partDir = join2(PART_STORAGE, messageID);
963
861
  if (!existsSync(partDir))
964
862
  return [];
965
863
  const parts = [];
@@ -967,7 +865,7 @@ function readPartsFromStorage(messageID) {
967
865
  if (!file.endsWith(".json"))
968
866
  continue;
969
867
  try {
970
- const content = readFileSync(join(partDir, file), "utf-8");
868
+ const content = readFileSync(join2(partDir, file), "utf-8");
971
869
  parts.push(JSON.parse(content));
972
870
  } catch {
973
871
  continue;
@@ -975,8 +873,29 @@ function readPartsFromStorage(messageID) {
975
873
  }
976
874
  return parts;
977
875
  }
978
- function injectTextPartToStorage(sessionID, messageID, text) {
979
- const partDir = join(PART_STORAGE, messageID);
876
+ function hasContent(part) {
877
+ if (THINKING_TYPES.has(part.type))
878
+ return false;
879
+ if (META_TYPES.has(part.type))
880
+ return false;
881
+ if (part.type === "text") {
882
+ const textPart = part;
883
+ return !!textPart.text?.trim();
884
+ }
885
+ if (part.type === "tool" || part.type === "tool_use") {
886
+ return true;
887
+ }
888
+ if (part.type === "tool_result") {
889
+ return true;
890
+ }
891
+ return false;
892
+ }
893
+ function messageHasContent(messageID) {
894
+ const parts = readParts(messageID);
895
+ return parts.some(hasContent);
896
+ }
897
+ function injectTextPart(sessionID, messageID, text) {
898
+ const partDir = join2(PART_STORAGE, messageID);
980
899
  if (!existsSync(partDir)) {
981
900
  mkdirSync(partDir, { recursive: true });
982
901
  }
@@ -986,17 +905,19 @@ function injectTextPartToStorage(sessionID, messageID, text) {
986
905
  sessionID,
987
906
  messageID,
988
907
  type: "text",
989
- text
908
+ text,
909
+ synthetic: true
990
910
  };
991
911
  try {
992
- writeFileSync(join(partDir, `${partId}.json`), JSON.stringify(part, null, 2));
912
+ writeFileSync(join2(partDir, `${partId}.json`), JSON.stringify(part, null, 2));
993
913
  return true;
994
914
  } catch {
995
915
  return false;
996
916
  }
997
917
  }
998
- function findEmptyContentMessageFromStorage(sessionID) {
999
- const messages = readMessagesFromStorage(sessionID);
918
+ function findEmptyMessages(sessionID) {
919
+ const messages = readMessages(sessionID);
920
+ const emptyIds = [];
1000
921
  for (let i = 0;i < messages.length; i++) {
1001
922
  const msg = messages[i];
1002
923
  if (msg.role !== "assistant")
@@ -1004,72 +925,187 @@ function findEmptyContentMessageFromStorage(sessionID) {
1004
925
  const isLastMessage = i === messages.length - 1;
1005
926
  if (isLastMessage)
1006
927
  continue;
1007
- const parts = readPartsFromStorage(msg.id);
1008
- const hasContent = parts.some((p) => {
1009
- if (THINKING_TYPES.has(p.type))
1010
- return false;
1011
- if (META_TYPES.has(p.type))
1012
- return false;
1013
- if (p.type === "text" && p.text?.trim())
1014
- return true;
1015
- if (p.type === "tool_use")
1016
- return true;
1017
- if (p.type === "tool_result")
1018
- return true;
1019
- return false;
1020
- });
1021
- if (!hasContent && parts.length > 0) {
1022
- return msg.id;
928
+ if (!messageHasContent(msg.id)) {
929
+ emptyIds.push(msg.id);
1023
930
  }
1024
931
  }
1025
- return null;
932
+ return emptyIds;
1026
933
  }
1027
- async function recoverEmptyContentMessage(_client, sessionID, failedAssistantMsg, _directory) {
1028
- const emptyMessageID = findEmptyContentMessageFromStorage(sessionID) || failedAssistantMsg.info?.id;
1029
- if (!emptyMessageID)
1030
- return false;
1031
- return injectTextPartToStorage(sessionID, emptyMessageID, "(interrupted)");
934
+ function findMessagesWithThinkingBlocks(sessionID) {
935
+ const messages = readMessages(sessionID);
936
+ const result = [];
937
+ for (let i = 0;i < messages.length; i++) {
938
+ const msg = messages[i];
939
+ if (msg.role !== "assistant")
940
+ continue;
941
+ const isLastMessage = i === messages.length - 1;
942
+ if (isLastMessage)
943
+ continue;
944
+ const parts = readParts(msg.id);
945
+ const hasThinking = parts.some((p) => THINKING_TYPES.has(p.type));
946
+ if (hasThinking) {
947
+ result.push(msg.id);
948
+ }
949
+ }
950
+ return result;
1032
951
  }
1033
- async function fallbackRevertStrategy(client, sessionID, failedAssistantMsg, directory) {
1034
- const parentMsgID = failedAssistantMsg.info?.parentID;
1035
- const messagesResp = await client.session.messages({
1036
- path: { id: sessionID },
1037
- query: { directory }
1038
- });
1039
- const msgs = messagesResp.data;
1040
- if (!msgs || msgs.length === 0) {
1041
- return false;
952
+ function findMessagesWithOrphanThinking(sessionID) {
953
+ const messages = readMessages(sessionID);
954
+ const result = [];
955
+ for (let i = 0;i < messages.length; i++) {
956
+ const msg = messages[i];
957
+ if (msg.role !== "assistant")
958
+ continue;
959
+ const isLastMessage = i === messages.length - 1;
960
+ if (isLastMessage)
961
+ continue;
962
+ const parts = readParts(msg.id);
963
+ if (parts.length === 0)
964
+ continue;
965
+ const sortedParts = [...parts].sort((a, b) => a.id.localeCompare(b.id));
966
+ const firstPart = sortedParts[0];
967
+ const hasThinking = parts.some((p) => THINKING_TYPES.has(p.type));
968
+ const firstIsThinking = THINKING_TYPES.has(firstPart.type);
969
+ if (hasThinking && !firstIsThinking) {
970
+ result.push(msg.id);
971
+ }
1042
972
  }
1043
- let targetUserMsg = null;
1044
- if (parentMsgID) {
1045
- targetUserMsg = msgs.find((m) => m.info?.id === parentMsgID) ?? null;
973
+ return result;
974
+ }
975
+ function prependThinkingPart(sessionID, messageID) {
976
+ const partDir = join2(PART_STORAGE, messageID);
977
+ if (!existsSync(partDir)) {
978
+ mkdirSync(partDir, { recursive: true });
1046
979
  }
1047
- if (!targetUserMsg) {
1048
- for (let i = msgs.length - 1;i >= 0; i--) {
1049
- if (msgs[i].info?.role === "user") {
1050
- targetUserMsg = msgs[i];
1051
- break;
980
+ const partId = `prt_0000000000_thinking`;
981
+ const part = {
982
+ id: partId,
983
+ sessionID,
984
+ messageID,
985
+ type: "thinking",
986
+ thinking: "",
987
+ synthetic: true
988
+ };
989
+ try {
990
+ writeFileSync(join2(partDir, `${partId}.json`), JSON.stringify(part, null, 2));
991
+ return true;
992
+ } catch {
993
+ return false;
994
+ }
995
+ }
996
+ function stripThinkingParts(messageID) {
997
+ const partDir = join2(PART_STORAGE, messageID);
998
+ if (!existsSync(partDir))
999
+ return false;
1000
+ let anyRemoved = false;
1001
+ for (const file of readdirSync(partDir)) {
1002
+ if (!file.endsWith(".json"))
1003
+ continue;
1004
+ try {
1005
+ const filePath = join2(partDir, file);
1006
+ const content = readFileSync(filePath, "utf-8");
1007
+ const part = JSON.parse(content);
1008
+ if (THINKING_TYPES.has(part.type)) {
1009
+ unlinkSync(filePath);
1010
+ anyRemoved = true;
1052
1011
  }
1012
+ } catch {
1013
+ continue;
1053
1014
  }
1054
1015
  }
1055
- if (!targetUserMsg?.parts?.length) {
1016
+ return anyRemoved;
1017
+ }
1018
+
1019
+ // src/hooks/session-recovery/index.ts
1020
+ function getErrorMessage(error) {
1021
+ if (!error)
1022
+ return "";
1023
+ if (typeof error === "string")
1024
+ return error.toLowerCase();
1025
+ const errorObj = error;
1026
+ return (errorObj.data?.message || errorObj.error?.message || errorObj.message || "").toLowerCase();
1027
+ }
1028
+ function detectErrorType(error) {
1029
+ const message = getErrorMessage(error);
1030
+ if (message.includes("tool_use") && message.includes("tool_result")) {
1031
+ return "tool_result_missing";
1032
+ }
1033
+ if (message.includes("thinking") && (message.includes("first block") || message.includes("must start with") || message.includes("preceeding"))) {
1034
+ return "thinking_block_order";
1035
+ }
1036
+ if (message.includes("thinking is disabled") && message.includes("cannot contain")) {
1037
+ return "thinking_disabled_violation";
1038
+ }
1039
+ if (message.includes("non-empty content") || message.includes("must have non-empty content")) {
1040
+ return "empty_content_message";
1041
+ }
1042
+ return null;
1043
+ }
1044
+ function extractToolUseIds(parts) {
1045
+ return parts.filter((p) => p.type === "tool_use" && !!p.id).map((p) => p.id);
1046
+ }
1047
+ async function recoverToolResultMissing(client, sessionID, failedAssistantMsg) {
1048
+ const parts = failedAssistantMsg.parts || [];
1049
+ const toolUseIds = extractToolUseIds(parts);
1050
+ if (toolUseIds.length === 0) {
1051
+ return false;
1052
+ }
1053
+ const toolResultParts = toolUseIds.map((id) => ({
1054
+ type: "tool_result",
1055
+ tool_use_id: id,
1056
+ content: "Operation cancelled by user (ESC pressed)"
1057
+ }));
1058
+ try {
1059
+ await client.session.prompt({
1060
+ path: { id: sessionID },
1061
+ body: { parts: toolResultParts }
1062
+ });
1063
+ return true;
1064
+ } catch {
1056
1065
  return false;
1057
1066
  }
1058
- await client.session.revert({
1059
- path: { id: sessionID },
1060
- body: { messageID: targetUserMsg.info?.id ?? "" },
1061
- query: { directory }
1062
- });
1063
- const textParts = targetUserMsg.parts.filter((p) => p.type === "text" && p.text).map((p) => ({ type: "text", text: p.text ?? "" }));
1064
- if (textParts.length === 0) {
1067
+ }
1068
+ async function recoverThinkingBlockOrder(_client, sessionID, _failedAssistantMsg, _directory) {
1069
+ const orphanMessages = findMessagesWithOrphanThinking(sessionID);
1070
+ if (orphanMessages.length === 0) {
1065
1071
  return false;
1066
1072
  }
1067
- await client.session.prompt({
1068
- path: { id: sessionID },
1069
- body: { parts: textParts },
1070
- query: { directory }
1071
- });
1072
- return true;
1073
+ let anySuccess = false;
1074
+ for (const messageID of orphanMessages) {
1075
+ if (prependThinkingPart(sessionID, messageID)) {
1076
+ anySuccess = true;
1077
+ }
1078
+ }
1079
+ return anySuccess;
1080
+ }
1081
+ async function recoverThinkingDisabledViolation(_client, sessionID, _failedAssistantMsg) {
1082
+ const messagesWithThinking = findMessagesWithThinkingBlocks(sessionID);
1083
+ if (messagesWithThinking.length === 0) {
1084
+ return false;
1085
+ }
1086
+ let anySuccess = false;
1087
+ for (const messageID of messagesWithThinking) {
1088
+ if (stripThinkingParts(messageID)) {
1089
+ anySuccess = true;
1090
+ }
1091
+ }
1092
+ return anySuccess;
1093
+ }
1094
+ async function recoverEmptyContentMessage(_client, sessionID, failedAssistantMsg, _directory) {
1095
+ const emptyMessageIDs = findEmptyMessages(sessionID);
1096
+ if (emptyMessageIDs.length === 0) {
1097
+ const fallbackID = failedAssistantMsg.info?.id;
1098
+ if (!fallbackID)
1099
+ return false;
1100
+ return injectTextPart(sessionID, fallbackID, "(interrupted)");
1101
+ }
1102
+ let anySuccess = false;
1103
+ for (const messageID of emptyMessageIDs) {
1104
+ if (injectTextPart(sessionID, messageID, "(interrupted)")) {
1105
+ anySuccess = true;
1106
+ }
1107
+ }
1108
+ return anySuccess;
1073
1109
  }
1074
1110
  function createSessionRecoveryHook(ctx) {
1075
1111
  const processingErrors = new Set;
@@ -1117,14 +1153,12 @@ function createSessionRecoveryHook(ctx) {
1117
1153
  tool_result_missing: "Injecting cancelled tool results...",
1118
1154
  thinking_block_order: "Fixing message structure...",
1119
1155
  thinking_disabled_violation: "Stripping thinking blocks...",
1120
- empty_content_message: "Deleting empty message..."
1156
+ empty_content_message: "Fixing empty message..."
1121
1157
  };
1122
- const toastTitle = toastTitles[errorType];
1123
- const toastMessage = toastMessages[errorType];
1124
1158
  await ctx.client.tui.showToast({
1125
1159
  body: {
1126
- title: toastTitle,
1127
- message: toastMessage,
1160
+ title: toastTitles[errorType],
1161
+ message: toastMessages[errorType],
1128
1162
  variant: "warning",
1129
1163
  duration: 3000
1130
1164
  }
@@ -1155,14 +1189,14 @@ function createSessionRecoveryHook(ctx) {
1155
1189
  // src/hooks/comment-checker/cli.ts
1156
1190
  var {spawn: spawn2 } = globalThis.Bun;
1157
1191
  import { createRequire as createRequire2 } from "module";
1158
- import { dirname, join as join3 } from "path";
1192
+ import { dirname, join as join4 } from "path";
1159
1193
  import { existsSync as existsSync3 } from "fs";
1160
1194
  import * as fs from "fs";
1161
1195
 
1162
1196
  // src/hooks/comment-checker/downloader.ts
1163
1197
  var {spawn } = globalThis.Bun;
1164
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, chmodSync, unlinkSync, appendFileSync } from "fs";
1165
- import { join as join2 } from "path";
1198
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, chmodSync, unlinkSync as unlinkSync2, appendFileSync } from "fs";
1199
+ import { join as join3 } from "path";
1166
1200
  import { homedir } from "os";
1167
1201
  import { createRequire } from "module";
1168
1202
  var DEBUG = process.env.COMMENT_CHECKER_DEBUG === "1";
@@ -1184,14 +1218,14 @@ var PLATFORM_MAP = {
1184
1218
  };
1185
1219
  function getCacheDir() {
1186
1220
  const xdgCache2 = process.env.XDG_CACHE_HOME;
1187
- const base = xdgCache2 || join2(homedir(), ".cache");
1188
- return join2(base, "oh-my-opencode", "bin");
1221
+ const base = xdgCache2 || join3(homedir(), ".cache");
1222
+ return join3(base, "oh-my-opencode", "bin");
1189
1223
  }
1190
1224
  function getBinaryName() {
1191
1225
  return process.platform === "win32" ? "comment-checker.exe" : "comment-checker";
1192
1226
  }
1193
1227
  function getCachedBinaryPath() {
1194
- const binaryPath = join2(getCacheDir(), getBinaryName());
1228
+ const binaryPath = join3(getCacheDir(), getBinaryName());
1195
1229
  return existsSync2(binaryPath) ? binaryPath : null;
1196
1230
  }
1197
1231
  function getPackageVersion() {
@@ -1239,7 +1273,7 @@ async function downloadCommentChecker() {
1239
1273
  }
1240
1274
  const cacheDir = getCacheDir();
1241
1275
  const binaryName = getBinaryName();
1242
- const binaryPath = join2(cacheDir, binaryName);
1276
+ const binaryPath = join3(cacheDir, binaryName);
1243
1277
  if (existsSync2(binaryPath)) {
1244
1278
  debugLog("Binary already cached at:", binaryPath);
1245
1279
  return binaryPath;
@@ -1258,7 +1292,7 @@ async function downloadCommentChecker() {
1258
1292
  if (!response.ok) {
1259
1293
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1260
1294
  }
1261
- const archivePath = join2(cacheDir, assetName);
1295
+ const archivePath = join3(cacheDir, assetName);
1262
1296
  const arrayBuffer = await response.arrayBuffer();
1263
1297
  await Bun.write(archivePath, arrayBuffer);
1264
1298
  debugLog(`Downloaded archive to: ${archivePath}`);
@@ -1268,7 +1302,7 @@ async function downloadCommentChecker() {
1268
1302
  await extractZip(archivePath, cacheDir);
1269
1303
  }
1270
1304
  if (existsSync2(archivePath)) {
1271
- unlinkSync(archivePath);
1305
+ unlinkSync2(archivePath);
1272
1306
  }
1273
1307
  if (process.platform !== "win32" && existsSync2(binaryPath)) {
1274
1308
  chmodSync(binaryPath, 493);
@@ -1323,7 +1357,7 @@ function findCommentCheckerPathSync() {
1323
1357
  const require2 = createRequire2(import.meta.url);
1324
1358
  const cliPkgPath = require2.resolve("@code-yeongyu/comment-checker/package.json");
1325
1359
  const cliDir = dirname(cliPkgPath);
1326
- const binaryPath = join3(cliDir, "bin", binaryName);
1360
+ const binaryPath = join4(cliDir, "bin", binaryName);
1327
1361
  if (existsSync3(binaryPath)) {
1328
1362
  debugLog2("found binary in main package:", binaryPath);
1329
1363
  return binaryPath;
@@ -1337,7 +1371,7 @@ function findCommentCheckerPathSync() {
1337
1371
  const require2 = createRequire2(import.meta.url);
1338
1372
  const pkgPath = require2.resolve(`${platformPkg}/package.json`);
1339
1373
  const pkgDir = dirname(pkgPath);
1340
- const binaryPath = join3(pkgDir, "bin", binaryName);
1374
+ const binaryPath = join4(pkgDir, "bin", binaryName);
1341
1375
  if (existsSync3(binaryPath)) {
1342
1376
  debugLog2("found binary in platform package:", binaryPath);
1343
1377
  return binaryPath;
@@ -1763,6 +1797,168 @@ function createPulseMonitorHook(ctx) {
1763
1797
  }
1764
1798
  };
1765
1799
  }
1800
+ // src/hooks/directory-agents-injector/index.ts
1801
+ import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
1802
+ import { dirname as dirname2, join as join7, resolve } from "path";
1803
+
1804
+ // src/hooks/directory-agents-injector/storage.ts
1805
+ import {
1806
+ existsSync as existsSync5,
1807
+ mkdirSync as mkdirSync3,
1808
+ readFileSync as readFileSync2,
1809
+ writeFileSync as writeFileSync2,
1810
+ unlinkSync as unlinkSync3
1811
+ } from "fs";
1812
+ import { join as join6 } from "path";
1813
+
1814
+ // src/hooks/directory-agents-injector/constants.ts
1815
+ import { join as join5 } from "path";
1816
+ var OPENCODE_STORAGE2 = join5(xdgData ?? "", "opencode", "storage");
1817
+ var AGENTS_INJECTOR_STORAGE = join5(OPENCODE_STORAGE2, "directory-agents");
1818
+ var AGENTS_FILENAME = "AGENTS.md";
1819
+
1820
+ // src/hooks/directory-agents-injector/storage.ts
1821
+ function getStoragePath(sessionID) {
1822
+ return join6(AGENTS_INJECTOR_STORAGE, `${sessionID}.json`);
1823
+ }
1824
+ function loadInjectedPaths(sessionID) {
1825
+ const filePath = getStoragePath(sessionID);
1826
+ if (!existsSync5(filePath))
1827
+ return new Set;
1828
+ try {
1829
+ const content = readFileSync2(filePath, "utf-8");
1830
+ const data = JSON.parse(content);
1831
+ return new Set(data.injectedPaths);
1832
+ } catch {
1833
+ return new Set;
1834
+ }
1835
+ }
1836
+ function saveInjectedPaths(sessionID, paths) {
1837
+ if (!existsSync5(AGENTS_INJECTOR_STORAGE)) {
1838
+ mkdirSync3(AGENTS_INJECTOR_STORAGE, { recursive: true });
1839
+ }
1840
+ const data = {
1841
+ sessionID,
1842
+ injectedPaths: [...paths],
1843
+ updatedAt: Date.now()
1844
+ };
1845
+ writeFileSync2(getStoragePath(sessionID), JSON.stringify(data, null, 2));
1846
+ }
1847
+ function clearInjectedPaths(sessionID) {
1848
+ const filePath = getStoragePath(sessionID);
1849
+ if (existsSync5(filePath)) {
1850
+ unlinkSync3(filePath);
1851
+ }
1852
+ }
1853
+
1854
+ // src/hooks/directory-agents-injector/index.ts
1855
+ function createDirectoryAgentsInjectorHook(ctx) {
1856
+ const sessionCaches = new Map;
1857
+ function getSessionCache(sessionID) {
1858
+ if (!sessionCaches.has(sessionID)) {
1859
+ sessionCaches.set(sessionID, loadInjectedPaths(sessionID));
1860
+ }
1861
+ return sessionCaches.get(sessionID);
1862
+ }
1863
+ function resolveFilePath(title) {
1864
+ if (!title)
1865
+ return null;
1866
+ if (title.startsWith("/"))
1867
+ return title;
1868
+ return resolve(ctx.directory, title);
1869
+ }
1870
+ function findAgentsMdUp(startDir) {
1871
+ const found = [];
1872
+ let current = startDir;
1873
+ while (true) {
1874
+ const agentsPath = join7(current, AGENTS_FILENAME);
1875
+ if (existsSync6(agentsPath)) {
1876
+ found.push(agentsPath);
1877
+ }
1878
+ if (current === ctx.directory)
1879
+ break;
1880
+ const parent = dirname2(current);
1881
+ if (parent === current)
1882
+ break;
1883
+ if (!parent.startsWith(ctx.directory))
1884
+ break;
1885
+ current = parent;
1886
+ }
1887
+ return found.reverse();
1888
+ }
1889
+ const toolExecuteAfter = async (input, output) => {
1890
+ if (input.tool.toLowerCase() !== "read")
1891
+ return;
1892
+ const filePath = resolveFilePath(output.title);
1893
+ if (!filePath)
1894
+ return;
1895
+ const dir = dirname2(filePath);
1896
+ const cache = getSessionCache(input.sessionID);
1897
+ const agentsPaths = findAgentsMdUp(dir);
1898
+ const toInject = [];
1899
+ for (const agentsPath of agentsPaths) {
1900
+ const agentsDir = dirname2(agentsPath);
1901
+ if (cache.has(agentsDir))
1902
+ continue;
1903
+ try {
1904
+ const content = readFileSync3(agentsPath, "utf-8");
1905
+ toInject.push({ path: agentsPath, content });
1906
+ cache.add(agentsDir);
1907
+ } catch {}
1908
+ }
1909
+ if (toInject.length === 0)
1910
+ return;
1911
+ for (const { path: path2, content } of toInject) {
1912
+ output.output += `
1913
+
1914
+ [Directory Context: ${path2}]
1915
+ ${content}`;
1916
+ }
1917
+ saveInjectedPaths(input.sessionID, cache);
1918
+ };
1919
+ const eventHandler = async ({ event }) => {
1920
+ const props = event.properties;
1921
+ if (event.type === "session.deleted") {
1922
+ const sessionInfo = props?.info;
1923
+ if (sessionInfo?.id) {
1924
+ sessionCaches.delete(sessionInfo.id);
1925
+ clearInjectedPaths(sessionInfo.id);
1926
+ }
1927
+ }
1928
+ if (event.type === "session.compacted") {
1929
+ const sessionID = props?.sessionID ?? props?.info?.id;
1930
+ if (sessionID) {
1931
+ sessionCaches.delete(sessionID);
1932
+ clearInjectedPaths(sessionID);
1933
+ }
1934
+ }
1935
+ };
1936
+ return {
1937
+ "tool.execute.after": toolExecuteAfter,
1938
+ event: eventHandler
1939
+ };
1940
+ }
1941
+ // src/hooks/empty-task-response-detector.ts
1942
+ var EMPTY_RESPONSE_WARNING = `[Task Empty Response Warning]
1943
+
1944
+ Task invocation completed but returned no response. This indicates the agent either:
1945
+ - Failed to execute properly
1946
+ - Did not terminate correctly
1947
+ - Returned an empty result
1948
+
1949
+ Note: The call has already completed - you are NOT waiting for a response. Proceed accordingly.`;
1950
+ function createEmptyTaskResponseDetectorHook(_ctx) {
1951
+ return {
1952
+ "tool.execute.after": async (input, output) => {
1953
+ if (input.tool !== "Task")
1954
+ return;
1955
+ const responseText = output.output?.trim() ?? "";
1956
+ if (responseText === "") {
1957
+ output.output = EMPTY_RESPONSE_WARNING;
1958
+ }
1959
+ }
1960
+ };
1961
+ }
1766
1962
  // src/features/terminal/title.ts
1767
1963
  var STATUS_ICONS = {
1768
1964
  ready: "",
@@ -1988,14 +2184,14 @@ var EXT_TO_LANG = {
1988
2184
  ".tfvars": "terraform"
1989
2185
  };
1990
2186
  // src/tools/lsp/config.ts
1991
- import { existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
1992
- import { join as join4 } from "path";
2187
+ import { existsSync as existsSync7, readFileSync as readFileSync4 } from "fs";
2188
+ import { join as join8 } from "path";
1993
2189
  import { homedir as homedir2 } from "os";
1994
2190
  function loadJsonFile(path2) {
1995
- if (!existsSync5(path2))
2191
+ if (!existsSync7(path2))
1996
2192
  return null;
1997
2193
  try {
1998
- return JSON.parse(readFileSync2(path2, "utf-8"));
2194
+ return JSON.parse(readFileSync4(path2, "utf-8"));
1999
2195
  } catch {
2000
2196
  return null;
2001
2197
  }
@@ -2003,9 +2199,9 @@ function loadJsonFile(path2) {
2003
2199
  function getConfigPaths() {
2004
2200
  const cwd = process.cwd();
2005
2201
  return {
2006
- project: join4(cwd, ".opencode", "oh-my-opencode.json"),
2007
- user: join4(homedir2(), ".config", "opencode", "oh-my-opencode.json"),
2008
- opencode: join4(homedir2(), ".config", "opencode", "opencode.json")
2202
+ project: join8(cwd, ".opencode", "oh-my-opencode.json"),
2203
+ user: join8(homedir2(), ".config", "opencode", "oh-my-opencode.json"),
2204
+ opencode: join8(homedir2(), ".config", "opencode", "opencode.json")
2009
2205
  };
2010
2206
  }
2011
2207
  function loadAllConfigs() {
@@ -2098,7 +2294,7 @@ function isServerInstalled(command) {
2098
2294
  const pathEnv = process.env.PATH || "";
2099
2295
  const paths = pathEnv.split(":");
2100
2296
  for (const p of paths) {
2101
- if (existsSync5(join4(p, cmd))) {
2297
+ if (existsSync7(join8(p, cmd))) {
2102
2298
  return true;
2103
2299
  }
2104
2300
  }
@@ -2148,8 +2344,8 @@ function getAllServers() {
2148
2344
  }
2149
2345
  // src/tools/lsp/client.ts
2150
2346
  var {spawn: spawn3 } = globalThis.Bun;
2151
- import { readFileSync as readFileSync3 } from "fs";
2152
- import { extname, resolve } from "path";
2347
+ import { readFileSync as readFileSync5 } from "fs";
2348
+ import { extname, resolve as resolve2 } from "path";
2153
2349
  class LSPServerManager {
2154
2350
  static instance;
2155
2351
  clients = new Map;
@@ -2299,7 +2495,7 @@ class LSPClient {
2299
2495
  }
2300
2496
  this.startReading();
2301
2497
  this.startStderrReading();
2302
- await new Promise((resolve2) => setTimeout(resolve2, 100));
2498
+ await new Promise((resolve3) => setTimeout(resolve3, 100));
2303
2499
  if (this.proc.exitCode !== null) {
2304
2500
  const stderr = this.stderrBuffer.join(`
2305
2501
  `);
@@ -2436,8 +2632,8 @@ stderr: ${stderr}` : ""));
2436
2632
  \r
2437
2633
  `;
2438
2634
  this.proc.stdin.write(header + msg);
2439
- return new Promise((resolve2, reject) => {
2440
- this.pending.set(id, { resolve: resolve2, reject });
2635
+ return new Promise((resolve3, reject) => {
2636
+ this.pending.set(id, { resolve: resolve3, reject });
2441
2637
  setTimeout(() => {
2442
2638
  if (this.pending.has(id)) {
2443
2639
  this.pending.delete(id);
@@ -2545,10 +2741,10 @@ ${msg}`);
2545
2741
  await new Promise((r) => setTimeout(r, 300));
2546
2742
  }
2547
2743
  async openFile(filePath) {
2548
- const absPath = resolve(filePath);
2744
+ const absPath = resolve2(filePath);
2549
2745
  if (this.openedFiles.has(absPath))
2550
2746
  return;
2551
- const text = readFileSync3(absPath, "utf-8");
2747
+ const text = readFileSync5(absPath, "utf-8");
2552
2748
  const ext = extname(absPath);
2553
2749
  const languageId = getLanguageId(ext);
2554
2750
  this.notify("textDocument/didOpen", {
@@ -2563,7 +2759,7 @@ ${msg}`);
2563
2759
  await new Promise((r) => setTimeout(r, 1000));
2564
2760
  }
2565
2761
  async hover(filePath, line, character) {
2566
- const absPath = resolve(filePath);
2762
+ const absPath = resolve2(filePath);
2567
2763
  await this.openFile(absPath);
2568
2764
  return this.send("textDocument/hover", {
2569
2765
  textDocument: { uri: `file://${absPath}` },
@@ -2571,7 +2767,7 @@ ${msg}`);
2571
2767
  });
2572
2768
  }
2573
2769
  async definition(filePath, line, character) {
2574
- const absPath = resolve(filePath);
2770
+ const absPath = resolve2(filePath);
2575
2771
  await this.openFile(absPath);
2576
2772
  return this.send("textDocument/definition", {
2577
2773
  textDocument: { uri: `file://${absPath}` },
@@ -2579,7 +2775,7 @@ ${msg}`);
2579
2775
  });
2580
2776
  }
2581
2777
  async references(filePath, line, character, includeDeclaration = true) {
2582
- const absPath = resolve(filePath);
2778
+ const absPath = resolve2(filePath);
2583
2779
  await this.openFile(absPath);
2584
2780
  return this.send("textDocument/references", {
2585
2781
  textDocument: { uri: `file://${absPath}` },
@@ -2588,7 +2784,7 @@ ${msg}`);
2588
2784
  });
2589
2785
  }
2590
2786
  async documentSymbols(filePath) {
2591
- const absPath = resolve(filePath);
2787
+ const absPath = resolve2(filePath);
2592
2788
  await this.openFile(absPath);
2593
2789
  return this.send("textDocument/documentSymbol", {
2594
2790
  textDocument: { uri: `file://${absPath}` }
@@ -2598,7 +2794,7 @@ ${msg}`);
2598
2794
  return this.send("workspace/symbol", { query });
2599
2795
  }
2600
2796
  async diagnostics(filePath) {
2601
- const absPath = resolve(filePath);
2797
+ const absPath = resolve2(filePath);
2602
2798
  const uri = `file://${absPath}`;
2603
2799
  await this.openFile(absPath);
2604
2800
  await new Promise((r) => setTimeout(r, 500));
@@ -2613,7 +2809,7 @@ ${msg}`);
2613
2809
  return { items: this.diagnosticsStore.get(uri) ?? [] };
2614
2810
  }
2615
2811
  async prepareRename(filePath, line, character) {
2616
- const absPath = resolve(filePath);
2812
+ const absPath = resolve2(filePath);
2617
2813
  await this.openFile(absPath);
2618
2814
  return this.send("textDocument/prepareRename", {
2619
2815
  textDocument: { uri: `file://${absPath}` },
@@ -2621,7 +2817,7 @@ ${msg}`);
2621
2817
  });
2622
2818
  }
2623
2819
  async rename(filePath, line, character, newName) {
2624
- const absPath = resolve(filePath);
2820
+ const absPath = resolve2(filePath);
2625
2821
  await this.openFile(absPath);
2626
2822
  return this.send("textDocument/rename", {
2627
2823
  textDocument: { uri: `file://${absPath}` },
@@ -2630,7 +2826,7 @@ ${msg}`);
2630
2826
  });
2631
2827
  }
2632
2828
  async codeAction(filePath, startLine, startChar, endLine, endChar, only) {
2633
- const absPath = resolve(filePath);
2829
+ const absPath = resolve2(filePath);
2634
2830
  await this.openFile(absPath);
2635
2831
  return this.send("textDocument/codeAction", {
2636
2832
  textDocument: { uri: `file://${absPath}` },
@@ -2662,26 +2858,26 @@ ${msg}`);
2662
2858
  }
2663
2859
  }
2664
2860
  // src/tools/lsp/utils.ts
2665
- import { extname as extname2, resolve as resolve2 } from "path";
2666
- import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
2861
+ import { extname as extname2, resolve as resolve3 } from "path";
2862
+ import { existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
2667
2863
  function findWorkspaceRoot(filePath) {
2668
- let dir = resolve2(filePath);
2669
- if (!existsSync6(dir) || !__require("fs").statSync(dir).isDirectory()) {
2864
+ let dir = resolve3(filePath);
2865
+ if (!existsSync8(dir) || !__require("fs").statSync(dir).isDirectory()) {
2670
2866
  dir = __require("path").dirname(dir);
2671
2867
  }
2672
2868
  const markers = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "build.gradle"];
2673
2869
  while (dir !== "/") {
2674
2870
  for (const marker of markers) {
2675
- if (existsSync6(__require("path").join(dir, marker))) {
2871
+ if (existsSync8(__require("path").join(dir, marker))) {
2676
2872
  return dir;
2677
2873
  }
2678
2874
  }
2679
2875
  dir = __require("path").dirname(dir);
2680
2876
  }
2681
- return __require("path").dirname(resolve2(filePath));
2877
+ return __require("path").dirname(resolve3(filePath));
2682
2878
  }
2683
2879
  async function withLspClient(filePath, fn) {
2684
- const absPath = resolve2(filePath);
2880
+ const absPath = resolve3(filePath);
2685
2881
  const ext = extname2(absPath);
2686
2882
  const server = findServerForExtension(ext);
2687
2883
  if (!server) {
@@ -2830,7 +3026,7 @@ function formatCodeActions(actions) {
2830
3026
  }
2831
3027
  function applyTextEditsToFile(filePath, edits) {
2832
3028
  try {
2833
- let content = readFileSync4(filePath, "utf-8");
3029
+ let content = readFileSync6(filePath, "utf-8");
2834
3030
  const lines = content.split(`
2835
3031
  `);
2836
3032
  const sortedEdits = [...edits].sort((a, b) => {
@@ -2855,7 +3051,7 @@ function applyTextEditsToFile(filePath, edits) {
2855
3051
  `));
2856
3052
  }
2857
3053
  }
2858
- writeFileSync2(filePath, lines.join(`
3054
+ writeFileSync3(filePath, lines.join(`
2859
3055
  `), "utf-8");
2860
3056
  return { success: true, editCount: edits.length };
2861
3057
  } catch (err) {
@@ -2886,7 +3082,7 @@ function applyWorkspaceEdit(edit) {
2886
3082
  if (change.kind === "create") {
2887
3083
  try {
2888
3084
  const filePath = change.uri.replace("file://", "");
2889
- writeFileSync2(filePath, "", "utf-8");
3085
+ writeFileSync3(filePath, "", "utf-8");
2890
3086
  result.filesModified.push(filePath);
2891
3087
  } catch (err) {
2892
3088
  result.success = false;
@@ -2896,8 +3092,8 @@ function applyWorkspaceEdit(edit) {
2896
3092
  try {
2897
3093
  const oldPath = change.oldUri.replace("file://", "");
2898
3094
  const newPath = change.newUri.replace("file://", "");
2899
- const content = readFileSync4(oldPath, "utf-8");
2900
- writeFileSync2(newPath, content, "utf-8");
3095
+ const content = readFileSync6(oldPath, "utf-8");
3096
+ writeFileSync3(newPath, content, "utf-8");
2901
3097
  __require("fs").unlinkSync(oldPath);
2902
3098
  result.filesModified.push(newPath);
2903
3099
  } catch (err) {
@@ -15597,13 +15793,13 @@ var lsp_code_action_resolve = tool({
15597
15793
  });
15598
15794
  // src/tools/ast-grep/constants.ts
15599
15795
  import { createRequire as createRequire4 } from "module";
15600
- import { dirname as dirname2, join as join6 } from "path";
15601
- import { existsSync as existsSync8, statSync } from "fs";
15796
+ import { dirname as dirname3, join as join10 } from "path";
15797
+ import { existsSync as existsSync10, statSync } from "fs";
15602
15798
 
15603
15799
  // src/tools/ast-grep/downloader.ts
15604
15800
  var {spawn: spawn4 } = globalThis.Bun;
15605
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, chmodSync as chmodSync2, unlinkSync as unlinkSync2 } from "fs";
15606
- import { join as join5 } from "path";
15801
+ import { existsSync as existsSync9, mkdirSync as mkdirSync4, chmodSync as chmodSync2, unlinkSync as unlinkSync4 } from "fs";
15802
+ import { join as join9 } from "path";
15607
15803
  import { homedir as homedir3 } from "os";
15608
15804
  import { createRequire as createRequire3 } from "module";
15609
15805
  var REPO2 = "ast-grep/ast-grep";
@@ -15629,19 +15825,19 @@ var PLATFORM_MAP2 = {
15629
15825
  function getCacheDir2() {
15630
15826
  if (process.platform === "win32") {
15631
15827
  const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
15632
- const base2 = localAppData || join5(homedir3(), "AppData", "Local");
15633
- return join5(base2, "oh-my-opencode", "bin");
15828
+ const base2 = localAppData || join9(homedir3(), "AppData", "Local");
15829
+ return join9(base2, "oh-my-opencode", "bin");
15634
15830
  }
15635
15831
  const xdgCache2 = process.env.XDG_CACHE_HOME;
15636
- const base = xdgCache2 || join5(homedir3(), ".cache");
15637
- return join5(base, "oh-my-opencode", "bin");
15832
+ const base = xdgCache2 || join9(homedir3(), ".cache");
15833
+ return join9(base, "oh-my-opencode", "bin");
15638
15834
  }
15639
15835
  function getBinaryName3() {
15640
15836
  return process.platform === "win32" ? "sg.exe" : "sg";
15641
15837
  }
15642
15838
  function getCachedBinaryPath2() {
15643
- const binaryPath = join5(getCacheDir2(), getBinaryName3());
15644
- return existsSync7(binaryPath) ? binaryPath : null;
15839
+ const binaryPath = join9(getCacheDir2(), getBinaryName3());
15840
+ return existsSync9(binaryPath) ? binaryPath : null;
15645
15841
  }
15646
15842
  async function extractZip2(archivePath, destDir) {
15647
15843
  const proc = process.platform === "win32" ? spawn4([
@@ -15667,8 +15863,8 @@ async function downloadAstGrep(version2 = DEFAULT_VERSION) {
15667
15863
  }
15668
15864
  const cacheDir = getCacheDir2();
15669
15865
  const binaryName = getBinaryName3();
15670
- const binaryPath = join5(cacheDir, binaryName);
15671
- if (existsSync7(binaryPath)) {
15866
+ const binaryPath = join9(cacheDir, binaryName);
15867
+ if (existsSync9(binaryPath)) {
15672
15868
  return binaryPath;
15673
15869
  }
15674
15870
  const { arch, os: os2 } = platformInfo;
@@ -15676,21 +15872,21 @@ async function downloadAstGrep(version2 = DEFAULT_VERSION) {
15676
15872
  const downloadUrl = `https://github.com/${REPO2}/releases/download/${version2}/${assetName}`;
15677
15873
  console.log(`[oh-my-opencode] Downloading ast-grep binary...`);
15678
15874
  try {
15679
- if (!existsSync7(cacheDir)) {
15680
- mkdirSync3(cacheDir, { recursive: true });
15875
+ if (!existsSync9(cacheDir)) {
15876
+ mkdirSync4(cacheDir, { recursive: true });
15681
15877
  }
15682
15878
  const response = await fetch(downloadUrl, { redirect: "follow" });
15683
15879
  if (!response.ok) {
15684
15880
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
15685
15881
  }
15686
- const archivePath = join5(cacheDir, assetName);
15882
+ const archivePath = join9(cacheDir, assetName);
15687
15883
  const arrayBuffer = await response.arrayBuffer();
15688
15884
  await Bun.write(archivePath, arrayBuffer);
15689
15885
  await extractZip2(archivePath, cacheDir);
15690
- if (existsSync7(archivePath)) {
15691
- unlinkSync2(archivePath);
15886
+ if (existsSync9(archivePath)) {
15887
+ unlinkSync4(archivePath);
15692
15888
  }
15693
- if (process.platform !== "win32" && existsSync7(binaryPath)) {
15889
+ if (process.platform !== "win32" && existsSync9(binaryPath)) {
15694
15890
  chmodSync2(binaryPath, 493);
15695
15891
  }
15696
15892
  console.log(`[oh-my-opencode] ast-grep binary ready.`);
@@ -15740,9 +15936,9 @@ function findSgCliPathSync() {
15740
15936
  try {
15741
15937
  const require2 = createRequire4(import.meta.url);
15742
15938
  const cliPkgPath = require2.resolve("@ast-grep/cli/package.json");
15743
- const cliDir = dirname2(cliPkgPath);
15744
- const sgPath = join6(cliDir, binaryName);
15745
- if (existsSync8(sgPath) && isValidBinary(sgPath)) {
15939
+ const cliDir = dirname3(cliPkgPath);
15940
+ const sgPath = join10(cliDir, binaryName);
15941
+ if (existsSync10(sgPath) && isValidBinary(sgPath)) {
15746
15942
  return sgPath;
15747
15943
  }
15748
15944
  } catch {}
@@ -15751,10 +15947,10 @@ function findSgCliPathSync() {
15751
15947
  try {
15752
15948
  const require2 = createRequire4(import.meta.url);
15753
15949
  const pkgPath = require2.resolve(`${platformPkg}/package.json`);
15754
- const pkgDir = dirname2(pkgPath);
15950
+ const pkgDir = dirname3(pkgPath);
15755
15951
  const astGrepName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep";
15756
- const binaryPath = join6(pkgDir, astGrepName);
15757
- if (existsSync8(binaryPath) && isValidBinary(binaryPath)) {
15952
+ const binaryPath = join10(pkgDir, astGrepName);
15953
+ if (existsSync10(binaryPath) && isValidBinary(binaryPath)) {
15758
15954
  return binaryPath;
15759
15955
  }
15760
15956
  } catch {}
@@ -15762,7 +15958,7 @@ function findSgCliPathSync() {
15762
15958
  if (process.platform === "darwin") {
15763
15959
  const homebrewPaths = ["/opt/homebrew/bin/sg", "/usr/local/bin/sg"];
15764
15960
  for (const path2 of homebrewPaths) {
15765
- if (existsSync8(path2) && isValidBinary(path2)) {
15961
+ if (existsSync10(path2) && isValidBinary(path2)) {
15766
15962
  return path2;
15767
15963
  }
15768
15964
  }
@@ -15846,11 +16042,11 @@ var LANG_EXTENSIONS = {
15846
16042
 
15847
16043
  // src/tools/ast-grep/cli.ts
15848
16044
  var {spawn: spawn5 } = globalThis.Bun;
15849
- import { existsSync as existsSync9 } from "fs";
16045
+ import { existsSync as existsSync11 } from "fs";
15850
16046
  var resolvedCliPath3 = null;
15851
16047
  var initPromise2 = null;
15852
16048
  async function getAstGrepPath() {
15853
- if (resolvedCliPath3 !== null && existsSync9(resolvedCliPath3)) {
16049
+ if (resolvedCliPath3 !== null && existsSync11(resolvedCliPath3)) {
15854
16050
  return resolvedCliPath3;
15855
16051
  }
15856
16052
  if (initPromise2) {
@@ -15858,7 +16054,7 @@ async function getAstGrepPath() {
15858
16054
  }
15859
16055
  initPromise2 = (async () => {
15860
16056
  const syncPath = findSgCliPathSync();
15861
- if (syncPath && existsSync9(syncPath)) {
16057
+ if (syncPath && existsSync11(syncPath)) {
15862
16058
  resolvedCliPath3 = syncPath;
15863
16059
  setSgCliPath(syncPath);
15864
16060
  return syncPath;
@@ -15892,7 +16088,7 @@ async function runSg(options) {
15892
16088
  const paths = options.paths && options.paths.length > 0 ? options.paths : ["."];
15893
16089
  args.push(...paths);
15894
16090
  let cliPath = getSgCliPath();
15895
- if (!existsSync9(cliPath) && cliPath !== "sg") {
16091
+ if (!existsSync11(cliPath) && cliPath !== "sg") {
15896
16092
  const downloadedPath = await getAstGrepPath();
15897
16093
  if (downloadedPath) {
15898
16094
  cliPath = downloadedPath;
@@ -16333,12 +16529,12 @@ var ast_grep_transform = tool({
16333
16529
  }
16334
16530
  }
16335
16531
  });
16336
- // src/tools/safe-grep/cli.ts
16532
+ // src/tools/grep/cli.ts
16337
16533
  var {spawn: spawn6 } = globalThis.Bun;
16338
16534
 
16339
- // src/tools/safe-grep/constants.ts
16340
- import { existsSync as existsSync10 } from "fs";
16341
- import { join as join7, dirname as dirname3 } from "path";
16535
+ // src/tools/grep/constants.ts
16536
+ import { existsSync as existsSync12 } from "fs";
16537
+ import { join as join11, dirname as dirname4 } from "path";
16342
16538
  import { spawnSync } from "child_process";
16343
16539
  var cachedCli = null;
16344
16540
  function findExecutable(name) {
@@ -16355,17 +16551,17 @@ function findExecutable(name) {
16355
16551
  }
16356
16552
  function getOpenCodeBundledRg() {
16357
16553
  const execPath = process.execPath;
16358
- const execDir = dirname3(execPath);
16554
+ const execDir = dirname4(execPath);
16359
16555
  const isWindows = process.platform === "win32";
16360
16556
  const rgName = isWindows ? "rg.exe" : "rg";
16361
16557
  const candidates = [
16362
- join7(execDir, rgName),
16363
- join7(execDir, "bin", rgName),
16364
- join7(execDir, "..", "bin", rgName),
16365
- join7(execDir, "..", "libexec", rgName)
16558
+ join11(execDir, rgName),
16559
+ join11(execDir, "bin", rgName),
16560
+ join11(execDir, "..", "bin", rgName),
16561
+ join11(execDir, "..", "libexec", rgName)
16366
16562
  ];
16367
16563
  for (const candidate of candidates) {
16368
- if (existsSync10(candidate)) {
16564
+ if (existsSync12(candidate)) {
16369
16565
  return candidate;
16370
16566
  }
16371
16567
  }
@@ -16407,7 +16603,7 @@ var RG_SAFETY_FLAGS = [
16407
16603
  ];
16408
16604
  var GREP_SAFETY_FLAGS = ["-n", "-H", "--color=never"];
16409
16605
 
16410
- // src/tools/safe-grep/cli.ts
16606
+ // src/tools/grep/cli.ts
16411
16607
  function buildRgArgs(options) {
16412
16608
  const args = [
16413
16609
  ...RG_SAFETY_FLAGS,
@@ -16551,7 +16747,7 @@ async function runRg(options) {
16551
16747
  }
16552
16748
  }
16553
16749
 
16554
- // src/tools/safe-grep/utils.ts
16750
+ // src/tools/grep/utils.ts
16555
16751
  function formatGrepResult(result) {
16556
16752
  if (result.error) {
16557
16753
  return `Error: ${result.error}`;
@@ -16582,8 +16778,8 @@ function formatGrepResult(result) {
16582
16778
  `);
16583
16779
  }
16584
16780
 
16585
- // src/tools/safe-grep/tools.ts
16586
- var safe_grep = tool({
16781
+ // src/tools/grep/tools.ts
16782
+ var grep = tool({
16587
16783
  description: "Fast content search tool with safety limits (60s timeout, 10MB output). " + "Searches file contents using regular expressions. " + 'Supports full regex syntax (eg. "log.*Error", "function\\s+\\w+", etc.). ' + 'Filter files by pattern with the include parameter (eg. "*.js", "*.{ts,tsx}"). ' + "Returns file paths with matches sorted by modification time.",
16588
16784
  args: {
16589
16785
  pattern: tool.schema.string().describe("The regex pattern to search for in file contents"),
@@ -16607,6 +16803,162 @@ var safe_grep = tool({
16607
16803
  }
16608
16804
  });
16609
16805
 
16806
+ // src/tools/glob/cli.ts
16807
+ var {spawn: spawn7 } = globalThis.Bun;
16808
+
16809
+ // src/tools/glob/constants.ts
16810
+ var DEFAULT_TIMEOUT_MS3 = 60000;
16811
+ var DEFAULT_LIMIT = 100;
16812
+ var DEFAULT_MAX_DEPTH2 = 20;
16813
+ var DEFAULT_MAX_OUTPUT_BYTES3 = 10 * 1024 * 1024;
16814
+ var RG_FILES_FLAGS = [
16815
+ "--files",
16816
+ "--color=never",
16817
+ "--glob=!.git/*"
16818
+ ];
16819
+
16820
+ // src/tools/glob/cli.ts
16821
+ import { stat } from "fs/promises";
16822
+ function buildRgArgs2(options) {
16823
+ const args = [
16824
+ ...RG_FILES_FLAGS,
16825
+ `--max-depth=${Math.min(options.maxDepth ?? DEFAULT_MAX_DEPTH2, DEFAULT_MAX_DEPTH2)}`
16826
+ ];
16827
+ if (options.hidden)
16828
+ args.push("--hidden");
16829
+ if (options.noIgnore)
16830
+ args.push("--no-ignore");
16831
+ args.push(`--glob=${options.pattern}`);
16832
+ return args;
16833
+ }
16834
+ function buildFindArgs(options) {
16835
+ const args = ["."];
16836
+ const maxDepth = Math.min(options.maxDepth ?? DEFAULT_MAX_DEPTH2, DEFAULT_MAX_DEPTH2);
16837
+ args.push("-maxdepth", String(maxDepth));
16838
+ args.push("-type", "f");
16839
+ args.push("-name", options.pattern);
16840
+ if (!options.hidden) {
16841
+ args.push("-not", "-path", "*/.*");
16842
+ }
16843
+ return args;
16844
+ }
16845
+ async function getFileMtime(filePath) {
16846
+ try {
16847
+ const stats = await stat(filePath);
16848
+ return stats.mtime.getTime();
16849
+ } catch {
16850
+ return 0;
16851
+ }
16852
+ }
16853
+ async function runRgFiles(options) {
16854
+ const cli = resolveGrepCli();
16855
+ const timeout = Math.min(options.timeout ?? DEFAULT_TIMEOUT_MS3, DEFAULT_TIMEOUT_MS3);
16856
+ const limit = Math.min(options.limit ?? DEFAULT_LIMIT, DEFAULT_LIMIT);
16857
+ const isRg = cli.backend === "rg";
16858
+ const args = isRg ? buildRgArgs2(options) : buildFindArgs(options);
16859
+ const paths = options.paths?.length ? options.paths : ["."];
16860
+ if (isRg) {
16861
+ args.push(...paths);
16862
+ }
16863
+ const cwd = paths[0] || ".";
16864
+ const proc = spawn7([cli.path, ...args], {
16865
+ stdout: "pipe",
16866
+ stderr: "pipe",
16867
+ cwd: isRg ? undefined : cwd
16868
+ });
16869
+ const timeoutPromise = new Promise((_, reject) => {
16870
+ const id = setTimeout(() => {
16871
+ proc.kill();
16872
+ reject(new Error(`Glob search timeout after ${timeout}ms`));
16873
+ }, timeout);
16874
+ proc.exited.then(() => clearTimeout(id));
16875
+ });
16876
+ try {
16877
+ const stdout = await Promise.race([new Response(proc.stdout).text(), timeoutPromise]);
16878
+ const stderr = await new Response(proc.stderr).text();
16879
+ const exitCode = await proc.exited;
16880
+ if (exitCode > 1 && stderr.trim()) {
16881
+ return {
16882
+ files: [],
16883
+ totalFiles: 0,
16884
+ truncated: false,
16885
+ error: stderr.trim()
16886
+ };
16887
+ }
16888
+ const truncatedOutput = stdout.length >= DEFAULT_MAX_OUTPUT_BYTES3;
16889
+ const outputToProcess = truncatedOutput ? stdout.substring(0, DEFAULT_MAX_OUTPUT_BYTES3) : stdout;
16890
+ const lines = outputToProcess.trim().split(`
16891
+ `).filter(Boolean);
16892
+ const files = [];
16893
+ let truncated = false;
16894
+ for (const line of lines) {
16895
+ if (files.length >= limit) {
16896
+ truncated = true;
16897
+ break;
16898
+ }
16899
+ const filePath = isRg ? line : `${cwd}/${line}`;
16900
+ const mtime = await getFileMtime(filePath);
16901
+ files.push({ path: filePath, mtime });
16902
+ }
16903
+ files.sort((a, b) => b.mtime - a.mtime);
16904
+ return {
16905
+ files,
16906
+ totalFiles: files.length,
16907
+ truncated: truncated || truncatedOutput
16908
+ };
16909
+ } catch (e) {
16910
+ return {
16911
+ files: [],
16912
+ totalFiles: 0,
16913
+ truncated: false,
16914
+ error: e instanceof Error ? e.message : String(e)
16915
+ };
16916
+ }
16917
+ }
16918
+
16919
+ // src/tools/glob/utils.ts
16920
+ function formatGlobResult(result) {
16921
+ if (result.error) {
16922
+ return `Error: ${result.error}`;
16923
+ }
16924
+ if (result.files.length === 0) {
16925
+ return "No files found";
16926
+ }
16927
+ const lines = [];
16928
+ lines.push(`Found ${result.totalFiles} file(s)`);
16929
+ lines.push("");
16930
+ for (const file2 of result.files) {
16931
+ lines.push(file2.path);
16932
+ }
16933
+ if (result.truncated) {
16934
+ lines.push("");
16935
+ lines.push("(Results are truncated. Consider using a more specific path or pattern.)");
16936
+ }
16937
+ return lines.join(`
16938
+ `);
16939
+ }
16940
+
16941
+ // src/tools/glob/tools.ts
16942
+ var glob = tool({
16943
+ description: "Fast file pattern matching tool with safety limits (60s timeout, 100 file limit). " + 'Supports glob patterns like "**/*.js" or "src/**/*.ts". ' + "Returns matching file paths sorted by modification time. " + "Use this tool when you need to find files by name patterns.",
16944
+ args: {
16945
+ pattern: tool.schema.string().describe("The glob pattern to match files against"),
16946
+ path: tool.schema.string().optional().describe("The directory to search in. If not specified, the current working directory will be used. " + 'IMPORTANT: Omit this field to use the default directory. DO NOT enter "undefined" or "null" - ' + "simply omit it for the default behavior. Must be a valid directory path if provided.")
16947
+ },
16948
+ execute: async (args) => {
16949
+ try {
16950
+ const paths = args.path ? [args.path] : undefined;
16951
+ const result = await runRgFiles({
16952
+ pattern: args.pattern,
16953
+ paths
16954
+ });
16955
+ return formatGlobResult(result);
16956
+ } catch (e) {
16957
+ return `Error: ${e instanceof Error ? e.message : String(e)}`;
16958
+ }
16959
+ }
16960
+ });
16961
+
16610
16962
  // src/tools/index.ts
16611
16963
  var builtinTools = {
16612
16964
  lsp_hover,
@@ -16622,7 +16974,8 @@ var builtinTools = {
16622
16974
  lsp_code_action_resolve,
16623
16975
  ast_grep_search,
16624
16976
  ast_grep_replace,
16625
- safe_grep
16977
+ grep,
16978
+ glob
16626
16979
  };
16627
16980
 
16628
16981
  // src/mcp/websearch-exa.ts
@@ -16730,6 +17083,8 @@ var OhMyOpenCodePlugin = async (ctx) => {
16730
17083
  const pulseMonitor = createPulseMonitorHook(ctx);
16731
17084
  const commentChecker = createCommentCheckerHooks();
16732
17085
  const grepOutputTruncator = createGrepOutputTruncatorHook(ctx);
17086
+ const directoryAgentsInjector = createDirectoryAgentsInjectorHook(ctx);
17087
+ const emptyTaskResponseDetector = createEmptyTaskResponseDetectorHook(ctx);
16733
17088
  updateTerminalTitle({ sessionId: "main" });
16734
17089
  const pluginConfig = loadPluginConfig(ctx.directory);
16735
17090
  let mainSessionID;
@@ -16744,8 +17099,7 @@ var OhMyOpenCodePlugin = async (ctx) => {
16744
17099
  ...agents
16745
17100
  };
16746
17101
  config3.tools = {
16747
- ...config3.tools,
16748
- grep: false
17102
+ ...config3.tools
16749
17103
  };
16750
17104
  config3.mcp = {
16751
17105
  ...config3.mcp,
@@ -16756,6 +17110,7 @@ var OhMyOpenCodePlugin = async (ctx) => {
16756
17110
  await todoContinuationEnforcer(input);
16757
17111
  await contextWindowMonitor.event(input);
16758
17112
  await pulseMonitor.event(input);
17113
+ await directoryAgentsInjector.event(input);
16759
17114
  const { event } = input;
16760
17115
  const props = event.properties;
16761
17116
  if (event.type === "session.created") {
@@ -16855,6 +17210,8 @@ var OhMyOpenCodePlugin = async (ctx) => {
16855
17210
  await grepOutputTruncator["tool.execute.after"](input, output);
16856
17211
  await contextWindowMonitor["tool.execute.after"](input, output);
16857
17212
  await commentChecker["tool.execute.after"](input, output);
17213
+ await directoryAgentsInjector["tool.execute.after"](input, output);
17214
+ await emptyTaskResponseDetector["tool.execute.after"](input, output);
16858
17215
  if (input.sessionID === mainSessionID) {
16859
17216
  updateTerminalTitle({
16860
17217
  sessionId: input.sessionID,