oh-my-opencode 0.1.24 → 0.1.26
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 +15 -0
- package/README.md +15 -1
- package/dist/hooks/grep-output-truncator.d.ts +12 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/pulse-monitor.d.ts +10 -0
- package/dist/hooks/session-recovery.d.ts +1 -1
- package/dist/index.js +506 -191
- package/dist/tools/ast-grep/index.d.ts +8 -8
- package/dist/tools/ast-grep/tools.d.ts +12 -12
- package/dist/tools/index.d.ts +8 -8
- package/dist/tools/lsp/client.d.ts +5 -1
- package/dist/tools/lsp/utils.d.ts +2 -2
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -602,18 +602,13 @@ function createBuiltinAgents(disabledAgents = [], agentOverrides = {}) {
|
|
|
602
602
|
return result;
|
|
603
603
|
}
|
|
604
604
|
// src/hooks/todo-continuation-enforcer.ts
|
|
605
|
-
var CONTINUATION_PROMPT = `[SYSTEM REMINDER - TODO
|
|
605
|
+
var CONTINUATION_PROMPT = `[SYSTEM REMINDER - TODO CONTINUATION]
|
|
606
606
|
|
|
607
|
-
|
|
607
|
+
Incomplete tasks remain in your todo list. Continue working on the next pending task.
|
|
608
608
|
|
|
609
|
-
|
|
610
|
-
-
|
|
611
|
-
-
|
|
612
|
-
- Work honestly and diligently to finish every task
|
|
613
|
-
- Do NOT ask for permission to continue - just proceed with the work
|
|
614
|
-
- Mark each task as completed as soon as you finish it
|
|
615
|
-
|
|
616
|
-
Resume your work NOW.`;
|
|
609
|
+
- Proceed without asking for permission
|
|
610
|
+
- Mark each task complete when finished
|
|
611
|
+
- Do not stop until all tasks are done`;
|
|
617
612
|
function detectInterrupt(error) {
|
|
618
613
|
if (!error)
|
|
619
614
|
return false;
|
|
@@ -694,7 +689,7 @@ function createTodoContinuationEnforcer(ctx) {
|
|
|
694
689
|
type: "text",
|
|
695
690
|
text: `${CONTINUATION_PROMPT}
|
|
696
691
|
|
|
697
|
-
[Status: ${incomplete.length}/${todos.length}
|
|
692
|
+
[Status: ${todos.length - incomplete.length}/${todos.length} completed, ${incomplete.length} remaining]`
|
|
698
693
|
}
|
|
699
694
|
]
|
|
700
695
|
},
|
|
@@ -790,6 +785,32 @@ ${CONTEXT_REMINDER}
|
|
|
790
785
|
};
|
|
791
786
|
}
|
|
792
787
|
// src/hooks/session-recovery.ts
|
|
788
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
789
|
+
import { join } from "path";
|
|
790
|
+
|
|
791
|
+
// node_modules/xdg-basedir/index.js
|
|
792
|
+
import os from "os";
|
|
793
|
+
import path from "path";
|
|
794
|
+
var homeDirectory = os.homedir();
|
|
795
|
+
var { env } = process;
|
|
796
|
+
var xdgData = env.XDG_DATA_HOME || (homeDirectory ? path.join(homeDirectory, ".local", "share") : undefined);
|
|
797
|
+
var xdgConfig = env.XDG_CONFIG_HOME || (homeDirectory ? path.join(homeDirectory, ".config") : undefined);
|
|
798
|
+
var xdgState = env.XDG_STATE_HOME || (homeDirectory ? path.join(homeDirectory, ".local", "state") : undefined);
|
|
799
|
+
var xdgCache = env.XDG_CACHE_HOME || (homeDirectory ? path.join(homeDirectory, ".cache") : undefined);
|
|
800
|
+
var xdgRuntime = env.XDG_RUNTIME_DIR || undefined;
|
|
801
|
+
var xdgDataDirectories = (env.XDG_DATA_DIRS || "/usr/local/share/:/usr/share/").split(":");
|
|
802
|
+
if (xdgData) {
|
|
803
|
+
xdgDataDirectories.unshift(xdgData);
|
|
804
|
+
}
|
|
805
|
+
var xdgConfigDirectories = (env.XDG_CONFIG_DIRS || "/etc/xdg").split(":");
|
|
806
|
+
if (xdgConfig) {
|
|
807
|
+
xdgConfigDirectories.unshift(xdgConfig);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// src/hooks/session-recovery.ts
|
|
811
|
+
var OPENCODE_STORAGE = join(xdgData ?? "", "opencode", "storage");
|
|
812
|
+
var MESSAGE_STORAGE = join(OPENCODE_STORAGE, "message");
|
|
813
|
+
var PART_STORAGE = join(OPENCODE_STORAGE, "part");
|
|
793
814
|
function getErrorMessage(error) {
|
|
794
815
|
if (!error)
|
|
795
816
|
return "";
|
|
@@ -894,80 +915,121 @@ async function recoverThinkingDisabledViolation(client, sessionID, failedAssista
|
|
|
894
915
|
return false;
|
|
895
916
|
}
|
|
896
917
|
var THINKING_TYPES = new Set(["thinking", "redacted_thinking", "reasoning"]);
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
return
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
918
|
+
var META_TYPES = new Set(["step-start", "step-finish"]);
|
|
919
|
+
function generatePartId() {
|
|
920
|
+
const timestamp = Date.now().toString(16);
|
|
921
|
+
const random = Math.random().toString(36).substring(2, 10);
|
|
922
|
+
return `prt_${timestamp}${random}`;
|
|
923
|
+
}
|
|
924
|
+
function getMessageDir(sessionID) {
|
|
925
|
+
const projectHash = readdirSync(MESSAGE_STORAGE).find((dir) => {
|
|
926
|
+
const sessionDir = join(MESSAGE_STORAGE, dir);
|
|
927
|
+
try {
|
|
928
|
+
return readdirSync(sessionDir).some((f) => f.includes(sessionID.replace("ses_", "")));
|
|
929
|
+
} catch {
|
|
905
930
|
return false;
|
|
906
|
-
|
|
907
|
-
return true;
|
|
908
|
-
if (p.type === "tool_use" && p.id)
|
|
909
|
-
return true;
|
|
910
|
-
if (p.type === "tool_result")
|
|
911
|
-
return true;
|
|
912
|
-
return false;
|
|
931
|
+
}
|
|
913
932
|
});
|
|
933
|
+
if (projectHash) {
|
|
934
|
+
return join(MESSAGE_STORAGE, projectHash, sessionID);
|
|
935
|
+
}
|
|
936
|
+
for (const dir of readdirSync(MESSAGE_STORAGE)) {
|
|
937
|
+
const sessionPath = join(MESSAGE_STORAGE, dir, sessionID);
|
|
938
|
+
if (existsSync(sessionPath)) {
|
|
939
|
+
return sessionPath;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
return "";
|
|
914
943
|
}
|
|
915
|
-
function
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
944
|
+
function readMessagesFromStorage(sessionID) {
|
|
945
|
+
const messageDir = getMessageDir(sessionID);
|
|
946
|
+
if (!messageDir || !existsSync(messageDir))
|
|
947
|
+
return [];
|
|
948
|
+
const messages = [];
|
|
949
|
+
for (const file of readdirSync(messageDir)) {
|
|
950
|
+
if (!file.endsWith(".json"))
|
|
951
|
+
continue;
|
|
952
|
+
try {
|
|
953
|
+
const content = readFileSync(join(messageDir, file), "utf-8");
|
|
954
|
+
messages.push(JSON.parse(content));
|
|
955
|
+
} catch {
|
|
921
956
|
continue;
|
|
922
|
-
if (!hasNonEmptyOutput(msg)) {
|
|
923
|
-
return msg;
|
|
924
957
|
}
|
|
925
958
|
}
|
|
926
|
-
return
|
|
959
|
+
return messages.sort((a, b) => a.id.localeCompare(b.id));
|
|
927
960
|
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
const existingParts = emptyMsg.parts || [];
|
|
942
|
-
const hasOnlyThinkingOrMeta = existingParts.length > 0 && existingParts.every((p) => THINKING_TYPES.has(p.type) || p.type === "step-start" || p.type === "step-finish");
|
|
943
|
-
if (hasOnlyThinkingOrMeta) {
|
|
944
|
-
const strippedParts = [{ type: "text", text: "(interrupted)" }];
|
|
945
|
-
try {
|
|
946
|
-
await client.message?.update?.({
|
|
947
|
-
path: { id: messageID },
|
|
948
|
-
body: { parts: strippedParts }
|
|
949
|
-
});
|
|
950
|
-
return true;
|
|
951
|
-
} catch {}
|
|
952
|
-
try {
|
|
953
|
-
await client.session.patch?.({
|
|
954
|
-
path: { id: sessionID },
|
|
955
|
-
body: { messageID, parts: strippedParts }
|
|
956
|
-
});
|
|
957
|
-
return true;
|
|
958
|
-
} catch {}
|
|
961
|
+
function readPartsFromStorage(messageID) {
|
|
962
|
+
const partDir = join(PART_STORAGE, messageID);
|
|
963
|
+
if (!existsSync(partDir))
|
|
964
|
+
return [];
|
|
965
|
+
const parts = [];
|
|
966
|
+
for (const file of readdirSync(partDir)) {
|
|
967
|
+
if (!file.endsWith(".json"))
|
|
968
|
+
continue;
|
|
969
|
+
try {
|
|
970
|
+
const content = readFileSync(join(partDir, file), "utf-8");
|
|
971
|
+
parts.push(JSON.parse(content));
|
|
972
|
+
} catch {
|
|
973
|
+
continue;
|
|
959
974
|
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
975
|
+
}
|
|
976
|
+
return parts;
|
|
977
|
+
}
|
|
978
|
+
function injectTextPartToStorage(sessionID, messageID, text) {
|
|
979
|
+
const partDir = join(PART_STORAGE, messageID);
|
|
980
|
+
if (!existsSync(partDir)) {
|
|
981
|
+
mkdirSync(partDir, { recursive: true });
|
|
982
|
+
}
|
|
983
|
+
const partId = generatePartId();
|
|
984
|
+
const part = {
|
|
985
|
+
id: partId,
|
|
986
|
+
sessionID,
|
|
987
|
+
messageID,
|
|
988
|
+
type: "text",
|
|
989
|
+
text
|
|
990
|
+
};
|
|
991
|
+
try {
|
|
992
|
+
writeFileSync(join(partDir, `${partId}.json`), JSON.stringify(part, null, 2));
|
|
966
993
|
return true;
|
|
967
994
|
} catch {
|
|
968
995
|
return false;
|
|
969
996
|
}
|
|
970
997
|
}
|
|
998
|
+
function findEmptyContentMessageFromStorage(sessionID) {
|
|
999
|
+
const messages = readMessagesFromStorage(sessionID);
|
|
1000
|
+
for (let i = 0;i < messages.length; i++) {
|
|
1001
|
+
const msg = messages[i];
|
|
1002
|
+
if (msg.role !== "assistant")
|
|
1003
|
+
continue;
|
|
1004
|
+
const isLastMessage = i === messages.length - 1;
|
|
1005
|
+
if (isLastMessage)
|
|
1006
|
+
continue;
|
|
1007
|
+
const parts = readPartsFromStorage(msg.id);
|
|
1008
|
+
const hasContent = parts.some((p) => {
|
|
1009
|
+
if (THINKING_TYPES.has(p.type))
|
|
1010
|
+
return false;
|
|
1011
|
+
if (META_TYPES.has(p.type))
|
|
1012
|
+
return false;
|
|
1013
|
+
if (p.type === "text" && p.text?.trim())
|
|
1014
|
+
return true;
|
|
1015
|
+
if (p.type === "tool_use")
|
|
1016
|
+
return true;
|
|
1017
|
+
if (p.type === "tool_result")
|
|
1018
|
+
return true;
|
|
1019
|
+
return false;
|
|
1020
|
+
});
|
|
1021
|
+
if (!hasContent && parts.length > 0) {
|
|
1022
|
+
return msg.id;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
return null;
|
|
1026
|
+
}
|
|
1027
|
+
async function recoverEmptyContentMessage(_client, sessionID, failedAssistantMsg, _directory) {
|
|
1028
|
+
const emptyMessageID = findEmptyContentMessageFromStorage(sessionID) || failedAssistantMsg.info?.id;
|
|
1029
|
+
if (!emptyMessageID)
|
|
1030
|
+
return false;
|
|
1031
|
+
return injectTextPartToStorage(sessionID, emptyMessageID, "(interrupted)");
|
|
1032
|
+
}
|
|
971
1033
|
async function fallbackRevertStrategy(client, sessionID, failedAssistantMsg, directory) {
|
|
972
1034
|
const parentMsgID = failedAssistantMsg.info?.parentID;
|
|
973
1035
|
const messagesResp = await client.session.messages({
|
|
@@ -1093,14 +1155,14 @@ function createSessionRecoveryHook(ctx) {
|
|
|
1093
1155
|
// src/hooks/comment-checker/cli.ts
|
|
1094
1156
|
var {spawn: spawn2 } = globalThis.Bun;
|
|
1095
1157
|
import { createRequire as createRequire2 } from "module";
|
|
1096
|
-
import { dirname, join as
|
|
1097
|
-
import { existsSync as
|
|
1158
|
+
import { dirname, join as join3 } from "path";
|
|
1159
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1098
1160
|
import * as fs from "fs";
|
|
1099
1161
|
|
|
1100
1162
|
// src/hooks/comment-checker/downloader.ts
|
|
1101
1163
|
var {spawn } = globalThis.Bun;
|
|
1102
|
-
import { existsSync, mkdirSync, chmodSync, unlinkSync, appendFileSync } from "fs";
|
|
1103
|
-
import { join } from "path";
|
|
1164
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, chmodSync, unlinkSync, appendFileSync } from "fs";
|
|
1165
|
+
import { join as join2 } from "path";
|
|
1104
1166
|
import { homedir } from "os";
|
|
1105
1167
|
import { createRequire } from "module";
|
|
1106
1168
|
var DEBUG = process.env.COMMENT_CHECKER_DEBUG === "1";
|
|
@@ -1121,16 +1183,16 @@ var PLATFORM_MAP = {
|
|
|
1121
1183
|
"win32-x64": { os: "windows", arch: "amd64", ext: "zip" }
|
|
1122
1184
|
};
|
|
1123
1185
|
function getCacheDir() {
|
|
1124
|
-
const
|
|
1125
|
-
const base =
|
|
1126
|
-
return
|
|
1186
|
+
const xdgCache2 = process.env.XDG_CACHE_HOME;
|
|
1187
|
+
const base = xdgCache2 || join2(homedir(), ".cache");
|
|
1188
|
+
return join2(base, "oh-my-opencode", "bin");
|
|
1127
1189
|
}
|
|
1128
1190
|
function getBinaryName() {
|
|
1129
1191
|
return process.platform === "win32" ? "comment-checker.exe" : "comment-checker";
|
|
1130
1192
|
}
|
|
1131
1193
|
function getCachedBinaryPath() {
|
|
1132
|
-
const binaryPath =
|
|
1133
|
-
return
|
|
1194
|
+
const binaryPath = join2(getCacheDir(), getBinaryName());
|
|
1195
|
+
return existsSync2(binaryPath) ? binaryPath : null;
|
|
1134
1196
|
}
|
|
1135
1197
|
function getPackageVersion() {
|
|
1136
1198
|
try {
|
|
@@ -1177,26 +1239,26 @@ async function downloadCommentChecker() {
|
|
|
1177
1239
|
}
|
|
1178
1240
|
const cacheDir = getCacheDir();
|
|
1179
1241
|
const binaryName = getBinaryName();
|
|
1180
|
-
const binaryPath =
|
|
1181
|
-
if (
|
|
1242
|
+
const binaryPath = join2(cacheDir, binaryName);
|
|
1243
|
+
if (existsSync2(binaryPath)) {
|
|
1182
1244
|
debugLog("Binary already cached at:", binaryPath);
|
|
1183
1245
|
return binaryPath;
|
|
1184
1246
|
}
|
|
1185
1247
|
const version = getPackageVersion();
|
|
1186
|
-
const { os, arch, ext } = platformInfo;
|
|
1187
|
-
const assetName = `comment-checker_v${version}_${
|
|
1248
|
+
const { os: os2, arch, ext } = platformInfo;
|
|
1249
|
+
const assetName = `comment-checker_v${version}_${os2}_${arch}.${ext}`;
|
|
1188
1250
|
const downloadUrl = `https://github.com/${REPO}/releases/download/v${version}/${assetName}`;
|
|
1189
1251
|
debugLog(`Downloading from: ${downloadUrl}`);
|
|
1190
1252
|
console.log(`[oh-my-opencode] Downloading comment-checker binary...`);
|
|
1191
1253
|
try {
|
|
1192
|
-
if (!
|
|
1193
|
-
|
|
1254
|
+
if (!existsSync2(cacheDir)) {
|
|
1255
|
+
mkdirSync2(cacheDir, { recursive: true });
|
|
1194
1256
|
}
|
|
1195
1257
|
const response = await fetch(downloadUrl, { redirect: "follow" });
|
|
1196
1258
|
if (!response.ok) {
|
|
1197
1259
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
1198
1260
|
}
|
|
1199
|
-
const archivePath =
|
|
1261
|
+
const archivePath = join2(cacheDir, assetName);
|
|
1200
1262
|
const arrayBuffer = await response.arrayBuffer();
|
|
1201
1263
|
await Bun.write(archivePath, arrayBuffer);
|
|
1202
1264
|
debugLog(`Downloaded archive to: ${archivePath}`);
|
|
@@ -1205,10 +1267,10 @@ async function downloadCommentChecker() {
|
|
|
1205
1267
|
} else {
|
|
1206
1268
|
await extractZip(archivePath, cacheDir);
|
|
1207
1269
|
}
|
|
1208
|
-
if (
|
|
1270
|
+
if (existsSync2(archivePath)) {
|
|
1209
1271
|
unlinkSync(archivePath);
|
|
1210
1272
|
}
|
|
1211
|
-
if (process.platform !== "win32" &&
|
|
1273
|
+
if (process.platform !== "win32" && existsSync2(binaryPath)) {
|
|
1212
1274
|
chmodSync(binaryPath, 493);
|
|
1213
1275
|
}
|
|
1214
1276
|
debugLog(`Successfully downloaded binary to: ${binaryPath}`);
|
|
@@ -1261,8 +1323,8 @@ function findCommentCheckerPathSync() {
|
|
|
1261
1323
|
const require2 = createRequire2(import.meta.url);
|
|
1262
1324
|
const cliPkgPath = require2.resolve("@code-yeongyu/comment-checker/package.json");
|
|
1263
1325
|
const cliDir = dirname(cliPkgPath);
|
|
1264
|
-
const binaryPath =
|
|
1265
|
-
if (
|
|
1326
|
+
const binaryPath = join3(cliDir, "bin", binaryName);
|
|
1327
|
+
if (existsSync3(binaryPath)) {
|
|
1266
1328
|
debugLog2("found binary in main package:", binaryPath);
|
|
1267
1329
|
return binaryPath;
|
|
1268
1330
|
}
|
|
@@ -1275,8 +1337,8 @@ function findCommentCheckerPathSync() {
|
|
|
1275
1337
|
const require2 = createRequire2(import.meta.url);
|
|
1276
1338
|
const pkgPath = require2.resolve(`${platformPkg}/package.json`);
|
|
1277
1339
|
const pkgDir = dirname(pkgPath);
|
|
1278
|
-
const binaryPath =
|
|
1279
|
-
if (
|
|
1340
|
+
const binaryPath = join3(pkgDir, "bin", binaryName);
|
|
1341
|
+
if (existsSync3(binaryPath)) {
|
|
1280
1342
|
debugLog2("found binary in platform package:", binaryPath);
|
|
1281
1343
|
return binaryPath;
|
|
1282
1344
|
}
|
|
@@ -1289,10 +1351,10 @@ function findCommentCheckerPathSync() {
|
|
|
1289
1351
|
"/opt/homebrew/bin/comment-checker",
|
|
1290
1352
|
"/usr/local/bin/comment-checker"
|
|
1291
1353
|
];
|
|
1292
|
-
for (const
|
|
1293
|
-
if (
|
|
1294
|
-
debugLog2("found binary via homebrew:",
|
|
1295
|
-
return
|
|
1354
|
+
for (const path2 of homebrewPaths) {
|
|
1355
|
+
if (existsSync3(path2)) {
|
|
1356
|
+
debugLog2("found binary via homebrew:", path2);
|
|
1357
|
+
return path2;
|
|
1296
1358
|
}
|
|
1297
1359
|
}
|
|
1298
1360
|
}
|
|
@@ -1315,7 +1377,7 @@ async function getCommentCheckerPath() {
|
|
|
1315
1377
|
}
|
|
1316
1378
|
initPromise = (async () => {
|
|
1317
1379
|
const syncPath = findCommentCheckerPathSync();
|
|
1318
|
-
if (syncPath &&
|
|
1380
|
+
if (syncPath && existsSync3(syncPath)) {
|
|
1319
1381
|
resolvedCliPath = syncPath;
|
|
1320
1382
|
debugLog2("using sync-resolved path:", syncPath);
|
|
1321
1383
|
return syncPath;
|
|
@@ -1335,8 +1397,8 @@ async function getCommentCheckerPath() {
|
|
|
1335
1397
|
function startBackgroundInit() {
|
|
1336
1398
|
if (!initPromise) {
|
|
1337
1399
|
initPromise = getCommentCheckerPath();
|
|
1338
|
-
initPromise.then((
|
|
1339
|
-
debugLog2("background init complete:",
|
|
1400
|
+
initPromise.then((path2) => {
|
|
1401
|
+
debugLog2("background init complete:", path2 || "no binary");
|
|
1340
1402
|
}).catch((err) => {
|
|
1341
1403
|
debugLog2("background init error:", err);
|
|
1342
1404
|
});
|
|
@@ -1349,7 +1411,7 @@ async function runCommentChecker(input, cliPath) {
|
|
|
1349
1411
|
debugLog2("comment-checker binary not found");
|
|
1350
1412
|
return { hasComments: false, message: "" };
|
|
1351
1413
|
}
|
|
1352
|
-
if (!
|
|
1414
|
+
if (!existsSync3(binaryPath)) {
|
|
1353
1415
|
debugLog2("comment-checker binary does not exist:", binaryPath);
|
|
1354
1416
|
return { hasComments: false, message: "" };
|
|
1355
1417
|
}
|
|
@@ -1383,7 +1445,7 @@ async function runCommentChecker(input, cliPath) {
|
|
|
1383
1445
|
|
|
1384
1446
|
// src/hooks/comment-checker/index.ts
|
|
1385
1447
|
import * as fs2 from "fs";
|
|
1386
|
-
import { existsSync as
|
|
1448
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1387
1449
|
var DEBUG3 = process.env.COMMENT_CHECKER_DEBUG === "1";
|
|
1388
1450
|
var DEBUG_FILE3 = "/tmp/comment-checker-debug.log";
|
|
1389
1451
|
function debugLog3(...args) {
|
|
@@ -1409,8 +1471,8 @@ function createCommentCheckerHooks() {
|
|
|
1409
1471
|
debugLog3("createCommentCheckerHooks called");
|
|
1410
1472
|
startBackgroundInit();
|
|
1411
1473
|
cliPathPromise = getCommentCheckerPath();
|
|
1412
|
-
cliPathPromise.then((
|
|
1413
|
-
debugLog3("CLI path resolved:",
|
|
1474
|
+
cliPathPromise.then((path2) => {
|
|
1475
|
+
debugLog3("CLI path resolved:", path2 || "disabled (no binary)");
|
|
1414
1476
|
}).catch((err) => {
|
|
1415
1477
|
debugLog3("CLI path resolution error:", err);
|
|
1416
1478
|
});
|
|
@@ -1461,7 +1523,7 @@ function createCommentCheckerHooks() {
|
|
|
1461
1523
|
}
|
|
1462
1524
|
try {
|
|
1463
1525
|
const cliPath = await cliPathPromise;
|
|
1464
|
-
if (!cliPath || !
|
|
1526
|
+
if (!cliPath || !existsSync4(cliPath)) {
|
|
1465
1527
|
debugLog3("CLI not available, skipping comment check");
|
|
1466
1528
|
return;
|
|
1467
1529
|
}
|
|
@@ -1499,6 +1561,208 @@ ${result.message}`;
|
|
|
1499
1561
|
debugLog3("CLI: no comments detected");
|
|
1500
1562
|
}
|
|
1501
1563
|
}
|
|
1564
|
+
// src/hooks/grep-output-truncator.ts
|
|
1565
|
+
var ANTHROPIC_ACTUAL_LIMIT2 = 200000;
|
|
1566
|
+
var CHARS_PER_TOKEN_ESTIMATE = 4;
|
|
1567
|
+
var TARGET_MAX_TOKENS = 50000;
|
|
1568
|
+
function estimateTokens(text) {
|
|
1569
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN_ESTIMATE);
|
|
1570
|
+
}
|
|
1571
|
+
function truncateToTokenLimit(output, maxTokens) {
|
|
1572
|
+
const currentTokens = estimateTokens(output);
|
|
1573
|
+
if (currentTokens <= maxTokens) {
|
|
1574
|
+
return { result: output, truncated: false };
|
|
1575
|
+
}
|
|
1576
|
+
const lines = output.split(`
|
|
1577
|
+
`);
|
|
1578
|
+
if (lines.length <= 3) {
|
|
1579
|
+
const maxChars = maxTokens * CHARS_PER_TOKEN_ESTIMATE;
|
|
1580
|
+
return {
|
|
1581
|
+
result: output.slice(0, maxChars) + `
|
|
1582
|
+
|
|
1583
|
+
[Output truncated due to context window limit]`,
|
|
1584
|
+
truncated: true
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
const headerLines = lines.slice(0, 3);
|
|
1588
|
+
const contentLines = lines.slice(3);
|
|
1589
|
+
const headerText = headerLines.join(`
|
|
1590
|
+
`);
|
|
1591
|
+
const headerTokens = estimateTokens(headerText);
|
|
1592
|
+
const availableTokens = maxTokens - headerTokens - 50;
|
|
1593
|
+
if (availableTokens <= 0) {
|
|
1594
|
+
return {
|
|
1595
|
+
result: headerText + `
|
|
1596
|
+
|
|
1597
|
+
[Content truncated due to context window limit]`,
|
|
1598
|
+
truncated: true
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1601
|
+
let resultLines = [];
|
|
1602
|
+
let currentTokenCount = 0;
|
|
1603
|
+
for (const line of contentLines) {
|
|
1604
|
+
const lineTokens = estimateTokens(line + `
|
|
1605
|
+
`);
|
|
1606
|
+
if (currentTokenCount + lineTokens > availableTokens) {
|
|
1607
|
+
break;
|
|
1608
|
+
}
|
|
1609
|
+
resultLines.push(line);
|
|
1610
|
+
currentTokenCount += lineTokens;
|
|
1611
|
+
}
|
|
1612
|
+
const truncatedContent = [...headerLines, ...resultLines].join(`
|
|
1613
|
+
`);
|
|
1614
|
+
const removedCount = contentLines.length - resultLines.length;
|
|
1615
|
+
return {
|
|
1616
|
+
result: truncatedContent + `
|
|
1617
|
+
|
|
1618
|
+
[${removedCount} more lines truncated due to context window limit]`,
|
|
1619
|
+
truncated: true
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
function createGrepOutputTruncatorHook(ctx) {
|
|
1623
|
+
const GREP_TOOLS = ["safe_grep", "Grep"];
|
|
1624
|
+
const toolExecuteAfter = async (input, output) => {
|
|
1625
|
+
if (!GREP_TOOLS.includes(input.tool))
|
|
1626
|
+
return;
|
|
1627
|
+
const { sessionID } = input;
|
|
1628
|
+
try {
|
|
1629
|
+
const response = await ctx.client.session.messages({
|
|
1630
|
+
path: { id: sessionID }
|
|
1631
|
+
});
|
|
1632
|
+
const messages = response.data ?? response;
|
|
1633
|
+
const assistantMessages = messages.filter((m) => m.info.role === "assistant").map((m) => m.info);
|
|
1634
|
+
if (assistantMessages.length === 0)
|
|
1635
|
+
return;
|
|
1636
|
+
const totalInputTokens = assistantMessages.reduce((sum, m) => {
|
|
1637
|
+
const inputTokens = m.tokens?.input ?? 0;
|
|
1638
|
+
const cacheReadTokens = m.tokens?.cache?.read ?? 0;
|
|
1639
|
+
return sum + inputTokens + cacheReadTokens;
|
|
1640
|
+
}, 0);
|
|
1641
|
+
const remainingTokens = ANTHROPIC_ACTUAL_LIMIT2 - totalInputTokens;
|
|
1642
|
+
const maxOutputTokens = Math.min(remainingTokens * 0.5, TARGET_MAX_TOKENS);
|
|
1643
|
+
if (maxOutputTokens <= 0) {
|
|
1644
|
+
output.output = "[Output suppressed - context window exhausted]";
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
const { result, truncated } = truncateToTokenLimit(output.output, maxOutputTokens);
|
|
1648
|
+
if (truncated) {
|
|
1649
|
+
output.output = result;
|
|
1650
|
+
}
|
|
1651
|
+
} catch {}
|
|
1652
|
+
};
|
|
1653
|
+
return {
|
|
1654
|
+
"tool.execute.after": toolExecuteAfter
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
// src/hooks/pulse-monitor.ts
|
|
1658
|
+
function createPulseMonitorHook(ctx) {
|
|
1659
|
+
const STANDARD_TIMEOUT = 5 * 60 * 1000;
|
|
1660
|
+
const THINKING_TIMEOUT = 5 * 60 * 1000;
|
|
1661
|
+
const CHECK_INTERVAL = 5 * 1000;
|
|
1662
|
+
let lastHeartbeat = Date.now();
|
|
1663
|
+
let isMonitoring = false;
|
|
1664
|
+
let currentSessionID = null;
|
|
1665
|
+
let monitorTimer = null;
|
|
1666
|
+
let isThinking = false;
|
|
1667
|
+
const startMonitoring = (sessionID) => {
|
|
1668
|
+
if (currentSessionID !== sessionID) {
|
|
1669
|
+
currentSessionID = sessionID;
|
|
1670
|
+
isThinking = false;
|
|
1671
|
+
}
|
|
1672
|
+
lastHeartbeat = Date.now();
|
|
1673
|
+
if (!isMonitoring) {
|
|
1674
|
+
isMonitoring = true;
|
|
1675
|
+
if (monitorTimer)
|
|
1676
|
+
clearInterval(monitorTimer);
|
|
1677
|
+
monitorTimer = setInterval(async () => {
|
|
1678
|
+
if (!isMonitoring || !currentSessionID)
|
|
1679
|
+
return;
|
|
1680
|
+
const timeSinceLastHeartbeat = Date.now() - lastHeartbeat;
|
|
1681
|
+
const currentTimeout = isThinking ? THINKING_TIMEOUT : STANDARD_TIMEOUT;
|
|
1682
|
+
if (timeSinceLastHeartbeat > currentTimeout) {
|
|
1683
|
+
await recoverStalledSession(currentSessionID, timeSinceLastHeartbeat, isThinking);
|
|
1684
|
+
}
|
|
1685
|
+
}, CHECK_INTERVAL);
|
|
1686
|
+
}
|
|
1687
|
+
};
|
|
1688
|
+
const stopMonitoring = () => {
|
|
1689
|
+
isMonitoring = false;
|
|
1690
|
+
if (monitorTimer) {
|
|
1691
|
+
clearInterval(monitorTimer);
|
|
1692
|
+
monitorTimer = null;
|
|
1693
|
+
}
|
|
1694
|
+
};
|
|
1695
|
+
const updateHeartbeat = (isThinkingUpdate) => {
|
|
1696
|
+
if (isMonitoring) {
|
|
1697
|
+
lastHeartbeat = Date.now();
|
|
1698
|
+
if (isThinkingUpdate !== undefined) {
|
|
1699
|
+
isThinking = isThinkingUpdate;
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
};
|
|
1703
|
+
const recoverStalledSession = async (sessionID, stalledDuration, wasThinking) => {
|
|
1704
|
+
stopMonitoring();
|
|
1705
|
+
try {
|
|
1706
|
+
const durationSec = Math.round(stalledDuration / 1000);
|
|
1707
|
+
const typeStr = wasThinking ? "Thinking" : "Standard";
|
|
1708
|
+
await ctx.client.tui.showToast({
|
|
1709
|
+
body: {
|
|
1710
|
+
title: "Pulse Monitor: Cardiac Arrest",
|
|
1711
|
+
message: `Session stalled (${typeStr}) for ${durationSec}s. Defibrillating...`,
|
|
1712
|
+
variant: "error",
|
|
1713
|
+
duration: 5000
|
|
1714
|
+
}
|
|
1715
|
+
}).catch(() => {});
|
|
1716
|
+
await ctx.client.session.abort({ path: { id: sessionID } }).catch(() => {});
|
|
1717
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
1718
|
+
await ctx.client.session.prompt({
|
|
1719
|
+
path: { id: sessionID },
|
|
1720
|
+
body: { parts: [{ type: "text", text: "The connection was unstable and stalled. Please continue from where you left off." }] },
|
|
1721
|
+
query: { directory: ctx.directory }
|
|
1722
|
+
});
|
|
1723
|
+
startMonitoring(sessionID);
|
|
1724
|
+
} catch (err) {
|
|
1725
|
+
console.error("[PulseMonitor] Recovery failed:", err);
|
|
1726
|
+
stopMonitoring();
|
|
1727
|
+
}
|
|
1728
|
+
};
|
|
1729
|
+
return {
|
|
1730
|
+
event: async (input) => {
|
|
1731
|
+
const { event } = input;
|
|
1732
|
+
const props = event.properties;
|
|
1733
|
+
if (event.type === "session.updated" || event.type === "message.part.updated") {
|
|
1734
|
+
const sessionID = props?.info?.id || props?.sessionID;
|
|
1735
|
+
if (sessionID) {
|
|
1736
|
+
if (!isMonitoring)
|
|
1737
|
+
startMonitoring(sessionID);
|
|
1738
|
+
let thinkingUpdate = undefined;
|
|
1739
|
+
if (event.type === "message.part.updated") {
|
|
1740
|
+
const part = props?.part;
|
|
1741
|
+
if (part) {
|
|
1742
|
+
const THINKING_TYPES2 = ["thinking", "redacted_thinking", "reasoning"];
|
|
1743
|
+
if (THINKING_TYPES2.includes(part.type)) {
|
|
1744
|
+
thinkingUpdate = true;
|
|
1745
|
+
} else if (part.type === "text" || part.type === "tool_use") {
|
|
1746
|
+
thinkingUpdate = false;
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
updateHeartbeat(thinkingUpdate);
|
|
1751
|
+
}
|
|
1752
|
+
} else if (event.type === "session.idle" || event.type === "session.error" || event.type === "session.stopped") {
|
|
1753
|
+
stopMonitoring();
|
|
1754
|
+
}
|
|
1755
|
+
},
|
|
1756
|
+
"tool.execute.before": async () => {
|
|
1757
|
+
stopMonitoring();
|
|
1758
|
+
},
|
|
1759
|
+
"tool.execute.after": async (input) => {
|
|
1760
|
+
if (input.sessionID) {
|
|
1761
|
+
startMonitoring(input.sessionID);
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1502
1766
|
// src/features/terminal/title.ts
|
|
1503
1767
|
var STATUS_ICONS = {
|
|
1504
1768
|
ready: "",
|
|
@@ -1707,17 +1971,31 @@ var EXT_TO_LANG = {
|
|
|
1707
1971
|
".svelte": "svelte",
|
|
1708
1972
|
".astro": "astro",
|
|
1709
1973
|
".yaml": "yaml",
|
|
1710
|
-
".yml": "yaml"
|
|
1974
|
+
".yml": "yaml",
|
|
1975
|
+
".json": "json",
|
|
1976
|
+
".jsonc": "jsonc",
|
|
1977
|
+
".html": "html",
|
|
1978
|
+
".htm": "html",
|
|
1979
|
+
".css": "css",
|
|
1980
|
+
".scss": "scss",
|
|
1981
|
+
".less": "less",
|
|
1982
|
+
".sh": "shellscript",
|
|
1983
|
+
".bash": "shellscript",
|
|
1984
|
+
".zsh": "shellscript",
|
|
1985
|
+
".fish": "fish",
|
|
1986
|
+
".md": "markdown",
|
|
1987
|
+
".tf": "terraform",
|
|
1988
|
+
".tfvars": "terraform"
|
|
1711
1989
|
};
|
|
1712
1990
|
// src/tools/lsp/config.ts
|
|
1713
|
-
import { existsSync as
|
|
1714
|
-
import { join as
|
|
1991
|
+
import { existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
|
|
1992
|
+
import { join as join4 } from "path";
|
|
1715
1993
|
import { homedir as homedir2 } from "os";
|
|
1716
|
-
function loadJsonFile(
|
|
1717
|
-
if (!
|
|
1994
|
+
function loadJsonFile(path2) {
|
|
1995
|
+
if (!existsSync5(path2))
|
|
1718
1996
|
return null;
|
|
1719
1997
|
try {
|
|
1720
|
-
return JSON.parse(
|
|
1998
|
+
return JSON.parse(readFileSync2(path2, "utf-8"));
|
|
1721
1999
|
} catch {
|
|
1722
2000
|
return null;
|
|
1723
2001
|
}
|
|
@@ -1725,9 +2003,9 @@ function loadJsonFile(path) {
|
|
|
1725
2003
|
function getConfigPaths() {
|
|
1726
2004
|
const cwd = process.cwd();
|
|
1727
2005
|
return {
|
|
1728
|
-
project:
|
|
1729
|
-
user:
|
|
1730
|
-
opencode:
|
|
2006
|
+
project: join4(cwd, ".opencode", "oh-my-opencode.json"),
|
|
2007
|
+
user: join4(homedir2(), ".config", "opencode", "oh-my-opencode.json"),
|
|
2008
|
+
opencode: join4(homedir2(), ".config", "opencode", "opencode.json")
|
|
1731
2009
|
};
|
|
1732
2010
|
}
|
|
1733
2011
|
function loadAllConfigs() {
|
|
@@ -1820,7 +2098,7 @@ function isServerInstalled(command) {
|
|
|
1820
2098
|
const pathEnv = process.env.PATH || "";
|
|
1821
2099
|
const paths = pathEnv.split(":");
|
|
1822
2100
|
for (const p of paths) {
|
|
1823
|
-
if (
|
|
2101
|
+
if (existsSync5(join4(p, cmd))) {
|
|
1824
2102
|
return true;
|
|
1825
2103
|
}
|
|
1826
2104
|
}
|
|
@@ -1870,7 +2148,7 @@ function getAllServers() {
|
|
|
1870
2148
|
}
|
|
1871
2149
|
// src/tools/lsp/client.ts
|
|
1872
2150
|
var {spawn: spawn3 } = globalThis.Bun;
|
|
1873
|
-
import { readFileSync as
|
|
2151
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
1874
2152
|
import { extname, resolve } from "path";
|
|
1875
2153
|
class LSPServerManager {
|
|
1876
2154
|
static instance;
|
|
@@ -2000,6 +2278,7 @@ class LSPClient {
|
|
|
2000
2278
|
openedFiles = new Set;
|
|
2001
2279
|
stderrBuffer = [];
|
|
2002
2280
|
processExited = false;
|
|
2281
|
+
diagnosticsStore = new Map;
|
|
2003
2282
|
constructor(root, server) {
|
|
2004
2283
|
this.root = root;
|
|
2005
2284
|
this.server = server;
|
|
@@ -2124,7 +2403,11 @@ stderr: ${stderr}` : ""));
|
|
|
2124
2403
|
this.buffer = this.buffer.slice(end);
|
|
2125
2404
|
try {
|
|
2126
2405
|
const msg = JSON.parse(content);
|
|
2127
|
-
if ("
|
|
2406
|
+
if ("method" in msg && !("id" in msg)) {
|
|
2407
|
+
if (msg.method === "textDocument/publishDiagnostics" && msg.params?.uri) {
|
|
2408
|
+
this.diagnosticsStore.set(msg.params.uri, msg.params.diagnostics ?? []);
|
|
2409
|
+
}
|
|
2410
|
+
} else if ("id" in msg && "method" in msg) {
|
|
2128
2411
|
this.handleServerRequest(msg.id, msg.method, msg.params);
|
|
2129
2412
|
} else if ("id" in msg && this.pending.has(msg.id)) {
|
|
2130
2413
|
const handler = this.pending.get(msg.id);
|
|
@@ -2186,9 +2469,15 @@ ${msg}`);
|
|
|
2186
2469
|
\r
|
|
2187
2470
|
${msg}`);
|
|
2188
2471
|
}
|
|
2189
|
-
handleServerRequest(id, method,
|
|
2472
|
+
handleServerRequest(id, method, params) {
|
|
2190
2473
|
if (method === "workspace/configuration") {
|
|
2191
|
-
|
|
2474
|
+
const items = params?.items ?? [];
|
|
2475
|
+
const result = items.map((item) => {
|
|
2476
|
+
if (item.section === "json")
|
|
2477
|
+
return { validate: { enable: true } };
|
|
2478
|
+
return {};
|
|
2479
|
+
});
|
|
2480
|
+
this.respond(id, result);
|
|
2192
2481
|
} else if (method === "client/registerCapability") {
|
|
2193
2482
|
this.respond(id, null);
|
|
2194
2483
|
} else if (method === "window/workDoneProgress/create") {
|
|
@@ -2250,14 +2539,16 @@ ${msg}`);
|
|
|
2250
2539
|
...this.server.initialization
|
|
2251
2540
|
});
|
|
2252
2541
|
this.notify("initialized");
|
|
2253
|
-
this.notify("workspace/didChangeConfiguration", {
|
|
2542
|
+
this.notify("workspace/didChangeConfiguration", {
|
|
2543
|
+
settings: { json: { validate: { enable: true } } }
|
|
2544
|
+
});
|
|
2254
2545
|
await new Promise((r) => setTimeout(r, 300));
|
|
2255
2546
|
}
|
|
2256
2547
|
async openFile(filePath) {
|
|
2257
2548
|
const absPath = resolve(filePath);
|
|
2258
2549
|
if (this.openedFiles.has(absPath))
|
|
2259
2550
|
return;
|
|
2260
|
-
const text =
|
|
2551
|
+
const text = readFileSync3(absPath, "utf-8");
|
|
2261
2552
|
const ext = extname(absPath);
|
|
2262
2553
|
const languageId = getLanguageId(ext);
|
|
2263
2554
|
this.notify("textDocument/didOpen", {
|
|
@@ -2308,11 +2599,18 @@ ${msg}`);
|
|
|
2308
2599
|
}
|
|
2309
2600
|
async diagnostics(filePath) {
|
|
2310
2601
|
const absPath = resolve(filePath);
|
|
2602
|
+
const uri = `file://${absPath}`;
|
|
2311
2603
|
await this.openFile(absPath);
|
|
2312
2604
|
await new Promise((r) => setTimeout(r, 500));
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2605
|
+
try {
|
|
2606
|
+
const result = await this.send("textDocument/diagnostic", {
|
|
2607
|
+
textDocument: { uri }
|
|
2608
|
+
});
|
|
2609
|
+
if (result && typeof result === "object" && "items" in result) {
|
|
2610
|
+
return result;
|
|
2611
|
+
}
|
|
2612
|
+
} catch {}
|
|
2613
|
+
return { items: this.diagnosticsStore.get(uri) ?? [] };
|
|
2316
2614
|
}
|
|
2317
2615
|
async prepareRename(filePath, line, character) {
|
|
2318
2616
|
const absPath = resolve(filePath);
|
|
@@ -2360,20 +2658,21 @@ ${msg}`);
|
|
|
2360
2658
|
this.proc?.kill();
|
|
2361
2659
|
this.proc = null;
|
|
2362
2660
|
this.processExited = true;
|
|
2661
|
+
this.diagnosticsStore.clear();
|
|
2363
2662
|
}
|
|
2364
2663
|
}
|
|
2365
2664
|
// src/tools/lsp/utils.ts
|
|
2366
2665
|
import { extname as extname2, resolve as resolve2 } from "path";
|
|
2367
|
-
import { existsSync as
|
|
2666
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
2368
2667
|
function findWorkspaceRoot(filePath) {
|
|
2369
2668
|
let dir = resolve2(filePath);
|
|
2370
|
-
if (!
|
|
2669
|
+
if (!existsSync6(dir) || !__require("fs").statSync(dir).isDirectory()) {
|
|
2371
2670
|
dir = __require("path").dirname(dir);
|
|
2372
2671
|
}
|
|
2373
2672
|
const markers = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "build.gradle"];
|
|
2374
2673
|
while (dir !== "/") {
|
|
2375
2674
|
for (const marker of markers) {
|
|
2376
|
-
if (
|
|
2675
|
+
if (existsSync6(__require("path").join(dir, marker))) {
|
|
2377
2676
|
return dir;
|
|
2378
2677
|
}
|
|
2379
2678
|
}
|
|
@@ -2487,12 +2786,22 @@ function formatPrepareRenameResult(result) {
|
|
|
2487
2786
|
if ("defaultBehavior" in result) {
|
|
2488
2787
|
return result.defaultBehavior ? "Rename supported (using default behavior)" : "Cannot rename at this position";
|
|
2489
2788
|
}
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2789
|
+
if ("range" in result && result.range) {
|
|
2790
|
+
const startLine = result.range.start.line + 1;
|
|
2791
|
+
const startChar = result.range.start.character;
|
|
2792
|
+
const endLine = result.range.end.line + 1;
|
|
2793
|
+
const endChar = result.range.end.character;
|
|
2794
|
+
const placeholder = result.placeholder ? ` (current: "${result.placeholder}")` : "";
|
|
2795
|
+
return `Rename available at ${startLine}:${startChar}-${endLine}:${endChar}${placeholder}`;
|
|
2796
|
+
}
|
|
2797
|
+
if ("start" in result && "end" in result) {
|
|
2798
|
+
const startLine = result.start.line + 1;
|
|
2799
|
+
const startChar = result.start.character;
|
|
2800
|
+
const endLine = result.end.line + 1;
|
|
2801
|
+
const endChar = result.end.character;
|
|
2802
|
+
return `Rename available at ${startLine}:${startChar}-${endLine}:${endChar}`;
|
|
2803
|
+
}
|
|
2804
|
+
return "Cannot rename at this position";
|
|
2496
2805
|
}
|
|
2497
2806
|
function formatCodeAction(action) {
|
|
2498
2807
|
let result = `[${action.kind || "action"}] ${action.title}`;
|
|
@@ -2521,7 +2830,7 @@ function formatCodeActions(actions) {
|
|
|
2521
2830
|
}
|
|
2522
2831
|
function applyTextEditsToFile(filePath, edits) {
|
|
2523
2832
|
try {
|
|
2524
|
-
let content =
|
|
2833
|
+
let content = readFileSync4(filePath, "utf-8");
|
|
2525
2834
|
const lines = content.split(`
|
|
2526
2835
|
`);
|
|
2527
2836
|
const sortedEdits = [...edits].sort((a, b) => {
|
|
@@ -2546,7 +2855,7 @@ function applyTextEditsToFile(filePath, edits) {
|
|
|
2546
2855
|
`));
|
|
2547
2856
|
}
|
|
2548
2857
|
}
|
|
2549
|
-
|
|
2858
|
+
writeFileSync2(filePath, lines.join(`
|
|
2550
2859
|
`), "utf-8");
|
|
2551
2860
|
return { success: true, editCount: edits.length };
|
|
2552
2861
|
} catch (err) {
|
|
@@ -2577,7 +2886,7 @@ function applyWorkspaceEdit(edit) {
|
|
|
2577
2886
|
if (change.kind === "create") {
|
|
2578
2887
|
try {
|
|
2579
2888
|
const filePath = change.uri.replace("file://", "");
|
|
2580
|
-
|
|
2889
|
+
writeFileSync2(filePath, "", "utf-8");
|
|
2581
2890
|
result.filesModified.push(filePath);
|
|
2582
2891
|
} catch (err) {
|
|
2583
2892
|
result.success = false;
|
|
@@ -2587,8 +2896,8 @@ function applyWorkspaceEdit(edit) {
|
|
|
2587
2896
|
try {
|
|
2588
2897
|
const oldPath = change.oldUri.replace("file://", "");
|
|
2589
2898
|
const newPath = change.newUri.replace("file://", "");
|
|
2590
|
-
const content =
|
|
2591
|
-
|
|
2899
|
+
const content = readFileSync4(oldPath, "utf-8");
|
|
2900
|
+
writeFileSync2(newPath, content, "utf-8");
|
|
2592
2901
|
__require("fs").unlinkSync(oldPath);
|
|
2593
2902
|
result.filesModified.push(newPath);
|
|
2594
2903
|
} catch (err) {
|
|
@@ -3368,10 +3677,10 @@ function mergeDefs(...defs) {
|
|
|
3368
3677
|
function cloneDef(schema) {
|
|
3369
3678
|
return mergeDefs(schema._zod.def);
|
|
3370
3679
|
}
|
|
3371
|
-
function getElementAtPath(obj,
|
|
3372
|
-
if (!
|
|
3680
|
+
function getElementAtPath(obj, path2) {
|
|
3681
|
+
if (!path2)
|
|
3373
3682
|
return obj;
|
|
3374
|
-
return
|
|
3683
|
+
return path2.reduce((acc, key) => acc?.[key], obj);
|
|
3375
3684
|
}
|
|
3376
3685
|
function promiseAllObject(promisesObj) {
|
|
3377
3686
|
const keys = Object.keys(promisesObj);
|
|
@@ -3730,11 +4039,11 @@ function aborted(x, startIndex = 0) {
|
|
|
3730
4039
|
}
|
|
3731
4040
|
return false;
|
|
3732
4041
|
}
|
|
3733
|
-
function prefixIssues(
|
|
4042
|
+
function prefixIssues(path2, issues) {
|
|
3734
4043
|
return issues.map((iss) => {
|
|
3735
4044
|
var _a;
|
|
3736
4045
|
(_a = iss).path ?? (_a.path = []);
|
|
3737
|
-
iss.path.unshift(
|
|
4046
|
+
iss.path.unshift(path2);
|
|
3738
4047
|
return iss;
|
|
3739
4048
|
});
|
|
3740
4049
|
}
|
|
@@ -3902,7 +4211,7 @@ function treeifyError(error, _mapper) {
|
|
|
3902
4211
|
return issue2.message;
|
|
3903
4212
|
};
|
|
3904
4213
|
const result = { errors: [] };
|
|
3905
|
-
const processError = (error2,
|
|
4214
|
+
const processError = (error2, path2 = []) => {
|
|
3906
4215
|
var _a, _b;
|
|
3907
4216
|
for (const issue2 of error2.issues) {
|
|
3908
4217
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
@@ -3912,7 +4221,7 @@ function treeifyError(error, _mapper) {
|
|
|
3912
4221
|
} else if (issue2.code === "invalid_element") {
|
|
3913
4222
|
processError({ issues: issue2.issues }, issue2.path);
|
|
3914
4223
|
} else {
|
|
3915
|
-
const fullpath = [...
|
|
4224
|
+
const fullpath = [...path2, ...issue2.path];
|
|
3916
4225
|
if (fullpath.length === 0) {
|
|
3917
4226
|
result.errors.push(mapper(issue2));
|
|
3918
4227
|
continue;
|
|
@@ -3944,8 +4253,8 @@ function treeifyError(error, _mapper) {
|
|
|
3944
4253
|
}
|
|
3945
4254
|
function toDotPath(_path) {
|
|
3946
4255
|
const segs = [];
|
|
3947
|
-
const
|
|
3948
|
-
for (const seg of
|
|
4256
|
+
const path2 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
4257
|
+
for (const seg of path2) {
|
|
3949
4258
|
if (typeof seg === "number")
|
|
3950
4259
|
segs.push(`[${seg}]`);
|
|
3951
4260
|
else if (typeof seg === "symbol")
|
|
@@ -15288,13 +15597,13 @@ var lsp_code_action_resolve = tool({
|
|
|
15288
15597
|
});
|
|
15289
15598
|
// src/tools/ast-grep/constants.ts
|
|
15290
15599
|
import { createRequire as createRequire4 } from "module";
|
|
15291
|
-
import { dirname as dirname2, join as
|
|
15292
|
-
import { existsSync as
|
|
15600
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
15601
|
+
import { existsSync as existsSync8, statSync } from "fs";
|
|
15293
15602
|
|
|
15294
15603
|
// src/tools/ast-grep/downloader.ts
|
|
15295
15604
|
var {spawn: spawn4 } = globalThis.Bun;
|
|
15296
|
-
import { existsSync as
|
|
15297
|
-
import { join as
|
|
15605
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, chmodSync as chmodSync2, unlinkSync as unlinkSync2 } from "fs";
|
|
15606
|
+
import { join as join5 } from "path";
|
|
15298
15607
|
import { homedir as homedir3 } from "os";
|
|
15299
15608
|
import { createRequire as createRequire3 } from "module";
|
|
15300
15609
|
var REPO2 = "ast-grep/ast-grep";
|
|
@@ -15320,19 +15629,19 @@ var PLATFORM_MAP2 = {
|
|
|
15320
15629
|
function getCacheDir2() {
|
|
15321
15630
|
if (process.platform === "win32") {
|
|
15322
15631
|
const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
|
|
15323
|
-
const base2 = localAppData ||
|
|
15324
|
-
return
|
|
15632
|
+
const base2 = localAppData || join5(homedir3(), "AppData", "Local");
|
|
15633
|
+
return join5(base2, "oh-my-opencode", "bin");
|
|
15325
15634
|
}
|
|
15326
|
-
const
|
|
15327
|
-
const base =
|
|
15328
|
-
return
|
|
15635
|
+
const xdgCache2 = process.env.XDG_CACHE_HOME;
|
|
15636
|
+
const base = xdgCache2 || join5(homedir3(), ".cache");
|
|
15637
|
+
return join5(base, "oh-my-opencode", "bin");
|
|
15329
15638
|
}
|
|
15330
15639
|
function getBinaryName3() {
|
|
15331
15640
|
return process.platform === "win32" ? "sg.exe" : "sg";
|
|
15332
15641
|
}
|
|
15333
15642
|
function getCachedBinaryPath2() {
|
|
15334
|
-
const binaryPath =
|
|
15335
|
-
return
|
|
15643
|
+
const binaryPath = join5(getCacheDir2(), getBinaryName3());
|
|
15644
|
+
return existsSync7(binaryPath) ? binaryPath : null;
|
|
15336
15645
|
}
|
|
15337
15646
|
async function extractZip2(archivePath, destDir) {
|
|
15338
15647
|
const proc = process.platform === "win32" ? spawn4([
|
|
@@ -15358,30 +15667,30 @@ async function downloadAstGrep(version2 = DEFAULT_VERSION) {
|
|
|
15358
15667
|
}
|
|
15359
15668
|
const cacheDir = getCacheDir2();
|
|
15360
15669
|
const binaryName = getBinaryName3();
|
|
15361
|
-
const binaryPath =
|
|
15362
|
-
if (
|
|
15670
|
+
const binaryPath = join5(cacheDir, binaryName);
|
|
15671
|
+
if (existsSync7(binaryPath)) {
|
|
15363
15672
|
return binaryPath;
|
|
15364
15673
|
}
|
|
15365
|
-
const { arch, os } = platformInfo;
|
|
15366
|
-
const assetName = `app-${arch}-${
|
|
15674
|
+
const { arch, os: os2 } = platformInfo;
|
|
15675
|
+
const assetName = `app-${arch}-${os2}.zip`;
|
|
15367
15676
|
const downloadUrl = `https://github.com/${REPO2}/releases/download/${version2}/${assetName}`;
|
|
15368
15677
|
console.log(`[oh-my-opencode] Downloading ast-grep binary...`);
|
|
15369
15678
|
try {
|
|
15370
|
-
if (!
|
|
15371
|
-
|
|
15679
|
+
if (!existsSync7(cacheDir)) {
|
|
15680
|
+
mkdirSync3(cacheDir, { recursive: true });
|
|
15372
15681
|
}
|
|
15373
15682
|
const response = await fetch(downloadUrl, { redirect: "follow" });
|
|
15374
15683
|
if (!response.ok) {
|
|
15375
15684
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
15376
15685
|
}
|
|
15377
|
-
const archivePath =
|
|
15686
|
+
const archivePath = join5(cacheDir, assetName);
|
|
15378
15687
|
const arrayBuffer = await response.arrayBuffer();
|
|
15379
15688
|
await Bun.write(archivePath, arrayBuffer);
|
|
15380
15689
|
await extractZip2(archivePath, cacheDir);
|
|
15381
|
-
if (
|
|
15690
|
+
if (existsSync7(archivePath)) {
|
|
15382
15691
|
unlinkSync2(archivePath);
|
|
15383
15692
|
}
|
|
15384
|
-
if (process.platform !== "win32" &&
|
|
15693
|
+
if (process.platform !== "win32" && existsSync7(binaryPath)) {
|
|
15385
15694
|
chmodSync2(binaryPath, 493);
|
|
15386
15695
|
}
|
|
15387
15696
|
console.log(`[oh-my-opencode] ast-grep binary ready.`);
|
|
@@ -15432,8 +15741,8 @@ function findSgCliPathSync() {
|
|
|
15432
15741
|
const require2 = createRequire4(import.meta.url);
|
|
15433
15742
|
const cliPkgPath = require2.resolve("@ast-grep/cli/package.json");
|
|
15434
15743
|
const cliDir = dirname2(cliPkgPath);
|
|
15435
|
-
const sgPath =
|
|
15436
|
-
if (
|
|
15744
|
+
const sgPath = join6(cliDir, binaryName);
|
|
15745
|
+
if (existsSync8(sgPath) && isValidBinary(sgPath)) {
|
|
15437
15746
|
return sgPath;
|
|
15438
15747
|
}
|
|
15439
15748
|
} catch {}
|
|
@@ -15444,17 +15753,17 @@ function findSgCliPathSync() {
|
|
|
15444
15753
|
const pkgPath = require2.resolve(`${platformPkg}/package.json`);
|
|
15445
15754
|
const pkgDir = dirname2(pkgPath);
|
|
15446
15755
|
const astGrepName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep";
|
|
15447
|
-
const binaryPath =
|
|
15448
|
-
if (
|
|
15756
|
+
const binaryPath = join6(pkgDir, astGrepName);
|
|
15757
|
+
if (existsSync8(binaryPath) && isValidBinary(binaryPath)) {
|
|
15449
15758
|
return binaryPath;
|
|
15450
15759
|
}
|
|
15451
15760
|
} catch {}
|
|
15452
15761
|
}
|
|
15453
15762
|
if (process.platform === "darwin") {
|
|
15454
15763
|
const homebrewPaths = ["/opt/homebrew/bin/sg", "/usr/local/bin/sg"];
|
|
15455
|
-
for (const
|
|
15456
|
-
if (
|
|
15457
|
-
return
|
|
15764
|
+
for (const path2 of homebrewPaths) {
|
|
15765
|
+
if (existsSync8(path2) && isValidBinary(path2)) {
|
|
15766
|
+
return path2;
|
|
15458
15767
|
}
|
|
15459
15768
|
}
|
|
15460
15769
|
}
|
|
@@ -15472,8 +15781,8 @@ function getSgCliPath() {
|
|
|
15472
15781
|
}
|
|
15473
15782
|
return "sg";
|
|
15474
15783
|
}
|
|
15475
|
-
function setSgCliPath(
|
|
15476
|
-
resolvedCliPath2 =
|
|
15784
|
+
function setSgCliPath(path2) {
|
|
15785
|
+
resolvedCliPath2 = path2;
|
|
15477
15786
|
}
|
|
15478
15787
|
var SG_CLI_PATH = getSgCliPath();
|
|
15479
15788
|
var CLI_LANGUAGES = [
|
|
@@ -15537,11 +15846,11 @@ var LANG_EXTENSIONS = {
|
|
|
15537
15846
|
|
|
15538
15847
|
// src/tools/ast-grep/cli.ts
|
|
15539
15848
|
var {spawn: spawn5 } = globalThis.Bun;
|
|
15540
|
-
import { existsSync as
|
|
15849
|
+
import { existsSync as existsSync9 } from "fs";
|
|
15541
15850
|
var resolvedCliPath3 = null;
|
|
15542
15851
|
var initPromise2 = null;
|
|
15543
15852
|
async function getAstGrepPath() {
|
|
15544
|
-
if (resolvedCliPath3 !== null &&
|
|
15853
|
+
if (resolvedCliPath3 !== null && existsSync9(resolvedCliPath3)) {
|
|
15545
15854
|
return resolvedCliPath3;
|
|
15546
15855
|
}
|
|
15547
15856
|
if (initPromise2) {
|
|
@@ -15549,7 +15858,7 @@ async function getAstGrepPath() {
|
|
|
15549
15858
|
}
|
|
15550
15859
|
initPromise2 = (async () => {
|
|
15551
15860
|
const syncPath = findSgCliPathSync();
|
|
15552
|
-
if (syncPath &&
|
|
15861
|
+
if (syncPath && existsSync9(syncPath)) {
|
|
15553
15862
|
resolvedCliPath3 = syncPath;
|
|
15554
15863
|
setSgCliPath(syncPath);
|
|
15555
15864
|
return syncPath;
|
|
@@ -15583,7 +15892,7 @@ async function runSg(options) {
|
|
|
15583
15892
|
const paths = options.paths && options.paths.length > 0 ? options.paths : ["."];
|
|
15584
15893
|
args.push(...paths);
|
|
15585
15894
|
let cliPath = getSgCliPath();
|
|
15586
|
-
if (!
|
|
15895
|
+
if (!existsSync9(cliPath) && cliPath !== "sg") {
|
|
15587
15896
|
const downloadedPath = await getAstGrepPath();
|
|
15588
15897
|
if (downloadedPath) {
|
|
15589
15898
|
cliPath = downloadedPath;
|
|
@@ -16028,8 +16337,8 @@ var ast_grep_transform = tool({
|
|
|
16028
16337
|
var {spawn: spawn6 } = globalThis.Bun;
|
|
16029
16338
|
|
|
16030
16339
|
// src/tools/safe-grep/constants.ts
|
|
16031
|
-
import { existsSync as
|
|
16032
|
-
import { join as
|
|
16340
|
+
import { existsSync as existsSync10 } from "fs";
|
|
16341
|
+
import { join as join7, dirname as dirname3 } from "path";
|
|
16033
16342
|
import { spawnSync } from "child_process";
|
|
16034
16343
|
var cachedCli = null;
|
|
16035
16344
|
function findExecutable(name) {
|
|
@@ -16050,13 +16359,13 @@ function getOpenCodeBundledRg() {
|
|
|
16050
16359
|
const isWindows = process.platform === "win32";
|
|
16051
16360
|
const rgName = isWindows ? "rg.exe" : "rg";
|
|
16052
16361
|
const candidates = [
|
|
16053
|
-
|
|
16054
|
-
|
|
16055
|
-
|
|
16056
|
-
|
|
16362
|
+
join7(execDir, rgName),
|
|
16363
|
+
join7(execDir, "bin", rgName),
|
|
16364
|
+
join7(execDir, "..", "bin", rgName),
|
|
16365
|
+
join7(execDir, "..", "libexec", rgName)
|
|
16057
16366
|
];
|
|
16058
16367
|
for (const candidate of candidates) {
|
|
16059
|
-
if (
|
|
16368
|
+
if (existsSync10(candidate)) {
|
|
16060
16369
|
return candidate;
|
|
16061
16370
|
}
|
|
16062
16371
|
}
|
|
@@ -16389,11 +16698,11 @@ var OhMyOpenCodeConfigSchema = exports_external.object({
|
|
|
16389
16698
|
});
|
|
16390
16699
|
// src/index.ts
|
|
16391
16700
|
import * as fs3 from "fs";
|
|
16392
|
-
import * as
|
|
16701
|
+
import * as path2 from "path";
|
|
16393
16702
|
function loadPluginConfig(directory) {
|
|
16394
16703
|
const configPaths = [
|
|
16395
|
-
|
|
16396
|
-
|
|
16704
|
+
path2.join(directory, "oh-my-opencode.json"),
|
|
16705
|
+
path2.join(directory, ".oh-my-opencode.json")
|
|
16397
16706
|
];
|
|
16398
16707
|
for (const configPath of configPaths) {
|
|
16399
16708
|
try {
|
|
@@ -16418,7 +16727,9 @@ var OhMyOpenCodePlugin = async (ctx) => {
|
|
|
16418
16727
|
const todoContinuationEnforcer = createTodoContinuationEnforcer(ctx);
|
|
16419
16728
|
const contextWindowMonitor = createContextWindowMonitorHook(ctx);
|
|
16420
16729
|
const sessionRecovery = createSessionRecoveryHook(ctx);
|
|
16730
|
+
const pulseMonitor = createPulseMonitorHook(ctx);
|
|
16421
16731
|
const commentChecker = createCommentCheckerHooks();
|
|
16732
|
+
const grepOutputTruncator = createGrepOutputTruncatorHook(ctx);
|
|
16422
16733
|
updateTerminalTitle({ sessionId: "main" });
|
|
16423
16734
|
const pluginConfig = loadPluginConfig(ctx.directory);
|
|
16424
16735
|
let mainSessionID;
|
|
@@ -16444,6 +16755,7 @@ var OhMyOpenCodePlugin = async (ctx) => {
|
|
|
16444
16755
|
event: async (input) => {
|
|
16445
16756
|
await todoContinuationEnforcer(input);
|
|
16446
16757
|
await contextWindowMonitor.event(input);
|
|
16758
|
+
await pulseMonitor.event(input);
|
|
16447
16759
|
const { event } = input;
|
|
16448
16760
|
const props = event.properties;
|
|
16449
16761
|
if (event.type === "session.created") {
|
|
@@ -16526,6 +16838,7 @@ var OhMyOpenCodePlugin = async (ctx) => {
|
|
|
16526
16838
|
}
|
|
16527
16839
|
},
|
|
16528
16840
|
"tool.execute.before": async (input, output) => {
|
|
16841
|
+
await pulseMonitor["tool.execute.before"]();
|
|
16529
16842
|
await commentChecker["tool.execute.before"](input, output);
|
|
16530
16843
|
if (input.sessionID === mainSessionID) {
|
|
16531
16844
|
updateTerminalTitle({
|
|
@@ -16538,6 +16851,8 @@ var OhMyOpenCodePlugin = async (ctx) => {
|
|
|
16538
16851
|
}
|
|
16539
16852
|
},
|
|
16540
16853
|
"tool.execute.after": async (input, output) => {
|
|
16854
|
+
await pulseMonitor["tool.execute.after"](input);
|
|
16855
|
+
await grepOutputTruncator["tool.execute.after"](input, output);
|
|
16541
16856
|
await contextWindowMonitor["tool.execute.after"](input, output);
|
|
16542
16857
|
await commentChecker["tool.execute.after"](input, output);
|
|
16543
16858
|
if (input.sessionID === mainSessionID) {
|