oh-my-opencode 0.3.0 → 0.3.2
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 +22 -7
- package/README.md +30 -14
- package/dist/hooks/directory-readme-injector/constants.d.ts +3 -0
- package/dist/hooks/directory-readme-injector/index.d.ts +22 -0
- package/dist/hooks/directory-readme-injector/storage.d.ts +3 -0
- package/dist/hooks/directory-readme-injector/types.d.ts +5 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/session-recovery/storage.d.ts +1 -0
- package/dist/index.js +824 -333
- package/dist/tools/skill/tools.d.ts +2 -1
- package/dist/tools/skill/types.d.ts +17 -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
|
+
}
|
|
1101
1135
|
}
|
|
1136
|
+
if (failedID) {
|
|
1137
|
+
if (injectTextPart(sessionID, failedID, "(interrupted)")) {
|
|
1138
|
+
return true;
|
|
1139
|
+
}
|
|
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);
|
|
@@ -1790,6 +1831,147 @@ ${content}`;
|
|
|
1790
1831
|
event: eventHandler
|
|
1791
1832
|
};
|
|
1792
1833
|
}
|
|
1834
|
+
// src/hooks/directory-readme-injector/index.ts
|
|
1835
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
|
|
1836
|
+
import { dirname as dirname3, join as join10, resolve as resolve2 } from "path";
|
|
1837
|
+
|
|
1838
|
+
// src/hooks/directory-readme-injector/storage.ts
|
|
1839
|
+
import {
|
|
1840
|
+
existsSync as existsSync7,
|
|
1841
|
+
mkdirSync as mkdirSync4,
|
|
1842
|
+
readFileSync as readFileSync4,
|
|
1843
|
+
writeFileSync as writeFileSync3,
|
|
1844
|
+
unlinkSync as unlinkSync4
|
|
1845
|
+
} from "fs";
|
|
1846
|
+
import { join as join9 } from "path";
|
|
1847
|
+
|
|
1848
|
+
// src/hooks/directory-readme-injector/constants.ts
|
|
1849
|
+
import { join as join8 } from "path";
|
|
1850
|
+
var OPENCODE_STORAGE3 = join8(xdgData ?? "", "opencode", "storage");
|
|
1851
|
+
var README_INJECTOR_STORAGE = join8(OPENCODE_STORAGE3, "directory-readme");
|
|
1852
|
+
var README_FILENAME = "README.md";
|
|
1853
|
+
|
|
1854
|
+
// src/hooks/directory-readme-injector/storage.ts
|
|
1855
|
+
function getStoragePath2(sessionID) {
|
|
1856
|
+
return join9(README_INJECTOR_STORAGE, `${sessionID}.json`);
|
|
1857
|
+
}
|
|
1858
|
+
function loadInjectedPaths2(sessionID) {
|
|
1859
|
+
const filePath = getStoragePath2(sessionID);
|
|
1860
|
+
if (!existsSync7(filePath))
|
|
1861
|
+
return new Set;
|
|
1862
|
+
try {
|
|
1863
|
+
const content = readFileSync4(filePath, "utf-8");
|
|
1864
|
+
const data = JSON.parse(content);
|
|
1865
|
+
return new Set(data.injectedPaths);
|
|
1866
|
+
} catch {
|
|
1867
|
+
return new Set;
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
function saveInjectedPaths2(sessionID, paths) {
|
|
1871
|
+
if (!existsSync7(README_INJECTOR_STORAGE)) {
|
|
1872
|
+
mkdirSync4(README_INJECTOR_STORAGE, { recursive: true });
|
|
1873
|
+
}
|
|
1874
|
+
const data = {
|
|
1875
|
+
sessionID,
|
|
1876
|
+
injectedPaths: [...paths],
|
|
1877
|
+
updatedAt: Date.now()
|
|
1878
|
+
};
|
|
1879
|
+
writeFileSync3(getStoragePath2(sessionID), JSON.stringify(data, null, 2));
|
|
1880
|
+
}
|
|
1881
|
+
function clearInjectedPaths2(sessionID) {
|
|
1882
|
+
const filePath = getStoragePath2(sessionID);
|
|
1883
|
+
if (existsSync7(filePath)) {
|
|
1884
|
+
unlinkSync4(filePath);
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
// src/hooks/directory-readme-injector/index.ts
|
|
1889
|
+
function createDirectoryReadmeInjectorHook(ctx) {
|
|
1890
|
+
const sessionCaches = new Map;
|
|
1891
|
+
function getSessionCache(sessionID) {
|
|
1892
|
+
if (!sessionCaches.has(sessionID)) {
|
|
1893
|
+
sessionCaches.set(sessionID, loadInjectedPaths2(sessionID));
|
|
1894
|
+
}
|
|
1895
|
+
return sessionCaches.get(sessionID);
|
|
1896
|
+
}
|
|
1897
|
+
function resolveFilePath(title) {
|
|
1898
|
+
if (!title)
|
|
1899
|
+
return null;
|
|
1900
|
+
if (title.startsWith("/"))
|
|
1901
|
+
return title;
|
|
1902
|
+
return resolve2(ctx.directory, title);
|
|
1903
|
+
}
|
|
1904
|
+
function findReadmeMdUp(startDir) {
|
|
1905
|
+
const found = [];
|
|
1906
|
+
let current = startDir;
|
|
1907
|
+
while (true) {
|
|
1908
|
+
const readmePath = join10(current, README_FILENAME);
|
|
1909
|
+
if (existsSync8(readmePath)) {
|
|
1910
|
+
found.push(readmePath);
|
|
1911
|
+
}
|
|
1912
|
+
if (current === ctx.directory)
|
|
1913
|
+
break;
|
|
1914
|
+
const parent = dirname3(current);
|
|
1915
|
+
if (parent === current)
|
|
1916
|
+
break;
|
|
1917
|
+
if (!parent.startsWith(ctx.directory))
|
|
1918
|
+
break;
|
|
1919
|
+
current = parent;
|
|
1920
|
+
}
|
|
1921
|
+
return found.reverse();
|
|
1922
|
+
}
|
|
1923
|
+
const toolExecuteAfter = async (input, output) => {
|
|
1924
|
+
if (input.tool.toLowerCase() !== "read")
|
|
1925
|
+
return;
|
|
1926
|
+
const filePath = resolveFilePath(output.title);
|
|
1927
|
+
if (!filePath)
|
|
1928
|
+
return;
|
|
1929
|
+
const dir = dirname3(filePath);
|
|
1930
|
+
const cache = getSessionCache(input.sessionID);
|
|
1931
|
+
const readmePaths = findReadmeMdUp(dir);
|
|
1932
|
+
const toInject = [];
|
|
1933
|
+
for (const readmePath of readmePaths) {
|
|
1934
|
+
const readmeDir = dirname3(readmePath);
|
|
1935
|
+
if (cache.has(readmeDir))
|
|
1936
|
+
continue;
|
|
1937
|
+
try {
|
|
1938
|
+
const content = readFileSync5(readmePath, "utf-8");
|
|
1939
|
+
toInject.push({ path: readmePath, content });
|
|
1940
|
+
cache.add(readmeDir);
|
|
1941
|
+
} catch {}
|
|
1942
|
+
}
|
|
1943
|
+
if (toInject.length === 0)
|
|
1944
|
+
return;
|
|
1945
|
+
for (const { path: path2, content } of toInject) {
|
|
1946
|
+
output.output += `
|
|
1947
|
+
|
|
1948
|
+
[Project README: ${path2}]
|
|
1949
|
+
${content}`;
|
|
1950
|
+
}
|
|
1951
|
+
saveInjectedPaths2(input.sessionID, cache);
|
|
1952
|
+
};
|
|
1953
|
+
const eventHandler = async ({ event }) => {
|
|
1954
|
+
const props = event.properties;
|
|
1955
|
+
if (event.type === "session.deleted") {
|
|
1956
|
+
const sessionInfo = props?.info;
|
|
1957
|
+
if (sessionInfo?.id) {
|
|
1958
|
+
sessionCaches.delete(sessionInfo.id);
|
|
1959
|
+
clearInjectedPaths2(sessionInfo.id);
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
if (event.type === "session.compacted") {
|
|
1963
|
+
const sessionID = props?.sessionID ?? props?.info?.id;
|
|
1964
|
+
if (sessionID) {
|
|
1965
|
+
sessionCaches.delete(sessionID);
|
|
1966
|
+
clearInjectedPaths2(sessionID);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
};
|
|
1970
|
+
return {
|
|
1971
|
+
"tool.execute.after": toolExecuteAfter,
|
|
1972
|
+
event: eventHandler
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1793
1975
|
// src/hooks/empty-task-response-detector.ts
|
|
1794
1976
|
var EMPTY_RESPONSE_WARNING = `[Task Empty Response Warning]
|
|
1795
1977
|
|
|
@@ -1811,6 +1993,280 @@ function createEmptyTaskResponseDetectorHook(_ctx) {
|
|
|
1811
1993
|
}
|
|
1812
1994
|
};
|
|
1813
1995
|
}
|
|
1996
|
+
// src/hooks/anthropic-auto-compact/parser.ts
|
|
1997
|
+
var TOKEN_LIMIT_PATTERNS = [
|
|
1998
|
+
/(\d+)\s*tokens?\s*>\s*(\d+)\s*maximum/i,
|
|
1999
|
+
/prompt.*?(\d+).*?tokens.*?exceeds.*?(\d+)/i,
|
|
2000
|
+
/(\d+).*?tokens.*?limit.*?(\d+)/i,
|
|
2001
|
+
/context.*?length.*?(\d+).*?maximum.*?(\d+)/i,
|
|
2002
|
+
/max.*?context.*?(\d+).*?but.*?(\d+)/i
|
|
2003
|
+
];
|
|
2004
|
+
var TOKEN_LIMIT_KEYWORDS = [
|
|
2005
|
+
"prompt is too long",
|
|
2006
|
+
"is too long",
|
|
2007
|
+
"context_length_exceeded",
|
|
2008
|
+
"max_tokens",
|
|
2009
|
+
"token limit",
|
|
2010
|
+
"context length",
|
|
2011
|
+
"too many tokens"
|
|
2012
|
+
];
|
|
2013
|
+
function extractTokensFromMessage(message) {
|
|
2014
|
+
for (const pattern of TOKEN_LIMIT_PATTERNS) {
|
|
2015
|
+
const match = message.match(pattern);
|
|
2016
|
+
if (match) {
|
|
2017
|
+
const num1 = parseInt(match[1], 10);
|
|
2018
|
+
const num2 = parseInt(match[2], 10);
|
|
2019
|
+
return num1 > num2 ? { current: num1, max: num2 } : { current: num2, max: num1 };
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
return null;
|
|
2023
|
+
}
|
|
2024
|
+
function isTokenLimitError(text) {
|
|
2025
|
+
const lower = text.toLowerCase();
|
|
2026
|
+
return TOKEN_LIMIT_KEYWORDS.some((kw) => lower.includes(kw.toLowerCase()));
|
|
2027
|
+
}
|
|
2028
|
+
function parseAnthropicTokenLimitError(err) {
|
|
2029
|
+
if (typeof err === "string") {
|
|
2030
|
+
if (isTokenLimitError(err)) {
|
|
2031
|
+
const tokens = extractTokensFromMessage(err);
|
|
2032
|
+
return {
|
|
2033
|
+
currentTokens: tokens?.current ?? 0,
|
|
2034
|
+
maxTokens: tokens?.max ?? 0,
|
|
2035
|
+
errorType: "token_limit_exceeded_string"
|
|
2036
|
+
};
|
|
2037
|
+
}
|
|
2038
|
+
return null;
|
|
2039
|
+
}
|
|
2040
|
+
if (!err || typeof err !== "object")
|
|
2041
|
+
return null;
|
|
2042
|
+
const errObj = err;
|
|
2043
|
+
const dataObj = errObj.data;
|
|
2044
|
+
const responseBody = dataObj?.responseBody;
|
|
2045
|
+
const errorMessage = errObj.message;
|
|
2046
|
+
const errorData = errObj.error;
|
|
2047
|
+
const nestedError = errorData?.error;
|
|
2048
|
+
const textSources = [];
|
|
2049
|
+
if (typeof responseBody === "string")
|
|
2050
|
+
textSources.push(responseBody);
|
|
2051
|
+
if (typeof errorMessage === "string")
|
|
2052
|
+
textSources.push(errorMessage);
|
|
2053
|
+
if (typeof errorData?.message === "string")
|
|
2054
|
+
textSources.push(errorData.message);
|
|
2055
|
+
if (typeof errObj.body === "string")
|
|
2056
|
+
textSources.push(errObj.body);
|
|
2057
|
+
if (typeof errObj.details === "string")
|
|
2058
|
+
textSources.push(errObj.details);
|
|
2059
|
+
if (typeof errObj.reason === "string")
|
|
2060
|
+
textSources.push(errObj.reason);
|
|
2061
|
+
if (typeof errObj.description === "string")
|
|
2062
|
+
textSources.push(errObj.description);
|
|
2063
|
+
if (typeof nestedError?.message === "string")
|
|
2064
|
+
textSources.push(nestedError.message);
|
|
2065
|
+
if (typeof dataObj?.message === "string")
|
|
2066
|
+
textSources.push(dataObj.message);
|
|
2067
|
+
if (typeof dataObj?.error === "string")
|
|
2068
|
+
textSources.push(dataObj.error);
|
|
2069
|
+
if (textSources.length === 0) {
|
|
2070
|
+
try {
|
|
2071
|
+
const jsonStr = JSON.stringify(errObj);
|
|
2072
|
+
if (isTokenLimitError(jsonStr)) {
|
|
2073
|
+
textSources.push(jsonStr);
|
|
2074
|
+
}
|
|
2075
|
+
} catch {}
|
|
2076
|
+
}
|
|
2077
|
+
const combinedText = textSources.join(" ");
|
|
2078
|
+
if (!isTokenLimitError(combinedText))
|
|
2079
|
+
return null;
|
|
2080
|
+
if (typeof responseBody === "string") {
|
|
2081
|
+
try {
|
|
2082
|
+
const jsonPatterns = [
|
|
2083
|
+
/data:\s*(\{[\s\S]*?\})\s*$/m,
|
|
2084
|
+
/(\{"type"\s*:\s*"error"[\s\S]*?\})/,
|
|
2085
|
+
/(\{[\s\S]*?"error"[\s\S]*?\})/
|
|
2086
|
+
];
|
|
2087
|
+
for (const pattern of jsonPatterns) {
|
|
2088
|
+
const dataMatch = responseBody.match(pattern);
|
|
2089
|
+
if (dataMatch) {
|
|
2090
|
+
try {
|
|
2091
|
+
const jsonData = JSON.parse(dataMatch[1]);
|
|
2092
|
+
const message = jsonData.error?.message || "";
|
|
2093
|
+
const tokens = extractTokensFromMessage(message);
|
|
2094
|
+
if (tokens) {
|
|
2095
|
+
return {
|
|
2096
|
+
currentTokens: tokens.current,
|
|
2097
|
+
maxTokens: tokens.max,
|
|
2098
|
+
requestId: jsonData.request_id,
|
|
2099
|
+
errorType: jsonData.error?.type || "token_limit_exceeded"
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2102
|
+
} catch {}
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
const bedrockJson = JSON.parse(responseBody);
|
|
2106
|
+
if (typeof bedrockJson.message === "string" && isTokenLimitError(bedrockJson.message)) {
|
|
2107
|
+
return {
|
|
2108
|
+
currentTokens: 0,
|
|
2109
|
+
maxTokens: 0,
|
|
2110
|
+
errorType: "bedrock_input_too_long"
|
|
2111
|
+
};
|
|
2112
|
+
}
|
|
2113
|
+
} catch {}
|
|
2114
|
+
}
|
|
2115
|
+
for (const text of textSources) {
|
|
2116
|
+
const tokens = extractTokensFromMessage(text);
|
|
2117
|
+
if (tokens) {
|
|
2118
|
+
return {
|
|
2119
|
+
currentTokens: tokens.current,
|
|
2120
|
+
maxTokens: tokens.max,
|
|
2121
|
+
errorType: "token_limit_exceeded"
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
if (isTokenLimitError(combinedText)) {
|
|
2126
|
+
return {
|
|
2127
|
+
currentTokens: 0,
|
|
2128
|
+
maxTokens: 0,
|
|
2129
|
+
errorType: "token_limit_exceeded_unknown"
|
|
2130
|
+
};
|
|
2131
|
+
}
|
|
2132
|
+
return null;
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
// src/hooks/anthropic-auto-compact/executor.ts
|
|
2136
|
+
async function getLastAssistant(sessionID, client, directory) {
|
|
2137
|
+
try {
|
|
2138
|
+
const resp = await client.session.messages({
|
|
2139
|
+
path: { id: sessionID },
|
|
2140
|
+
query: { directory }
|
|
2141
|
+
});
|
|
2142
|
+
const data = resp.data;
|
|
2143
|
+
if (!Array.isArray(data))
|
|
2144
|
+
return null;
|
|
2145
|
+
const reversed = [...data].reverse();
|
|
2146
|
+
const last = reversed.find((m) => {
|
|
2147
|
+
const msg = m;
|
|
2148
|
+
const info = msg.info;
|
|
2149
|
+
return info?.role === "assistant";
|
|
2150
|
+
});
|
|
2151
|
+
if (!last)
|
|
2152
|
+
return null;
|
|
2153
|
+
return last.info ?? null;
|
|
2154
|
+
} catch {
|
|
2155
|
+
return null;
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
async function executeCompact(sessionID, msg, autoCompactState, client, directory) {
|
|
2159
|
+
try {
|
|
2160
|
+
const providerID = msg.providerID;
|
|
2161
|
+
const modelID = msg.modelID;
|
|
2162
|
+
if (providerID && modelID) {
|
|
2163
|
+
await client.session.summarize({
|
|
2164
|
+
path: { id: sessionID },
|
|
2165
|
+
body: { providerID, modelID },
|
|
2166
|
+
query: { directory }
|
|
2167
|
+
});
|
|
2168
|
+
setTimeout(async () => {
|
|
2169
|
+
try {
|
|
2170
|
+
await client.tui.submitPrompt({ query: { directory } });
|
|
2171
|
+
} catch {}
|
|
2172
|
+
}, 500);
|
|
2173
|
+
}
|
|
2174
|
+
autoCompactState.pendingCompact.delete(sessionID);
|
|
2175
|
+
autoCompactState.errorDataBySession.delete(sessionID);
|
|
2176
|
+
} catch {}
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
// src/hooks/anthropic-auto-compact/index.ts
|
|
2180
|
+
function createAutoCompactState() {
|
|
2181
|
+
return {
|
|
2182
|
+
pendingCompact: new Set,
|
|
2183
|
+
errorDataBySession: new Map
|
|
2184
|
+
};
|
|
2185
|
+
}
|
|
2186
|
+
function createAnthropicAutoCompactHook(ctx) {
|
|
2187
|
+
const autoCompactState = createAutoCompactState();
|
|
2188
|
+
const eventHandler = async ({ event }) => {
|
|
2189
|
+
const props = event.properties;
|
|
2190
|
+
if (event.type === "session.deleted") {
|
|
2191
|
+
const sessionInfo = props?.info;
|
|
2192
|
+
if (sessionInfo?.id) {
|
|
2193
|
+
autoCompactState.pendingCompact.delete(sessionInfo.id);
|
|
2194
|
+
autoCompactState.errorDataBySession.delete(sessionInfo.id);
|
|
2195
|
+
}
|
|
2196
|
+
return;
|
|
2197
|
+
}
|
|
2198
|
+
if (event.type === "session.error") {
|
|
2199
|
+
const sessionID = props?.sessionID;
|
|
2200
|
+
if (!sessionID)
|
|
2201
|
+
return;
|
|
2202
|
+
const parsed = parseAnthropicTokenLimitError(props?.error);
|
|
2203
|
+
if (parsed) {
|
|
2204
|
+
autoCompactState.pendingCompact.add(sessionID);
|
|
2205
|
+
autoCompactState.errorDataBySession.set(sessionID, parsed);
|
|
2206
|
+
}
|
|
2207
|
+
return;
|
|
2208
|
+
}
|
|
2209
|
+
if (event.type === "message.updated") {
|
|
2210
|
+
const info = props?.info;
|
|
2211
|
+
const sessionID = info?.sessionID;
|
|
2212
|
+
if (sessionID && info?.role === "assistant" && info.error) {
|
|
2213
|
+
const parsed = parseAnthropicTokenLimitError(info.error);
|
|
2214
|
+
if (parsed) {
|
|
2215
|
+
parsed.providerID = info.providerID;
|
|
2216
|
+
parsed.modelID = info.modelID;
|
|
2217
|
+
autoCompactState.pendingCompact.add(sessionID);
|
|
2218
|
+
autoCompactState.errorDataBySession.set(sessionID, parsed);
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
return;
|
|
2222
|
+
}
|
|
2223
|
+
if (event.type === "session.idle") {
|
|
2224
|
+
const sessionID = props?.sessionID;
|
|
2225
|
+
if (!sessionID)
|
|
2226
|
+
return;
|
|
2227
|
+
if (!autoCompactState.pendingCompact.has(sessionID))
|
|
2228
|
+
return;
|
|
2229
|
+
const errorData = autoCompactState.errorDataBySession.get(sessionID);
|
|
2230
|
+
if (errorData?.providerID && errorData?.modelID) {
|
|
2231
|
+
await ctx.client.tui.showToast({
|
|
2232
|
+
body: {
|
|
2233
|
+
title: "Auto Compact",
|
|
2234
|
+
message: "Token limit exceeded. Summarizing session...",
|
|
2235
|
+
variant: "warning",
|
|
2236
|
+
duration: 3000
|
|
2237
|
+
}
|
|
2238
|
+
}).catch(() => {});
|
|
2239
|
+
await executeCompact(sessionID, { providerID: errorData.providerID, modelID: errorData.modelID }, autoCompactState, ctx.client, ctx.directory);
|
|
2240
|
+
return;
|
|
2241
|
+
}
|
|
2242
|
+
const lastAssistant = await getLastAssistant(sessionID, ctx.client, ctx.directory);
|
|
2243
|
+
if (!lastAssistant) {
|
|
2244
|
+
autoCompactState.pendingCompact.delete(sessionID);
|
|
2245
|
+
return;
|
|
2246
|
+
}
|
|
2247
|
+
if (lastAssistant.summary === true) {
|
|
2248
|
+
autoCompactState.pendingCompact.delete(sessionID);
|
|
2249
|
+
return;
|
|
2250
|
+
}
|
|
2251
|
+
if (!lastAssistant.modelID || !lastAssistant.providerID) {
|
|
2252
|
+
autoCompactState.pendingCompact.delete(sessionID);
|
|
2253
|
+
return;
|
|
2254
|
+
}
|
|
2255
|
+
await ctx.client.tui.showToast({
|
|
2256
|
+
body: {
|
|
2257
|
+
title: "Auto Compact",
|
|
2258
|
+
message: "Token limit exceeded. Summarizing session...",
|
|
2259
|
+
variant: "warning",
|
|
2260
|
+
duration: 3000
|
|
2261
|
+
}
|
|
2262
|
+
}).catch(() => {});
|
|
2263
|
+
await executeCompact(sessionID, lastAssistant, autoCompactState, ctx.client, ctx.directory);
|
|
2264
|
+
}
|
|
2265
|
+
};
|
|
2266
|
+
return {
|
|
2267
|
+
event: eventHandler
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
1814
2270
|
// src/hooks/think-mode/detector.ts
|
|
1815
2271
|
var ENGLISH_PATTERNS = [/\bultrathink\b/i, /\bthink\b/i];
|
|
1816
2272
|
var MULTILINGUAL_KEYWORDS = [
|
|
@@ -1991,8 +2447,8 @@ function createThinkModeHook() {
|
|
|
1991
2447
|
}
|
|
1992
2448
|
// src/hooks/claude-code-hooks/config.ts
|
|
1993
2449
|
import { homedir as homedir2 } from "os";
|
|
1994
|
-
import { join as
|
|
1995
|
-
import { existsSync as
|
|
2450
|
+
import { join as join11 } from "path";
|
|
2451
|
+
import { existsSync as existsSync9 } from "fs";
|
|
1996
2452
|
function normalizeHookMatcher(raw) {
|
|
1997
2453
|
return {
|
|
1998
2454
|
matcher: raw.matcher ?? raw.pattern ?? "*",
|
|
@@ -2017,11 +2473,11 @@ function normalizeHooksConfig(raw) {
|
|
|
2017
2473
|
function getClaudeSettingsPaths(customPath) {
|
|
2018
2474
|
const home = homedir2();
|
|
2019
2475
|
const paths = [
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2476
|
+
join11(home, ".claude", "settings.json"),
|
|
2477
|
+
join11(process.cwd(), ".claude", "settings.json"),
|
|
2478
|
+
join11(process.cwd(), ".claude", "settings.local.json")
|
|
2023
2479
|
];
|
|
2024
|
-
if (customPath &&
|
|
2480
|
+
if (customPath && existsSync9(customPath)) {
|
|
2025
2481
|
paths.unshift(customPath);
|
|
2026
2482
|
}
|
|
2027
2483
|
return paths;
|
|
@@ -2045,7 +2501,7 @@ async function loadClaudeHooksConfig(customSettingsPath) {
|
|
|
2045
2501
|
const paths = getClaudeSettingsPaths(customSettingsPath);
|
|
2046
2502
|
let mergedConfig = {};
|
|
2047
2503
|
for (const settingsPath of paths) {
|
|
2048
|
-
if (
|
|
2504
|
+
if (existsSync9(settingsPath)) {
|
|
2049
2505
|
try {
|
|
2050
2506
|
const content = await Bun.file(settingsPath).text();
|
|
2051
2507
|
const settings = JSON.parse(content);
|
|
@@ -2062,9 +2518,9 @@ async function loadClaudeHooksConfig(customSettingsPath) {
|
|
|
2062
2518
|
}
|
|
2063
2519
|
|
|
2064
2520
|
// src/hooks/claude-code-hooks/config-loader.ts
|
|
2065
|
-
import { existsSync as
|
|
2521
|
+
import { existsSync as existsSync10 } from "fs";
|
|
2066
2522
|
import { homedir as homedir3 } from "os";
|
|
2067
|
-
import { join as
|
|
2523
|
+
import { join as join13 } from "path";
|
|
2068
2524
|
|
|
2069
2525
|
// src/shared/logger.ts
|
|
2070
2526
|
import * as fs3 from "fs";
|
|
@@ -2081,12 +2537,12 @@ function log(message, data) {
|
|
|
2081
2537
|
}
|
|
2082
2538
|
|
|
2083
2539
|
// src/hooks/claude-code-hooks/config-loader.ts
|
|
2084
|
-
var USER_CONFIG_PATH =
|
|
2540
|
+
var USER_CONFIG_PATH = join13(homedir3(), ".config", "opencode", "opencode-cc-plugin.json");
|
|
2085
2541
|
function getProjectConfigPath() {
|
|
2086
|
-
return
|
|
2542
|
+
return join13(process.cwd(), ".opencode", "opencode-cc-plugin.json");
|
|
2087
2543
|
}
|
|
2088
2544
|
async function loadConfigFromPath(path3) {
|
|
2089
|
-
if (!
|
|
2545
|
+
if (!existsSync10(path3)) {
|
|
2090
2546
|
return null;
|
|
2091
2547
|
}
|
|
2092
2548
|
try {
|
|
@@ -2179,14 +2635,14 @@ function parseFrontmatter(content) {
|
|
|
2179
2635
|
import { spawn as spawn3 } from "child_process";
|
|
2180
2636
|
import { exec } from "child_process";
|
|
2181
2637
|
import { promisify } from "util";
|
|
2182
|
-
import { existsSync as
|
|
2638
|
+
import { existsSync as existsSync11 } from "fs";
|
|
2183
2639
|
var DEFAULT_ZSH_PATHS = ["/bin/zsh", "/usr/bin/zsh", "/usr/local/bin/zsh"];
|
|
2184
2640
|
function findZshPath(customZshPath) {
|
|
2185
|
-
if (customZshPath &&
|
|
2641
|
+
if (customZshPath && existsSync11(customZshPath)) {
|
|
2186
2642
|
return customZshPath;
|
|
2187
2643
|
}
|
|
2188
2644
|
for (const path3 of DEFAULT_ZSH_PATHS) {
|
|
2189
|
-
if (
|
|
2645
|
+
if (existsSync11(path3)) {
|
|
2190
2646
|
return path3;
|
|
2191
2647
|
}
|
|
2192
2648
|
}
|
|
@@ -2204,7 +2660,7 @@ async function executeHookCommand(command, stdin, cwd, options) {
|
|
|
2204
2660
|
finalCommand = `${zshPath} -lc '${escapedCommand}'`;
|
|
2205
2661
|
}
|
|
2206
2662
|
}
|
|
2207
|
-
return new Promise((
|
|
2663
|
+
return new Promise((resolve3) => {
|
|
2208
2664
|
const proc = spawn3(finalCommand, {
|
|
2209
2665
|
cwd,
|
|
2210
2666
|
shell: true,
|
|
@@ -2221,14 +2677,14 @@ async function executeHookCommand(command, stdin, cwd, options) {
|
|
|
2221
2677
|
proc.stdin?.write(stdin);
|
|
2222
2678
|
proc.stdin?.end();
|
|
2223
2679
|
proc.on("close", (code) => {
|
|
2224
|
-
|
|
2680
|
+
resolve3({
|
|
2225
2681
|
exitCode: code ?? 0,
|
|
2226
2682
|
stdout: stdout.trim(),
|
|
2227
2683
|
stderr: stderr.trim()
|
|
2228
2684
|
});
|
|
2229
2685
|
});
|
|
2230
2686
|
proc.on("error", (err) => {
|
|
2231
|
-
|
|
2687
|
+
resolve3({
|
|
2232
2688
|
exitCode: 1,
|
|
2233
2689
|
stderr: err.message
|
|
2234
2690
|
});
|
|
@@ -2304,8 +2760,8 @@ async function resolveCommandsInText(text, depth = 0, maxDepth = 3) {
|
|
|
2304
2760
|
return resolved;
|
|
2305
2761
|
}
|
|
2306
2762
|
// src/shared/file-reference-resolver.ts
|
|
2307
|
-
import { existsSync as
|
|
2308
|
-
import { join as
|
|
2763
|
+
import { existsSync as existsSync12, readFileSync as readFileSync6, statSync } from "fs";
|
|
2764
|
+
import { join as join14, isAbsolute } from "path";
|
|
2309
2765
|
var FILE_REFERENCE_PATTERN = /@([^\s@]+)/g;
|
|
2310
2766
|
function findFileReferences(text) {
|
|
2311
2767
|
const matches = [];
|
|
@@ -2325,17 +2781,17 @@ function resolveFilePath(filePath, cwd) {
|
|
|
2325
2781
|
if (isAbsolute(filePath)) {
|
|
2326
2782
|
return filePath;
|
|
2327
2783
|
}
|
|
2328
|
-
return
|
|
2784
|
+
return join14(cwd, filePath);
|
|
2329
2785
|
}
|
|
2330
2786
|
function readFileContent(resolvedPath) {
|
|
2331
|
-
if (!
|
|
2787
|
+
if (!existsSync12(resolvedPath)) {
|
|
2332
2788
|
return `[file not found: ${resolvedPath}]`;
|
|
2333
2789
|
}
|
|
2334
2790
|
const stat = statSync(resolvedPath);
|
|
2335
2791
|
if (stat.isDirectory()) {
|
|
2336
2792
|
return `[cannot read directory: ${resolvedPath}]`;
|
|
2337
2793
|
}
|
|
2338
|
-
const content =
|
|
2794
|
+
const content = readFileSync6(resolvedPath, "utf-8");
|
|
2339
2795
|
return content;
|
|
2340
2796
|
}
|
|
2341
2797
|
async function resolveFileReferencesInText(text, cwd = process.cwd(), depth = 0, maxDepth = 3) {
|
|
@@ -2558,17 +3014,17 @@ async function executePreToolUseHooks(ctx, config, extendedConfig) {
|
|
|
2558
3014
|
}
|
|
2559
3015
|
|
|
2560
3016
|
// src/hooks/claude-code-hooks/transcript.ts
|
|
2561
|
-
import { join as
|
|
2562
|
-
import { mkdirSync as
|
|
3017
|
+
import { join as join15 } from "path";
|
|
3018
|
+
import { mkdirSync as mkdirSync5, appendFileSync as appendFileSync5, existsSync as existsSync13, writeFileSync as writeFileSync4, unlinkSync as unlinkSync5 } from "fs";
|
|
2563
3019
|
import { homedir as homedir4, tmpdir as tmpdir2 } from "os";
|
|
2564
3020
|
import { randomUUID } from "crypto";
|
|
2565
|
-
var TRANSCRIPT_DIR =
|
|
3021
|
+
var TRANSCRIPT_DIR = join15(homedir4(), ".claude", "transcripts");
|
|
2566
3022
|
function getTranscriptPath(sessionId) {
|
|
2567
|
-
return
|
|
3023
|
+
return join15(TRANSCRIPT_DIR, `${sessionId}.jsonl`);
|
|
2568
3024
|
}
|
|
2569
3025
|
function ensureTranscriptDir() {
|
|
2570
|
-
if (!
|
|
2571
|
-
|
|
3026
|
+
if (!existsSync13(TRANSCRIPT_DIR)) {
|
|
3027
|
+
mkdirSync5(TRANSCRIPT_DIR, { recursive: true });
|
|
2572
3028
|
}
|
|
2573
3029
|
}
|
|
2574
3030
|
function appendTranscriptEntry(sessionId, entry) {
|
|
@@ -2654,8 +3110,8 @@ async function buildTranscriptFromSession(client, sessionId, directory, currentT
|
|
|
2654
3110
|
}
|
|
2655
3111
|
};
|
|
2656
3112
|
entries.push(JSON.stringify(currentEntry));
|
|
2657
|
-
const tempPath =
|
|
2658
|
-
|
|
3113
|
+
const tempPath = join15(tmpdir2(), `opencode-transcript-${sessionId}-${randomUUID()}.jsonl`);
|
|
3114
|
+
writeFileSync4(tempPath, entries.join(`
|
|
2659
3115
|
`) + `
|
|
2660
3116
|
`);
|
|
2661
3117
|
return tempPath;
|
|
@@ -2674,8 +3130,8 @@ async function buildTranscriptFromSession(client, sessionId, directory, currentT
|
|
|
2674
3130
|
]
|
|
2675
3131
|
}
|
|
2676
3132
|
};
|
|
2677
|
-
const tempPath =
|
|
2678
|
-
|
|
3133
|
+
const tempPath = join15(tmpdir2(), `opencode-transcript-${sessionId}-${randomUUID()}.jsonl`);
|
|
3134
|
+
writeFileSync4(tempPath, JSON.stringify(currentEntry) + `
|
|
2679
3135
|
`);
|
|
2680
3136
|
return tempPath;
|
|
2681
3137
|
} catch {
|
|
@@ -2687,7 +3143,7 @@ function deleteTempTranscript(path3) {
|
|
|
2687
3143
|
if (!path3)
|
|
2688
3144
|
return;
|
|
2689
3145
|
try {
|
|
2690
|
-
|
|
3146
|
+
unlinkSync5(path3);
|
|
2691
3147
|
} catch {}
|
|
2692
3148
|
}
|
|
2693
3149
|
|
|
@@ -2886,11 +3342,11 @@ ${USER_PROMPT_SUBMIT_TAG_CLOSE}`);
|
|
|
2886
3342
|
}
|
|
2887
3343
|
|
|
2888
3344
|
// src/hooks/claude-code-hooks/todo.ts
|
|
2889
|
-
import { join as
|
|
3345
|
+
import { join as join16 } from "path";
|
|
2890
3346
|
import { homedir as homedir5 } from "os";
|
|
2891
|
-
var TODO_DIR =
|
|
3347
|
+
var TODO_DIR = join16(homedir5(), ".claude", "todos");
|
|
2892
3348
|
function getTodoPath(sessionId) {
|
|
2893
|
-
return
|
|
3349
|
+
return join16(TODO_DIR, `${sessionId}-agent-${sessionId}.json`);
|
|
2894
3350
|
}
|
|
2895
3351
|
|
|
2896
3352
|
// src/hooks/claude-code-hooks/stop.ts
|
|
@@ -2982,16 +3438,16 @@ setInterval(() => {
|
|
|
2982
3438
|
}, CACHE_TTL);
|
|
2983
3439
|
|
|
2984
3440
|
// src/features/hook-message-injector/injector.ts
|
|
2985
|
-
import { existsSync as
|
|
2986
|
-
import { join as
|
|
3441
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync6, readFileSync as readFileSync7, readdirSync as readdirSync2, writeFileSync as writeFileSync5 } from "fs";
|
|
3442
|
+
import { join as join18 } from "path";
|
|
2987
3443
|
|
|
2988
3444
|
// src/features/hook-message-injector/constants.ts
|
|
2989
|
-
import { join as
|
|
3445
|
+
import { join as join17 } from "path";
|
|
2990
3446
|
import { homedir as homedir6 } from "os";
|
|
2991
|
-
var xdgData2 = process.env.XDG_DATA_HOME ||
|
|
2992
|
-
var
|
|
2993
|
-
var MESSAGE_STORAGE2 =
|
|
2994
|
-
var PART_STORAGE2 =
|
|
3447
|
+
var xdgData2 = process.env.XDG_DATA_HOME || join17(homedir6(), ".local", "share");
|
|
3448
|
+
var OPENCODE_STORAGE4 = join17(xdgData2, "opencode", "storage");
|
|
3449
|
+
var MESSAGE_STORAGE2 = join17(OPENCODE_STORAGE4, "message");
|
|
3450
|
+
var PART_STORAGE2 = join17(OPENCODE_STORAGE4, "part");
|
|
2995
3451
|
|
|
2996
3452
|
// src/features/hook-message-injector/injector.ts
|
|
2997
3453
|
function findNearestMessageWithFields(messageDir) {
|
|
@@ -2999,7 +3455,7 @@ function findNearestMessageWithFields(messageDir) {
|
|
|
2999
3455
|
const files = readdirSync2(messageDir).filter((f) => f.endsWith(".json")).sort().reverse();
|
|
3000
3456
|
for (const file of files) {
|
|
3001
3457
|
try {
|
|
3002
|
-
const content =
|
|
3458
|
+
const content = readFileSync7(join18(messageDir, file), "utf-8");
|
|
3003
3459
|
const msg = JSON.parse(content);
|
|
3004
3460
|
if (msg.agent && msg.model?.providerID && msg.model?.modelID) {
|
|
3005
3461
|
return msg;
|
|
@@ -3024,20 +3480,20 @@ function generatePartId2() {
|
|
|
3024
3480
|
return `prt_${timestamp}${random}`;
|
|
3025
3481
|
}
|
|
3026
3482
|
function getOrCreateMessageDir(sessionID) {
|
|
3027
|
-
if (!
|
|
3028
|
-
|
|
3483
|
+
if (!existsSync14(MESSAGE_STORAGE2)) {
|
|
3484
|
+
mkdirSync6(MESSAGE_STORAGE2, { recursive: true });
|
|
3029
3485
|
}
|
|
3030
|
-
const directPath =
|
|
3031
|
-
if (
|
|
3486
|
+
const directPath = join18(MESSAGE_STORAGE2, sessionID);
|
|
3487
|
+
if (existsSync14(directPath)) {
|
|
3032
3488
|
return directPath;
|
|
3033
3489
|
}
|
|
3034
3490
|
for (const dir of readdirSync2(MESSAGE_STORAGE2)) {
|
|
3035
|
-
const sessionPath =
|
|
3036
|
-
if (
|
|
3491
|
+
const sessionPath = join18(MESSAGE_STORAGE2, dir, sessionID);
|
|
3492
|
+
if (existsSync14(sessionPath)) {
|
|
3037
3493
|
return sessionPath;
|
|
3038
3494
|
}
|
|
3039
3495
|
}
|
|
3040
|
-
|
|
3496
|
+
mkdirSync6(directPath, { recursive: true });
|
|
3041
3497
|
return directPath;
|
|
3042
3498
|
}
|
|
3043
3499
|
function injectHookMessage(sessionID, hookContent, originalMessage) {
|
|
@@ -3078,12 +3534,12 @@ function injectHookMessage(sessionID, hookContent, originalMessage) {
|
|
|
3078
3534
|
sessionID
|
|
3079
3535
|
};
|
|
3080
3536
|
try {
|
|
3081
|
-
|
|
3082
|
-
const partDir =
|
|
3083
|
-
if (!
|
|
3084
|
-
|
|
3537
|
+
writeFileSync5(join18(messageDir, `${messageID}.json`), JSON.stringify(messageMeta, null, 2));
|
|
3538
|
+
const partDir = join18(PART_STORAGE2, messageID);
|
|
3539
|
+
if (!existsSync14(partDir)) {
|
|
3540
|
+
mkdirSync6(partDir, { recursive: true });
|
|
3085
3541
|
}
|
|
3086
|
-
|
|
3542
|
+
writeFileSync5(join18(partDir, `${partID}.json`), JSON.stringify(textPart, null, 2));
|
|
3087
3543
|
return true;
|
|
3088
3544
|
} catch {
|
|
3089
3545
|
return false;
|
|
@@ -3329,14 +3785,14 @@ ${result.message}`;
|
|
|
3329
3785
|
};
|
|
3330
3786
|
}
|
|
3331
3787
|
// src/features/claude-code-command-loader/loader.ts
|
|
3332
|
-
import { existsSync as
|
|
3788
|
+
import { existsSync as existsSync15, readdirSync as readdirSync3, readFileSync as readFileSync8 } from "fs";
|
|
3333
3789
|
import { homedir as homedir7 } from "os";
|
|
3334
|
-
import { join as
|
|
3790
|
+
import { join as join19, basename } from "path";
|
|
3335
3791
|
function isMarkdownFile(entry) {
|
|
3336
3792
|
return !entry.name.startsWith(".") && entry.name.endsWith(".md") && entry.isFile();
|
|
3337
3793
|
}
|
|
3338
3794
|
function loadCommandsFromDir(commandsDir, scope) {
|
|
3339
|
-
if (!
|
|
3795
|
+
if (!existsSync15(commandsDir)) {
|
|
3340
3796
|
return [];
|
|
3341
3797
|
}
|
|
3342
3798
|
const entries = readdirSync3(commandsDir, { withFileTypes: true });
|
|
@@ -3344,10 +3800,10 @@ function loadCommandsFromDir(commandsDir, scope) {
|
|
|
3344
3800
|
for (const entry of entries) {
|
|
3345
3801
|
if (!isMarkdownFile(entry))
|
|
3346
3802
|
continue;
|
|
3347
|
-
const commandPath =
|
|
3803
|
+
const commandPath = join19(commandsDir, entry.name);
|
|
3348
3804
|
const commandName = basename(entry.name, ".md");
|
|
3349
3805
|
try {
|
|
3350
|
-
const content =
|
|
3806
|
+
const content = readFileSync8(commandPath, "utf-8");
|
|
3351
3807
|
const { data, body } = parseFrontmatter(content);
|
|
3352
3808
|
const wrappedTemplate = `<command-instruction>
|
|
3353
3809
|
${body.trim()}
|
|
@@ -3386,31 +3842,31 @@ function commandsToRecord(commands) {
|
|
|
3386
3842
|
return result;
|
|
3387
3843
|
}
|
|
3388
3844
|
function loadUserCommands() {
|
|
3389
|
-
const userCommandsDir =
|
|
3845
|
+
const userCommandsDir = join19(homedir7(), ".claude", "commands");
|
|
3390
3846
|
const commands = loadCommandsFromDir(userCommandsDir, "user");
|
|
3391
3847
|
return commandsToRecord(commands);
|
|
3392
3848
|
}
|
|
3393
3849
|
function loadProjectCommands() {
|
|
3394
|
-
const projectCommandsDir =
|
|
3850
|
+
const projectCommandsDir = join19(process.cwd(), ".claude", "commands");
|
|
3395
3851
|
const commands = loadCommandsFromDir(projectCommandsDir, "project");
|
|
3396
3852
|
return commandsToRecord(commands);
|
|
3397
3853
|
}
|
|
3398
3854
|
function loadOpencodeGlobalCommands() {
|
|
3399
|
-
const opencodeCommandsDir =
|
|
3855
|
+
const opencodeCommandsDir = join19(homedir7(), ".config", "opencode", "command");
|
|
3400
3856
|
const commands = loadCommandsFromDir(opencodeCommandsDir, "opencode");
|
|
3401
3857
|
return commandsToRecord(commands);
|
|
3402
3858
|
}
|
|
3403
3859
|
function loadOpencodeProjectCommands() {
|
|
3404
|
-
const opencodeProjectDir =
|
|
3860
|
+
const opencodeProjectDir = join19(process.cwd(), ".opencode", "command");
|
|
3405
3861
|
const commands = loadCommandsFromDir(opencodeProjectDir, "opencode-project");
|
|
3406
3862
|
return commandsToRecord(commands);
|
|
3407
3863
|
}
|
|
3408
3864
|
// src/features/claude-code-skill-loader/loader.ts
|
|
3409
|
-
import { existsSync as
|
|
3865
|
+
import { existsSync as existsSync16, readdirSync as readdirSync4, readFileSync as readFileSync9, statSync as statSync2, readlinkSync } from "fs";
|
|
3410
3866
|
import { homedir as homedir8 } from "os";
|
|
3411
|
-
import { join as
|
|
3867
|
+
import { join as join20, resolve as resolve3 } from "path";
|
|
3412
3868
|
function loadSkillsFromDir(skillsDir, scope) {
|
|
3413
|
-
if (!
|
|
3869
|
+
if (!existsSync16(skillsDir)) {
|
|
3414
3870
|
return [];
|
|
3415
3871
|
}
|
|
3416
3872
|
const entries = readdirSync4(skillsDir, { withFileTypes: true });
|
|
@@ -3418,18 +3874,18 @@ function loadSkillsFromDir(skillsDir, scope) {
|
|
|
3418
3874
|
for (const entry of entries) {
|
|
3419
3875
|
if (entry.name.startsWith("."))
|
|
3420
3876
|
continue;
|
|
3421
|
-
const skillPath =
|
|
3877
|
+
const skillPath = join20(skillsDir, entry.name);
|
|
3422
3878
|
if (!entry.isDirectory() && !entry.isSymbolicLink())
|
|
3423
3879
|
continue;
|
|
3424
3880
|
let resolvedPath = skillPath;
|
|
3425
3881
|
if (statSync2(skillPath, { throwIfNoEntry: false })?.isSymbolicLink()) {
|
|
3426
|
-
resolvedPath =
|
|
3882
|
+
resolvedPath = resolve3(skillPath, "..", readlinkSync(skillPath));
|
|
3427
3883
|
}
|
|
3428
|
-
const skillMdPath =
|
|
3429
|
-
if (!
|
|
3884
|
+
const skillMdPath = join20(resolvedPath, "SKILL.md");
|
|
3885
|
+
if (!existsSync16(skillMdPath))
|
|
3430
3886
|
continue;
|
|
3431
3887
|
try {
|
|
3432
|
-
const content =
|
|
3888
|
+
const content = readFileSync9(skillMdPath, "utf-8");
|
|
3433
3889
|
const { data, body } = parseFrontmatter(content);
|
|
3434
3890
|
const skillName = data.name || entry.name;
|
|
3435
3891
|
const originalDescription = data.description || "";
|
|
@@ -3460,7 +3916,7 @@ $ARGUMENTS
|
|
|
3460
3916
|
return skills;
|
|
3461
3917
|
}
|
|
3462
3918
|
function loadUserSkillsAsCommands() {
|
|
3463
|
-
const userSkillsDir =
|
|
3919
|
+
const userSkillsDir = join20(homedir8(), ".claude", "skills");
|
|
3464
3920
|
const skills = loadSkillsFromDir(userSkillsDir, "user");
|
|
3465
3921
|
return skills.reduce((acc, skill) => {
|
|
3466
3922
|
acc[skill.name] = skill.definition;
|
|
@@ -3468,7 +3924,7 @@ function loadUserSkillsAsCommands() {
|
|
|
3468
3924
|
}, {});
|
|
3469
3925
|
}
|
|
3470
3926
|
function loadProjectSkillsAsCommands() {
|
|
3471
|
-
const projectSkillsDir =
|
|
3927
|
+
const projectSkillsDir = join20(process.cwd(), ".claude", "skills");
|
|
3472
3928
|
const skills = loadSkillsFromDir(projectSkillsDir, "project");
|
|
3473
3929
|
return skills.reduce((acc, skill) => {
|
|
3474
3930
|
acc[skill.name] = skill.definition;
|
|
@@ -3476,9 +3932,9 @@ function loadProjectSkillsAsCommands() {
|
|
|
3476
3932
|
}, {});
|
|
3477
3933
|
}
|
|
3478
3934
|
// src/features/claude-code-agent-loader/loader.ts
|
|
3479
|
-
import { existsSync as
|
|
3935
|
+
import { existsSync as existsSync17, readdirSync as readdirSync5, readFileSync as readFileSync10 } from "fs";
|
|
3480
3936
|
import { homedir as homedir9 } from "os";
|
|
3481
|
-
import { join as
|
|
3937
|
+
import { join as join21, basename as basename2 } from "path";
|
|
3482
3938
|
function parseToolsConfig(toolsStr) {
|
|
3483
3939
|
if (!toolsStr)
|
|
3484
3940
|
return;
|
|
@@ -3495,7 +3951,7 @@ function isMarkdownFile2(entry) {
|
|
|
3495
3951
|
return !entry.name.startsWith(".") && entry.name.endsWith(".md") && entry.isFile();
|
|
3496
3952
|
}
|
|
3497
3953
|
function loadAgentsFromDir(agentsDir, scope) {
|
|
3498
|
-
if (!
|
|
3954
|
+
if (!existsSync17(agentsDir)) {
|
|
3499
3955
|
return [];
|
|
3500
3956
|
}
|
|
3501
3957
|
const entries = readdirSync5(agentsDir, { withFileTypes: true });
|
|
@@ -3503,10 +3959,10 @@ function loadAgentsFromDir(agentsDir, scope) {
|
|
|
3503
3959
|
for (const entry of entries) {
|
|
3504
3960
|
if (!isMarkdownFile2(entry))
|
|
3505
3961
|
continue;
|
|
3506
|
-
const agentPath =
|
|
3962
|
+
const agentPath = join21(agentsDir, entry.name);
|
|
3507
3963
|
const agentName = basename2(entry.name, ".md");
|
|
3508
3964
|
try {
|
|
3509
|
-
const content =
|
|
3965
|
+
const content = readFileSync10(agentPath, "utf-8");
|
|
3510
3966
|
const { data, body } = parseFrontmatter(content);
|
|
3511
3967
|
const name = data.name || agentName;
|
|
3512
3968
|
const originalDescription = data.description || "";
|
|
@@ -3533,7 +3989,7 @@ function loadAgentsFromDir(agentsDir, scope) {
|
|
|
3533
3989
|
return agents;
|
|
3534
3990
|
}
|
|
3535
3991
|
function loadUserAgents() {
|
|
3536
|
-
const userAgentsDir =
|
|
3992
|
+
const userAgentsDir = join21(homedir9(), ".claude", "agents");
|
|
3537
3993
|
const agents = loadAgentsFromDir(userAgentsDir, "user");
|
|
3538
3994
|
const result = {};
|
|
3539
3995
|
for (const agent of agents) {
|
|
@@ -3542,7 +3998,7 @@ function loadUserAgents() {
|
|
|
3542
3998
|
return result;
|
|
3543
3999
|
}
|
|
3544
4000
|
function loadProjectAgents() {
|
|
3545
|
-
const projectAgentsDir =
|
|
4001
|
+
const projectAgentsDir = join21(process.cwd(), ".claude", "agents");
|
|
3546
4002
|
const agents = loadAgentsFromDir(projectAgentsDir, "project");
|
|
3547
4003
|
const result = {};
|
|
3548
4004
|
for (const agent of agents) {
|
|
@@ -3551,9 +4007,9 @@ function loadProjectAgents() {
|
|
|
3551
4007
|
return result;
|
|
3552
4008
|
}
|
|
3553
4009
|
// src/features/claude-code-mcp-loader/loader.ts
|
|
3554
|
-
import { existsSync as
|
|
4010
|
+
import { existsSync as existsSync18 } from "fs";
|
|
3555
4011
|
import { homedir as homedir10 } from "os";
|
|
3556
|
-
import { join as
|
|
4012
|
+
import { join as join22 } from "path";
|
|
3557
4013
|
|
|
3558
4014
|
// src/features/claude-code-mcp-loader/env-expander.ts
|
|
3559
4015
|
function expandEnvVars(value) {
|
|
@@ -3622,13 +4078,13 @@ function getMcpConfigPaths() {
|
|
|
3622
4078
|
const home = homedir10();
|
|
3623
4079
|
const cwd = process.cwd();
|
|
3624
4080
|
return [
|
|
3625
|
-
{ path:
|
|
3626
|
-
{ path:
|
|
3627
|
-
{ path:
|
|
4081
|
+
{ path: join22(home, ".claude", ".mcp.json"), scope: "user" },
|
|
4082
|
+
{ path: join22(cwd, ".mcp.json"), scope: "project" },
|
|
4083
|
+
{ path: join22(cwd, ".claude", ".mcp.json"), scope: "local" }
|
|
3628
4084
|
];
|
|
3629
4085
|
}
|
|
3630
4086
|
async function loadMcpConfigFile(filePath) {
|
|
3631
|
-
if (!
|
|
4087
|
+
if (!existsSync18(filePath)) {
|
|
3632
4088
|
return null;
|
|
3633
4089
|
}
|
|
3634
4090
|
try {
|
|
@@ -3914,14 +4370,14 @@ var EXT_TO_LANG = {
|
|
|
3914
4370
|
".tfvars": "terraform"
|
|
3915
4371
|
};
|
|
3916
4372
|
// src/tools/lsp/config.ts
|
|
3917
|
-
import { existsSync as
|
|
3918
|
-
import { join as
|
|
4373
|
+
import { existsSync as existsSync19, readFileSync as readFileSync11 } from "fs";
|
|
4374
|
+
import { join as join23 } from "path";
|
|
3919
4375
|
import { homedir as homedir11 } from "os";
|
|
3920
4376
|
function loadJsonFile(path3) {
|
|
3921
|
-
if (!
|
|
4377
|
+
if (!existsSync19(path3))
|
|
3922
4378
|
return null;
|
|
3923
4379
|
try {
|
|
3924
|
-
return JSON.parse(
|
|
4380
|
+
return JSON.parse(readFileSync11(path3, "utf-8"));
|
|
3925
4381
|
} catch {
|
|
3926
4382
|
return null;
|
|
3927
4383
|
}
|
|
@@ -3929,9 +4385,9 @@ function loadJsonFile(path3) {
|
|
|
3929
4385
|
function getConfigPaths() {
|
|
3930
4386
|
const cwd = process.cwd();
|
|
3931
4387
|
return {
|
|
3932
|
-
project:
|
|
3933
|
-
user:
|
|
3934
|
-
opencode:
|
|
4388
|
+
project: join23(cwd, ".opencode", "oh-my-opencode.json"),
|
|
4389
|
+
user: join23(homedir11(), ".config", "opencode", "oh-my-opencode.json"),
|
|
4390
|
+
opencode: join23(homedir11(), ".config", "opencode", "opencode.json")
|
|
3935
4391
|
};
|
|
3936
4392
|
}
|
|
3937
4393
|
function loadAllConfigs() {
|
|
@@ -4024,7 +4480,7 @@ function isServerInstalled(command) {
|
|
|
4024
4480
|
const pathEnv = process.env.PATH || "";
|
|
4025
4481
|
const paths = pathEnv.split(":");
|
|
4026
4482
|
for (const p of paths) {
|
|
4027
|
-
if (
|
|
4483
|
+
if (existsSync19(join23(p, cmd))) {
|
|
4028
4484
|
return true;
|
|
4029
4485
|
}
|
|
4030
4486
|
}
|
|
@@ -4074,8 +4530,8 @@ function getAllServers() {
|
|
|
4074
4530
|
}
|
|
4075
4531
|
// src/tools/lsp/client.ts
|
|
4076
4532
|
var {spawn: spawn4 } = globalThis.Bun;
|
|
4077
|
-
import { readFileSync as
|
|
4078
|
-
import { extname, resolve as
|
|
4533
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
4534
|
+
import { extname, resolve as resolve4 } from "path";
|
|
4079
4535
|
class LSPServerManager {
|
|
4080
4536
|
static instance;
|
|
4081
4537
|
clients = new Map;
|
|
@@ -4225,7 +4681,7 @@ class LSPClient {
|
|
|
4225
4681
|
}
|
|
4226
4682
|
this.startReading();
|
|
4227
4683
|
this.startStderrReading();
|
|
4228
|
-
await new Promise((
|
|
4684
|
+
await new Promise((resolve5) => setTimeout(resolve5, 100));
|
|
4229
4685
|
if (this.proc.exitCode !== null) {
|
|
4230
4686
|
const stderr = this.stderrBuffer.join(`
|
|
4231
4687
|
`);
|
|
@@ -4362,8 +4818,8 @@ stderr: ${stderr}` : ""));
|
|
|
4362
4818
|
\r
|
|
4363
4819
|
`;
|
|
4364
4820
|
this.proc.stdin.write(header + msg);
|
|
4365
|
-
return new Promise((
|
|
4366
|
-
this.pending.set(id, { resolve:
|
|
4821
|
+
return new Promise((resolve5, reject) => {
|
|
4822
|
+
this.pending.set(id, { resolve: resolve5, reject });
|
|
4367
4823
|
setTimeout(() => {
|
|
4368
4824
|
if (this.pending.has(id)) {
|
|
4369
4825
|
this.pending.delete(id);
|
|
@@ -4471,10 +4927,10 @@ ${msg}`);
|
|
|
4471
4927
|
await new Promise((r) => setTimeout(r, 300));
|
|
4472
4928
|
}
|
|
4473
4929
|
async openFile(filePath) {
|
|
4474
|
-
const absPath =
|
|
4930
|
+
const absPath = resolve4(filePath);
|
|
4475
4931
|
if (this.openedFiles.has(absPath))
|
|
4476
4932
|
return;
|
|
4477
|
-
const text =
|
|
4933
|
+
const text = readFileSync12(absPath, "utf-8");
|
|
4478
4934
|
const ext = extname(absPath);
|
|
4479
4935
|
const languageId = getLanguageId(ext);
|
|
4480
4936
|
this.notify("textDocument/didOpen", {
|
|
@@ -4489,7 +4945,7 @@ ${msg}`);
|
|
|
4489
4945
|
await new Promise((r) => setTimeout(r, 1000));
|
|
4490
4946
|
}
|
|
4491
4947
|
async hover(filePath, line, character) {
|
|
4492
|
-
const absPath =
|
|
4948
|
+
const absPath = resolve4(filePath);
|
|
4493
4949
|
await this.openFile(absPath);
|
|
4494
4950
|
return this.send("textDocument/hover", {
|
|
4495
4951
|
textDocument: { uri: `file://${absPath}` },
|
|
@@ -4497,7 +4953,7 @@ ${msg}`);
|
|
|
4497
4953
|
});
|
|
4498
4954
|
}
|
|
4499
4955
|
async definition(filePath, line, character) {
|
|
4500
|
-
const absPath =
|
|
4956
|
+
const absPath = resolve4(filePath);
|
|
4501
4957
|
await this.openFile(absPath);
|
|
4502
4958
|
return this.send("textDocument/definition", {
|
|
4503
4959
|
textDocument: { uri: `file://${absPath}` },
|
|
@@ -4505,7 +4961,7 @@ ${msg}`);
|
|
|
4505
4961
|
});
|
|
4506
4962
|
}
|
|
4507
4963
|
async references(filePath, line, character, includeDeclaration = true) {
|
|
4508
|
-
const absPath =
|
|
4964
|
+
const absPath = resolve4(filePath);
|
|
4509
4965
|
await this.openFile(absPath);
|
|
4510
4966
|
return this.send("textDocument/references", {
|
|
4511
4967
|
textDocument: { uri: `file://${absPath}` },
|
|
@@ -4514,7 +4970,7 @@ ${msg}`);
|
|
|
4514
4970
|
});
|
|
4515
4971
|
}
|
|
4516
4972
|
async documentSymbols(filePath) {
|
|
4517
|
-
const absPath =
|
|
4973
|
+
const absPath = resolve4(filePath);
|
|
4518
4974
|
await this.openFile(absPath);
|
|
4519
4975
|
return this.send("textDocument/documentSymbol", {
|
|
4520
4976
|
textDocument: { uri: `file://${absPath}` }
|
|
@@ -4524,7 +4980,7 @@ ${msg}`);
|
|
|
4524
4980
|
return this.send("workspace/symbol", { query });
|
|
4525
4981
|
}
|
|
4526
4982
|
async diagnostics(filePath) {
|
|
4527
|
-
const absPath =
|
|
4983
|
+
const absPath = resolve4(filePath);
|
|
4528
4984
|
const uri = `file://${absPath}`;
|
|
4529
4985
|
await this.openFile(absPath);
|
|
4530
4986
|
await new Promise((r) => setTimeout(r, 500));
|
|
@@ -4539,7 +4995,7 @@ ${msg}`);
|
|
|
4539
4995
|
return { items: this.diagnosticsStore.get(uri) ?? [] };
|
|
4540
4996
|
}
|
|
4541
4997
|
async prepareRename(filePath, line, character) {
|
|
4542
|
-
const absPath =
|
|
4998
|
+
const absPath = resolve4(filePath);
|
|
4543
4999
|
await this.openFile(absPath);
|
|
4544
5000
|
return this.send("textDocument/prepareRename", {
|
|
4545
5001
|
textDocument: { uri: `file://${absPath}` },
|
|
@@ -4547,7 +5003,7 @@ ${msg}`);
|
|
|
4547
5003
|
});
|
|
4548
5004
|
}
|
|
4549
5005
|
async rename(filePath, line, character, newName) {
|
|
4550
|
-
const absPath =
|
|
5006
|
+
const absPath = resolve4(filePath);
|
|
4551
5007
|
await this.openFile(absPath);
|
|
4552
5008
|
return this.send("textDocument/rename", {
|
|
4553
5009
|
textDocument: { uri: `file://${absPath}` },
|
|
@@ -4556,7 +5012,7 @@ ${msg}`);
|
|
|
4556
5012
|
});
|
|
4557
5013
|
}
|
|
4558
5014
|
async codeAction(filePath, startLine, startChar, endLine, endChar, only) {
|
|
4559
|
-
const absPath =
|
|
5015
|
+
const absPath = resolve4(filePath);
|
|
4560
5016
|
await this.openFile(absPath);
|
|
4561
5017
|
return this.send("textDocument/codeAction", {
|
|
4562
5018
|
textDocument: { uri: `file://${absPath}` },
|
|
@@ -4588,26 +5044,26 @@ ${msg}`);
|
|
|
4588
5044
|
}
|
|
4589
5045
|
}
|
|
4590
5046
|
// src/tools/lsp/utils.ts
|
|
4591
|
-
import { extname as extname2, resolve as
|
|
4592
|
-
import { existsSync as
|
|
5047
|
+
import { extname as extname2, resolve as resolve5 } from "path";
|
|
5048
|
+
import { existsSync as existsSync20, readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "fs";
|
|
4593
5049
|
function findWorkspaceRoot(filePath) {
|
|
4594
|
-
let dir =
|
|
4595
|
-
if (!
|
|
5050
|
+
let dir = resolve5(filePath);
|
|
5051
|
+
if (!existsSync20(dir) || !__require("fs").statSync(dir).isDirectory()) {
|
|
4596
5052
|
dir = __require("path").dirname(dir);
|
|
4597
5053
|
}
|
|
4598
5054
|
const markers = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "build.gradle"];
|
|
4599
5055
|
while (dir !== "/") {
|
|
4600
5056
|
for (const marker of markers) {
|
|
4601
|
-
if (
|
|
5057
|
+
if (existsSync20(__require("path").join(dir, marker))) {
|
|
4602
5058
|
return dir;
|
|
4603
5059
|
}
|
|
4604
5060
|
}
|
|
4605
5061
|
dir = __require("path").dirname(dir);
|
|
4606
5062
|
}
|
|
4607
|
-
return __require("path").dirname(
|
|
5063
|
+
return __require("path").dirname(resolve5(filePath));
|
|
4608
5064
|
}
|
|
4609
5065
|
async function withLspClient(filePath, fn) {
|
|
4610
|
-
const absPath =
|
|
5066
|
+
const absPath = resolve5(filePath);
|
|
4611
5067
|
const ext = extname2(absPath);
|
|
4612
5068
|
const server = findServerForExtension(ext);
|
|
4613
5069
|
if (!server) {
|
|
@@ -4756,7 +5212,7 @@ function formatCodeActions(actions) {
|
|
|
4756
5212
|
}
|
|
4757
5213
|
function applyTextEditsToFile(filePath, edits) {
|
|
4758
5214
|
try {
|
|
4759
|
-
let content =
|
|
5215
|
+
let content = readFileSync13(filePath, "utf-8");
|
|
4760
5216
|
const lines = content.split(`
|
|
4761
5217
|
`);
|
|
4762
5218
|
const sortedEdits = [...edits].sort((a, b) => {
|
|
@@ -4781,7 +5237,7 @@ function applyTextEditsToFile(filePath, edits) {
|
|
|
4781
5237
|
`));
|
|
4782
5238
|
}
|
|
4783
5239
|
}
|
|
4784
|
-
|
|
5240
|
+
writeFileSync6(filePath, lines.join(`
|
|
4785
5241
|
`), "utf-8");
|
|
4786
5242
|
return { success: true, editCount: edits.length };
|
|
4787
5243
|
} catch (err) {
|
|
@@ -4812,7 +5268,7 @@ function applyWorkspaceEdit(edit) {
|
|
|
4812
5268
|
if (change.kind === "create") {
|
|
4813
5269
|
try {
|
|
4814
5270
|
const filePath = change.uri.replace("file://", "");
|
|
4815
|
-
|
|
5271
|
+
writeFileSync6(filePath, "", "utf-8");
|
|
4816
5272
|
result.filesModified.push(filePath);
|
|
4817
5273
|
} catch (err) {
|
|
4818
5274
|
result.success = false;
|
|
@@ -4822,8 +5278,8 @@ function applyWorkspaceEdit(edit) {
|
|
|
4822
5278
|
try {
|
|
4823
5279
|
const oldPath = change.oldUri.replace("file://", "");
|
|
4824
5280
|
const newPath = change.newUri.replace("file://", "");
|
|
4825
|
-
const content =
|
|
4826
|
-
|
|
5281
|
+
const content = readFileSync13(oldPath, "utf-8");
|
|
5282
|
+
writeFileSync6(newPath, content, "utf-8");
|
|
4827
5283
|
__require("fs").unlinkSync(oldPath);
|
|
4828
5284
|
result.filesModified.push(newPath);
|
|
4829
5285
|
} catch (err) {
|
|
@@ -17523,13 +17979,13 @@ var lsp_code_action_resolve = tool({
|
|
|
17523
17979
|
});
|
|
17524
17980
|
// src/tools/ast-grep/constants.ts
|
|
17525
17981
|
import { createRequire as createRequire4 } from "module";
|
|
17526
|
-
import { dirname as
|
|
17527
|
-
import { existsSync as
|
|
17982
|
+
import { dirname as dirname4, join as join25 } from "path";
|
|
17983
|
+
import { existsSync as existsSync22, statSync as statSync3 } from "fs";
|
|
17528
17984
|
|
|
17529
17985
|
// src/tools/ast-grep/downloader.ts
|
|
17530
17986
|
var {spawn: spawn5 } = globalThis.Bun;
|
|
17531
|
-
import { existsSync as
|
|
17532
|
-
import { join as
|
|
17987
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync7, chmodSync as chmodSync2, unlinkSync as unlinkSync6 } from "fs";
|
|
17988
|
+
import { join as join24 } from "path";
|
|
17533
17989
|
import { homedir as homedir12 } from "os";
|
|
17534
17990
|
import { createRequire as createRequire3 } from "module";
|
|
17535
17991
|
var REPO2 = "ast-grep/ast-grep";
|
|
@@ -17555,19 +18011,19 @@ var PLATFORM_MAP2 = {
|
|
|
17555
18011
|
function getCacheDir2() {
|
|
17556
18012
|
if (process.platform === "win32") {
|
|
17557
18013
|
const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
|
|
17558
|
-
const base2 = localAppData ||
|
|
17559
|
-
return
|
|
18014
|
+
const base2 = localAppData || join24(homedir12(), "AppData", "Local");
|
|
18015
|
+
return join24(base2, "oh-my-opencode", "bin");
|
|
17560
18016
|
}
|
|
17561
18017
|
const xdgCache2 = process.env.XDG_CACHE_HOME;
|
|
17562
|
-
const base = xdgCache2 ||
|
|
17563
|
-
return
|
|
18018
|
+
const base = xdgCache2 || join24(homedir12(), ".cache");
|
|
18019
|
+
return join24(base, "oh-my-opencode", "bin");
|
|
17564
18020
|
}
|
|
17565
18021
|
function getBinaryName3() {
|
|
17566
18022
|
return process.platform === "win32" ? "sg.exe" : "sg";
|
|
17567
18023
|
}
|
|
17568
18024
|
function getCachedBinaryPath2() {
|
|
17569
|
-
const binaryPath =
|
|
17570
|
-
return
|
|
18025
|
+
const binaryPath = join24(getCacheDir2(), getBinaryName3());
|
|
18026
|
+
return existsSync21(binaryPath) ? binaryPath : null;
|
|
17571
18027
|
}
|
|
17572
18028
|
async function extractZip2(archivePath, destDir) {
|
|
17573
18029
|
const proc = process.platform === "win32" ? spawn5([
|
|
@@ -17593,8 +18049,8 @@ async function downloadAstGrep(version2 = DEFAULT_VERSION) {
|
|
|
17593
18049
|
}
|
|
17594
18050
|
const cacheDir = getCacheDir2();
|
|
17595
18051
|
const binaryName = getBinaryName3();
|
|
17596
|
-
const binaryPath =
|
|
17597
|
-
if (
|
|
18052
|
+
const binaryPath = join24(cacheDir, binaryName);
|
|
18053
|
+
if (existsSync21(binaryPath)) {
|
|
17598
18054
|
return binaryPath;
|
|
17599
18055
|
}
|
|
17600
18056
|
const { arch, os: os3 } = platformInfo;
|
|
@@ -17602,21 +18058,21 @@ async function downloadAstGrep(version2 = DEFAULT_VERSION) {
|
|
|
17602
18058
|
const downloadUrl = `https://github.com/${REPO2}/releases/download/${version2}/${assetName}`;
|
|
17603
18059
|
console.log(`[oh-my-opencode] Downloading ast-grep binary...`);
|
|
17604
18060
|
try {
|
|
17605
|
-
if (!
|
|
17606
|
-
|
|
18061
|
+
if (!existsSync21(cacheDir)) {
|
|
18062
|
+
mkdirSync7(cacheDir, { recursive: true });
|
|
17607
18063
|
}
|
|
17608
18064
|
const response = await fetch(downloadUrl, { redirect: "follow" });
|
|
17609
18065
|
if (!response.ok) {
|
|
17610
18066
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
17611
18067
|
}
|
|
17612
|
-
const archivePath =
|
|
18068
|
+
const archivePath = join24(cacheDir, assetName);
|
|
17613
18069
|
const arrayBuffer = await response.arrayBuffer();
|
|
17614
18070
|
await Bun.write(archivePath, arrayBuffer);
|
|
17615
18071
|
await extractZip2(archivePath, cacheDir);
|
|
17616
|
-
if (
|
|
17617
|
-
|
|
18072
|
+
if (existsSync21(archivePath)) {
|
|
18073
|
+
unlinkSync6(archivePath);
|
|
17618
18074
|
}
|
|
17619
|
-
if (process.platform !== "win32" &&
|
|
18075
|
+
if (process.platform !== "win32" && existsSync21(binaryPath)) {
|
|
17620
18076
|
chmodSync2(binaryPath, 493);
|
|
17621
18077
|
}
|
|
17622
18078
|
console.log(`[oh-my-opencode] ast-grep binary ready.`);
|
|
@@ -17666,9 +18122,9 @@ function findSgCliPathSync() {
|
|
|
17666
18122
|
try {
|
|
17667
18123
|
const require2 = createRequire4(import.meta.url);
|
|
17668
18124
|
const cliPkgPath = require2.resolve("@ast-grep/cli/package.json");
|
|
17669
|
-
const cliDir =
|
|
17670
|
-
const sgPath =
|
|
17671
|
-
if (
|
|
18125
|
+
const cliDir = dirname4(cliPkgPath);
|
|
18126
|
+
const sgPath = join25(cliDir, binaryName);
|
|
18127
|
+
if (existsSync22(sgPath) && isValidBinary(sgPath)) {
|
|
17672
18128
|
return sgPath;
|
|
17673
18129
|
}
|
|
17674
18130
|
} catch {}
|
|
@@ -17677,10 +18133,10 @@ function findSgCliPathSync() {
|
|
|
17677
18133
|
try {
|
|
17678
18134
|
const require2 = createRequire4(import.meta.url);
|
|
17679
18135
|
const pkgPath = require2.resolve(`${platformPkg}/package.json`);
|
|
17680
|
-
const pkgDir =
|
|
18136
|
+
const pkgDir = dirname4(pkgPath);
|
|
17681
18137
|
const astGrepName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep";
|
|
17682
|
-
const binaryPath =
|
|
17683
|
-
if (
|
|
18138
|
+
const binaryPath = join25(pkgDir, astGrepName);
|
|
18139
|
+
if (existsSync22(binaryPath) && isValidBinary(binaryPath)) {
|
|
17684
18140
|
return binaryPath;
|
|
17685
18141
|
}
|
|
17686
18142
|
} catch {}
|
|
@@ -17688,7 +18144,7 @@ function findSgCliPathSync() {
|
|
|
17688
18144
|
if (process.platform === "darwin") {
|
|
17689
18145
|
const homebrewPaths = ["/opt/homebrew/bin/sg", "/usr/local/bin/sg"];
|
|
17690
18146
|
for (const path3 of homebrewPaths) {
|
|
17691
|
-
if (
|
|
18147
|
+
if (existsSync22(path3) && isValidBinary(path3)) {
|
|
17692
18148
|
return path3;
|
|
17693
18149
|
}
|
|
17694
18150
|
}
|
|
@@ -17744,11 +18200,11 @@ var DEFAULT_MAX_MATCHES = 500;
|
|
|
17744
18200
|
|
|
17745
18201
|
// src/tools/ast-grep/cli.ts
|
|
17746
18202
|
var {spawn: spawn6 } = globalThis.Bun;
|
|
17747
|
-
import { existsSync as
|
|
18203
|
+
import { existsSync as existsSync23 } from "fs";
|
|
17748
18204
|
var resolvedCliPath3 = null;
|
|
17749
18205
|
var initPromise2 = null;
|
|
17750
18206
|
async function getAstGrepPath() {
|
|
17751
|
-
if (resolvedCliPath3 !== null &&
|
|
18207
|
+
if (resolvedCliPath3 !== null && existsSync23(resolvedCliPath3)) {
|
|
17752
18208
|
return resolvedCliPath3;
|
|
17753
18209
|
}
|
|
17754
18210
|
if (initPromise2) {
|
|
@@ -17756,7 +18212,7 @@ async function getAstGrepPath() {
|
|
|
17756
18212
|
}
|
|
17757
18213
|
initPromise2 = (async () => {
|
|
17758
18214
|
const syncPath = findSgCliPathSync();
|
|
17759
|
-
if (syncPath &&
|
|
18215
|
+
if (syncPath && existsSync23(syncPath)) {
|
|
17760
18216
|
resolvedCliPath3 = syncPath;
|
|
17761
18217
|
setSgCliPath(syncPath);
|
|
17762
18218
|
return syncPath;
|
|
@@ -17790,7 +18246,7 @@ async function runSg(options) {
|
|
|
17790
18246
|
const paths = options.paths && options.paths.length > 0 ? options.paths : ["."];
|
|
17791
18247
|
args.push(...paths);
|
|
17792
18248
|
let cliPath = getSgCliPath();
|
|
17793
|
-
if (!
|
|
18249
|
+
if (!existsSync23(cliPath) && cliPath !== "sg") {
|
|
17794
18250
|
const downloadedPath = await getAstGrepPath();
|
|
17795
18251
|
if (downloadedPath) {
|
|
17796
18252
|
cliPath = downloadedPath;
|
|
@@ -18054,8 +18510,8 @@ var ast_grep_replace = tool({
|
|
|
18054
18510
|
var {spawn: spawn7 } = globalThis.Bun;
|
|
18055
18511
|
|
|
18056
18512
|
// src/tools/grep/constants.ts
|
|
18057
|
-
import { existsSync as
|
|
18058
|
-
import { join as
|
|
18513
|
+
import { existsSync as existsSync24 } from "fs";
|
|
18514
|
+
import { join as join26, dirname as dirname5 } from "path";
|
|
18059
18515
|
import { spawnSync } from "child_process";
|
|
18060
18516
|
var cachedCli = null;
|
|
18061
18517
|
function findExecutable(name) {
|
|
@@ -18072,17 +18528,17 @@ function findExecutable(name) {
|
|
|
18072
18528
|
}
|
|
18073
18529
|
function getOpenCodeBundledRg() {
|
|
18074
18530
|
const execPath = process.execPath;
|
|
18075
|
-
const execDir =
|
|
18531
|
+
const execDir = dirname5(execPath);
|
|
18076
18532
|
const isWindows = process.platform === "win32";
|
|
18077
18533
|
const rgName = isWindows ? "rg.exe" : "rg";
|
|
18078
18534
|
const candidates = [
|
|
18079
|
-
|
|
18080
|
-
|
|
18081
|
-
|
|
18082
|
-
|
|
18535
|
+
join26(execDir, rgName),
|
|
18536
|
+
join26(execDir, "bin", rgName),
|
|
18537
|
+
join26(execDir, "..", "bin", rgName),
|
|
18538
|
+
join26(execDir, "..", "libexec", rgName)
|
|
18083
18539
|
];
|
|
18084
18540
|
for (const candidate of candidates) {
|
|
18085
|
-
if (
|
|
18541
|
+
if (existsSync24(candidate)) {
|
|
18086
18542
|
return candidate;
|
|
18087
18543
|
}
|
|
18088
18544
|
}
|
|
@@ -18480,11 +18936,11 @@ var glob = tool({
|
|
|
18480
18936
|
}
|
|
18481
18937
|
});
|
|
18482
18938
|
// src/tools/slashcommand/tools.ts
|
|
18483
|
-
import { existsSync as
|
|
18939
|
+
import { existsSync as existsSync25, readdirSync as readdirSync6, readFileSync as readFileSync14 } from "fs";
|
|
18484
18940
|
import { homedir as homedir13 } from "os";
|
|
18485
|
-
import { join as
|
|
18941
|
+
import { join as join27, basename as basename3, dirname as dirname6 } from "path";
|
|
18486
18942
|
function discoverCommandsFromDir(commandsDir, scope) {
|
|
18487
|
-
if (!
|
|
18943
|
+
if (!existsSync25(commandsDir)) {
|
|
18488
18944
|
return [];
|
|
18489
18945
|
}
|
|
18490
18946
|
const entries = readdirSync6(commandsDir, { withFileTypes: true });
|
|
@@ -18496,10 +18952,10 @@ function discoverCommandsFromDir(commandsDir, scope) {
|
|
|
18496
18952
|
continue;
|
|
18497
18953
|
if (!entry.isFile())
|
|
18498
18954
|
continue;
|
|
18499
|
-
const commandPath =
|
|
18955
|
+
const commandPath = join27(commandsDir, entry.name);
|
|
18500
18956
|
const commandName = basename3(entry.name, ".md");
|
|
18501
18957
|
try {
|
|
18502
|
-
const content =
|
|
18958
|
+
const content = readFileSync14(commandPath, "utf-8");
|
|
18503
18959
|
const { data, body } = parseFrontmatter(content);
|
|
18504
18960
|
const metadata = {
|
|
18505
18961
|
name: commandName,
|
|
@@ -18523,10 +18979,10 @@ function discoverCommandsFromDir(commandsDir, scope) {
|
|
|
18523
18979
|
return commands;
|
|
18524
18980
|
}
|
|
18525
18981
|
function discoverCommandsSync() {
|
|
18526
|
-
const userCommandsDir =
|
|
18527
|
-
const projectCommandsDir =
|
|
18528
|
-
const opencodeGlobalDir =
|
|
18529
|
-
const opencodeProjectDir =
|
|
18982
|
+
const userCommandsDir = join27(homedir13(), ".claude", "commands");
|
|
18983
|
+
const projectCommandsDir = join27(process.cwd(), ".claude", "commands");
|
|
18984
|
+
const opencodeGlobalDir = join27(homedir13(), ".config", "opencode", "command");
|
|
18985
|
+
const opencodeProjectDir = join27(process.cwd(), ".opencode", "command");
|
|
18530
18986
|
const userCommands = discoverCommandsFromDir(userCommandsDir, "user");
|
|
18531
18987
|
const opencodeGlobalCommands = discoverCommandsFromDir(opencodeGlobalDir, "opencode");
|
|
18532
18988
|
const projectCommands = discoverCommandsFromDir(projectCommandsDir, "project");
|
|
@@ -18569,7 +19025,7 @@ async function formatLoadedCommand(cmd) {
|
|
|
18569
19025
|
`);
|
|
18570
19026
|
sections.push(`## Command Instructions
|
|
18571
19027
|
`);
|
|
18572
|
-
const commandDir =
|
|
19028
|
+
const commandDir = dirname6(cmd.path);
|
|
18573
19029
|
const withFileRefs = await resolveFileReferencesInText(cmd.content, commandDir);
|
|
18574
19030
|
const resolvedContent = await resolveCommandsInText(withFileRefs);
|
|
18575
19031
|
sections.push(resolvedContent.trim());
|
|
@@ -18649,12 +19105,29 @@ Provide a command name to execute.`;
|
|
|
18649
19105
|
Try a different command name.`;
|
|
18650
19106
|
}
|
|
18651
19107
|
});
|
|
19108
|
+
// src/tools/skill/types.ts
|
|
19109
|
+
var SkillFrontmatterSchema = exports_external.object({
|
|
19110
|
+
name: exports_external.string().regex(/^[a-z0-9-]+$/, "Name must be lowercase alphanumeric with hyphens only").min(1, "Name cannot be empty"),
|
|
19111
|
+
description: exports_external.string().min(20, "Description must be at least 20 characters for discoverability"),
|
|
19112
|
+
license: exports_external.string().optional(),
|
|
19113
|
+
"allowed-tools": exports_external.array(exports_external.string()).optional(),
|
|
19114
|
+
metadata: exports_external.record(exports_external.string(), exports_external.string()).optional()
|
|
19115
|
+
});
|
|
18652
19116
|
// src/tools/skill/tools.ts
|
|
18653
|
-
import { existsSync as
|
|
19117
|
+
import { existsSync as existsSync26, readdirSync as readdirSync7, statSync as statSync4, readlinkSync as readlinkSync2, readFileSync as readFileSync15 } from "fs";
|
|
18654
19118
|
import { homedir as homedir14 } from "os";
|
|
18655
|
-
import { join as
|
|
19119
|
+
import { join as join28, resolve as resolve6, basename as basename4 } from "path";
|
|
19120
|
+
function parseSkillFrontmatter(data) {
|
|
19121
|
+
return {
|
|
19122
|
+
name: typeof data.name === "string" ? data.name : "",
|
|
19123
|
+
description: typeof data.description === "string" ? data.description : "",
|
|
19124
|
+
license: typeof data.license === "string" ? data.license : undefined,
|
|
19125
|
+
"allowed-tools": Array.isArray(data["allowed-tools"]) ? data["allowed-tools"] : undefined,
|
|
19126
|
+
metadata: typeof data.metadata === "object" && data.metadata !== null ? data.metadata : undefined
|
|
19127
|
+
};
|
|
19128
|
+
}
|
|
18656
19129
|
function discoverSkillsFromDir(skillsDir, scope) {
|
|
18657
|
-
if (!
|
|
19130
|
+
if (!existsSync26(skillsDir)) {
|
|
18658
19131
|
return [];
|
|
18659
19132
|
}
|
|
18660
19133
|
const entries = readdirSync7(skillsDir, { withFileTypes: true });
|
|
@@ -18662,22 +19135,22 @@ function discoverSkillsFromDir(skillsDir, scope) {
|
|
|
18662
19135
|
for (const entry of entries) {
|
|
18663
19136
|
if (entry.name.startsWith("."))
|
|
18664
19137
|
continue;
|
|
18665
|
-
const skillPath =
|
|
19138
|
+
const skillPath = join28(skillsDir, entry.name);
|
|
18666
19139
|
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
|
18667
19140
|
let resolvedPath = skillPath;
|
|
18668
19141
|
try {
|
|
18669
19142
|
const stats = statSync4(skillPath, { throwIfNoEntry: false });
|
|
18670
19143
|
if (stats?.isSymbolicLink()) {
|
|
18671
|
-
resolvedPath =
|
|
19144
|
+
resolvedPath = resolve6(skillPath, "..", readlinkSync2(skillPath));
|
|
18672
19145
|
}
|
|
18673
19146
|
} catch {
|
|
18674
19147
|
continue;
|
|
18675
19148
|
}
|
|
18676
|
-
const skillMdPath =
|
|
18677
|
-
if (!
|
|
19149
|
+
const skillMdPath = join28(resolvedPath, "SKILL.md");
|
|
19150
|
+
if (!existsSync26(skillMdPath))
|
|
18678
19151
|
continue;
|
|
18679
19152
|
try {
|
|
18680
|
-
const content =
|
|
19153
|
+
const content = readFileSync15(skillMdPath, "utf-8");
|
|
18681
19154
|
const { data } = parseFrontmatter(content);
|
|
18682
19155
|
skills.push({
|
|
18683
19156
|
name: data.name || entry.name,
|
|
@@ -18692,8 +19165,8 @@ function discoverSkillsFromDir(skillsDir, scope) {
|
|
|
18692
19165
|
return skills;
|
|
18693
19166
|
}
|
|
18694
19167
|
function discoverSkillsSync() {
|
|
18695
|
-
const userSkillsDir =
|
|
18696
|
-
const projectSkillsDir =
|
|
19168
|
+
const userSkillsDir = join28(homedir14(), ".claude", "skills");
|
|
19169
|
+
const projectSkillsDir = join28(process.cwd(), ".claude", "skills");
|
|
18697
19170
|
const userSkills = discoverSkillsFromDir(userSkillsDir, "user");
|
|
18698
19171
|
const projectSkills = discoverSkillsFromDir(projectSkillsDir, "project");
|
|
18699
19172
|
return [...projectSkills, ...userSkills];
|
|
@@ -18705,7 +19178,7 @@ function resolveSymlink(skillPath) {
|
|
|
18705
19178
|
try {
|
|
18706
19179
|
const stats = statSync4(skillPath, { throwIfNoEntry: false });
|
|
18707
19180
|
if (stats?.isSymbolicLink()) {
|
|
18708
|
-
return
|
|
19181
|
+
return resolve6(skillPath, "..", readlinkSync2(skillPath));
|
|
18709
19182
|
}
|
|
18710
19183
|
return skillPath;
|
|
18711
19184
|
} catch {
|
|
@@ -18714,28 +19187,32 @@ function resolveSymlink(skillPath) {
|
|
|
18714
19187
|
}
|
|
18715
19188
|
async function parseSkillMd(skillPath) {
|
|
18716
19189
|
const resolvedPath = resolveSymlink(skillPath);
|
|
18717
|
-
const skillMdPath =
|
|
18718
|
-
if (!
|
|
19190
|
+
const skillMdPath = join28(resolvedPath, "SKILL.md");
|
|
19191
|
+
if (!existsSync26(skillMdPath)) {
|
|
18719
19192
|
return null;
|
|
18720
19193
|
}
|
|
18721
19194
|
try {
|
|
18722
|
-
let content =
|
|
19195
|
+
let content = readFileSync15(skillMdPath, "utf-8");
|
|
18723
19196
|
content = await resolveCommandsInText(content);
|
|
18724
19197
|
const { data, body } = parseFrontmatter(content);
|
|
19198
|
+
const frontmatter2 = parseSkillFrontmatter(data);
|
|
18725
19199
|
const metadata = {
|
|
18726
|
-
name:
|
|
18727
|
-
description:
|
|
18728
|
-
license:
|
|
19200
|
+
name: frontmatter2.name || basename4(skillPath),
|
|
19201
|
+
description: frontmatter2.description,
|
|
19202
|
+
license: frontmatter2.license,
|
|
19203
|
+
allowedTools: frontmatter2["allowed-tools"],
|
|
19204
|
+
metadata: frontmatter2.metadata
|
|
18729
19205
|
};
|
|
18730
|
-
const referencesDir =
|
|
18731
|
-
const scriptsDir =
|
|
18732
|
-
const assetsDir =
|
|
18733
|
-
const references =
|
|
18734
|
-
const scripts =
|
|
18735
|
-
const assets =
|
|
19206
|
+
const referencesDir = join28(resolvedPath, "references");
|
|
19207
|
+
const scriptsDir = join28(resolvedPath, "scripts");
|
|
19208
|
+
const assetsDir = join28(resolvedPath, "assets");
|
|
19209
|
+
const references = existsSync26(referencesDir) ? readdirSync7(referencesDir).filter((f) => !f.startsWith(".")) : [];
|
|
19210
|
+
const scripts = existsSync26(scriptsDir) ? readdirSync7(scriptsDir).filter((f) => !f.startsWith(".") && !f.startsWith("__")) : [];
|
|
19211
|
+
const assets = existsSync26(assetsDir) ? readdirSync7(assetsDir).filter((f) => !f.startsWith(".")) : [];
|
|
18736
19212
|
return {
|
|
18737
19213
|
name: metadata.name,
|
|
18738
19214
|
path: resolvedPath,
|
|
19215
|
+
basePath: resolvedPath,
|
|
18739
19216
|
metadata,
|
|
18740
19217
|
content: body,
|
|
18741
19218
|
references,
|
|
@@ -18747,7 +19224,7 @@ async function parseSkillMd(skillPath) {
|
|
|
18747
19224
|
}
|
|
18748
19225
|
}
|
|
18749
19226
|
async function discoverSkillsFromDirAsync(skillsDir) {
|
|
18750
|
-
if (!
|
|
19227
|
+
if (!existsSync26(skillsDir)) {
|
|
18751
19228
|
return [];
|
|
18752
19229
|
}
|
|
18753
19230
|
const entries = readdirSync7(skillsDir, { withFileTypes: true });
|
|
@@ -18755,7 +19232,7 @@ async function discoverSkillsFromDirAsync(skillsDir) {
|
|
|
18755
19232
|
for (const entry of entries) {
|
|
18756
19233
|
if (entry.name.startsWith("."))
|
|
18757
19234
|
continue;
|
|
18758
|
-
const skillPath =
|
|
19235
|
+
const skillPath = join28(skillsDir, entry.name);
|
|
18759
19236
|
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
|
18760
19237
|
const skillInfo = await parseSkillMd(skillPath);
|
|
18761
19238
|
if (skillInfo) {
|
|
@@ -18766,8 +19243,8 @@ async function discoverSkillsFromDirAsync(skillsDir) {
|
|
|
18766
19243
|
return skills;
|
|
18767
19244
|
}
|
|
18768
19245
|
async function discoverSkills() {
|
|
18769
|
-
const userSkillsDir =
|
|
18770
|
-
const projectSkillsDir =
|
|
19246
|
+
const userSkillsDir = join28(homedir14(), ".claude", "skills");
|
|
19247
|
+
const projectSkillsDir = join28(process.cwd(), ".claude", "skills");
|
|
18771
19248
|
const userSkills = await discoverSkillsFromDirAsync(userSkillsDir);
|
|
18772
19249
|
const projectSkills = await discoverSkillsFromDirAsync(projectSkillsDir);
|
|
18773
19250
|
return [...projectSkills, ...userSkills];
|
|
@@ -18796,9 +19273,9 @@ async function loadSkillWithReferences(skill, includeRefs) {
|
|
|
18796
19273
|
const referencesLoaded = [];
|
|
18797
19274
|
if (includeRefs && skill.references.length > 0) {
|
|
18798
19275
|
for (const ref of skill.references) {
|
|
18799
|
-
const refPath =
|
|
19276
|
+
const refPath = join28(skill.path, "references", ref);
|
|
18800
19277
|
try {
|
|
18801
|
-
let content =
|
|
19278
|
+
let content = readFileSync15(refPath, "utf-8");
|
|
18802
19279
|
content = await resolveCommandsInText(content);
|
|
18803
19280
|
referencesLoaded.push({ path: ref, content });
|
|
18804
19281
|
} catch {}
|
|
@@ -18807,6 +19284,7 @@ async function loadSkillWithReferences(skill, includeRefs) {
|
|
|
18807
19284
|
return {
|
|
18808
19285
|
name: skill.name,
|
|
18809
19286
|
metadata: skill.metadata,
|
|
19287
|
+
basePath: skill.basePath,
|
|
18810
19288
|
body: skill.content,
|
|
18811
19289
|
referencesLoaded
|
|
18812
19290
|
};
|
|
@@ -18829,62 +19307,34 @@ function formatLoadedSkills(loadedSkills) {
|
|
|
18829
19307
|
if (loadedSkills.length === 0) {
|
|
18830
19308
|
return "No skills loaded.";
|
|
18831
19309
|
}
|
|
18832
|
-
const
|
|
18833
|
-
|
|
18834
|
-
for
|
|
18835
|
-
|
|
18836
|
-
|
|
18837
|
-
|
|
18838
|
-
`
|
|
18839
|
-
|
|
18840
|
-
`);
|
|
18841
|
-
sections.push(skill.body.trim());
|
|
18842
|
-
if (skill.referencesLoaded.length > 0) {
|
|
18843
|
-
sections.push(`
|
|
19310
|
+
const skill = loadedSkills[0];
|
|
19311
|
+
const sections = [];
|
|
19312
|
+
sections.push(`Base directory for this skill: ${skill.basePath}/`);
|
|
19313
|
+
sections.push("");
|
|
19314
|
+
sections.push(skill.body.trim());
|
|
19315
|
+
if (skill.referencesLoaded.length > 0) {
|
|
19316
|
+
sections.push(`
|
|
19317
|
+
---
|
|
18844
19318
|
### Loaded References
|
|
18845
19319
|
`);
|
|
18846
|
-
|
|
18847
|
-
|
|
19320
|
+
for (const ref of skill.referencesLoaded) {
|
|
19321
|
+
sections.push(`#### ${ref.path}
|
|
18848
19322
|
`);
|
|
18849
|
-
|
|
18850
|
-
|
|
18851
|
-
|
|
18852
|
-
}
|
|
19323
|
+
sections.push("```");
|
|
19324
|
+
sections.push(ref.content.trim());
|
|
19325
|
+
sections.push("```\n");
|
|
18853
19326
|
}
|
|
18854
|
-
sections.push(`
|
|
18855
|
-
---
|
|
18856
|
-
`);
|
|
18857
19327
|
}
|
|
18858
|
-
const skillNames = loadedSkills.map((s) => s.metadata.name).join(", ");
|
|
18859
|
-
sections.push(`**Skills loaded**: ${skillNames}`);
|
|
18860
|
-
sections.push(`**Total**: ${loadedSkills.length} skill(s)`);
|
|
18861
19328
|
sections.push(`
|
|
18862
|
-
|
|
19329
|
+
---
|
|
19330
|
+
**Launched skill**: ${skill.metadata.name}`);
|
|
18863
19331
|
return sections.join(`
|
|
18864
19332
|
`);
|
|
18865
19333
|
}
|
|
18866
19334
|
var skill = tool({
|
|
18867
19335
|
description: `Execute a skill within the main conversation.
|
|
18868
19336
|
|
|
18869
|
-
When
|
|
18870
|
-
|
|
18871
|
-
How to use skills:
|
|
18872
|
-
- Invoke skills using this tool with the skill name only (no arguments)
|
|
18873
|
-
- When you invoke a skill, the skill's prompt will expand and provide detailed instructions on how to complete the task
|
|
18874
|
-
|
|
18875
|
-
Important:
|
|
18876
|
-
- Only use skills listed in Available Skills below
|
|
18877
|
-
- Do not invoke a skill that is already running
|
|
18878
|
-
|
|
18879
|
-
Skills are loaded from:
|
|
18880
|
-
- ~/.claude/skills/ (user scope - global skills)
|
|
18881
|
-
- ./.claude/skills/ (project scope - project-specific skills)
|
|
18882
|
-
|
|
18883
|
-
Each skill contains:
|
|
18884
|
-
- SKILL.md: Main instructions with YAML frontmatter (name, description)
|
|
18885
|
-
- references/: Documentation files loaded into context as needed
|
|
18886
|
-
- scripts/: Executable code for deterministic operations
|
|
18887
|
-
- assets/: Files used in output (templates, icons, etc.)
|
|
19337
|
+
When you invoke a skill, the skill's prompt will expand and provide detailed instructions on how to complete the task.
|
|
18888
19338
|
|
|
18889
19339
|
Available Skills:
|
|
18890
19340
|
${skillListForDescription}`,
|
|
@@ -19009,26 +19459,62 @@ var OhMyOpenCodeConfigSchema = exports_external.object({
|
|
|
19009
19459
|
// src/index.ts
|
|
19010
19460
|
import * as fs4 from "fs";
|
|
19011
19461
|
import * as path3 from "path";
|
|
19462
|
+
import * as os3 from "os";
|
|
19463
|
+
function loadConfigFromPath2(configPath) {
|
|
19464
|
+
try {
|
|
19465
|
+
if (fs4.existsSync(configPath)) {
|
|
19466
|
+
const content = fs4.readFileSync(configPath, "utf-8");
|
|
19467
|
+
const rawConfig = JSON.parse(content);
|
|
19468
|
+
const result = OhMyOpenCodeConfigSchema.safeParse(rawConfig);
|
|
19469
|
+
if (!result.success) {
|
|
19470
|
+
log(`Config validation error in ${configPath}:`, result.error.issues);
|
|
19471
|
+
return null;
|
|
19472
|
+
}
|
|
19473
|
+
log(`Config loaded from ${configPath}`, { agents: result.data.agents });
|
|
19474
|
+
return result.data;
|
|
19475
|
+
}
|
|
19476
|
+
} catch (err) {
|
|
19477
|
+
log(`Error loading config from ${configPath}:`, err);
|
|
19478
|
+
}
|
|
19479
|
+
return null;
|
|
19480
|
+
}
|
|
19481
|
+
function mergeConfigs(base, override) {
|
|
19482
|
+
return {
|
|
19483
|
+
...base,
|
|
19484
|
+
...override,
|
|
19485
|
+
agents: override.agents !== undefined ? { ...base.agents ?? {}, ...override.agents } : base.agents,
|
|
19486
|
+
disabled_agents: [
|
|
19487
|
+
...new Set([...base.disabled_agents ?? [], ...override.disabled_agents ?? []])
|
|
19488
|
+
],
|
|
19489
|
+
disabled_mcps: [
|
|
19490
|
+
...new Set([...base.disabled_mcps ?? [], ...override.disabled_mcps ?? []])
|
|
19491
|
+
]
|
|
19492
|
+
};
|
|
19493
|
+
}
|
|
19012
19494
|
function loadPluginConfig(directory) {
|
|
19013
|
-
const
|
|
19014
|
-
path3.join(
|
|
19015
|
-
path3.join(directory, ".oh-my-opencode.json")
|
|
19495
|
+
const userConfigPaths = [
|
|
19496
|
+
path3.join(os3.homedir(), ".config", "opencode", "oh-my-opencode.json")
|
|
19016
19497
|
];
|
|
19017
|
-
|
|
19018
|
-
|
|
19019
|
-
|
|
19020
|
-
|
|
19021
|
-
|
|
19022
|
-
|
|
19023
|
-
|
|
19024
|
-
|
|
19025
|
-
|
|
19026
|
-
|
|
19027
|
-
|
|
19028
|
-
|
|
19029
|
-
|
|
19498
|
+
const projectConfigPaths = [
|
|
19499
|
+
path3.join(directory, ".opencode", "oh-my-opencode.json")
|
|
19500
|
+
];
|
|
19501
|
+
let config3 = {};
|
|
19502
|
+
for (const configPath of userConfigPaths) {
|
|
19503
|
+
const userConfig = loadConfigFromPath2(configPath);
|
|
19504
|
+
if (userConfig) {
|
|
19505
|
+
config3 = userConfig;
|
|
19506
|
+
break;
|
|
19507
|
+
}
|
|
19508
|
+
}
|
|
19509
|
+
for (const configPath of projectConfigPaths) {
|
|
19510
|
+
const projectConfig = loadConfigFromPath2(configPath);
|
|
19511
|
+
if (projectConfig) {
|
|
19512
|
+
config3 = mergeConfigs(config3, projectConfig);
|
|
19513
|
+
break;
|
|
19514
|
+
}
|
|
19030
19515
|
}
|
|
19031
|
-
|
|
19516
|
+
log("Final merged config", { agents: config3.agents, disabled_agents: config3.disabled_agents, disabled_mcps: config3.disabled_mcps });
|
|
19517
|
+
return config3;
|
|
19032
19518
|
}
|
|
19033
19519
|
var OhMyOpenCodePlugin = async (ctx) => {
|
|
19034
19520
|
const pluginConfig = loadPluginConfig(ctx.directory);
|
|
@@ -19038,9 +19524,11 @@ var OhMyOpenCodePlugin = async (ctx) => {
|
|
|
19038
19524
|
const commentChecker = createCommentCheckerHooks();
|
|
19039
19525
|
const grepOutputTruncator = createGrepOutputTruncatorHook(ctx);
|
|
19040
19526
|
const directoryAgentsInjector = createDirectoryAgentsInjectorHook(ctx);
|
|
19527
|
+
const directoryReadmeInjector = createDirectoryReadmeInjectorHook(ctx);
|
|
19041
19528
|
const emptyTaskResponseDetector = createEmptyTaskResponseDetectorHook(ctx);
|
|
19042
19529
|
const thinkMode = createThinkModeHook();
|
|
19043
19530
|
const claudeCodeHooks = createClaudeCodeHooksHook(ctx, {});
|
|
19531
|
+
const anthropicAutoCompact = createAnthropicAutoCompactHook(ctx);
|
|
19044
19532
|
updateTerminalTitle({ sessionId: "main" });
|
|
19045
19533
|
return {
|
|
19046
19534
|
tool: builtinTools,
|
|
@@ -19088,7 +19576,9 @@ var OhMyOpenCodePlugin = async (ctx) => {
|
|
|
19088
19576
|
await todoContinuationEnforcer(input);
|
|
19089
19577
|
await contextWindowMonitor.event(input);
|
|
19090
19578
|
await directoryAgentsInjector.event(input);
|
|
19579
|
+
await directoryReadmeInjector.event(input);
|
|
19091
19580
|
await thinkMode.event(input);
|
|
19581
|
+
await anthropicAutoCompact.event(input);
|
|
19092
19582
|
const { event } = input;
|
|
19093
19583
|
const props = event.properties;
|
|
19094
19584
|
if (event.type === "session.created") {
|
|
@@ -19186,6 +19676,7 @@ var OhMyOpenCodePlugin = async (ctx) => {
|
|
|
19186
19676
|
await contextWindowMonitor["tool.execute.after"](input, output);
|
|
19187
19677
|
await commentChecker["tool.execute.after"](input, output);
|
|
19188
19678
|
await directoryAgentsInjector["tool.execute.after"](input, output);
|
|
19679
|
+
await directoryReadmeInjector["tool.execute.after"](input, output);
|
|
19189
19680
|
await emptyTaskResponseDetector["tool.execute.after"](input, output);
|
|
19190
19681
|
if (input.sessionID === getMainSessionID()) {
|
|
19191
19682
|
updateTerminalTitle({
|