oh-my-opencode 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -633,6 +633,7 @@ function createTodoContinuationEnforcer(ctx) {
633
633
  const remindedSessions = new Set;
634
634
  const interruptedSessions = new Set;
635
635
  const errorSessions = new Set;
636
+ const pendingTimers = new Map;
636
637
  return async ({ event }) => {
637
638
  const props = event.properties;
638
639
  if (event.type === "session.error") {
@@ -642,6 +643,11 @@ function createTodoContinuationEnforcer(ctx) {
642
643
  if (detectInterrupt(props?.error)) {
643
644
  interruptedSessions.add(sessionID);
644
645
  }
646
+ const timer = pendingTimers.get(sessionID);
647
+ if (timer) {
648
+ clearTimeout(timer);
649
+ pendingTimers.delete(sessionID);
650
+ }
645
651
  }
646
652
  return;
647
653
  }
@@ -649,61 +655,73 @@ function createTodoContinuationEnforcer(ctx) {
649
655
  const sessionID = props?.sessionID;
650
656
  if (!sessionID)
651
657
  return;
652
- await new Promise((resolve) => setTimeout(resolve, 150));
653
- const shouldBypass = interruptedSessions.has(sessionID) || errorSessions.has(sessionID);
654
- interruptedSessions.delete(sessionID);
655
- errorSessions.delete(sessionID);
656
- if (shouldBypass) {
657
- return;
658
- }
659
- if (remindedSessions.has(sessionID)) {
660
- return;
661
- }
662
- let todos = [];
663
- try {
664
- const response = await ctx.client.session.todo({
665
- path: { id: sessionID }
666
- });
667
- todos = response.data ?? response;
668
- } catch {
669
- return;
670
- }
671
- if (!todos || todos.length === 0) {
672
- return;
673
- }
674
- const incomplete = todos.filter((t) => t.status !== "completed" && t.status !== "cancelled");
675
- if (incomplete.length === 0) {
676
- return;
677
- }
678
- remindedSessions.add(sessionID);
679
- if (interruptedSessions.has(sessionID) || errorSessions.has(sessionID)) {
680
- remindedSessions.delete(sessionID);
681
- return;
682
- }
683
- try {
684
- await ctx.client.session.prompt({
685
- path: { id: sessionID },
686
- body: {
687
- parts: [
688
- {
689
- type: "text",
690
- text: `${CONTINUATION_PROMPT}
658
+ const existingTimer = pendingTimers.get(sessionID);
659
+ if (existingTimer) {
660
+ clearTimeout(existingTimer);
661
+ }
662
+ const timer = setTimeout(async () => {
663
+ pendingTimers.delete(sessionID);
664
+ const shouldBypass = interruptedSessions.has(sessionID) || errorSessions.has(sessionID);
665
+ interruptedSessions.delete(sessionID);
666
+ errorSessions.delete(sessionID);
667
+ if (shouldBypass) {
668
+ return;
669
+ }
670
+ if (remindedSessions.has(sessionID)) {
671
+ return;
672
+ }
673
+ let todos = [];
674
+ try {
675
+ const response = await ctx.client.session.todo({
676
+ path: { id: sessionID }
677
+ });
678
+ todos = response.data ?? response;
679
+ } catch {
680
+ return;
681
+ }
682
+ if (!todos || todos.length === 0) {
683
+ return;
684
+ }
685
+ const incomplete = todos.filter((t) => t.status !== "completed" && t.status !== "cancelled");
686
+ if (incomplete.length === 0) {
687
+ return;
688
+ }
689
+ remindedSessions.add(sessionID);
690
+ if (interruptedSessions.has(sessionID) || errorSessions.has(sessionID)) {
691
+ remindedSessions.delete(sessionID);
692
+ return;
693
+ }
694
+ try {
695
+ await ctx.client.session.prompt({
696
+ path: { id: sessionID },
697
+ body: {
698
+ parts: [
699
+ {
700
+ type: "text",
701
+ text: `${CONTINUATION_PROMPT}
691
702
 
692
703
  [Status: ${todos.length - incomplete.length}/${todos.length} completed, ${incomplete.length} remaining]`
693
- }
694
- ]
695
- },
696
- query: { directory: ctx.directory }
697
- });
698
- } catch {
699
- remindedSessions.delete(sessionID);
700
- }
704
+ }
705
+ ]
706
+ },
707
+ query: { directory: ctx.directory }
708
+ });
709
+ } catch {
710
+ remindedSessions.delete(sessionID);
711
+ }
712
+ }, 200);
713
+ pendingTimers.set(sessionID, timer);
701
714
  }
702
715
  if (event.type === "message.updated") {
703
716
  const info = props?.info;
704
717
  const sessionID = info?.sessionID;
705
718
  if (sessionID && info?.role === "user") {
706
719
  remindedSessions.delete(sessionID);
720
+ const timer = pendingTimers.get(sessionID);
721
+ if (timer) {
722
+ clearTimeout(timer);
723
+ pendingTimers.delete(sessionID);
724
+ }
707
725
  }
708
726
  }
709
727
  if (event.type === "session.deleted") {
@@ -712,6 +730,11 @@ function createTodoContinuationEnforcer(ctx) {
712
730
  remindedSessions.delete(sessionInfo.id);
713
731
  interruptedSessions.delete(sessionInfo.id);
714
732
  errorSessions.delete(sessionInfo.id);
733
+ const timer = pendingTimers.get(sessionInfo.id);
734
+ if (timer) {
735
+ clearTimeout(timer);
736
+ pendingTimers.delete(sessionInfo.id);
737
+ }
715
738
  }
716
739
  }
717
740
  };
@@ -723,16 +746,8 @@ var CONTEXT_WARNING_THRESHOLD = 0.7;
723
746
  var CONTEXT_REMINDER = `[SYSTEM REMINDER - 1M Context Window]
724
747
 
725
748
  You are using Anthropic Claude with 1M context window.
726
- Current usage has exceeded 75%.
727
-
728
- RECOMMENDATIONS:
729
- - Consider compacting the session if available
730
- - Break complex tasks into smaller, focused sessions
731
- - Be concise in your responses
732
- - Avoid redundant file reads
733
-
734
- You have access to 1M tokens - use them wisely. Do NOT rush or skip tasks.
735
- Complete your work thoroughly despite the context usage warning.`;
749
+ You have plenty of context remaining - do NOT rush or skip tasks.
750
+ Complete your work thoroughly and methodically.`;
736
751
  function createContextWindowMonitorHook(ctx) {
737
752
  const remindedSessions = new Set;
738
753
  const toolExecuteAfter = async (input, output) => {
@@ -854,7 +869,13 @@ function readMessages(sessionID) {
854
869
  continue;
855
870
  }
856
871
  }
857
- return messages.sort((a, b) => a.id.localeCompare(b.id));
872
+ return messages.sort((a, b) => {
873
+ const aTime = a.time?.created ?? 0;
874
+ const bTime = b.time?.created ?? 0;
875
+ if (aTime !== bTime)
876
+ return aTime - bTime;
877
+ return a.id.localeCompare(b.id);
878
+ });
858
879
  }
859
880
  function readParts(messageID) {
860
881
  const partDir = join2(PART_STORAGE, messageID);
@@ -918,19 +939,26 @@ function injectTextPart(sessionID, messageID, text) {
918
939
  function findEmptyMessages(sessionID) {
919
940
  const messages = readMessages(sessionID);
920
941
  const emptyIds = [];
921
- for (let i = 0;i < messages.length; i++) {
922
- const msg = messages[i];
942
+ for (const msg of messages) {
923
943
  if (msg.role !== "assistant")
924
944
  continue;
925
- const isLastMessage = i === messages.length - 1;
926
- if (isLastMessage)
927
- continue;
928
945
  if (!messageHasContent(msg.id)) {
929
946
  emptyIds.push(msg.id);
930
947
  }
931
948
  }
932
949
  return emptyIds;
933
950
  }
951
+ function findEmptyMessageByIndex(sessionID, targetIndex) {
952
+ const messages = readMessages(sessionID);
953
+ if (targetIndex < 0 || targetIndex >= messages.length)
954
+ return null;
955
+ const targetMsg = messages[targetIndex];
956
+ if (targetMsg.role !== "assistant")
957
+ return null;
958
+ if (messageHasContent(targetMsg.id))
959
+ return null;
960
+ return targetMsg.id;
961
+ }
934
962
  function findMessagesWithThinkingBlocks(sessionID) {
935
963
  const messages = readMessages(sessionID);
936
964
  const result = [];
@@ -1025,6 +1053,11 @@ function getErrorMessage(error) {
1025
1053
  const errorObj = error;
1026
1054
  return (errorObj.data?.message || errorObj.error?.message || errorObj.message || "").toLowerCase();
1027
1055
  }
1056
+ function extractMessageIndex(error) {
1057
+ const message = getErrorMessage(error);
1058
+ const match = message.match(/messages\.(\d+)/);
1059
+ return match ? parseInt(match[1], 10) : null;
1060
+ }
1028
1061
  function detectErrorType(error) {
1029
1062
  const message = getErrorMessage(error);
1030
1063
  if (message.includes("tool_use") && message.includes("tool_result")) {
@@ -1091,14 +1124,21 @@ async function recoverThinkingDisabledViolation(_client, sessionID, _failedAssis
1091
1124
  }
1092
1125
  return anySuccess;
1093
1126
  }
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)");
1127
+ async function recoverEmptyContentMessage(_client, sessionID, failedAssistantMsg, _directory, error) {
1128
+ const targetIndex = extractMessageIndex(error);
1129
+ const failedID = failedAssistantMsg.info?.id;
1130
+ if (targetIndex !== null) {
1131
+ const targetMessageID = findEmptyMessageByIndex(sessionID, targetIndex);
1132
+ if (targetMessageID) {
1133
+ return injectTextPart(sessionID, targetMessageID, "(interrupted)");
1134
+ }
1135
+ }
1136
+ if (failedID) {
1137
+ if (injectTextPart(sessionID, failedID, "(interrupted)")) {
1138
+ return true;
1139
+ }
1101
1140
  }
1141
+ const emptyMessageIDs = findEmptyMessages(sessionID);
1102
1142
  let anySuccess = false;
1103
1143
  for (const messageID of emptyMessageIDs) {
1104
1144
  if (injectTextPart(sessionID, messageID, "(interrupted)")) {
@@ -1171,10 +1211,11 @@ function createSessionRecoveryHook(ctx) {
1171
1211
  } else if (errorType === "thinking_disabled_violation") {
1172
1212
  success = await recoverThinkingDisabledViolation(ctx.client, sessionID, failedMsg);
1173
1213
  } else if (errorType === "empty_content_message") {
1174
- success = await recoverEmptyContentMessage(ctx.client, sessionID, failedMsg, ctx.directory);
1214
+ success = await recoverEmptyContentMessage(ctx.client, sessionID, failedMsg, ctx.directory, info.error);
1175
1215
  }
1176
1216
  return success;
1177
- } catch {
1217
+ } catch (err) {
1218
+ console.error("[session-recovery] Recovery failed:", err);
1178
1219
  return false;
1179
1220
  } finally {
1180
1221
  processingErrors.delete(assistantMsgID);
@@ -1811,6 +1852,280 @@ function createEmptyTaskResponseDetectorHook(_ctx) {
1811
1852
  }
1812
1853
  };
1813
1854
  }
1855
+ // src/hooks/anthropic-auto-compact/parser.ts
1856
+ var TOKEN_LIMIT_PATTERNS = [
1857
+ /(\d+)\s*tokens?\s*>\s*(\d+)\s*maximum/i,
1858
+ /prompt.*?(\d+).*?tokens.*?exceeds.*?(\d+)/i,
1859
+ /(\d+).*?tokens.*?limit.*?(\d+)/i,
1860
+ /context.*?length.*?(\d+).*?maximum.*?(\d+)/i,
1861
+ /max.*?context.*?(\d+).*?but.*?(\d+)/i
1862
+ ];
1863
+ var TOKEN_LIMIT_KEYWORDS = [
1864
+ "prompt is too long",
1865
+ "is too long",
1866
+ "context_length_exceeded",
1867
+ "max_tokens",
1868
+ "token limit",
1869
+ "context length",
1870
+ "too many tokens"
1871
+ ];
1872
+ function extractTokensFromMessage(message) {
1873
+ for (const pattern of TOKEN_LIMIT_PATTERNS) {
1874
+ const match = message.match(pattern);
1875
+ if (match) {
1876
+ const num1 = parseInt(match[1], 10);
1877
+ const num2 = parseInt(match[2], 10);
1878
+ return num1 > num2 ? { current: num1, max: num2 } : { current: num2, max: num1 };
1879
+ }
1880
+ }
1881
+ return null;
1882
+ }
1883
+ function isTokenLimitError(text) {
1884
+ const lower = text.toLowerCase();
1885
+ return TOKEN_LIMIT_KEYWORDS.some((kw) => lower.includes(kw.toLowerCase()));
1886
+ }
1887
+ function parseAnthropicTokenLimitError(err) {
1888
+ if (typeof err === "string") {
1889
+ if (isTokenLimitError(err)) {
1890
+ const tokens = extractTokensFromMessage(err);
1891
+ return {
1892
+ currentTokens: tokens?.current ?? 0,
1893
+ maxTokens: tokens?.max ?? 0,
1894
+ errorType: "token_limit_exceeded_string"
1895
+ };
1896
+ }
1897
+ return null;
1898
+ }
1899
+ if (!err || typeof err !== "object")
1900
+ return null;
1901
+ const errObj = err;
1902
+ const dataObj = errObj.data;
1903
+ const responseBody = dataObj?.responseBody;
1904
+ const errorMessage = errObj.message;
1905
+ const errorData = errObj.error;
1906
+ const nestedError = errorData?.error;
1907
+ const textSources = [];
1908
+ if (typeof responseBody === "string")
1909
+ textSources.push(responseBody);
1910
+ if (typeof errorMessage === "string")
1911
+ textSources.push(errorMessage);
1912
+ if (typeof errorData?.message === "string")
1913
+ textSources.push(errorData.message);
1914
+ if (typeof errObj.body === "string")
1915
+ textSources.push(errObj.body);
1916
+ if (typeof errObj.details === "string")
1917
+ textSources.push(errObj.details);
1918
+ if (typeof errObj.reason === "string")
1919
+ textSources.push(errObj.reason);
1920
+ if (typeof errObj.description === "string")
1921
+ textSources.push(errObj.description);
1922
+ if (typeof nestedError?.message === "string")
1923
+ textSources.push(nestedError.message);
1924
+ if (typeof dataObj?.message === "string")
1925
+ textSources.push(dataObj.message);
1926
+ if (typeof dataObj?.error === "string")
1927
+ textSources.push(dataObj.error);
1928
+ if (textSources.length === 0) {
1929
+ try {
1930
+ const jsonStr = JSON.stringify(errObj);
1931
+ if (isTokenLimitError(jsonStr)) {
1932
+ textSources.push(jsonStr);
1933
+ }
1934
+ } catch {}
1935
+ }
1936
+ const combinedText = textSources.join(" ");
1937
+ if (!isTokenLimitError(combinedText))
1938
+ return null;
1939
+ if (typeof responseBody === "string") {
1940
+ try {
1941
+ const jsonPatterns = [
1942
+ /data:\s*(\{[\s\S]*?\})\s*$/m,
1943
+ /(\{"type"\s*:\s*"error"[\s\S]*?\})/,
1944
+ /(\{[\s\S]*?"error"[\s\S]*?\})/
1945
+ ];
1946
+ for (const pattern of jsonPatterns) {
1947
+ const dataMatch = responseBody.match(pattern);
1948
+ if (dataMatch) {
1949
+ try {
1950
+ const jsonData = JSON.parse(dataMatch[1]);
1951
+ const message = jsonData.error?.message || "";
1952
+ const tokens = extractTokensFromMessage(message);
1953
+ if (tokens) {
1954
+ return {
1955
+ currentTokens: tokens.current,
1956
+ maxTokens: tokens.max,
1957
+ requestId: jsonData.request_id,
1958
+ errorType: jsonData.error?.type || "token_limit_exceeded"
1959
+ };
1960
+ }
1961
+ } catch {}
1962
+ }
1963
+ }
1964
+ const bedrockJson = JSON.parse(responseBody);
1965
+ if (typeof bedrockJson.message === "string" && isTokenLimitError(bedrockJson.message)) {
1966
+ return {
1967
+ currentTokens: 0,
1968
+ maxTokens: 0,
1969
+ errorType: "bedrock_input_too_long"
1970
+ };
1971
+ }
1972
+ } catch {}
1973
+ }
1974
+ for (const text of textSources) {
1975
+ const tokens = extractTokensFromMessage(text);
1976
+ if (tokens) {
1977
+ return {
1978
+ currentTokens: tokens.current,
1979
+ maxTokens: tokens.max,
1980
+ errorType: "token_limit_exceeded"
1981
+ };
1982
+ }
1983
+ }
1984
+ if (isTokenLimitError(combinedText)) {
1985
+ return {
1986
+ currentTokens: 0,
1987
+ maxTokens: 0,
1988
+ errorType: "token_limit_exceeded_unknown"
1989
+ };
1990
+ }
1991
+ return null;
1992
+ }
1993
+
1994
+ // src/hooks/anthropic-auto-compact/executor.ts
1995
+ async function getLastAssistant(sessionID, client, directory) {
1996
+ try {
1997
+ const resp = await client.session.messages({
1998
+ path: { id: sessionID },
1999
+ query: { directory }
2000
+ });
2001
+ const data = resp.data;
2002
+ if (!Array.isArray(data))
2003
+ return null;
2004
+ const reversed = [...data].reverse();
2005
+ const last = reversed.find((m) => {
2006
+ const msg = m;
2007
+ const info = msg.info;
2008
+ return info?.role === "assistant";
2009
+ });
2010
+ if (!last)
2011
+ return null;
2012
+ return last.info ?? null;
2013
+ } catch {
2014
+ return null;
2015
+ }
2016
+ }
2017
+ async function executeCompact(sessionID, msg, autoCompactState, client, directory) {
2018
+ try {
2019
+ const providerID = msg.providerID;
2020
+ const modelID = msg.modelID;
2021
+ if (providerID && modelID) {
2022
+ await client.session.summarize({
2023
+ path: { id: sessionID },
2024
+ body: { providerID, modelID },
2025
+ query: { directory }
2026
+ });
2027
+ setTimeout(async () => {
2028
+ try {
2029
+ await client.tui.submitPrompt({ query: { directory } });
2030
+ } catch {}
2031
+ }, 500);
2032
+ }
2033
+ autoCompactState.pendingCompact.delete(sessionID);
2034
+ autoCompactState.errorDataBySession.delete(sessionID);
2035
+ } catch {}
2036
+ }
2037
+
2038
+ // src/hooks/anthropic-auto-compact/index.ts
2039
+ function createAutoCompactState() {
2040
+ return {
2041
+ pendingCompact: new Set,
2042
+ errorDataBySession: new Map
2043
+ };
2044
+ }
2045
+ function createAnthropicAutoCompactHook(ctx) {
2046
+ const autoCompactState = createAutoCompactState();
2047
+ const eventHandler = async ({ event }) => {
2048
+ const props = event.properties;
2049
+ if (event.type === "session.deleted") {
2050
+ const sessionInfo = props?.info;
2051
+ if (sessionInfo?.id) {
2052
+ autoCompactState.pendingCompact.delete(sessionInfo.id);
2053
+ autoCompactState.errorDataBySession.delete(sessionInfo.id);
2054
+ }
2055
+ return;
2056
+ }
2057
+ if (event.type === "session.error") {
2058
+ const sessionID = props?.sessionID;
2059
+ if (!sessionID)
2060
+ return;
2061
+ const parsed = parseAnthropicTokenLimitError(props?.error);
2062
+ if (parsed) {
2063
+ autoCompactState.pendingCompact.add(sessionID);
2064
+ autoCompactState.errorDataBySession.set(sessionID, parsed);
2065
+ }
2066
+ return;
2067
+ }
2068
+ if (event.type === "message.updated") {
2069
+ const info = props?.info;
2070
+ const sessionID = info?.sessionID;
2071
+ if (sessionID && info?.role === "assistant" && info.error) {
2072
+ const parsed = parseAnthropicTokenLimitError(info.error);
2073
+ if (parsed) {
2074
+ parsed.providerID = info.providerID;
2075
+ parsed.modelID = info.modelID;
2076
+ autoCompactState.pendingCompact.add(sessionID);
2077
+ autoCompactState.errorDataBySession.set(sessionID, parsed);
2078
+ }
2079
+ }
2080
+ return;
2081
+ }
2082
+ if (event.type === "session.idle") {
2083
+ const sessionID = props?.sessionID;
2084
+ if (!sessionID)
2085
+ return;
2086
+ if (!autoCompactState.pendingCompact.has(sessionID))
2087
+ return;
2088
+ const errorData = autoCompactState.errorDataBySession.get(sessionID);
2089
+ if (errorData?.providerID && errorData?.modelID) {
2090
+ await ctx.client.tui.showToast({
2091
+ body: {
2092
+ title: "Auto Compact",
2093
+ message: "Token limit exceeded. Summarizing session...",
2094
+ variant: "warning",
2095
+ duration: 3000
2096
+ }
2097
+ }).catch(() => {});
2098
+ await executeCompact(sessionID, { providerID: errorData.providerID, modelID: errorData.modelID }, autoCompactState, ctx.client, ctx.directory);
2099
+ return;
2100
+ }
2101
+ const lastAssistant = await getLastAssistant(sessionID, ctx.client, ctx.directory);
2102
+ if (!lastAssistant) {
2103
+ autoCompactState.pendingCompact.delete(sessionID);
2104
+ return;
2105
+ }
2106
+ if (lastAssistant.summary === true) {
2107
+ autoCompactState.pendingCompact.delete(sessionID);
2108
+ return;
2109
+ }
2110
+ if (!lastAssistant.modelID || !lastAssistant.providerID) {
2111
+ autoCompactState.pendingCompact.delete(sessionID);
2112
+ return;
2113
+ }
2114
+ await ctx.client.tui.showToast({
2115
+ body: {
2116
+ title: "Auto Compact",
2117
+ message: "Token limit exceeded. Summarizing session...",
2118
+ variant: "warning",
2119
+ duration: 3000
2120
+ }
2121
+ }).catch(() => {});
2122
+ await executeCompact(sessionID, lastAssistant, autoCompactState, ctx.client, ctx.directory);
2123
+ }
2124
+ };
2125
+ return {
2126
+ event: eventHandler
2127
+ };
2128
+ }
1814
2129
  // src/hooks/think-mode/detector.ts
1815
2130
  var ENGLISH_PATTERNS = [/\bultrathink\b/i, /\bthink\b/i];
1816
2131
  var MULTILINGUAL_KEYWORDS = [
@@ -1961,87 +2276,1393 @@ function createThinkModeHook() {
1961
2276
  thinkModeState.set(sessionID, state);
1962
2277
  return;
1963
2278
  }
1964
- state.providerID = currentModel.providerID;
1965
- state.modelID = currentModel.modelID;
1966
- if (isAlreadyHighVariant(currentModel.modelID)) {
1967
- thinkModeState.set(sessionID, state);
1968
- return;
2279
+ state.providerID = currentModel.providerID;
2280
+ state.modelID = currentModel.modelID;
2281
+ if (isAlreadyHighVariant(currentModel.modelID)) {
2282
+ thinkModeState.set(sessionID, state);
2283
+ return;
2284
+ }
2285
+ const highVariant = getHighVariant(currentModel.modelID);
2286
+ if (!highVariant) {
2287
+ thinkModeState.set(sessionID, state);
2288
+ return;
2289
+ }
2290
+ output.message.model = {
2291
+ providerID: currentModel.providerID,
2292
+ modelID: highVariant
2293
+ };
2294
+ state.modelSwitched = true;
2295
+ thinkModeState.set(sessionID, state);
2296
+ },
2297
+ event: async ({ event }) => {
2298
+ if (event.type === "session.deleted") {
2299
+ const props = event.properties;
2300
+ if (props?.info?.id) {
2301
+ thinkModeState.delete(props.info.id);
2302
+ }
2303
+ }
2304
+ }
2305
+ };
2306
+ }
2307
+ // src/hooks/claude-code-hooks/config.ts
2308
+ import { homedir as homedir2 } from "os";
2309
+ import { join as join8 } from "path";
2310
+ import { existsSync as existsSync7 } from "fs";
2311
+ function normalizeHookMatcher(raw) {
2312
+ return {
2313
+ matcher: raw.matcher ?? raw.pattern ?? "*",
2314
+ hooks: raw.hooks
2315
+ };
2316
+ }
2317
+ function normalizeHooksConfig(raw) {
2318
+ const result = {};
2319
+ const eventTypes = [
2320
+ "PreToolUse",
2321
+ "PostToolUse",
2322
+ "UserPromptSubmit",
2323
+ "Stop"
2324
+ ];
2325
+ for (const eventType of eventTypes) {
2326
+ if (raw[eventType]) {
2327
+ result[eventType] = raw[eventType].map(normalizeHookMatcher);
2328
+ }
2329
+ }
2330
+ return result;
2331
+ }
2332
+ function getClaudeSettingsPaths(customPath) {
2333
+ const home = homedir2();
2334
+ const paths = [
2335
+ join8(home, ".claude", "settings.json"),
2336
+ join8(process.cwd(), ".claude", "settings.json"),
2337
+ join8(process.cwd(), ".claude", "settings.local.json")
2338
+ ];
2339
+ if (customPath && existsSync7(customPath)) {
2340
+ paths.unshift(customPath);
2341
+ }
2342
+ return paths;
2343
+ }
2344
+ function mergeHooksConfig(base, override) {
2345
+ const result = { ...base };
2346
+ const eventTypes = [
2347
+ "PreToolUse",
2348
+ "PostToolUse",
2349
+ "UserPromptSubmit",
2350
+ "Stop"
2351
+ ];
2352
+ for (const eventType of eventTypes) {
2353
+ if (override[eventType]) {
2354
+ result[eventType] = [...base[eventType] || [], ...override[eventType]];
2355
+ }
2356
+ }
2357
+ return result;
2358
+ }
2359
+ async function loadClaudeHooksConfig(customSettingsPath) {
2360
+ const paths = getClaudeSettingsPaths(customSettingsPath);
2361
+ let mergedConfig = {};
2362
+ for (const settingsPath of paths) {
2363
+ if (existsSync7(settingsPath)) {
2364
+ try {
2365
+ const content = await Bun.file(settingsPath).text();
2366
+ const settings = JSON.parse(content);
2367
+ if (settings.hooks) {
2368
+ const normalizedHooks = normalizeHooksConfig(settings.hooks);
2369
+ mergedConfig = mergeHooksConfig(mergedConfig, normalizedHooks);
2370
+ }
2371
+ } catch {
2372
+ continue;
2373
+ }
2374
+ }
2375
+ }
2376
+ return Object.keys(mergedConfig).length > 0 ? mergedConfig : null;
2377
+ }
2378
+
2379
+ // src/hooks/claude-code-hooks/config-loader.ts
2380
+ import { existsSync as existsSync8 } from "fs";
2381
+ import { homedir as homedir3 } from "os";
2382
+ import { join as join10 } from "path";
2383
+
2384
+ // src/shared/logger.ts
2385
+ import * as fs3 from "fs";
2386
+ import * as os2 from "os";
2387
+ import * as path2 from "path";
2388
+ var logFile = path2.join(os2.tmpdir(), "oh-my-opencode.log");
2389
+ function log(message, data) {
2390
+ try {
2391
+ const timestamp = new Date().toISOString();
2392
+ const logEntry = `[${timestamp}] ${message} ${data ? JSON.stringify(data) : ""}
2393
+ `;
2394
+ fs3.appendFileSync(logFile, logEntry);
2395
+ } catch {}
2396
+ }
2397
+
2398
+ // src/hooks/claude-code-hooks/config-loader.ts
2399
+ var USER_CONFIG_PATH = join10(homedir3(), ".config", "opencode", "opencode-cc-plugin.json");
2400
+ function getProjectConfigPath() {
2401
+ return join10(process.cwd(), ".opencode", "opencode-cc-plugin.json");
2402
+ }
2403
+ async function loadConfigFromPath(path3) {
2404
+ if (!existsSync8(path3)) {
2405
+ return null;
2406
+ }
2407
+ try {
2408
+ const content = await Bun.file(path3).text();
2409
+ return JSON.parse(content);
2410
+ } catch (error) {
2411
+ log("Failed to load config", { path: path3, error });
2412
+ return null;
2413
+ }
2414
+ }
2415
+ function mergeDisabledHooks(base, override) {
2416
+ if (!override)
2417
+ return base ?? {};
2418
+ if (!base)
2419
+ return override;
2420
+ return {
2421
+ Stop: override.Stop ?? base.Stop,
2422
+ PreToolUse: override.PreToolUse ?? base.PreToolUse,
2423
+ PostToolUse: override.PostToolUse ?? base.PostToolUse,
2424
+ UserPromptSubmit: override.UserPromptSubmit ?? base.UserPromptSubmit
2425
+ };
2426
+ }
2427
+ async function loadPluginExtendedConfig() {
2428
+ const userConfig = await loadConfigFromPath(USER_CONFIG_PATH);
2429
+ const projectConfig = await loadConfigFromPath(getProjectConfigPath());
2430
+ const merged = {
2431
+ disabledHooks: mergeDisabledHooks(userConfig?.disabledHooks, projectConfig?.disabledHooks)
2432
+ };
2433
+ if (userConfig || projectConfig) {
2434
+ log("Plugin extended config loaded", {
2435
+ userConfigExists: userConfig !== null,
2436
+ projectConfigExists: projectConfig !== null,
2437
+ mergedDisabledHooks: merged.disabledHooks
2438
+ });
2439
+ }
2440
+ return merged;
2441
+ }
2442
+ var regexCache = new Map;
2443
+ function getRegex(pattern) {
2444
+ let regex = regexCache.get(pattern);
2445
+ if (!regex) {
2446
+ try {
2447
+ regex = new RegExp(pattern);
2448
+ regexCache.set(pattern, regex);
2449
+ } catch {
2450
+ regex = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
2451
+ regexCache.set(pattern, regex);
2452
+ }
2453
+ }
2454
+ return regex;
2455
+ }
2456
+ function isHookCommandDisabled(eventType, command, config) {
2457
+ if (!config?.disabledHooks)
2458
+ return false;
2459
+ const patterns = config.disabledHooks[eventType];
2460
+ if (!patterns || patterns.length === 0)
2461
+ return false;
2462
+ return patterns.some((pattern) => {
2463
+ const regex = getRegex(pattern);
2464
+ return regex.test(command);
2465
+ });
2466
+ }
2467
+
2468
+ // src/shared/frontmatter.ts
2469
+ function parseFrontmatter(content) {
2470
+ const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
2471
+ const match = content.match(frontmatterRegex);
2472
+ if (!match) {
2473
+ return { data: {}, body: content };
2474
+ }
2475
+ const yamlContent = match[1];
2476
+ const body = match[2];
2477
+ const data = {};
2478
+ for (const line of yamlContent.split(`
2479
+ `)) {
2480
+ const colonIndex = line.indexOf(":");
2481
+ if (colonIndex !== -1) {
2482
+ const key = line.slice(0, colonIndex).trim();
2483
+ let value = line.slice(colonIndex + 1).trim();
2484
+ if (value === "true")
2485
+ value = true;
2486
+ else if (value === "false")
2487
+ value = false;
2488
+ data[key] = value;
2489
+ }
2490
+ }
2491
+ return { data, body };
2492
+ }
2493
+ // src/shared/command-executor.ts
2494
+ import { spawn as spawn3 } from "child_process";
2495
+ import { exec } from "child_process";
2496
+ import { promisify } from "util";
2497
+ import { existsSync as existsSync9 } from "fs";
2498
+ var DEFAULT_ZSH_PATHS = ["/bin/zsh", "/usr/bin/zsh", "/usr/local/bin/zsh"];
2499
+ function findZshPath(customZshPath) {
2500
+ if (customZshPath && existsSync9(customZshPath)) {
2501
+ return customZshPath;
2502
+ }
2503
+ for (const path3 of DEFAULT_ZSH_PATHS) {
2504
+ if (existsSync9(path3)) {
2505
+ return path3;
2506
+ }
2507
+ }
2508
+ return null;
2509
+ }
2510
+ var execAsync = promisify(exec);
2511
+ async function executeHookCommand(command, stdin, cwd, options) {
2512
+ const home = process.env.HOME ?? "";
2513
+ let expandedCommand = command.replace(/^~(?=\/|$)/g, home).replace(/\s~(?=\/)/g, ` ${home}`).replace(/\$CLAUDE_PROJECT_DIR/g, cwd).replace(/\$\{CLAUDE_PROJECT_DIR\}/g, cwd);
2514
+ let finalCommand = expandedCommand;
2515
+ if (options?.forceZsh) {
2516
+ const zshPath = options.zshPath || findZshPath();
2517
+ if (zshPath) {
2518
+ const escapedCommand = expandedCommand.replace(/'/g, "'\\''");
2519
+ finalCommand = `${zshPath} -lc '${escapedCommand}'`;
2520
+ }
2521
+ }
2522
+ return new Promise((resolve2) => {
2523
+ const proc = spawn3(finalCommand, {
2524
+ cwd,
2525
+ shell: true,
2526
+ env: { ...process.env, HOME: home, CLAUDE_PROJECT_DIR: cwd }
2527
+ });
2528
+ let stdout = "";
2529
+ let stderr = "";
2530
+ proc.stdout?.on("data", (data) => {
2531
+ stdout += data.toString();
2532
+ });
2533
+ proc.stderr?.on("data", (data) => {
2534
+ stderr += data.toString();
2535
+ });
2536
+ proc.stdin?.write(stdin);
2537
+ proc.stdin?.end();
2538
+ proc.on("close", (code) => {
2539
+ resolve2({
2540
+ exitCode: code ?? 0,
2541
+ stdout: stdout.trim(),
2542
+ stderr: stderr.trim()
2543
+ });
2544
+ });
2545
+ proc.on("error", (err) => {
2546
+ resolve2({
2547
+ exitCode: 1,
2548
+ stderr: err.message
2549
+ });
2550
+ });
2551
+ });
2552
+ }
2553
+ async function executeCommand(command) {
2554
+ try {
2555
+ const { stdout, stderr } = await execAsync(command);
2556
+ const out = stdout?.toString().trim() ?? "";
2557
+ const err = stderr?.toString().trim() ?? "";
2558
+ if (err) {
2559
+ if (out) {
2560
+ return `${out}
2561
+ [stderr: ${err}]`;
2562
+ }
2563
+ return `[stderr: ${err}]`;
2564
+ }
2565
+ return out;
2566
+ } catch (error) {
2567
+ const e = error;
2568
+ const stdout = e?.stdout?.toString().trim() ?? "";
2569
+ const stderr = e?.stderr?.toString().trim() ?? "";
2570
+ const errMsg = stderr || e?.message || String(error);
2571
+ if (stdout) {
2572
+ return `${stdout}
2573
+ [stderr: ${errMsg}]`;
2574
+ }
2575
+ return `[stderr: ${errMsg}]`;
2576
+ }
2577
+ }
2578
+ var COMMAND_PATTERN = /!`([^`]+)`/g;
2579
+ function findCommands(text) {
2580
+ const matches = [];
2581
+ let match;
2582
+ COMMAND_PATTERN.lastIndex = 0;
2583
+ while ((match = COMMAND_PATTERN.exec(text)) !== null) {
2584
+ matches.push({
2585
+ fullMatch: match[0],
2586
+ command: match[1],
2587
+ start: match.index,
2588
+ end: match.index + match[0].length
2589
+ });
2590
+ }
2591
+ return matches;
2592
+ }
2593
+ async function resolveCommandsInText(text, depth = 0, maxDepth = 3) {
2594
+ if (depth >= maxDepth) {
2595
+ return text;
2596
+ }
2597
+ const matches = findCommands(text);
2598
+ if (matches.length === 0) {
2599
+ return text;
2600
+ }
2601
+ const tasks = matches.map((m) => executeCommand(m.command));
2602
+ const results = await Promise.allSettled(tasks);
2603
+ const replacements = new Map;
2604
+ matches.forEach((match, idx) => {
2605
+ const result = results[idx];
2606
+ if (result.status === "rejected") {
2607
+ replacements.set(match.fullMatch, `[error: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}]`);
2608
+ } else {
2609
+ replacements.set(match.fullMatch, result.value);
2610
+ }
2611
+ });
2612
+ let resolved = text;
2613
+ for (const [pattern, replacement] of replacements.entries()) {
2614
+ resolved = resolved.split(pattern).join(replacement);
2615
+ }
2616
+ if (findCommands(resolved).length > 0) {
2617
+ return resolveCommandsInText(resolved, depth + 1, maxDepth);
2618
+ }
2619
+ return resolved;
2620
+ }
2621
+ // src/shared/file-reference-resolver.ts
2622
+ import { existsSync as existsSync10, readFileSync as readFileSync4, statSync } from "fs";
2623
+ import { join as join11, isAbsolute } from "path";
2624
+ var FILE_REFERENCE_PATTERN = /@([^\s@]+)/g;
2625
+ function findFileReferences(text) {
2626
+ const matches = [];
2627
+ let match;
2628
+ FILE_REFERENCE_PATTERN.lastIndex = 0;
2629
+ while ((match = FILE_REFERENCE_PATTERN.exec(text)) !== null) {
2630
+ matches.push({
2631
+ fullMatch: match[0],
2632
+ filePath: match[1],
2633
+ start: match.index,
2634
+ end: match.index + match[0].length
2635
+ });
2636
+ }
2637
+ return matches;
2638
+ }
2639
+ function resolveFilePath(filePath, cwd) {
2640
+ if (isAbsolute(filePath)) {
2641
+ return filePath;
2642
+ }
2643
+ return join11(cwd, filePath);
2644
+ }
2645
+ function readFileContent(resolvedPath) {
2646
+ if (!existsSync10(resolvedPath)) {
2647
+ return `[file not found: ${resolvedPath}]`;
2648
+ }
2649
+ const stat = statSync(resolvedPath);
2650
+ if (stat.isDirectory()) {
2651
+ return `[cannot read directory: ${resolvedPath}]`;
2652
+ }
2653
+ const content = readFileSync4(resolvedPath, "utf-8");
2654
+ return content;
2655
+ }
2656
+ async function resolveFileReferencesInText(text, cwd = process.cwd(), depth = 0, maxDepth = 3) {
2657
+ if (depth >= maxDepth) {
2658
+ return text;
2659
+ }
2660
+ const matches = findFileReferences(text);
2661
+ if (matches.length === 0) {
2662
+ return text;
2663
+ }
2664
+ const replacements = new Map;
2665
+ for (const match of matches) {
2666
+ const resolvedPath = resolveFilePath(match.filePath, cwd);
2667
+ const content = readFileContent(resolvedPath);
2668
+ replacements.set(match.fullMatch, content);
2669
+ }
2670
+ let resolved = text;
2671
+ for (const [pattern, replacement] of replacements.entries()) {
2672
+ resolved = resolved.split(pattern).join(replacement);
2673
+ }
2674
+ if (findFileReferences(resolved).length > 0 && depth + 1 < maxDepth) {
2675
+ return resolveFileReferencesInText(resolved, cwd, depth + 1, maxDepth);
2676
+ }
2677
+ return resolved;
2678
+ }
2679
+ // src/shared/model-sanitizer.ts
2680
+ function sanitizeModelField(_model) {
2681
+ return;
2682
+ }
2683
+ // src/shared/snake-case.ts
2684
+ function camelToSnake(str) {
2685
+ return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
2686
+ }
2687
+ function isPlainObject(value) {
2688
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2689
+ }
2690
+ function objectToSnakeCase(obj, deep = true) {
2691
+ const result = {};
2692
+ for (const [key, value] of Object.entries(obj)) {
2693
+ const snakeKey = camelToSnake(key);
2694
+ if (deep && isPlainObject(value)) {
2695
+ result[snakeKey] = objectToSnakeCase(value, true);
2696
+ } else if (deep && Array.isArray(value)) {
2697
+ result[snakeKey] = value.map((item) => isPlainObject(item) ? objectToSnakeCase(item, true) : item);
2698
+ } else {
2699
+ result[snakeKey] = value;
2700
+ }
2701
+ }
2702
+ return result;
2703
+ }
2704
+ // src/shared/tool-name.ts
2705
+ var SPECIAL_TOOL_MAPPINGS = {
2706
+ webfetch: "WebFetch",
2707
+ websearch: "WebSearch",
2708
+ todoread: "TodoRead",
2709
+ todowrite: "TodoWrite"
2710
+ };
2711
+ function toPascalCase(str) {
2712
+ return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
2713
+ }
2714
+ function transformToolName(toolName) {
2715
+ const lower = toolName.toLowerCase();
2716
+ if (lower in SPECIAL_TOOL_MAPPINGS) {
2717
+ return SPECIAL_TOOL_MAPPINGS[lower];
2718
+ }
2719
+ if (toolName.includes("-") || toolName.includes("_")) {
2720
+ return toPascalCase(toolName);
2721
+ }
2722
+ return toolName.charAt(0).toUpperCase() + toolName.slice(1);
2723
+ }
2724
+ // src/shared/pattern-matcher.ts
2725
+ function matchesToolMatcher(toolName, matcher) {
2726
+ if (!matcher) {
2727
+ return true;
2728
+ }
2729
+ const patterns = matcher.split("|").map((p) => p.trim());
2730
+ return patterns.some((p) => {
2731
+ if (p.includes("*")) {
2732
+ const regex = new RegExp(`^${p.replace(/\*/g, ".*")}$`, "i");
2733
+ return regex.test(toolName);
2734
+ }
2735
+ return p.toLowerCase() === toolName.toLowerCase();
2736
+ });
2737
+ }
2738
+ function findMatchingHooks(config, eventName, toolName) {
2739
+ const hookMatchers = config[eventName];
2740
+ if (!hookMatchers)
2741
+ return [];
2742
+ return hookMatchers.filter((hookMatcher) => {
2743
+ if (!toolName)
2744
+ return true;
2745
+ return matchesToolMatcher(toolName, hookMatcher.matcher);
2746
+ });
2747
+ }
2748
+ // src/shared/hook-disabled.ts
2749
+ function isHookDisabled(config, hookType) {
2750
+ const { disabledHooks } = config;
2751
+ if (disabledHooks === undefined) {
2752
+ return false;
2753
+ }
2754
+ if (disabledHooks === true) {
2755
+ return true;
2756
+ }
2757
+ if (Array.isArray(disabledHooks)) {
2758
+ return disabledHooks.includes(hookType);
2759
+ }
2760
+ return false;
2761
+ }
2762
+ // src/hooks/claude-code-hooks/plugin-config.ts
2763
+ var DEFAULT_CONFIG = {
2764
+ forceZsh: true,
2765
+ zshPath: "/bin/zsh"
2766
+ };
2767
+
2768
+ // src/hooks/claude-code-hooks/pre-tool-use.ts
2769
+ function buildInputLines(toolInput) {
2770
+ return Object.entries(toolInput).slice(0, 3).map(([key, val]) => {
2771
+ const valStr = String(val).slice(0, 40);
2772
+ return ` ${key}: ${valStr}${String(val).length > 40 ? "..." : ""}`;
2773
+ }).join(`
2774
+ `);
2775
+ }
2776
+ async function executePreToolUseHooks(ctx, config, extendedConfig) {
2777
+ if (!config) {
2778
+ return { decision: "allow" };
2779
+ }
2780
+ const transformedToolName = transformToolName(ctx.toolName);
2781
+ const matchers = findMatchingHooks(config, "PreToolUse", transformedToolName);
2782
+ if (matchers.length === 0) {
2783
+ return { decision: "allow" };
2784
+ }
2785
+ const stdinData = {
2786
+ session_id: ctx.sessionId,
2787
+ transcript_path: ctx.transcriptPath,
2788
+ cwd: ctx.cwd,
2789
+ permission_mode: ctx.permissionMode ?? "bypassPermissions",
2790
+ hook_event_name: "PreToolUse",
2791
+ tool_name: transformedToolName,
2792
+ tool_input: objectToSnakeCase(ctx.toolInput),
2793
+ tool_use_id: ctx.toolUseId,
2794
+ hook_source: "opencode-plugin"
2795
+ };
2796
+ const startTime = Date.now();
2797
+ let firstHookName;
2798
+ const inputLines = buildInputLines(ctx.toolInput);
2799
+ for (const matcher of matchers) {
2800
+ for (const hook of matcher.hooks) {
2801
+ if (hook.type !== "command")
2802
+ continue;
2803
+ if (isHookCommandDisabled("PreToolUse", hook.command, extendedConfig ?? null)) {
2804
+ log("PreToolUse hook command skipped (disabled by config)", { command: hook.command, toolName: ctx.toolName });
2805
+ continue;
2806
+ }
2807
+ const hookName = hook.command.split("/").pop() || hook.command;
2808
+ if (!firstHookName)
2809
+ firstHookName = hookName;
2810
+ const result = await executeHookCommand(hook.command, JSON.stringify(stdinData), ctx.cwd, { forceZsh: DEFAULT_CONFIG.forceZsh, zshPath: DEFAULT_CONFIG.zshPath });
2811
+ if (result.exitCode === 2) {
2812
+ return {
2813
+ decision: "deny",
2814
+ reason: result.stderr || result.stdout || "Hook blocked the operation",
2815
+ elapsedMs: Date.now() - startTime,
2816
+ hookName: firstHookName,
2817
+ toolName: transformedToolName,
2818
+ inputLines
2819
+ };
2820
+ }
2821
+ if (result.exitCode === 1) {
2822
+ return {
2823
+ decision: "ask",
2824
+ reason: result.stderr || result.stdout,
2825
+ elapsedMs: Date.now() - startTime,
2826
+ hookName: firstHookName,
2827
+ toolName: transformedToolName,
2828
+ inputLines
2829
+ };
2830
+ }
2831
+ if (result.stdout) {
2832
+ try {
2833
+ const output = JSON.parse(result.stdout);
2834
+ let decision;
2835
+ let reason;
2836
+ let modifiedInput;
2837
+ if (output.hookSpecificOutput?.permissionDecision) {
2838
+ decision = output.hookSpecificOutput.permissionDecision;
2839
+ reason = output.hookSpecificOutput.permissionDecisionReason;
2840
+ modifiedInput = output.hookSpecificOutput.updatedInput;
2841
+ } else if (output.decision) {
2842
+ const legacyDecision = output.decision;
2843
+ if (legacyDecision === "approve" || legacyDecision === "allow") {
2844
+ decision = "allow";
2845
+ } else if (legacyDecision === "block" || legacyDecision === "deny") {
2846
+ decision = "deny";
2847
+ } else if (legacyDecision === "ask") {
2848
+ decision = "ask";
2849
+ }
2850
+ reason = output.reason;
2851
+ }
2852
+ const hasCommonFields = output.continue !== undefined || output.stopReason !== undefined || output.suppressOutput !== undefined || output.systemMessage !== undefined;
2853
+ if (decision || hasCommonFields) {
2854
+ return {
2855
+ decision: decision ?? "allow",
2856
+ reason,
2857
+ modifiedInput,
2858
+ elapsedMs: Date.now() - startTime,
2859
+ hookName: firstHookName,
2860
+ toolName: transformedToolName,
2861
+ inputLines,
2862
+ continue: output.continue,
2863
+ stopReason: output.stopReason,
2864
+ suppressOutput: output.suppressOutput,
2865
+ systemMessage: output.systemMessage
2866
+ };
2867
+ }
2868
+ } catch {}
2869
+ }
2870
+ }
2871
+ }
2872
+ return { decision: "allow" };
2873
+ }
2874
+
2875
+ // src/hooks/claude-code-hooks/transcript.ts
2876
+ import { join as join12 } from "path";
2877
+ import { mkdirSync as mkdirSync4, appendFileSync as appendFileSync5, existsSync as existsSync11, writeFileSync as writeFileSync3, unlinkSync as unlinkSync4 } from "fs";
2878
+ import { homedir as homedir4, tmpdir as tmpdir2 } from "os";
2879
+ import { randomUUID } from "crypto";
2880
+ var TRANSCRIPT_DIR = join12(homedir4(), ".claude", "transcripts");
2881
+ function getTranscriptPath(sessionId) {
2882
+ return join12(TRANSCRIPT_DIR, `${sessionId}.jsonl`);
2883
+ }
2884
+ function ensureTranscriptDir() {
2885
+ if (!existsSync11(TRANSCRIPT_DIR)) {
2886
+ mkdirSync4(TRANSCRIPT_DIR, { recursive: true });
2887
+ }
2888
+ }
2889
+ function appendTranscriptEntry(sessionId, entry) {
2890
+ ensureTranscriptDir();
2891
+ const path3 = getTranscriptPath(sessionId);
2892
+ const line = JSON.stringify(entry) + `
2893
+ `;
2894
+ appendFileSync5(path3, line);
2895
+ }
2896
+ function recordToolUse(sessionId, toolName, toolInput) {
2897
+ appendTranscriptEntry(sessionId, {
2898
+ type: "tool_use",
2899
+ timestamp: new Date().toISOString(),
2900
+ tool_name: toolName,
2901
+ tool_input: toolInput
2902
+ });
2903
+ }
2904
+ function recordToolResult(sessionId, toolName, toolInput, toolOutput) {
2905
+ appendTranscriptEntry(sessionId, {
2906
+ type: "tool_result",
2907
+ timestamp: new Date().toISOString(),
2908
+ tool_name: toolName,
2909
+ tool_input: toolInput,
2910
+ tool_output: toolOutput
2911
+ });
2912
+ }
2913
+ function recordUserMessage(sessionId, content) {
2914
+ appendTranscriptEntry(sessionId, {
2915
+ type: "user",
2916
+ timestamp: new Date().toISOString(),
2917
+ content
2918
+ });
2919
+ }
2920
+ async function buildTranscriptFromSession(client, sessionId, directory, currentToolName, currentToolInput) {
2921
+ try {
2922
+ const response = await client.session.messages({
2923
+ path: { id: sessionId },
2924
+ query: { directory }
2925
+ });
2926
+ const messages = response["200"] ?? response.data ?? (Array.isArray(response) ? response : []);
2927
+ const entries = [];
2928
+ if (Array.isArray(messages)) {
2929
+ for (const msg of messages) {
2930
+ if (msg.info?.role !== "assistant")
2931
+ continue;
2932
+ for (const part of msg.parts || []) {
2933
+ if (part.type !== "tool")
2934
+ continue;
2935
+ if (part.state?.status !== "completed")
2936
+ continue;
2937
+ if (!part.state?.input)
2938
+ continue;
2939
+ const rawToolName = part.tool;
2940
+ const toolName = transformToolName(rawToolName);
2941
+ const entry = {
2942
+ type: "assistant",
2943
+ message: {
2944
+ role: "assistant",
2945
+ content: [
2946
+ {
2947
+ type: "tool_use",
2948
+ name: toolName,
2949
+ input: part.state.input
2950
+ }
2951
+ ]
2952
+ }
2953
+ };
2954
+ entries.push(JSON.stringify(entry));
2955
+ }
2956
+ }
2957
+ }
2958
+ const currentEntry = {
2959
+ type: "assistant",
2960
+ message: {
2961
+ role: "assistant",
2962
+ content: [
2963
+ {
2964
+ type: "tool_use",
2965
+ name: transformToolName(currentToolName),
2966
+ input: currentToolInput
2967
+ }
2968
+ ]
2969
+ }
2970
+ };
2971
+ entries.push(JSON.stringify(currentEntry));
2972
+ const tempPath = join12(tmpdir2(), `opencode-transcript-${sessionId}-${randomUUID()}.jsonl`);
2973
+ writeFileSync3(tempPath, entries.join(`
2974
+ `) + `
2975
+ `);
2976
+ return tempPath;
2977
+ } catch {
2978
+ try {
2979
+ const currentEntry = {
2980
+ type: "assistant",
2981
+ message: {
2982
+ role: "assistant",
2983
+ content: [
2984
+ {
2985
+ type: "tool_use",
2986
+ name: transformToolName(currentToolName),
2987
+ input: currentToolInput
2988
+ }
2989
+ ]
2990
+ }
2991
+ };
2992
+ const tempPath = join12(tmpdir2(), `opencode-transcript-${sessionId}-${randomUUID()}.jsonl`);
2993
+ writeFileSync3(tempPath, JSON.stringify(currentEntry) + `
2994
+ `);
2995
+ return tempPath;
2996
+ } catch {
2997
+ return null;
2998
+ }
2999
+ }
3000
+ }
3001
+ function deleteTempTranscript(path3) {
3002
+ if (!path3)
3003
+ return;
3004
+ try {
3005
+ unlinkSync4(path3);
3006
+ } catch {}
3007
+ }
3008
+
3009
+ // src/hooks/claude-code-hooks/post-tool-use.ts
3010
+ async function executePostToolUseHooks(ctx, config, extendedConfig) {
3011
+ if (!config) {
3012
+ return { block: false };
3013
+ }
3014
+ const transformedToolName = transformToolName(ctx.toolName);
3015
+ const matchers = findMatchingHooks(config, "PostToolUse", transformedToolName);
3016
+ if (matchers.length === 0) {
3017
+ return { block: false };
3018
+ }
3019
+ let tempTranscriptPath = null;
3020
+ try {
3021
+ if (ctx.client) {
3022
+ tempTranscriptPath = await buildTranscriptFromSession(ctx.client, ctx.sessionId, ctx.cwd, ctx.toolName, ctx.toolInput);
3023
+ }
3024
+ const stdinData = {
3025
+ session_id: ctx.sessionId,
3026
+ transcript_path: tempTranscriptPath ?? ctx.transcriptPath,
3027
+ cwd: ctx.cwd,
3028
+ permission_mode: ctx.permissionMode ?? "bypassPermissions",
3029
+ hook_event_name: "PostToolUse",
3030
+ tool_name: transformedToolName,
3031
+ tool_input: objectToSnakeCase(ctx.toolInput),
3032
+ tool_response: objectToSnakeCase(ctx.toolOutput),
3033
+ tool_use_id: ctx.toolUseId,
3034
+ hook_source: "opencode-plugin"
3035
+ };
3036
+ const messages = [];
3037
+ const warnings = [];
3038
+ let firstHookName;
3039
+ const startTime = Date.now();
3040
+ for (const matcher of matchers) {
3041
+ for (const hook of matcher.hooks) {
3042
+ if (hook.type !== "command")
3043
+ continue;
3044
+ if (isHookCommandDisabled("PostToolUse", hook.command, extendedConfig ?? null)) {
3045
+ log("PostToolUse hook command skipped (disabled by config)", { command: hook.command, toolName: ctx.toolName });
3046
+ continue;
3047
+ }
3048
+ const hookName = hook.command.split("/").pop() || hook.command;
3049
+ if (!firstHookName)
3050
+ firstHookName = hookName;
3051
+ const result = await executeHookCommand(hook.command, JSON.stringify(stdinData), ctx.cwd, { forceZsh: DEFAULT_CONFIG.forceZsh, zshPath: DEFAULT_CONFIG.zshPath });
3052
+ if (result.stdout) {
3053
+ messages.push(result.stdout);
3054
+ }
3055
+ if (result.exitCode === 2) {
3056
+ if (result.stderr) {
3057
+ warnings.push(`[${hookName}]
3058
+ ${result.stderr.trim()}`);
3059
+ }
3060
+ continue;
3061
+ }
3062
+ if (result.exitCode === 0 && result.stdout) {
3063
+ try {
3064
+ const output = JSON.parse(result.stdout);
3065
+ if (output.decision === "block") {
3066
+ return {
3067
+ block: true,
3068
+ reason: output.reason || result.stderr,
3069
+ message: messages.join(`
3070
+ `),
3071
+ warnings: warnings.length > 0 ? warnings : undefined,
3072
+ elapsedMs: Date.now() - startTime,
3073
+ hookName: firstHookName,
3074
+ toolName: transformedToolName,
3075
+ additionalContext: output.hookSpecificOutput?.additionalContext,
3076
+ continue: output.continue,
3077
+ stopReason: output.stopReason,
3078
+ suppressOutput: output.suppressOutput,
3079
+ systemMessage: output.systemMessage
3080
+ };
3081
+ }
3082
+ if (output.hookSpecificOutput?.additionalContext || output.continue !== undefined || output.systemMessage || output.suppressOutput === true || output.stopReason !== undefined) {
3083
+ return {
3084
+ block: false,
3085
+ message: messages.join(`
3086
+ `),
3087
+ warnings: warnings.length > 0 ? warnings : undefined,
3088
+ elapsedMs: Date.now() - startTime,
3089
+ hookName: firstHookName,
3090
+ toolName: transformedToolName,
3091
+ additionalContext: output.hookSpecificOutput?.additionalContext,
3092
+ continue: output.continue,
3093
+ stopReason: output.stopReason,
3094
+ suppressOutput: output.suppressOutput,
3095
+ systemMessage: output.systemMessage
3096
+ };
3097
+ }
3098
+ } catch {}
3099
+ } else if (result.exitCode !== 0 && result.exitCode !== 2) {
3100
+ try {
3101
+ const output = JSON.parse(result.stdout || "{}");
3102
+ if (output.decision === "block") {
3103
+ return {
3104
+ block: true,
3105
+ reason: output.reason || result.stderr,
3106
+ message: messages.join(`
3107
+ `),
3108
+ warnings: warnings.length > 0 ? warnings : undefined,
3109
+ elapsedMs: Date.now() - startTime,
3110
+ hookName: firstHookName,
3111
+ toolName: transformedToolName,
3112
+ additionalContext: output.hookSpecificOutput?.additionalContext,
3113
+ continue: output.continue,
3114
+ stopReason: output.stopReason,
3115
+ suppressOutput: output.suppressOutput,
3116
+ systemMessage: output.systemMessage
3117
+ };
3118
+ }
3119
+ } catch {}
3120
+ }
3121
+ }
3122
+ }
3123
+ const elapsedMs = Date.now() - startTime;
3124
+ return {
3125
+ block: false,
3126
+ message: messages.length > 0 ? messages.join(`
3127
+ `) : undefined,
3128
+ warnings: warnings.length > 0 ? warnings : undefined,
3129
+ elapsedMs,
3130
+ hookName: firstHookName,
3131
+ toolName: transformedToolName
3132
+ };
3133
+ } finally {
3134
+ deleteTempTranscript(tempTranscriptPath);
3135
+ }
3136
+ }
3137
+
3138
+ // src/hooks/claude-code-hooks/user-prompt-submit.ts
3139
+ var USER_PROMPT_SUBMIT_TAG_OPEN = "<user-prompt-submit-hook>";
3140
+ var USER_PROMPT_SUBMIT_TAG_CLOSE = "</user-prompt-submit-hook>";
3141
+ async function executeUserPromptSubmitHooks(ctx, config, extendedConfig) {
3142
+ const modifiedParts = ctx.parts;
3143
+ const messages = [];
3144
+ if (ctx.parentSessionId) {
3145
+ return { block: false, modifiedParts, messages };
3146
+ }
3147
+ if (ctx.prompt.includes(USER_PROMPT_SUBMIT_TAG_OPEN) && ctx.prompt.includes(USER_PROMPT_SUBMIT_TAG_CLOSE)) {
3148
+ return { block: false, modifiedParts, messages };
3149
+ }
3150
+ if (!config) {
3151
+ return { block: false, modifiedParts, messages };
3152
+ }
3153
+ const matchers = findMatchingHooks(config, "UserPromptSubmit");
3154
+ if (matchers.length === 0) {
3155
+ return { block: false, modifiedParts, messages };
3156
+ }
3157
+ const stdinData = {
3158
+ session_id: ctx.sessionId,
3159
+ cwd: ctx.cwd,
3160
+ permission_mode: ctx.permissionMode ?? "bypassPermissions",
3161
+ hook_event_name: "UserPromptSubmit",
3162
+ prompt: ctx.prompt,
3163
+ session: { id: ctx.sessionId },
3164
+ hook_source: "opencode-plugin"
3165
+ };
3166
+ for (const matcher of matchers) {
3167
+ for (const hook of matcher.hooks) {
3168
+ if (hook.type !== "command")
3169
+ continue;
3170
+ if (isHookCommandDisabled("UserPromptSubmit", hook.command, extendedConfig ?? null)) {
3171
+ log("UserPromptSubmit hook command skipped (disabled by config)", { command: hook.command });
3172
+ continue;
3173
+ }
3174
+ const result = await executeHookCommand(hook.command, JSON.stringify(stdinData), ctx.cwd, { forceZsh: DEFAULT_CONFIG.forceZsh, zshPath: DEFAULT_CONFIG.zshPath });
3175
+ if (result.stdout) {
3176
+ const output = result.stdout.trim();
3177
+ if (output.startsWith(USER_PROMPT_SUBMIT_TAG_OPEN)) {
3178
+ messages.push(output);
3179
+ } else {
3180
+ messages.push(`${USER_PROMPT_SUBMIT_TAG_OPEN}
3181
+ ${output}
3182
+ ${USER_PROMPT_SUBMIT_TAG_CLOSE}`);
3183
+ }
3184
+ }
3185
+ if (result.exitCode !== 0) {
3186
+ try {
3187
+ const output = JSON.parse(result.stdout || "{}");
3188
+ if (output.decision === "block") {
3189
+ return {
3190
+ block: true,
3191
+ reason: output.reason || result.stderr,
3192
+ modifiedParts,
3193
+ messages
3194
+ };
3195
+ }
3196
+ } catch {}
3197
+ }
3198
+ }
3199
+ }
3200
+ return { block: false, modifiedParts, messages };
3201
+ }
3202
+
3203
+ // src/hooks/claude-code-hooks/todo.ts
3204
+ import { join as join13 } from "path";
3205
+ import { homedir as homedir5 } from "os";
3206
+ var TODO_DIR = join13(homedir5(), ".claude", "todos");
3207
+ function getTodoPath(sessionId) {
3208
+ return join13(TODO_DIR, `${sessionId}-agent-${sessionId}.json`);
3209
+ }
3210
+
3211
+ // src/hooks/claude-code-hooks/stop.ts
3212
+ var stopHookActiveState = new Map;
3213
+ async function executeStopHooks(ctx, config, extendedConfig) {
3214
+ if (ctx.parentSessionId) {
3215
+ return { block: false };
3216
+ }
3217
+ if (!config) {
3218
+ return { block: false };
3219
+ }
3220
+ const matchers = findMatchingHooks(config, "Stop");
3221
+ if (matchers.length === 0) {
3222
+ return { block: false };
3223
+ }
3224
+ const stdinData = {
3225
+ session_id: ctx.sessionId,
3226
+ transcript_path: ctx.transcriptPath,
3227
+ cwd: ctx.cwd,
3228
+ permission_mode: ctx.permissionMode ?? "bypassPermissions",
3229
+ hook_event_name: "Stop",
3230
+ stop_hook_active: stopHookActiveState.get(ctx.sessionId) ?? false,
3231
+ todo_path: getTodoPath(ctx.sessionId),
3232
+ hook_source: "opencode-plugin"
3233
+ };
3234
+ for (const matcher of matchers) {
3235
+ for (const hook of matcher.hooks) {
3236
+ if (hook.type !== "command")
3237
+ continue;
3238
+ if (isHookCommandDisabled("Stop", hook.command, extendedConfig ?? null)) {
3239
+ log("Stop hook command skipped (disabled by config)", { command: hook.command });
3240
+ continue;
3241
+ }
3242
+ const result = await executeHookCommand(hook.command, JSON.stringify(stdinData), ctx.cwd, { forceZsh: DEFAULT_CONFIG.forceZsh, zshPath: DEFAULT_CONFIG.zshPath });
3243
+ if (result.exitCode === 2) {
3244
+ const reason = result.stderr || result.stdout || "Blocked by stop hook";
3245
+ return {
3246
+ block: true,
3247
+ reason,
3248
+ injectPrompt: reason
3249
+ };
3250
+ }
3251
+ if (result.stdout) {
3252
+ try {
3253
+ const output = JSON.parse(result.stdout);
3254
+ if (output.stop_hook_active !== undefined) {
3255
+ stopHookActiveState.set(ctx.sessionId, output.stop_hook_active);
3256
+ }
3257
+ const isBlock = output.decision === "block";
3258
+ const injectPrompt = output.inject_prompt ?? (isBlock && output.reason ? output.reason : undefined);
3259
+ return {
3260
+ block: isBlock,
3261
+ reason: output.reason,
3262
+ stopHookActive: output.stop_hook_active,
3263
+ permissionMode: output.permission_mode,
3264
+ injectPrompt
3265
+ };
3266
+ } catch {}
3267
+ }
3268
+ }
3269
+ }
3270
+ return { block: false };
3271
+ }
3272
+
3273
+ // src/hooks/claude-code-hooks/tool-input-cache.ts
3274
+ var cache = new Map;
3275
+ var CACHE_TTL = 60000;
3276
+ function cacheToolInput(sessionId, toolName, invocationId, toolInput) {
3277
+ const key = `${sessionId}:${toolName}:${invocationId}`;
3278
+ cache.set(key, { toolInput, timestamp: Date.now() });
3279
+ }
3280
+ function getToolInput(sessionId, toolName, invocationId) {
3281
+ const key = `${sessionId}:${toolName}:${invocationId}`;
3282
+ const entry = cache.get(key);
3283
+ if (!entry)
3284
+ return null;
3285
+ cache.delete(key);
3286
+ if (Date.now() - entry.timestamp > CACHE_TTL)
3287
+ return null;
3288
+ return entry.toolInput;
3289
+ }
3290
+ setInterval(() => {
3291
+ const now = Date.now();
3292
+ for (const [key, entry] of cache.entries()) {
3293
+ if (now - entry.timestamp > CACHE_TTL) {
3294
+ cache.delete(key);
3295
+ }
3296
+ }
3297
+ }, CACHE_TTL);
3298
+
3299
+ // src/features/hook-message-injector/injector.ts
3300
+ import { existsSync as existsSync12, mkdirSync as mkdirSync5, readFileSync as readFileSync5, readdirSync as readdirSync2, writeFileSync as writeFileSync4 } from "fs";
3301
+ import { join as join15 } from "path";
3302
+
3303
+ // src/features/hook-message-injector/constants.ts
3304
+ import { join as join14 } from "path";
3305
+ import { homedir as homedir6 } from "os";
3306
+ var xdgData2 = process.env.XDG_DATA_HOME || join14(homedir6(), ".local", "share");
3307
+ var OPENCODE_STORAGE3 = join14(xdgData2, "opencode", "storage");
3308
+ var MESSAGE_STORAGE2 = join14(OPENCODE_STORAGE3, "message");
3309
+ var PART_STORAGE2 = join14(OPENCODE_STORAGE3, "part");
3310
+
3311
+ // src/features/hook-message-injector/injector.ts
3312
+ function findNearestMessageWithFields(messageDir) {
3313
+ try {
3314
+ const files = readdirSync2(messageDir).filter((f) => f.endsWith(".json")).sort().reverse();
3315
+ for (const file of files) {
3316
+ try {
3317
+ const content = readFileSync5(join15(messageDir, file), "utf-8");
3318
+ const msg = JSON.parse(content);
3319
+ if (msg.agent && msg.model?.providerID && msg.model?.modelID) {
3320
+ return msg;
3321
+ }
3322
+ } catch {
3323
+ continue;
3324
+ }
3325
+ }
3326
+ } catch {
3327
+ return null;
3328
+ }
3329
+ return null;
3330
+ }
3331
+ function generateMessageId() {
3332
+ const timestamp = Date.now().toString(16);
3333
+ const random = Math.random().toString(36).substring(2, 14);
3334
+ return `msg_${timestamp}${random}`;
3335
+ }
3336
+ function generatePartId2() {
3337
+ const timestamp = Date.now().toString(16);
3338
+ const random = Math.random().toString(36).substring(2, 10);
3339
+ return `prt_${timestamp}${random}`;
3340
+ }
3341
+ function getOrCreateMessageDir(sessionID) {
3342
+ if (!existsSync12(MESSAGE_STORAGE2)) {
3343
+ mkdirSync5(MESSAGE_STORAGE2, { recursive: true });
3344
+ }
3345
+ const directPath = join15(MESSAGE_STORAGE2, sessionID);
3346
+ if (existsSync12(directPath)) {
3347
+ return directPath;
3348
+ }
3349
+ for (const dir of readdirSync2(MESSAGE_STORAGE2)) {
3350
+ const sessionPath = join15(MESSAGE_STORAGE2, dir, sessionID);
3351
+ if (existsSync12(sessionPath)) {
3352
+ return sessionPath;
3353
+ }
3354
+ }
3355
+ mkdirSync5(directPath, { recursive: true });
3356
+ return directPath;
3357
+ }
3358
+ function injectHookMessage(sessionID, hookContent, originalMessage) {
3359
+ const messageDir = getOrCreateMessageDir(sessionID);
3360
+ const needsFallback = !originalMessage.agent || !originalMessage.model?.providerID || !originalMessage.model?.modelID;
3361
+ const fallback = needsFallback ? findNearestMessageWithFields(messageDir) : null;
3362
+ const now = Date.now();
3363
+ const messageID = generateMessageId();
3364
+ const partID = generatePartId2();
3365
+ const resolvedAgent = originalMessage.agent ?? fallback?.agent ?? "general";
3366
+ const resolvedModel = originalMessage.model?.providerID && originalMessage.model?.modelID ? { providerID: originalMessage.model.providerID, modelID: originalMessage.model.modelID } : fallback?.model?.providerID && fallback?.model?.modelID ? { providerID: fallback.model.providerID, modelID: fallback.model.modelID } : undefined;
3367
+ const resolvedTools = originalMessage.tools ?? fallback?.tools;
3368
+ const messageMeta = {
3369
+ id: messageID,
3370
+ sessionID,
3371
+ role: "user",
3372
+ time: {
3373
+ created: now
3374
+ },
3375
+ agent: resolvedAgent,
3376
+ model: resolvedModel,
3377
+ path: originalMessage.path?.cwd ? {
3378
+ cwd: originalMessage.path.cwd,
3379
+ root: originalMessage.path.root ?? "/"
3380
+ } : undefined,
3381
+ tools: resolvedTools
3382
+ };
3383
+ const textPart = {
3384
+ id: partID,
3385
+ type: "text",
3386
+ text: hookContent,
3387
+ synthetic: true,
3388
+ time: {
3389
+ start: now,
3390
+ end: now
3391
+ },
3392
+ messageID,
3393
+ sessionID
3394
+ };
3395
+ try {
3396
+ writeFileSync4(join15(messageDir, `${messageID}.json`), JSON.stringify(messageMeta, null, 2));
3397
+ const partDir = join15(PART_STORAGE2, messageID);
3398
+ if (!existsSync12(partDir)) {
3399
+ mkdirSync5(partDir, { recursive: true });
3400
+ }
3401
+ writeFileSync4(join15(partDir, `${partID}.json`), JSON.stringify(textPart, null, 2));
3402
+ return true;
3403
+ } catch {
3404
+ return false;
3405
+ }
3406
+ }
3407
+ // src/hooks/claude-code-hooks/index.ts
3408
+ var sessionFirstMessageProcessed = new Set;
3409
+ var sessionErrorState = new Map;
3410
+ var sessionInterruptState = new Map;
3411
+ function createClaudeCodeHooksHook(ctx, config = {}) {
3412
+ return {
3413
+ "chat.message": async (input, output) => {
3414
+ const interruptState = sessionInterruptState.get(input.sessionID);
3415
+ if (interruptState?.interrupted) {
3416
+ log("chat.message hook skipped - session interrupted", { sessionID: input.sessionID });
3417
+ return;
3418
+ }
3419
+ const claudeConfig = await loadClaudeHooksConfig();
3420
+ const extendedConfig = await loadPluginExtendedConfig();
3421
+ const textParts = output.parts.filter((p) => p.type === "text" && p.text);
3422
+ const prompt = textParts.map((p) => p.text ?? "").join(`
3423
+ `);
3424
+ recordUserMessage(input.sessionID, prompt);
3425
+ const messageParts = textParts.map((p) => ({
3426
+ type: p.type,
3427
+ text: p.text
3428
+ }));
3429
+ const interruptStateBeforeHooks = sessionInterruptState.get(input.sessionID);
3430
+ if (interruptStateBeforeHooks?.interrupted) {
3431
+ log("chat.message hooks skipped - interrupted during preparation", { sessionID: input.sessionID });
3432
+ return;
3433
+ }
3434
+ let parentSessionId;
3435
+ try {
3436
+ const sessionInfo = await ctx.client.session.get({
3437
+ path: { id: input.sessionID }
3438
+ });
3439
+ parentSessionId = sessionInfo.data?.parentID;
3440
+ } catch {}
3441
+ const isFirstMessage = !sessionFirstMessageProcessed.has(input.sessionID);
3442
+ sessionFirstMessageProcessed.add(input.sessionID);
3443
+ if (isFirstMessage) {
3444
+ log("Skipping UserPromptSubmit hooks on first message for title generation", { sessionID: input.sessionID });
3445
+ return;
3446
+ }
3447
+ if (!isHookDisabled(config, "UserPromptSubmit")) {
3448
+ const userPromptCtx = {
3449
+ sessionId: input.sessionID,
3450
+ parentSessionId,
3451
+ prompt,
3452
+ parts: messageParts,
3453
+ cwd: ctx.directory
3454
+ };
3455
+ const result = await executeUserPromptSubmitHooks(userPromptCtx, claudeConfig, extendedConfig);
3456
+ if (result.block) {
3457
+ throw new Error(result.reason ?? "Hook blocked the prompt");
3458
+ }
3459
+ const interruptStateAfterHooks = sessionInterruptState.get(input.sessionID);
3460
+ if (interruptStateAfterHooks?.interrupted) {
3461
+ log("chat.message injection skipped - interrupted during hooks", { sessionID: input.sessionID });
3462
+ return;
3463
+ }
3464
+ if (result.messages.length > 0) {
3465
+ const hookContent = result.messages.join(`
3466
+
3467
+ `);
3468
+ const message = output.message;
3469
+ const success = injectHookMessage(input.sessionID, hookContent, {
3470
+ agent: message.agent,
3471
+ model: message.model,
3472
+ path: message.path ?? { cwd: ctx.directory, root: "/" },
3473
+ tools: message.tools
3474
+ });
3475
+ log(success ? "Hook message injected via file system" : "File injection failed", {
3476
+ sessionID: input.sessionID
3477
+ });
3478
+ }
3479
+ }
3480
+ },
3481
+ "tool.execute.before": async (input, output) => {
3482
+ const claudeConfig = await loadClaudeHooksConfig();
3483
+ const extendedConfig = await loadPluginExtendedConfig();
3484
+ recordToolUse(input.sessionID, input.tool, output.args);
3485
+ cacheToolInput(input.sessionID, input.tool, input.callID, output.args);
3486
+ if (!isHookDisabled(config, "PreToolUse")) {
3487
+ const preCtx = {
3488
+ sessionId: input.sessionID,
3489
+ toolName: input.tool,
3490
+ toolInput: output.args,
3491
+ cwd: ctx.directory,
3492
+ toolUseId: input.callID
3493
+ };
3494
+ const result = await executePreToolUseHooks(preCtx, claudeConfig, extendedConfig);
3495
+ if (result.decision === "deny") {
3496
+ ctx.client.tui.showToast({
3497
+ body: {
3498
+ title: "PreToolUse Hook Executed",
3499
+ message: `\u2717 ${result.toolName ?? input.tool} ${result.hookName ?? "hook"}: BLOCKED ${result.elapsedMs ?? 0}ms
3500
+ ${result.inputLines ?? ""}`,
3501
+ variant: "error",
3502
+ duration: 4000
3503
+ }
3504
+ }).catch(() => {});
3505
+ throw new Error(result.reason ?? "Hook blocked the operation");
3506
+ }
3507
+ if (result.modifiedInput) {
3508
+ Object.assign(output.args, result.modifiedInput);
3509
+ }
3510
+ }
3511
+ },
3512
+ "tool.execute.after": async (input, output) => {
3513
+ const claudeConfig = await loadClaudeHooksConfig();
3514
+ const extendedConfig = await loadPluginExtendedConfig();
3515
+ const cachedInput = getToolInput(input.sessionID, input.tool, input.callID) || {};
3516
+ recordToolResult(input.sessionID, input.tool, cachedInput, output.metadata || {});
3517
+ if (!isHookDisabled(config, "PostToolUse")) {
3518
+ const postClient = {
3519
+ session: {
3520
+ messages: (opts) => ctx.client.session.messages(opts)
3521
+ }
3522
+ };
3523
+ const postCtx = {
3524
+ sessionId: input.sessionID,
3525
+ toolName: input.tool,
3526
+ toolInput: cachedInput,
3527
+ toolOutput: {
3528
+ title: input.tool,
3529
+ output: output.output,
3530
+ metadata: output.metadata
3531
+ },
3532
+ cwd: ctx.directory,
3533
+ transcriptPath: getTranscriptPath(input.sessionID),
3534
+ toolUseId: input.callID,
3535
+ client: postClient,
3536
+ permissionMode: "bypassPermissions"
3537
+ };
3538
+ const result = await executePostToolUseHooks(postCtx, claudeConfig, extendedConfig);
3539
+ if (result.block) {
3540
+ ctx.client.tui.showToast({
3541
+ body: {
3542
+ title: "PostToolUse Hook Warning",
3543
+ message: result.reason ?? "Hook returned warning",
3544
+ variant: "warning",
3545
+ duration: 4000
3546
+ }
3547
+ }).catch(() => {});
3548
+ }
3549
+ if (result.warnings && result.warnings.length > 0) {
3550
+ output.output = `${output.output}
3551
+
3552
+ ${result.warnings.join(`
3553
+ `)}`;
3554
+ }
3555
+ if (result.message) {
3556
+ output.output = `${output.output}
3557
+
3558
+ ${result.message}`;
3559
+ }
3560
+ if (result.hookName) {
3561
+ ctx.client.tui.showToast({
3562
+ body: {
3563
+ title: "PostToolUse Hook Executed",
3564
+ message: `\u25B6 ${result.toolName ?? input.tool} ${result.hookName}: ${result.elapsedMs ?? 0}ms`,
3565
+ variant: "success",
3566
+ duration: 2000
3567
+ }
3568
+ }).catch(() => {});
3569
+ }
1969
3570
  }
1970
- const highVariant = getHighVariant(currentModel.modelID);
1971
- if (!highVariant) {
1972
- thinkModeState.set(sessionID, state);
3571
+ },
3572
+ event: async (input) => {
3573
+ const { event } = input;
3574
+ if (event.type === "session.error") {
3575
+ const props = event.properties;
3576
+ const sessionID = props?.sessionID;
3577
+ if (sessionID) {
3578
+ sessionErrorState.set(sessionID, {
3579
+ hasError: true,
3580
+ errorMessage: String(props?.error ?? "Unknown error")
3581
+ });
3582
+ }
1973
3583
  return;
1974
3584
  }
1975
- output.message.model = {
1976
- providerID: currentModel.providerID,
1977
- modelID: highVariant
1978
- };
1979
- state.modelSwitched = true;
1980
- thinkModeState.set(sessionID, state);
1981
- },
1982
- event: async ({ event }) => {
1983
3585
  if (event.type === "session.deleted") {
1984
3586
  const props = event.properties;
1985
- if (props?.info?.id) {
1986
- thinkModeState.delete(props.info.id);
3587
+ const sessionInfo = props?.info;
3588
+ if (sessionInfo?.id) {
3589
+ sessionErrorState.delete(sessionInfo.id);
3590
+ sessionInterruptState.delete(sessionInfo.id);
3591
+ sessionFirstMessageProcessed.delete(sessionInfo.id);
3592
+ }
3593
+ return;
3594
+ }
3595
+ if (event.type === "session.idle") {
3596
+ const props = event.properties;
3597
+ const sessionID = props?.sessionID;
3598
+ if (!sessionID)
3599
+ return;
3600
+ const claudeConfig = await loadClaudeHooksConfig();
3601
+ const extendedConfig = await loadPluginExtendedConfig();
3602
+ const errorStateBefore = sessionErrorState.get(sessionID);
3603
+ const endedWithErrorBefore = errorStateBefore?.hasError === true;
3604
+ const interruptStateBefore = sessionInterruptState.get(sessionID);
3605
+ const interruptedBefore = interruptStateBefore?.interrupted === true;
3606
+ let parentSessionId;
3607
+ try {
3608
+ const sessionInfo = await ctx.client.session.get({
3609
+ path: { id: sessionID }
3610
+ });
3611
+ parentSessionId = sessionInfo.data?.parentID;
3612
+ } catch {}
3613
+ if (!isHookDisabled(config, "Stop")) {
3614
+ const stopCtx = {
3615
+ sessionId: sessionID,
3616
+ parentSessionId,
3617
+ cwd: ctx.directory
3618
+ };
3619
+ const stopResult = await executeStopHooks(stopCtx, claudeConfig, extendedConfig);
3620
+ const errorStateAfter = sessionErrorState.get(sessionID);
3621
+ const endedWithErrorAfter = errorStateAfter?.hasError === true;
3622
+ const interruptStateAfter = sessionInterruptState.get(sessionID);
3623
+ const interruptedAfter = interruptStateAfter?.interrupted === true;
3624
+ const shouldBypass = endedWithErrorBefore || endedWithErrorAfter || interruptedBefore || interruptedAfter;
3625
+ if (shouldBypass && stopResult.block) {
3626
+ const interrupted = interruptedBefore || interruptedAfter;
3627
+ const endedWithError = endedWithErrorBefore || endedWithErrorAfter;
3628
+ log("Stop hook block ignored", { sessionID, block: stopResult.block, interrupted, endedWithError });
3629
+ } else if (stopResult.block && stopResult.injectPrompt) {
3630
+ log("Stop hook returned block with inject_prompt", { sessionID });
3631
+ ctx.client.session.prompt({
3632
+ path: { id: sessionID },
3633
+ body: { parts: [{ type: "text", text: stopResult.injectPrompt }] },
3634
+ query: { directory: ctx.directory }
3635
+ }).catch((err) => log("Failed to inject prompt from Stop hook", err));
3636
+ } else if (stopResult.block) {
3637
+ log("Stop hook returned block", { sessionID, reason: stopResult.reason });
3638
+ }
1987
3639
  }
3640
+ sessionErrorState.delete(sessionID);
3641
+ sessionInterruptState.delete(sessionID);
1988
3642
  }
1989
3643
  }
1990
3644
  };
1991
3645
  }
1992
3646
  // src/features/claude-code-command-loader/loader.ts
1993
- import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
1994
- import { homedir as homedir2 } from "os";
1995
- import { join as join8, basename } from "path";
1996
-
1997
- // src/shared/frontmatter.ts
1998
- function parseFrontmatter(content) {
1999
- const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
2000
- const match = content.match(frontmatterRegex);
2001
- if (!match) {
2002
- return { data: {}, body: content };
2003
- }
2004
- const yamlContent = match[1];
2005
- const body = match[2];
2006
- const data = {};
2007
- for (const line of yamlContent.split(`
2008
- `)) {
2009
- const colonIndex = line.indexOf(":");
2010
- if (colonIndex !== -1) {
2011
- const key = line.slice(0, colonIndex).trim();
2012
- let value = line.slice(colonIndex + 1).trim();
2013
- if (value === "true")
2014
- value = true;
2015
- else if (value === "false")
2016
- value = false;
2017
- data[key] = value;
2018
- }
2019
- }
2020
- return { data, body };
2021
- }
2022
-
2023
- // src/shared/model-sanitizer.ts
2024
- function sanitizeModelField(_model) {
2025
- return;
2026
- }
2027
-
2028
- // src/features/claude-code-command-loader/loader.ts
3647
+ import { existsSync as existsSync13, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
3648
+ import { homedir as homedir7 } from "os";
3649
+ import { join as join16, basename } from "path";
2029
3650
  function isMarkdownFile(entry) {
2030
3651
  return !entry.name.startsWith(".") && entry.name.endsWith(".md") && entry.isFile();
2031
3652
  }
2032
3653
  function loadCommandsFromDir(commandsDir, scope) {
2033
- if (!existsSync7(commandsDir)) {
3654
+ if (!existsSync13(commandsDir)) {
2034
3655
  return [];
2035
3656
  }
2036
- const entries = readdirSync2(commandsDir, { withFileTypes: true });
3657
+ const entries = readdirSync3(commandsDir, { withFileTypes: true });
2037
3658
  const commands = [];
2038
3659
  for (const entry of entries) {
2039
3660
  if (!isMarkdownFile(entry))
2040
3661
  continue;
2041
- const commandPath = join8(commandsDir, entry.name);
3662
+ const commandPath = join16(commandsDir, entry.name);
2042
3663
  const commandName = basename(entry.name, ".md");
2043
3664
  try {
2044
- const content = readFileSync4(commandPath, "utf-8");
3665
+ const content = readFileSync6(commandPath, "utf-8");
2045
3666
  const { data, body } = parseFrontmatter(content);
2046
3667
  const wrappedTemplate = `<command-instruction>
2047
3668
  ${body.trim()}
@@ -2080,50 +3701,50 @@ function commandsToRecord(commands) {
2080
3701
  return result;
2081
3702
  }
2082
3703
  function loadUserCommands() {
2083
- const userCommandsDir = join8(homedir2(), ".claude", "commands");
3704
+ const userCommandsDir = join16(homedir7(), ".claude", "commands");
2084
3705
  const commands = loadCommandsFromDir(userCommandsDir, "user");
2085
3706
  return commandsToRecord(commands);
2086
3707
  }
2087
3708
  function loadProjectCommands() {
2088
- const projectCommandsDir = join8(process.cwd(), ".claude", "commands");
3709
+ const projectCommandsDir = join16(process.cwd(), ".claude", "commands");
2089
3710
  const commands = loadCommandsFromDir(projectCommandsDir, "project");
2090
3711
  return commandsToRecord(commands);
2091
3712
  }
2092
3713
  function loadOpencodeGlobalCommands() {
2093
- const opencodeCommandsDir = join8(homedir2(), ".config", "opencode", "command");
3714
+ const opencodeCommandsDir = join16(homedir7(), ".config", "opencode", "command");
2094
3715
  const commands = loadCommandsFromDir(opencodeCommandsDir, "opencode");
2095
3716
  return commandsToRecord(commands);
2096
3717
  }
2097
3718
  function loadOpencodeProjectCommands() {
2098
- const opencodeProjectDir = join8(process.cwd(), ".opencode", "command");
3719
+ const opencodeProjectDir = join16(process.cwd(), ".opencode", "command");
2099
3720
  const commands = loadCommandsFromDir(opencodeProjectDir, "opencode-project");
2100
3721
  return commandsToRecord(commands);
2101
3722
  }
2102
3723
  // src/features/claude-code-skill-loader/loader.ts
2103
- import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync5, statSync, readlinkSync } from "fs";
2104
- import { homedir as homedir3 } from "os";
2105
- import { join as join9, resolve as resolve2 } from "path";
3724
+ import { existsSync as existsSync14, readdirSync as readdirSync4, readFileSync as readFileSync7, statSync as statSync2, readlinkSync } from "fs";
3725
+ import { homedir as homedir8 } from "os";
3726
+ import { join as join17, resolve as resolve2 } from "path";
2106
3727
  function loadSkillsFromDir(skillsDir, scope) {
2107
- if (!existsSync8(skillsDir)) {
3728
+ if (!existsSync14(skillsDir)) {
2108
3729
  return [];
2109
3730
  }
2110
- const entries = readdirSync3(skillsDir, { withFileTypes: true });
3731
+ const entries = readdirSync4(skillsDir, { withFileTypes: true });
2111
3732
  const skills = [];
2112
3733
  for (const entry of entries) {
2113
3734
  if (entry.name.startsWith("."))
2114
3735
  continue;
2115
- const skillPath = join9(skillsDir, entry.name);
3736
+ const skillPath = join17(skillsDir, entry.name);
2116
3737
  if (!entry.isDirectory() && !entry.isSymbolicLink())
2117
3738
  continue;
2118
3739
  let resolvedPath = skillPath;
2119
- if (statSync(skillPath, { throwIfNoEntry: false })?.isSymbolicLink()) {
3740
+ if (statSync2(skillPath, { throwIfNoEntry: false })?.isSymbolicLink()) {
2120
3741
  resolvedPath = resolve2(skillPath, "..", readlinkSync(skillPath));
2121
3742
  }
2122
- const skillMdPath = join9(resolvedPath, "SKILL.md");
2123
- if (!existsSync8(skillMdPath))
3743
+ const skillMdPath = join17(resolvedPath, "SKILL.md");
3744
+ if (!existsSync14(skillMdPath))
2124
3745
  continue;
2125
3746
  try {
2126
- const content = readFileSync5(skillMdPath, "utf-8");
3747
+ const content = readFileSync7(skillMdPath, "utf-8");
2127
3748
  const { data, body } = parseFrontmatter(content);
2128
3749
  const skillName = data.name || entry.name;
2129
3750
  const originalDescription = data.description || "";
@@ -2154,7 +3775,7 @@ $ARGUMENTS
2154
3775
  return skills;
2155
3776
  }
2156
3777
  function loadUserSkillsAsCommands() {
2157
- const userSkillsDir = join9(homedir3(), ".claude", "skills");
3778
+ const userSkillsDir = join17(homedir8(), ".claude", "skills");
2158
3779
  const skills = loadSkillsFromDir(userSkillsDir, "user");
2159
3780
  return skills.reduce((acc, skill) => {
2160
3781
  acc[skill.name] = skill.definition;
@@ -2162,7 +3783,7 @@ function loadUserSkillsAsCommands() {
2162
3783
  }, {});
2163
3784
  }
2164
3785
  function loadProjectSkillsAsCommands() {
2165
- const projectSkillsDir = join9(process.cwd(), ".claude", "skills");
3786
+ const projectSkillsDir = join17(process.cwd(), ".claude", "skills");
2166
3787
  const skills = loadSkillsFromDir(projectSkillsDir, "project");
2167
3788
  return skills.reduce((acc, skill) => {
2168
3789
  acc[skill.name] = skill.definition;
@@ -2170,9 +3791,9 @@ function loadProjectSkillsAsCommands() {
2170
3791
  }, {});
2171
3792
  }
2172
3793
  // src/features/claude-code-agent-loader/loader.ts
2173
- import { existsSync as existsSync9, readdirSync as readdirSync4, readFileSync as readFileSync6 } from "fs";
2174
- import { homedir as homedir4 } from "os";
2175
- import { join as join10, basename as basename2 } from "path";
3794
+ import { existsSync as existsSync15, readdirSync as readdirSync5, readFileSync as readFileSync8 } from "fs";
3795
+ import { homedir as homedir9 } from "os";
3796
+ import { join as join18, basename as basename2 } from "path";
2176
3797
  function parseToolsConfig(toolsStr) {
2177
3798
  if (!toolsStr)
2178
3799
  return;
@@ -2189,18 +3810,18 @@ function isMarkdownFile2(entry) {
2189
3810
  return !entry.name.startsWith(".") && entry.name.endsWith(".md") && entry.isFile();
2190
3811
  }
2191
3812
  function loadAgentsFromDir(agentsDir, scope) {
2192
- if (!existsSync9(agentsDir)) {
3813
+ if (!existsSync15(agentsDir)) {
2193
3814
  return [];
2194
3815
  }
2195
- const entries = readdirSync4(agentsDir, { withFileTypes: true });
3816
+ const entries = readdirSync5(agentsDir, { withFileTypes: true });
2196
3817
  const agents = [];
2197
3818
  for (const entry of entries) {
2198
3819
  if (!isMarkdownFile2(entry))
2199
3820
  continue;
2200
- const agentPath = join10(agentsDir, entry.name);
3821
+ const agentPath = join18(agentsDir, entry.name);
2201
3822
  const agentName = basename2(entry.name, ".md");
2202
3823
  try {
2203
- const content = readFileSync6(agentPath, "utf-8");
3824
+ const content = readFileSync8(agentPath, "utf-8");
2204
3825
  const { data, body } = parseFrontmatter(content);
2205
3826
  const name = data.name || agentName;
2206
3827
  const originalDescription = data.description || "";
@@ -2227,7 +3848,7 @@ function loadAgentsFromDir(agentsDir, scope) {
2227
3848
  return agents;
2228
3849
  }
2229
3850
  function loadUserAgents() {
2230
- const userAgentsDir = join10(homedir4(), ".claude", "agents");
3851
+ const userAgentsDir = join18(homedir9(), ".claude", "agents");
2231
3852
  const agents = loadAgentsFromDir(userAgentsDir, "user");
2232
3853
  const result = {};
2233
3854
  for (const agent of agents) {
@@ -2236,7 +3857,7 @@ function loadUserAgents() {
2236
3857
  return result;
2237
3858
  }
2238
3859
  function loadProjectAgents() {
2239
- const projectAgentsDir = join10(process.cwd(), ".claude", "agents");
3860
+ const projectAgentsDir = join18(process.cwd(), ".claude", "agents");
2240
3861
  const agents = loadAgentsFromDir(projectAgentsDir, "project");
2241
3862
  const result = {};
2242
3863
  for (const agent of agents) {
@@ -2245,9 +3866,9 @@ function loadProjectAgents() {
2245
3866
  return result;
2246
3867
  }
2247
3868
  // src/features/claude-code-mcp-loader/loader.ts
2248
- import { existsSync as existsSync10 } from "fs";
2249
- import { homedir as homedir5 } from "os";
2250
- import { join as join12 } from "path";
3869
+ import { existsSync as existsSync16 } from "fs";
3870
+ import { homedir as homedir10 } from "os";
3871
+ import { join as join19 } from "path";
2251
3872
 
2252
3873
  // src/features/claude-code-mcp-loader/env-expander.ts
2253
3874
  function expandEnvVars(value) {
@@ -2311,32 +3932,18 @@ function transformMcpServer(name, server) {
2311
3932
  return config;
2312
3933
  }
2313
3934
 
2314
- // src/shared/logger.ts
2315
- import * as fs3 from "fs";
2316
- import * as os2 from "os";
2317
- import * as path2 from "path";
2318
- var logFile = path2.join(os2.tmpdir(), "oh-my-opencode.log");
2319
- function log(message, data) {
2320
- try {
2321
- const timestamp = new Date().toISOString();
2322
- const logEntry = `[${timestamp}] ${message} ${data ? JSON.stringify(data) : ""}
2323
- `;
2324
- fs3.appendFileSync(logFile, logEntry);
2325
- } catch {}
2326
- }
2327
-
2328
3935
  // src/features/claude-code-mcp-loader/loader.ts
2329
3936
  function getMcpConfigPaths() {
2330
- const home = homedir5();
3937
+ const home = homedir10();
2331
3938
  const cwd = process.cwd();
2332
3939
  return [
2333
- { path: join12(home, ".claude", ".mcp.json"), scope: "user" },
2334
- { path: join12(cwd, ".mcp.json"), scope: "project" },
2335
- { path: join12(cwd, ".claude", ".mcp.json"), scope: "local" }
3940
+ { path: join19(home, ".claude", ".mcp.json"), scope: "user" },
3941
+ { path: join19(cwd, ".mcp.json"), scope: "project" },
3942
+ { path: join19(cwd, ".claude", ".mcp.json"), scope: "local" }
2336
3943
  ];
2337
3944
  }
2338
3945
  async function loadMcpConfigFile(filePath) {
2339
- if (!existsSync10(filePath)) {
3946
+ if (!existsSync16(filePath)) {
2340
3947
  return null;
2341
3948
  }
2342
3949
  try {
@@ -2377,10 +3984,10 @@ async function loadMcpConfigs() {
2377
3984
  return { servers, loadedServers };
2378
3985
  }
2379
3986
  // src/features/claude-code-session-state/state.ts
2380
- var sessionErrorState = new Map;
2381
- var sessionInterruptState = new Map;
3987
+ var sessionErrorState2 = new Map;
3988
+ var sessionInterruptState2 = new Map;
2382
3989
  var subagentSessions = new Set;
2383
- var sessionFirstMessageProcessed = new Set;
3990
+ var sessionFirstMessageProcessed2 = new Set;
2384
3991
  var currentSessionID;
2385
3992
  var currentSessionTitle;
2386
3993
  var mainSessionID;
@@ -2622,14 +4229,14 @@ var EXT_TO_LANG = {
2622
4229
  ".tfvars": "terraform"
2623
4230
  };
2624
4231
  // src/tools/lsp/config.ts
2625
- import { existsSync as existsSync11, readFileSync as readFileSync7 } from "fs";
2626
- import { join as join13 } from "path";
2627
- import { homedir as homedir6 } from "os";
4232
+ import { existsSync as existsSync17, readFileSync as readFileSync9 } from "fs";
4233
+ import { join as join20 } from "path";
4234
+ import { homedir as homedir11 } from "os";
2628
4235
  function loadJsonFile(path3) {
2629
- if (!existsSync11(path3))
4236
+ if (!existsSync17(path3))
2630
4237
  return null;
2631
4238
  try {
2632
- return JSON.parse(readFileSync7(path3, "utf-8"));
4239
+ return JSON.parse(readFileSync9(path3, "utf-8"));
2633
4240
  } catch {
2634
4241
  return null;
2635
4242
  }
@@ -2637,9 +4244,9 @@ function loadJsonFile(path3) {
2637
4244
  function getConfigPaths() {
2638
4245
  const cwd = process.cwd();
2639
4246
  return {
2640
- project: join13(cwd, ".opencode", "oh-my-opencode.json"),
2641
- user: join13(homedir6(), ".config", "opencode", "oh-my-opencode.json"),
2642
- opencode: join13(homedir6(), ".config", "opencode", "opencode.json")
4247
+ project: join20(cwd, ".opencode", "oh-my-opencode.json"),
4248
+ user: join20(homedir11(), ".config", "opencode", "oh-my-opencode.json"),
4249
+ opencode: join20(homedir11(), ".config", "opencode", "opencode.json")
2643
4250
  };
2644
4251
  }
2645
4252
  function loadAllConfigs() {
@@ -2732,7 +4339,7 @@ function isServerInstalled(command) {
2732
4339
  const pathEnv = process.env.PATH || "";
2733
4340
  const paths = pathEnv.split(":");
2734
4341
  for (const p of paths) {
2735
- if (existsSync11(join13(p, cmd))) {
4342
+ if (existsSync17(join20(p, cmd))) {
2736
4343
  return true;
2737
4344
  }
2738
4345
  }
@@ -2781,8 +4388,8 @@ function getAllServers() {
2781
4388
  return result;
2782
4389
  }
2783
4390
  // src/tools/lsp/client.ts
2784
- var {spawn: spawn3 } = globalThis.Bun;
2785
- import { readFileSync as readFileSync8 } from "fs";
4391
+ var {spawn: spawn4 } = globalThis.Bun;
4392
+ import { readFileSync as readFileSync10 } from "fs";
2786
4393
  import { extname, resolve as resolve3 } from "path";
2787
4394
  class LSPServerManager {
2788
4395
  static instance;
@@ -2918,7 +4525,7 @@ class LSPClient {
2918
4525
  this.server = server;
2919
4526
  }
2920
4527
  async start() {
2921
- this.proc = spawn3(this.server.command, {
4528
+ this.proc = spawn4(this.server.command, {
2922
4529
  stdin: "pipe",
2923
4530
  stdout: "pipe",
2924
4531
  stderr: "pipe",
@@ -3182,7 +4789,7 @@ ${msg}`);
3182
4789
  const absPath = resolve3(filePath);
3183
4790
  if (this.openedFiles.has(absPath))
3184
4791
  return;
3185
- const text = readFileSync8(absPath, "utf-8");
4792
+ const text = readFileSync10(absPath, "utf-8");
3186
4793
  const ext = extname(absPath);
3187
4794
  const languageId = getLanguageId(ext);
3188
4795
  this.notify("textDocument/didOpen", {
@@ -3297,16 +4904,16 @@ ${msg}`);
3297
4904
  }
3298
4905
  // src/tools/lsp/utils.ts
3299
4906
  import { extname as extname2, resolve as resolve4 } from "path";
3300
- import { existsSync as existsSync12, readFileSync as readFileSync9, writeFileSync as writeFileSync3 } from "fs";
4907
+ import { existsSync as existsSync18, readFileSync as readFileSync11, writeFileSync as writeFileSync5 } from "fs";
3301
4908
  function findWorkspaceRoot(filePath) {
3302
4909
  let dir = resolve4(filePath);
3303
- if (!existsSync12(dir) || !__require("fs").statSync(dir).isDirectory()) {
4910
+ if (!existsSync18(dir) || !__require("fs").statSync(dir).isDirectory()) {
3304
4911
  dir = __require("path").dirname(dir);
3305
4912
  }
3306
4913
  const markers = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "build.gradle"];
3307
4914
  while (dir !== "/") {
3308
4915
  for (const marker of markers) {
3309
- if (existsSync12(__require("path").join(dir, marker))) {
4916
+ if (existsSync18(__require("path").join(dir, marker))) {
3310
4917
  return dir;
3311
4918
  }
3312
4919
  }
@@ -3464,7 +5071,7 @@ function formatCodeActions(actions) {
3464
5071
  }
3465
5072
  function applyTextEditsToFile(filePath, edits) {
3466
5073
  try {
3467
- let content = readFileSync9(filePath, "utf-8");
5074
+ let content = readFileSync11(filePath, "utf-8");
3468
5075
  const lines = content.split(`
3469
5076
  `);
3470
5077
  const sortedEdits = [...edits].sort((a, b) => {
@@ -3489,7 +5096,7 @@ function applyTextEditsToFile(filePath, edits) {
3489
5096
  `));
3490
5097
  }
3491
5098
  }
3492
- writeFileSync3(filePath, lines.join(`
5099
+ writeFileSync5(filePath, lines.join(`
3493
5100
  `), "utf-8");
3494
5101
  return { success: true, editCount: edits.length };
3495
5102
  } catch (err) {
@@ -3520,7 +5127,7 @@ function applyWorkspaceEdit(edit) {
3520
5127
  if (change.kind === "create") {
3521
5128
  try {
3522
5129
  const filePath = change.uri.replace("file://", "");
3523
- writeFileSync3(filePath, "", "utf-8");
5130
+ writeFileSync5(filePath, "", "utf-8");
3524
5131
  result.filesModified.push(filePath);
3525
5132
  } catch (err) {
3526
5133
  result.success = false;
@@ -3530,8 +5137,8 @@ function applyWorkspaceEdit(edit) {
3530
5137
  try {
3531
5138
  const oldPath = change.oldUri.replace("file://", "");
3532
5139
  const newPath = change.newUri.replace("file://", "");
3533
- const content = readFileSync9(oldPath, "utf-8");
3534
- writeFileSync3(newPath, content, "utf-8");
5140
+ const content = readFileSync11(oldPath, "utf-8");
5141
+ writeFileSync5(newPath, content, "utf-8");
3535
5142
  __require("fs").unlinkSync(oldPath);
3536
5143
  result.filesModified.push(newPath);
3537
5144
  } catch (err) {
@@ -4172,7 +5779,7 @@ __export(exports_util, {
4172
5779
  jsonStringifyReplacer: () => jsonStringifyReplacer,
4173
5780
  joinValues: () => joinValues,
4174
5781
  issue: () => issue,
4175
- isPlainObject: () => isPlainObject,
5782
+ isPlainObject: () => isPlainObject2,
4176
5783
  isObject: () => isObject,
4177
5784
  hexToUint8Array: () => hexToUint8Array,
4178
5785
  getSizableOrigin: () => getSizableOrigin,
@@ -4354,7 +5961,7 @@ var allowsEval = cached(() => {
4354
5961
  return false;
4355
5962
  }
4356
5963
  });
4357
- function isPlainObject(o) {
5964
+ function isPlainObject2(o) {
4358
5965
  if (isObject(o) === false)
4359
5966
  return false;
4360
5967
  const ctor = o.constructor;
@@ -4369,7 +5976,7 @@ function isPlainObject(o) {
4369
5976
  return true;
4370
5977
  }
4371
5978
  function shallowClone(o) {
4372
- if (isPlainObject(o))
5979
+ if (isPlainObject2(o))
4373
5980
  return { ...o };
4374
5981
  if (Array.isArray(o))
4375
5982
  return [...o];
@@ -4552,7 +6159,7 @@ function omit(schema, mask) {
4552
6159
  return clone(schema, def);
4553
6160
  }
4554
6161
  function extend(schema, shape) {
4555
- if (!isPlainObject(shape)) {
6162
+ if (!isPlainObject2(shape)) {
4556
6163
  throw new Error("Invalid input to extend: expected a plain object");
4557
6164
  }
4558
6165
  const checks = schema._zod.def.checks;
@@ -4571,7 +6178,7 @@ function extend(schema, shape) {
4571
6178
  return clone(schema, def);
4572
6179
  }
4573
6180
  function safeExtend(schema, shape) {
4574
- if (!isPlainObject(shape)) {
6181
+ if (!isPlainObject2(shape)) {
4575
6182
  throw new Error("Invalid input to safeExtend: expected a plain object");
4576
6183
  }
4577
6184
  const def = {
@@ -6721,7 +8328,7 @@ function mergeValues(a, b) {
6721
8328
  if (a instanceof Date && b instanceof Date && +a === +b) {
6722
8329
  return { valid: true, data: a };
6723
8330
  }
6724
- if (isPlainObject(a) && isPlainObject(b)) {
8331
+ if (isPlainObject2(a) && isPlainObject2(b)) {
6725
8332
  const bKeys = Object.keys(b);
6726
8333
  const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);
6727
8334
  const newObj = { ...a, ...b };
@@ -6851,7 +8458,7 @@ var $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
6851
8458
  $ZodType.init(inst, def);
6852
8459
  inst._zod.parse = (payload, ctx) => {
6853
8460
  const input = payload.value;
6854
- if (!isPlainObject(input)) {
8461
+ if (!isPlainObject2(input)) {
6855
8462
  payload.issues.push({
6856
8463
  expected: "record",
6857
8464
  code: "invalid_type",
@@ -16231,14 +17838,14 @@ var lsp_code_action_resolve = tool({
16231
17838
  });
16232
17839
  // src/tools/ast-grep/constants.ts
16233
17840
  import { createRequire as createRequire4 } from "module";
16234
- import { dirname as dirname3, join as join15 } from "path";
16235
- import { existsSync as existsSync14, statSync as statSync2 } from "fs";
17841
+ import { dirname as dirname3, join as join22 } from "path";
17842
+ import { existsSync as existsSync20, statSync as statSync3 } from "fs";
16236
17843
 
16237
17844
  // src/tools/ast-grep/downloader.ts
16238
- var {spawn: spawn4 } = globalThis.Bun;
16239
- import { existsSync as existsSync13, mkdirSync as mkdirSync4, chmodSync as chmodSync2, unlinkSync as unlinkSync4 } from "fs";
16240
- import { join as join14 } from "path";
16241
- import { homedir as homedir7 } from "os";
17845
+ var {spawn: spawn5 } = globalThis.Bun;
17846
+ import { existsSync as existsSync19, mkdirSync as mkdirSync6, chmodSync as chmodSync2, unlinkSync as unlinkSync5 } from "fs";
17847
+ import { join as join21 } from "path";
17848
+ import { homedir as homedir12 } from "os";
16242
17849
  import { createRequire as createRequire3 } from "module";
16243
17850
  var REPO2 = "ast-grep/ast-grep";
16244
17851
  var DEFAULT_VERSION = "0.40.0";
@@ -16263,26 +17870,26 @@ var PLATFORM_MAP2 = {
16263
17870
  function getCacheDir2() {
16264
17871
  if (process.platform === "win32") {
16265
17872
  const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
16266
- const base2 = localAppData || join14(homedir7(), "AppData", "Local");
16267
- return join14(base2, "oh-my-opencode", "bin");
17873
+ const base2 = localAppData || join21(homedir12(), "AppData", "Local");
17874
+ return join21(base2, "oh-my-opencode", "bin");
16268
17875
  }
16269
17876
  const xdgCache2 = process.env.XDG_CACHE_HOME;
16270
- const base = xdgCache2 || join14(homedir7(), ".cache");
16271
- return join14(base, "oh-my-opencode", "bin");
17877
+ const base = xdgCache2 || join21(homedir12(), ".cache");
17878
+ return join21(base, "oh-my-opencode", "bin");
16272
17879
  }
16273
17880
  function getBinaryName3() {
16274
17881
  return process.platform === "win32" ? "sg.exe" : "sg";
16275
17882
  }
16276
17883
  function getCachedBinaryPath2() {
16277
- const binaryPath = join14(getCacheDir2(), getBinaryName3());
16278
- return existsSync13(binaryPath) ? binaryPath : null;
17884
+ const binaryPath = join21(getCacheDir2(), getBinaryName3());
17885
+ return existsSync19(binaryPath) ? binaryPath : null;
16279
17886
  }
16280
17887
  async function extractZip2(archivePath, destDir) {
16281
- const proc = process.platform === "win32" ? spawn4([
17888
+ const proc = process.platform === "win32" ? spawn5([
16282
17889
  "powershell",
16283
17890
  "-command",
16284
17891
  `Expand-Archive -Path '${archivePath}' -DestinationPath '${destDir}' -Force`
16285
- ], { stdout: "pipe", stderr: "pipe" }) : spawn4(["unzip", "-o", archivePath, "-d", destDir], { stdout: "pipe", stderr: "pipe" });
17892
+ ], { stdout: "pipe", stderr: "pipe" }) : spawn5(["unzip", "-o", archivePath, "-d", destDir], { stdout: "pipe", stderr: "pipe" });
16286
17893
  const exitCode = await proc.exited;
16287
17894
  if (exitCode !== 0) {
16288
17895
  const stderr = await new Response(proc.stderr).text();
@@ -16301,8 +17908,8 @@ async function downloadAstGrep(version2 = DEFAULT_VERSION) {
16301
17908
  }
16302
17909
  const cacheDir = getCacheDir2();
16303
17910
  const binaryName = getBinaryName3();
16304
- const binaryPath = join14(cacheDir, binaryName);
16305
- if (existsSync13(binaryPath)) {
17911
+ const binaryPath = join21(cacheDir, binaryName);
17912
+ if (existsSync19(binaryPath)) {
16306
17913
  return binaryPath;
16307
17914
  }
16308
17915
  const { arch, os: os3 } = platformInfo;
@@ -16310,21 +17917,21 @@ async function downloadAstGrep(version2 = DEFAULT_VERSION) {
16310
17917
  const downloadUrl = `https://github.com/${REPO2}/releases/download/${version2}/${assetName}`;
16311
17918
  console.log(`[oh-my-opencode] Downloading ast-grep binary...`);
16312
17919
  try {
16313
- if (!existsSync13(cacheDir)) {
16314
- mkdirSync4(cacheDir, { recursive: true });
17920
+ if (!existsSync19(cacheDir)) {
17921
+ mkdirSync6(cacheDir, { recursive: true });
16315
17922
  }
16316
17923
  const response = await fetch(downloadUrl, { redirect: "follow" });
16317
17924
  if (!response.ok) {
16318
17925
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
16319
17926
  }
16320
- const archivePath = join14(cacheDir, assetName);
17927
+ const archivePath = join21(cacheDir, assetName);
16321
17928
  const arrayBuffer = await response.arrayBuffer();
16322
17929
  await Bun.write(archivePath, arrayBuffer);
16323
17930
  await extractZip2(archivePath, cacheDir);
16324
- if (existsSync13(archivePath)) {
16325
- unlinkSync4(archivePath);
17931
+ if (existsSync19(archivePath)) {
17932
+ unlinkSync5(archivePath);
16326
17933
  }
16327
- if (process.platform !== "win32" && existsSync13(binaryPath)) {
17934
+ if (process.platform !== "win32" && existsSync19(binaryPath)) {
16328
17935
  chmodSync2(binaryPath, 493);
16329
17936
  }
16330
17937
  console.log(`[oh-my-opencode] ast-grep binary ready.`);
@@ -16346,7 +17953,7 @@ async function ensureAstGrepBinary() {
16346
17953
  // src/tools/ast-grep/constants.ts
16347
17954
  function isValidBinary(filePath) {
16348
17955
  try {
16349
- return statSync2(filePath).size > 1e4;
17956
+ return statSync3(filePath).size > 1e4;
16350
17957
  } catch {
16351
17958
  return false;
16352
17959
  }
@@ -16375,8 +17982,8 @@ function findSgCliPathSync() {
16375
17982
  const require2 = createRequire4(import.meta.url);
16376
17983
  const cliPkgPath = require2.resolve("@ast-grep/cli/package.json");
16377
17984
  const cliDir = dirname3(cliPkgPath);
16378
- const sgPath = join15(cliDir, binaryName);
16379
- if (existsSync14(sgPath) && isValidBinary(sgPath)) {
17985
+ const sgPath = join22(cliDir, binaryName);
17986
+ if (existsSync20(sgPath) && isValidBinary(sgPath)) {
16380
17987
  return sgPath;
16381
17988
  }
16382
17989
  } catch {}
@@ -16387,8 +17994,8 @@ function findSgCliPathSync() {
16387
17994
  const pkgPath = require2.resolve(`${platformPkg}/package.json`);
16388
17995
  const pkgDir = dirname3(pkgPath);
16389
17996
  const astGrepName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep";
16390
- const binaryPath = join15(pkgDir, astGrepName);
16391
- if (existsSync14(binaryPath) && isValidBinary(binaryPath)) {
17997
+ const binaryPath = join22(pkgDir, astGrepName);
17998
+ if (existsSync20(binaryPath) && isValidBinary(binaryPath)) {
16392
17999
  return binaryPath;
16393
18000
  }
16394
18001
  } catch {}
@@ -16396,7 +18003,7 @@ function findSgCliPathSync() {
16396
18003
  if (process.platform === "darwin") {
16397
18004
  const homebrewPaths = ["/opt/homebrew/bin/sg", "/usr/local/bin/sg"];
16398
18005
  for (const path3 of homebrewPaths) {
16399
- if (existsSync14(path3) && isValidBinary(path3)) {
18006
+ if (existsSync20(path3) && isValidBinary(path3)) {
16400
18007
  return path3;
16401
18008
  }
16402
18009
  }
@@ -16451,12 +18058,12 @@ var DEFAULT_MAX_OUTPUT_BYTES = 1 * 1024 * 1024;
16451
18058
  var DEFAULT_MAX_MATCHES = 500;
16452
18059
 
16453
18060
  // src/tools/ast-grep/cli.ts
16454
- var {spawn: spawn5 } = globalThis.Bun;
16455
- import { existsSync as existsSync15 } from "fs";
18061
+ var {spawn: spawn6 } = globalThis.Bun;
18062
+ import { existsSync as existsSync21 } from "fs";
16456
18063
  var resolvedCliPath3 = null;
16457
18064
  var initPromise2 = null;
16458
18065
  async function getAstGrepPath() {
16459
- if (resolvedCliPath3 !== null && existsSync15(resolvedCliPath3)) {
18066
+ if (resolvedCliPath3 !== null && existsSync21(resolvedCliPath3)) {
16460
18067
  return resolvedCliPath3;
16461
18068
  }
16462
18069
  if (initPromise2) {
@@ -16464,7 +18071,7 @@ async function getAstGrepPath() {
16464
18071
  }
16465
18072
  initPromise2 = (async () => {
16466
18073
  const syncPath = findSgCliPathSync();
16467
- if (syncPath && existsSync15(syncPath)) {
18074
+ if (syncPath && existsSync21(syncPath)) {
16468
18075
  resolvedCliPath3 = syncPath;
16469
18076
  setSgCliPath(syncPath);
16470
18077
  return syncPath;
@@ -16498,14 +18105,14 @@ async function runSg(options) {
16498
18105
  const paths = options.paths && options.paths.length > 0 ? options.paths : ["."];
16499
18106
  args.push(...paths);
16500
18107
  let cliPath = getSgCliPath();
16501
- if (!existsSync15(cliPath) && cliPath !== "sg") {
18108
+ if (!existsSync21(cliPath) && cliPath !== "sg") {
16502
18109
  const downloadedPath = await getAstGrepPath();
16503
18110
  if (downloadedPath) {
16504
18111
  cliPath = downloadedPath;
16505
18112
  }
16506
18113
  }
16507
18114
  const timeout = DEFAULT_TIMEOUT_MS;
16508
- const proc = spawn5([cliPath, ...args], {
18115
+ const proc = spawn6([cliPath, ...args], {
16509
18116
  stdout: "pipe",
16510
18117
  stderr: "pipe"
16511
18118
  });
@@ -16759,11 +18366,11 @@ var ast_grep_replace = tool({
16759
18366
  }
16760
18367
  });
16761
18368
  // src/tools/grep/cli.ts
16762
- var {spawn: spawn6 } = globalThis.Bun;
18369
+ var {spawn: spawn7 } = globalThis.Bun;
16763
18370
 
16764
18371
  // src/tools/grep/constants.ts
16765
- import { existsSync as existsSync16 } from "fs";
16766
- import { join as join16, dirname as dirname4 } from "path";
18372
+ import { existsSync as existsSync22 } from "fs";
18373
+ import { join as join23, dirname as dirname4 } from "path";
16767
18374
  import { spawnSync } from "child_process";
16768
18375
  var cachedCli = null;
16769
18376
  function findExecutable(name) {
@@ -16784,13 +18391,13 @@ function getOpenCodeBundledRg() {
16784
18391
  const isWindows = process.platform === "win32";
16785
18392
  const rgName = isWindows ? "rg.exe" : "rg";
16786
18393
  const candidates = [
16787
- join16(execDir, rgName),
16788
- join16(execDir, "bin", rgName),
16789
- join16(execDir, "..", "bin", rgName),
16790
- join16(execDir, "..", "libexec", rgName)
18394
+ join23(execDir, rgName),
18395
+ join23(execDir, "bin", rgName),
18396
+ join23(execDir, "..", "bin", rgName),
18397
+ join23(execDir, "..", "libexec", rgName)
16791
18398
  ];
16792
18399
  for (const candidate of candidates) {
16793
- if (existsSync16(candidate)) {
18400
+ if (existsSync22(candidate)) {
16794
18401
  return candidate;
16795
18402
  }
16796
18403
  }
@@ -16931,7 +18538,7 @@ async function runRg(options) {
16931
18538
  }
16932
18539
  const paths = options.paths?.length ? options.paths : ["."];
16933
18540
  args.push(...paths);
16934
- const proc = spawn6([cli.path, ...args], {
18541
+ const proc = spawn7([cli.path, ...args], {
16935
18542
  stdout: "pipe",
16936
18543
  stderr: "pipe"
16937
18544
  });
@@ -17033,7 +18640,7 @@ var grep = tool({
17033
18640
  });
17034
18641
 
17035
18642
  // src/tools/glob/cli.ts
17036
- var {spawn: spawn7 } = globalThis.Bun;
18643
+ var {spawn: spawn8 } = globalThis.Bun;
17037
18644
 
17038
18645
  // src/tools/glob/constants.ts
17039
18646
  var DEFAULT_TIMEOUT_MS3 = 60000;
@@ -17090,7 +18697,7 @@ async function runRgFiles(options) {
17090
18697
  args.push(...paths);
17091
18698
  }
17092
18699
  const cwd = paths[0] || ".";
17093
- const proc = spawn7([cli.path, ...args], {
18700
+ const proc = spawn8([cli.path, ...args], {
17094
18701
  stdout: "pipe",
17095
18702
  stderr: "pipe",
17096
18703
  cwd: isRg ? undefined : cwd
@@ -17188,145 +18795,14 @@ var glob = tool({
17188
18795
  }
17189
18796
  });
17190
18797
  // src/tools/slashcommand/tools.ts
17191
- import { existsSync as existsSync18, readdirSync as readdirSync5, readFileSync as readFileSync11 } from "fs";
17192
- import { homedir as homedir8 } from "os";
17193
- import { join as join18, basename as basename3, dirname as dirname5 } from "path";
17194
- // src/shared/command-executor.ts
17195
- import { exec } from "child_process";
17196
- import { promisify } from "util";
17197
- var execAsync = promisify(exec);
17198
- async function executeCommand(command) {
17199
- try {
17200
- const { stdout, stderr } = await execAsync(command);
17201
- const out = stdout?.toString().trim() ?? "";
17202
- const err = stderr?.toString().trim() ?? "";
17203
- if (err) {
17204
- if (out) {
17205
- return `${out}
17206
- [stderr: ${err}]`;
17207
- }
17208
- return `[stderr: ${err}]`;
17209
- }
17210
- return out;
17211
- } catch (error45) {
17212
- const e = error45;
17213
- const stdout = e?.stdout?.toString().trim() ?? "";
17214
- const stderr = e?.stderr?.toString().trim() ?? "";
17215
- const errMsg = stderr || e?.message || String(error45);
17216
- if (stdout) {
17217
- return `${stdout}
17218
- [stderr: ${errMsg}]`;
17219
- }
17220
- return `[stderr: ${errMsg}]`;
17221
- }
17222
- }
17223
- var COMMAND_PATTERN = /!`([^`]+)`/g;
17224
- function findCommands(text) {
17225
- const matches = [];
17226
- let match;
17227
- COMMAND_PATTERN.lastIndex = 0;
17228
- while ((match = COMMAND_PATTERN.exec(text)) !== null) {
17229
- matches.push({
17230
- fullMatch: match[0],
17231
- command: match[1],
17232
- start: match.index,
17233
- end: match.index + match[0].length
17234
- });
17235
- }
17236
- return matches;
17237
- }
17238
- async function resolveCommandsInText(text, depth = 0, maxDepth = 3) {
17239
- if (depth >= maxDepth) {
17240
- return text;
17241
- }
17242
- const matches = findCommands(text);
17243
- if (matches.length === 0) {
17244
- return text;
17245
- }
17246
- const tasks = matches.map((m) => executeCommand(m.command));
17247
- const results = await Promise.allSettled(tasks);
17248
- const replacements = new Map;
17249
- matches.forEach((match, idx) => {
17250
- const result = results[idx];
17251
- if (result.status === "rejected") {
17252
- replacements.set(match.fullMatch, `[error: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}]`);
17253
- } else {
17254
- replacements.set(match.fullMatch, result.value);
17255
- }
17256
- });
17257
- let resolved = text;
17258
- for (const [pattern, replacement] of replacements.entries()) {
17259
- resolved = resolved.split(pattern).join(replacement);
17260
- }
17261
- if (findCommands(resolved).length > 0) {
17262
- return resolveCommandsInText(resolved, depth + 1, maxDepth);
17263
- }
17264
- return resolved;
17265
- }
17266
- // src/shared/file-reference-resolver.ts
17267
- import { existsSync as existsSync17, readFileSync as readFileSync10, statSync as statSync3 } from "fs";
17268
- import { join as join17, isAbsolute } from "path";
17269
- var FILE_REFERENCE_PATTERN = /@([^\s@]+)/g;
17270
- function findFileReferences(text) {
17271
- const matches = [];
17272
- let match;
17273
- FILE_REFERENCE_PATTERN.lastIndex = 0;
17274
- while ((match = FILE_REFERENCE_PATTERN.exec(text)) !== null) {
17275
- matches.push({
17276
- fullMatch: match[0],
17277
- filePath: match[1],
17278
- start: match.index,
17279
- end: match.index + match[0].length
17280
- });
17281
- }
17282
- return matches;
17283
- }
17284
- function resolveFilePath(filePath, cwd) {
17285
- if (isAbsolute(filePath)) {
17286
- return filePath;
17287
- }
17288
- return join17(cwd, filePath);
17289
- }
17290
- function readFileContent(resolvedPath) {
17291
- if (!existsSync17(resolvedPath)) {
17292
- return `[file not found: ${resolvedPath}]`;
17293
- }
17294
- const stat2 = statSync3(resolvedPath);
17295
- if (stat2.isDirectory()) {
17296
- return `[cannot read directory: ${resolvedPath}]`;
17297
- }
17298
- const content = readFileSync10(resolvedPath, "utf-8");
17299
- return content;
17300
- }
17301
- async function resolveFileReferencesInText(text, cwd = process.cwd(), depth = 0, maxDepth = 3) {
17302
- if (depth >= maxDepth) {
17303
- return text;
17304
- }
17305
- const matches = findFileReferences(text);
17306
- if (matches.length === 0) {
17307
- return text;
17308
- }
17309
- const replacements = new Map;
17310
- for (const match of matches) {
17311
- const resolvedPath = resolveFilePath(match.filePath, cwd);
17312
- const content = readFileContent(resolvedPath);
17313
- replacements.set(match.fullMatch, content);
17314
- }
17315
- let resolved = text;
17316
- for (const [pattern, replacement] of replacements.entries()) {
17317
- resolved = resolved.split(pattern).join(replacement);
17318
- }
17319
- if (findFileReferences(resolved).length > 0 && depth + 1 < maxDepth) {
17320
- return resolveFileReferencesInText(resolved, cwd, depth + 1, maxDepth);
17321
- }
17322
- return resolved;
17323
- }
17324
- // src/tools/slashcommand/tools.ts
18798
+ import { existsSync as existsSync23, readdirSync as readdirSync6, readFileSync as readFileSync12 } from "fs";
18799
+ import { homedir as homedir13 } from "os";
18800
+ import { join as join24, basename as basename3, dirname as dirname5 } from "path";
17325
18801
  function discoverCommandsFromDir(commandsDir, scope) {
17326
- if (!existsSync18(commandsDir)) {
18802
+ if (!existsSync23(commandsDir)) {
17327
18803
  return [];
17328
18804
  }
17329
- const entries = readdirSync5(commandsDir, { withFileTypes: true });
18805
+ const entries = readdirSync6(commandsDir, { withFileTypes: true });
17330
18806
  const commands = [];
17331
18807
  for (const entry of entries) {
17332
18808
  if (entry.name.startsWith("."))
@@ -17335,10 +18811,10 @@ function discoverCommandsFromDir(commandsDir, scope) {
17335
18811
  continue;
17336
18812
  if (!entry.isFile())
17337
18813
  continue;
17338
- const commandPath = join18(commandsDir, entry.name);
18814
+ const commandPath = join24(commandsDir, entry.name);
17339
18815
  const commandName = basename3(entry.name, ".md");
17340
18816
  try {
17341
- const content = readFileSync11(commandPath, "utf-8");
18817
+ const content = readFileSync12(commandPath, "utf-8");
17342
18818
  const { data, body } = parseFrontmatter(content);
17343
18819
  const metadata = {
17344
18820
  name: commandName,
@@ -17362,10 +18838,10 @@ function discoverCommandsFromDir(commandsDir, scope) {
17362
18838
  return commands;
17363
18839
  }
17364
18840
  function discoverCommandsSync() {
17365
- const userCommandsDir = join18(homedir8(), ".claude", "commands");
17366
- const projectCommandsDir = join18(process.cwd(), ".claude", "commands");
17367
- const opencodeGlobalDir = join18(homedir8(), ".config", "opencode", "command");
17368
- const opencodeProjectDir = join18(process.cwd(), ".opencode", "command");
18841
+ const userCommandsDir = join24(homedir13(), ".claude", "commands");
18842
+ const projectCommandsDir = join24(process.cwd(), ".claude", "commands");
18843
+ const opencodeGlobalDir = join24(homedir13(), ".config", "opencode", "command");
18844
+ const opencodeProjectDir = join24(process.cwd(), ".opencode", "command");
17369
18845
  const userCommands = discoverCommandsFromDir(userCommandsDir, "user");
17370
18846
  const opencodeGlobalCommands = discoverCommandsFromDir(opencodeGlobalDir, "opencode");
17371
18847
  const projectCommands = discoverCommandsFromDir(projectCommandsDir, "project");
@@ -17489,19 +18965,19 @@ Try a different command name.`;
17489
18965
  }
17490
18966
  });
17491
18967
  // src/tools/skill/tools.ts
17492
- import { existsSync as existsSync19, readdirSync as readdirSync6, statSync as statSync4, readlinkSync as readlinkSync2, readFileSync as readFileSync12 } from "fs";
17493
- import { homedir as homedir9 } from "os";
17494
- import { join as join19, resolve as resolve5, basename as basename4 } from "path";
18968
+ import { existsSync as existsSync24, readdirSync as readdirSync7, statSync as statSync4, readlinkSync as readlinkSync2, readFileSync as readFileSync13 } from "fs";
18969
+ import { homedir as homedir14 } from "os";
18970
+ import { join as join25, resolve as resolve5, basename as basename4 } from "path";
17495
18971
  function discoverSkillsFromDir(skillsDir, scope) {
17496
- if (!existsSync19(skillsDir)) {
18972
+ if (!existsSync24(skillsDir)) {
17497
18973
  return [];
17498
18974
  }
17499
- const entries = readdirSync6(skillsDir, { withFileTypes: true });
18975
+ const entries = readdirSync7(skillsDir, { withFileTypes: true });
17500
18976
  const skills = [];
17501
18977
  for (const entry of entries) {
17502
18978
  if (entry.name.startsWith("."))
17503
18979
  continue;
17504
- const skillPath = join19(skillsDir, entry.name);
18980
+ const skillPath = join25(skillsDir, entry.name);
17505
18981
  if (entry.isDirectory() || entry.isSymbolicLink()) {
17506
18982
  let resolvedPath = skillPath;
17507
18983
  try {
@@ -17512,11 +18988,11 @@ function discoverSkillsFromDir(skillsDir, scope) {
17512
18988
  } catch {
17513
18989
  continue;
17514
18990
  }
17515
- const skillMdPath = join19(resolvedPath, "SKILL.md");
17516
- if (!existsSync19(skillMdPath))
18991
+ const skillMdPath = join25(resolvedPath, "SKILL.md");
18992
+ if (!existsSync24(skillMdPath))
17517
18993
  continue;
17518
18994
  try {
17519
- const content = readFileSync12(skillMdPath, "utf-8");
18995
+ const content = readFileSync13(skillMdPath, "utf-8");
17520
18996
  const { data } = parseFrontmatter(content);
17521
18997
  skills.push({
17522
18998
  name: data.name || entry.name,
@@ -17531,8 +19007,8 @@ function discoverSkillsFromDir(skillsDir, scope) {
17531
19007
  return skills;
17532
19008
  }
17533
19009
  function discoverSkillsSync() {
17534
- const userSkillsDir = join19(homedir9(), ".claude", "skills");
17535
- const projectSkillsDir = join19(process.cwd(), ".claude", "skills");
19010
+ const userSkillsDir = join25(homedir14(), ".claude", "skills");
19011
+ const projectSkillsDir = join25(process.cwd(), ".claude", "skills");
17536
19012
  const userSkills = discoverSkillsFromDir(userSkillsDir, "user");
17537
19013
  const projectSkills = discoverSkillsFromDir(projectSkillsDir, "project");
17538
19014
  return [...projectSkills, ...userSkills];
@@ -17553,12 +19029,12 @@ function resolveSymlink(skillPath) {
17553
19029
  }
17554
19030
  async function parseSkillMd(skillPath) {
17555
19031
  const resolvedPath = resolveSymlink(skillPath);
17556
- const skillMdPath = join19(resolvedPath, "SKILL.md");
17557
- if (!existsSync19(skillMdPath)) {
19032
+ const skillMdPath = join25(resolvedPath, "SKILL.md");
19033
+ if (!existsSync24(skillMdPath)) {
17558
19034
  return null;
17559
19035
  }
17560
19036
  try {
17561
- let content = readFileSync12(skillMdPath, "utf-8");
19037
+ let content = readFileSync13(skillMdPath, "utf-8");
17562
19038
  content = await resolveCommandsInText(content);
17563
19039
  const { data, body } = parseFrontmatter(content);
17564
19040
  const metadata = {
@@ -17566,12 +19042,12 @@ async function parseSkillMd(skillPath) {
17566
19042
  description: data.description || "",
17567
19043
  license: data.license
17568
19044
  };
17569
- const referencesDir = join19(resolvedPath, "references");
17570
- const scriptsDir = join19(resolvedPath, "scripts");
17571
- const assetsDir = join19(resolvedPath, "assets");
17572
- const references = existsSync19(referencesDir) ? readdirSync6(referencesDir).filter((f) => !f.startsWith(".")) : [];
17573
- const scripts = existsSync19(scriptsDir) ? readdirSync6(scriptsDir).filter((f) => !f.startsWith(".") && !f.startsWith("__")) : [];
17574
- const assets = existsSync19(assetsDir) ? readdirSync6(assetsDir).filter((f) => !f.startsWith(".")) : [];
19045
+ const referencesDir = join25(resolvedPath, "references");
19046
+ const scriptsDir = join25(resolvedPath, "scripts");
19047
+ const assetsDir = join25(resolvedPath, "assets");
19048
+ const references = existsSync24(referencesDir) ? readdirSync7(referencesDir).filter((f) => !f.startsWith(".")) : [];
19049
+ const scripts = existsSync24(scriptsDir) ? readdirSync7(scriptsDir).filter((f) => !f.startsWith(".") && !f.startsWith("__")) : [];
19050
+ const assets = existsSync24(assetsDir) ? readdirSync7(assetsDir).filter((f) => !f.startsWith(".")) : [];
17575
19051
  return {
17576
19052
  name: metadata.name,
17577
19053
  path: resolvedPath,
@@ -17586,15 +19062,15 @@ async function parseSkillMd(skillPath) {
17586
19062
  }
17587
19063
  }
17588
19064
  async function discoverSkillsFromDirAsync(skillsDir) {
17589
- if (!existsSync19(skillsDir)) {
19065
+ if (!existsSync24(skillsDir)) {
17590
19066
  return [];
17591
19067
  }
17592
- const entries = readdirSync6(skillsDir, { withFileTypes: true });
19068
+ const entries = readdirSync7(skillsDir, { withFileTypes: true });
17593
19069
  const skills = [];
17594
19070
  for (const entry of entries) {
17595
19071
  if (entry.name.startsWith("."))
17596
19072
  continue;
17597
- const skillPath = join19(skillsDir, entry.name);
19073
+ const skillPath = join25(skillsDir, entry.name);
17598
19074
  if (entry.isDirectory() || entry.isSymbolicLink()) {
17599
19075
  const skillInfo = await parseSkillMd(skillPath);
17600
19076
  if (skillInfo) {
@@ -17605,8 +19081,8 @@ async function discoverSkillsFromDirAsync(skillsDir) {
17605
19081
  return skills;
17606
19082
  }
17607
19083
  async function discoverSkills() {
17608
- const userSkillsDir = join19(homedir9(), ".claude", "skills");
17609
- const projectSkillsDir = join19(process.cwd(), ".claude", "skills");
19084
+ const userSkillsDir = join25(homedir14(), ".claude", "skills");
19085
+ const projectSkillsDir = join25(process.cwd(), ".claude", "skills");
17610
19086
  const userSkills = await discoverSkillsFromDirAsync(userSkillsDir);
17611
19087
  const projectSkills = await discoverSkillsFromDirAsync(projectSkillsDir);
17612
19088
  return [...projectSkills, ...userSkills];
@@ -17635,9 +19111,9 @@ async function loadSkillWithReferences(skill, includeRefs) {
17635
19111
  const referencesLoaded = [];
17636
19112
  if (includeRefs && skill.references.length > 0) {
17637
19113
  for (const ref of skill.references) {
17638
- const refPath = join19(skill.path, "references", ref);
19114
+ const refPath = join25(skill.path, "references", ref);
17639
19115
  try {
17640
- let content = readFileSync12(refPath, "utf-8");
19116
+ let content = readFileSync13(refPath, "utf-8");
17641
19117
  content = await resolveCommandsInText(content);
17642
19118
  referencesLoaded.push({ path: ref, content });
17643
19119
  } catch {}
@@ -17870,6 +19346,7 @@ function loadPluginConfig(directory) {
17870
19346
  return {};
17871
19347
  }
17872
19348
  var OhMyOpenCodePlugin = async (ctx) => {
19349
+ const pluginConfig = loadPluginConfig(ctx.directory);
17873
19350
  const todoContinuationEnforcer = createTodoContinuationEnforcer(ctx);
17874
19351
  const contextWindowMonitor = createContextWindowMonitorHook(ctx);
17875
19352
  const sessionRecovery = createSessionRecoveryHook(ctx);
@@ -17878,19 +19355,23 @@ var OhMyOpenCodePlugin = async (ctx) => {
17878
19355
  const directoryAgentsInjector = createDirectoryAgentsInjectorHook(ctx);
17879
19356
  const emptyTaskResponseDetector = createEmptyTaskResponseDetectorHook(ctx);
17880
19357
  const thinkMode = createThinkModeHook();
19358
+ const claudeCodeHooks = createClaudeCodeHooksHook(ctx, {});
19359
+ const anthropicAutoCompact = createAnthropicAutoCompactHook(ctx);
17881
19360
  updateTerminalTitle({ sessionId: "main" });
17882
- const pluginConfig = loadPluginConfig(ctx.directory);
17883
19361
  return {
17884
19362
  tool: builtinTools,
19363
+ "chat.message": async (input, output) => {
19364
+ await claudeCodeHooks["chat.message"]?.(input, output);
19365
+ },
17885
19366
  config: async (config3) => {
17886
19367
  const builtinAgents = createBuiltinAgents(pluginConfig.disabled_agents, pluginConfig.agents);
17887
19368
  const userAgents = loadUserAgents();
17888
19369
  const projectAgents = loadProjectAgents();
17889
19370
  config3.agent = {
17890
- ...config3.agent,
17891
19371
  ...builtinAgents,
17892
19372
  ...userAgents,
17893
- ...projectAgents
19373
+ ...projectAgents,
19374
+ ...config3.agent
17894
19375
  };
17895
19376
  config3.tools = {
17896
19377
  ...config3.tools
@@ -17919,10 +19400,12 @@ var OhMyOpenCodePlugin = async (ctx) => {
17919
19400
  };
17920
19401
  },
17921
19402
  event: async (input) => {
19403
+ await claudeCodeHooks.event(input);
17922
19404
  await todoContinuationEnforcer(input);
17923
19405
  await contextWindowMonitor.event(input);
17924
19406
  await directoryAgentsInjector.event(input);
17925
19407
  await thinkMode.event(input);
19408
+ await anthropicAutoCompact.event(input);
17926
19409
  const { event } = input;
17927
19410
  const props = event.properties;
17928
19411
  if (event.type === "session.created") {
@@ -18002,6 +19485,7 @@ var OhMyOpenCodePlugin = async (ctx) => {
18002
19485
  }
18003
19486
  },
18004
19487
  "tool.execute.before": async (input, output) => {
19488
+ await claudeCodeHooks["tool.execute.before"](input, output);
18005
19489
  await commentChecker["tool.execute.before"](input, output);
18006
19490
  if (input.sessionID === getMainSessionID()) {
18007
19491
  updateTerminalTitle({
@@ -18014,6 +19498,7 @@ var OhMyOpenCodePlugin = async (ctx) => {
18014
19498
  }
18015
19499
  },
18016
19500
  "tool.execute.after": async (input, output) => {
19501
+ await claudeCodeHooks["tool.execute.after"](input, output);
18017
19502
  await grepOutputTruncator["tool.execute.after"](input, output);
18018
19503
  await contextWindowMonitor["tool.execute.after"](input, output);
18019
19504
  await commentChecker["tool.execute.after"](input, output);