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/README.ko.md +85 -15
- package/README.md +93 -34
- package/dist/features/hook-message-injector/constants.d.ts +3 -0
- package/dist/features/hook-message-injector/index.d.ts +2 -0
- package/dist/features/hook-message-injector/injector.d.ts +2 -0
- package/dist/features/hook-message-injector/types.d.ts +43 -0
- package/dist/hooks/claude-code-hooks/config-loader.d.ts +12 -0
- package/dist/hooks/claude-code-hooks/config.d.ts +3 -0
- package/dist/hooks/claude-code-hooks/index.d.ts +42 -0
- package/dist/hooks/claude-code-hooks/plugin-config.d.ts +8 -0
- package/dist/hooks/claude-code-hooks/post-tool-use.d.ts +40 -0
- package/dist/hooks/claude-code-hooks/pre-tool-use.d.ts +25 -0
- package/dist/hooks/claude-code-hooks/stop.d.ts +20 -0
- package/dist/hooks/claude-code-hooks/todo.d.ts +12 -0
- package/dist/hooks/claude-code-hooks/tool-input-cache.d.ts +5 -0
- package/dist/hooks/claude-code-hooks/transcript.d.ts +38 -0
- package/dist/hooks/claude-code-hooks/types.d.ts +166 -0
- package/dist/hooks/claude-code-hooks/user-prompt-submit.d.ts +22 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/session-recovery/storage.d.ts +1 -0
- package/dist/index.js +1910 -425
- package/dist/shared/hook-disabled.d.ts +2 -0
- package/dist/shared/index.d.ts +4 -0
- package/dist/shared/pattern-matcher.d.ts +3 -0
- package/dist/shared/snake-case.d.ts +4 -0
- package/dist/shared/tool-name.d.ts +1 -0
- package/package.json +1 -1
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
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
}
|
|
667
|
-
todos =
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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
|
-
|
|
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) =>
|
|
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 (
|
|
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
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
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
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
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
|
-
|
|
1986
|
-
|
|
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
|
|
1994
|
-
import { homedir as
|
|
1995
|
-
import { join as
|
|
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 (!
|
|
3654
|
+
if (!existsSync13(commandsDir)) {
|
|
2034
3655
|
return [];
|
|
2035
3656
|
}
|
|
2036
|
-
const entries =
|
|
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 =
|
|
3662
|
+
const commandPath = join16(commandsDir, entry.name);
|
|
2042
3663
|
const commandName = basename(entry.name, ".md");
|
|
2043
3664
|
try {
|
|
2044
|
-
const content =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
2104
|
-
import { homedir as
|
|
2105
|
-
import { join as
|
|
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 (!
|
|
3728
|
+
if (!existsSync14(skillsDir)) {
|
|
2108
3729
|
return [];
|
|
2109
3730
|
}
|
|
2110
|
-
const entries =
|
|
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 =
|
|
3736
|
+
const skillPath = join17(skillsDir, entry.name);
|
|
2116
3737
|
if (!entry.isDirectory() && !entry.isSymbolicLink())
|
|
2117
3738
|
continue;
|
|
2118
3739
|
let resolvedPath = skillPath;
|
|
2119
|
-
if (
|
|
3740
|
+
if (statSync2(skillPath, { throwIfNoEntry: false })?.isSymbolicLink()) {
|
|
2120
3741
|
resolvedPath = resolve2(skillPath, "..", readlinkSync(skillPath));
|
|
2121
3742
|
}
|
|
2122
|
-
const skillMdPath =
|
|
2123
|
-
if (!
|
|
3743
|
+
const skillMdPath = join17(resolvedPath, "SKILL.md");
|
|
3744
|
+
if (!existsSync14(skillMdPath))
|
|
2124
3745
|
continue;
|
|
2125
3746
|
try {
|
|
2126
|
-
const content =
|
|
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 =
|
|
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 =
|
|
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
|
|
2174
|
-
import { homedir as
|
|
2175
|
-
import { join as
|
|
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 (!
|
|
3813
|
+
if (!existsSync15(agentsDir)) {
|
|
2193
3814
|
return [];
|
|
2194
3815
|
}
|
|
2195
|
-
const entries =
|
|
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 =
|
|
3821
|
+
const agentPath = join18(agentsDir, entry.name);
|
|
2201
3822
|
const agentName = basename2(entry.name, ".md");
|
|
2202
3823
|
try {
|
|
2203
|
-
const content =
|
|
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 =
|
|
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 =
|
|
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
|
|
2249
|
-
import { homedir as
|
|
2250
|
-
import { join as
|
|
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 =
|
|
3937
|
+
const home = homedir10();
|
|
2331
3938
|
const cwd = process.cwd();
|
|
2332
3939
|
return [
|
|
2333
|
-
{ path:
|
|
2334
|
-
{ path:
|
|
2335
|
-
{ path:
|
|
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 (!
|
|
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
|
|
2381
|
-
var
|
|
3987
|
+
var sessionErrorState2 = new Map;
|
|
3988
|
+
var sessionInterruptState2 = new Map;
|
|
2382
3989
|
var subagentSessions = new Set;
|
|
2383
|
-
var
|
|
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
|
|
2626
|
-
import { join as
|
|
2627
|
-
import { homedir as
|
|
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 (!
|
|
4236
|
+
if (!existsSync17(path3))
|
|
2630
4237
|
return null;
|
|
2631
4238
|
try {
|
|
2632
|
-
return JSON.parse(
|
|
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:
|
|
2641
|
-
user:
|
|
2642
|
-
opencode:
|
|
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 (
|
|
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:
|
|
2785
|
-
import { readFileSync as
|
|
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 =
|
|
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 =
|
|
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
|
|
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 (!
|
|
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 (
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
3534
|
-
|
|
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: () =>
|
|
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
|
|
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 (
|
|
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 (!
|
|
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 (!
|
|
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 (
|
|
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 (!
|
|
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
|
|
16235
|
-
import { existsSync as
|
|
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:
|
|
16239
|
-
import { existsSync as
|
|
16240
|
-
import { join as
|
|
16241
|
-
import { homedir as
|
|
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 ||
|
|
16267
|
-
return
|
|
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 ||
|
|
16271
|
-
return
|
|
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 =
|
|
16278
|
-
return
|
|
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" ?
|
|
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" }) :
|
|
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 =
|
|
16305
|
-
if (
|
|
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 (!
|
|
16314
|
-
|
|
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 =
|
|
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 (
|
|
16325
|
-
|
|
17931
|
+
if (existsSync19(archivePath)) {
|
|
17932
|
+
unlinkSync5(archivePath);
|
|
16326
17933
|
}
|
|
16327
|
-
if (process.platform !== "win32" &&
|
|
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
|
|
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 =
|
|
16379
|
-
if (
|
|
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 =
|
|
16391
|
-
if (
|
|
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 (
|
|
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:
|
|
16455
|
-
import { existsSync as
|
|
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 &&
|
|
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 &&
|
|
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 (!
|
|
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 =
|
|
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:
|
|
18369
|
+
var {spawn: spawn7 } = globalThis.Bun;
|
|
16763
18370
|
|
|
16764
18371
|
// src/tools/grep/constants.ts
|
|
16765
|
-
import { existsSync as
|
|
16766
|
-
import { join as
|
|
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
|
-
|
|
16788
|
-
|
|
16789
|
-
|
|
16790
|
-
|
|
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 (
|
|
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 =
|
|
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:
|
|
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 =
|
|
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
|
|
17192
|
-
import { homedir as
|
|
17193
|
-
import { join as
|
|
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 (!
|
|
18802
|
+
if (!existsSync23(commandsDir)) {
|
|
17327
18803
|
return [];
|
|
17328
18804
|
}
|
|
17329
|
-
const entries =
|
|
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 =
|
|
18814
|
+
const commandPath = join24(commandsDir, entry.name);
|
|
17339
18815
|
const commandName = basename3(entry.name, ".md");
|
|
17340
18816
|
try {
|
|
17341
|
-
const content =
|
|
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 =
|
|
17366
|
-
const projectCommandsDir =
|
|
17367
|
-
const opencodeGlobalDir =
|
|
17368
|
-
const opencodeProjectDir =
|
|
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
|
|
17493
|
-
import { homedir as
|
|
17494
|
-
import { join as
|
|
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 (!
|
|
18972
|
+
if (!existsSync24(skillsDir)) {
|
|
17497
18973
|
return [];
|
|
17498
18974
|
}
|
|
17499
|
-
const entries =
|
|
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 =
|
|
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 =
|
|
17516
|
-
if (!
|
|
18991
|
+
const skillMdPath = join25(resolvedPath, "SKILL.md");
|
|
18992
|
+
if (!existsSync24(skillMdPath))
|
|
17517
18993
|
continue;
|
|
17518
18994
|
try {
|
|
17519
|
-
const content =
|
|
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 =
|
|
17535
|
-
const projectSkillsDir =
|
|
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 =
|
|
17557
|
-
if (!
|
|
19032
|
+
const skillMdPath = join25(resolvedPath, "SKILL.md");
|
|
19033
|
+
if (!existsSync24(skillMdPath)) {
|
|
17558
19034
|
return null;
|
|
17559
19035
|
}
|
|
17560
19036
|
try {
|
|
17561
|
-
let content =
|
|
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 =
|
|
17570
|
-
const scriptsDir =
|
|
17571
|
-
const assetsDir =
|
|
17572
|
-
const references =
|
|
17573
|
-
const scripts =
|
|
17574
|
-
const assets =
|
|
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 (!
|
|
19065
|
+
if (!existsSync24(skillsDir)) {
|
|
17590
19066
|
return [];
|
|
17591
19067
|
}
|
|
17592
|
-
const entries =
|
|
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 =
|
|
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 =
|
|
17609
|
-
const projectSkillsDir =
|
|
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 =
|
|
19114
|
+
const refPath = join25(skill.path, "references", ref);
|
|
17639
19115
|
try {
|
|
17640
|
-
let content =
|
|
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);
|