oh-my-opencode 0.1.23 → 0.1.25

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 CHANGED
@@ -136,6 +136,11 @@ OpenCode 는 아주 확장가능하고 아주 커스터마이저블합니다.
136
136
  - **Context Window Monitor**: [컨텍스트 윈도우 불안 관리](https://agentic-patterns.com/patterns/context-window-anxiety-management/) 패턴을 구현합니다.
137
137
  - 사용량이 70%를 넘으면 에이전트에게 아직 토큰이 충분하다고 상기시켜, 급하게 불완전한 작업을 하는 것을 완화합니다.
138
138
  - **Session Notification**: 에이전트가 작업을 마치면 OS 네이티브 알림을 보냅니다 (macOS, Linux, Windows).
139
+ - **Session Recovery**: API 에러로부터 자동으로 복구하여 세션 안정성을 보장합니다. 네 가지 시나리오를 처리합니다:
140
+ - **Tool Result Missing**: `tool_use` 블록이 있지만 `tool_result`가 없을 때 (ESC 인터럽트) → "cancelled" tool result 주입
141
+ - **Thinking Block Order**: thinking 블록이 첫 번째여야 하는데 아닐 때 → 빈 thinking 블록 추가
142
+ - **Thinking Disabled Violation**: thinking 이 비활성화인데 thinking 블록이 있을 때 → thinking 블록 제거
143
+ - **Empty Content Message**: 메시지가 thinking/meta 블록만 있고 실제 내용이 없을 때 → 파일시스템을 통해 "(interrupted)" 텍스트 주입
139
144
  - **Comment Checker**: 코드 수정 후 불필요한 주석을 감지하여 보고합니다. BDD 패턴, 지시어, 독스트링 등 유효한 주석은 똑똑하게 제외하고, AI가 남긴 흔적을 제거하여 코드를 깨끗하게 유지합니다.
140
145
 
141
146
  ### Agents
package/README.md CHANGED
@@ -132,7 +132,11 @@ I believe in the right tool for the job. For your wallet's sake, use CLIProxyAPI
132
132
  - **Todo Continuation Enforcer**: Forces the agent to complete all tasks before exiting. Eliminates the common LLM issue of "giving up halfway".
133
133
  - **Context Window Monitor**: Implements [Context Window Anxiety Management](https://agentic-patterns.com/patterns/context-window-anxiety-management/). When context usage exceeds 70%, it reminds the agent that resources are sufficient, preventing rushed or low-quality output.
134
134
  - **Session Notification**: Sends a native OS notification when the job is done (macOS, Linux, Windows).
135
- - **Session Recovery**: Automatically recovers from API errors by injecting missing tool results and correcting thinking block violations, ensuring session stability.
135
+ - **Session Recovery**: Automatically recovers from API errors, ensuring session stability. Handles four scenarios:
136
+ - **Tool Result Missing**: When `tool_use` block exists without `tool_result` (ESC interrupt) → injects "cancelled" tool results
137
+ - **Thinking Block Order**: When thinking block must be first but isn't → prepends empty thinking block
138
+ - **Thinking Disabled Violation**: When thinking blocks exist but thinking is disabled → strips thinking blocks
139
+ - **Empty Content Message**: When message has only thinking/meta blocks without actual content → injects "(interrupted)" text via filesystem
136
140
  - **Comment Checker**: Detects and reports unnecessary comments after code modifications. Smartly ignores valid patterns (BDD, directives, docstrings, shebangs) to keep the codebase clean from AI-generated artifacts.
137
141
 
138
142
  ### Agents
@@ -12,7 +12,7 @@
12
12
  * - Recovery: strip thinking/redacted_thinking blocks
13
13
  *
14
14
  * 4. Empty content message (non-empty content required)
15
- * - Recovery: delete the empty message via revert
15
+ * - Recovery: inject text part directly via filesystem
16
16
  */
17
17
  import type { PluginInput } from "@opencode-ai/plugin";
18
18
  interface MessageInfo {
package/dist/index.js CHANGED
@@ -790,6 +790,32 @@ ${CONTEXT_REMINDER}
790
790
  };
791
791
  }
792
792
  // src/hooks/session-recovery.ts
793
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
794
+ import { join } from "path";
795
+
796
+ // node_modules/xdg-basedir/index.js
797
+ import os from "os";
798
+ import path from "path";
799
+ var homeDirectory = os.homedir();
800
+ var { env } = process;
801
+ var xdgData = env.XDG_DATA_HOME || (homeDirectory ? path.join(homeDirectory, ".local", "share") : undefined);
802
+ var xdgConfig = env.XDG_CONFIG_HOME || (homeDirectory ? path.join(homeDirectory, ".config") : undefined);
803
+ var xdgState = env.XDG_STATE_HOME || (homeDirectory ? path.join(homeDirectory, ".local", "state") : undefined);
804
+ var xdgCache = env.XDG_CACHE_HOME || (homeDirectory ? path.join(homeDirectory, ".cache") : undefined);
805
+ var xdgRuntime = env.XDG_RUNTIME_DIR || undefined;
806
+ var xdgDataDirectories = (env.XDG_DATA_DIRS || "/usr/local/share/:/usr/share/").split(":");
807
+ if (xdgData) {
808
+ xdgDataDirectories.unshift(xdgData);
809
+ }
810
+ var xdgConfigDirectories = (env.XDG_CONFIG_DIRS || "/etc/xdg").split(":");
811
+ if (xdgConfig) {
812
+ xdgConfigDirectories.unshift(xdgConfig);
813
+ }
814
+
815
+ // src/hooks/session-recovery.ts
816
+ var OPENCODE_STORAGE = join(xdgData ?? "", "opencode", "storage");
817
+ var MESSAGE_STORAGE = join(OPENCODE_STORAGE, "message");
818
+ var PART_STORAGE = join(OPENCODE_STORAGE, "part");
793
819
  function getErrorMessage(error) {
794
820
  if (!error)
795
821
  return "";
@@ -894,80 +920,121 @@ async function recoverThinkingDisabledViolation(client, sessionID, failedAssista
894
920
  return false;
895
921
  }
896
922
  var THINKING_TYPES = new Set(["thinking", "redacted_thinking", "reasoning"]);
897
- function hasNonEmptyOutput(msg) {
898
- const parts = msg.parts;
899
- if (!parts || parts.length === 0)
900
- return false;
901
- return parts.some((p) => {
902
- if (THINKING_TYPES.has(p.type))
903
- return false;
904
- if (p.type === "step-start" || p.type === "step-finish")
923
+ var META_TYPES = new Set(["step-start", "step-finish"]);
924
+ function generatePartId() {
925
+ const timestamp = Date.now().toString(16);
926
+ const random = Math.random().toString(36).substring(2, 10);
927
+ return `prt_${timestamp}${random}`;
928
+ }
929
+ function getMessageDir(sessionID) {
930
+ const projectHash = readdirSync(MESSAGE_STORAGE).find((dir) => {
931
+ const sessionDir = join(MESSAGE_STORAGE, dir);
932
+ try {
933
+ return readdirSync(sessionDir).some((f) => f.includes(sessionID.replace("ses_", "")));
934
+ } catch {
905
935
  return false;
906
- if (p.type === "text" && p.text && p.text.trim())
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;
936
+ }
913
937
  });
938
+ if (projectHash) {
939
+ return join(MESSAGE_STORAGE, projectHash, sessionID);
940
+ }
941
+ for (const dir of readdirSync(MESSAGE_STORAGE)) {
942
+ const sessionPath = join(MESSAGE_STORAGE, dir, sessionID);
943
+ if (existsSync(sessionPath)) {
944
+ return sessionPath;
945
+ }
946
+ }
947
+ return "";
914
948
  }
915
- function findEmptyContentMessage(msgs) {
916
- for (let i = 0;i < msgs.length; i++) {
917
- const msg = msgs[i];
918
- const isLastMessage = i === msgs.length - 1;
919
- const isAssistant = msg.info?.role === "assistant";
920
- if (isLastMessage && isAssistant)
949
+ function readMessagesFromStorage(sessionID) {
950
+ const messageDir = getMessageDir(sessionID);
951
+ if (!messageDir || !existsSync(messageDir))
952
+ return [];
953
+ const messages = [];
954
+ for (const file of readdirSync(messageDir)) {
955
+ if (!file.endsWith(".json"))
956
+ continue;
957
+ try {
958
+ const content = readFileSync(join(messageDir, file), "utf-8");
959
+ messages.push(JSON.parse(content));
960
+ } catch {
921
961
  continue;
922
- if (!hasNonEmptyOutput(msg)) {
923
- return msg;
924
962
  }
925
963
  }
926
- return null;
964
+ return messages.sort((a, b) => a.id.localeCompare(b.id));
927
965
  }
928
- async function recoverEmptyContentMessage(client, sessionID, failedAssistantMsg, directory) {
929
- try {
930
- const messagesResp = await client.session.messages({
931
- path: { id: sessionID },
932
- query: { directory }
933
- });
934
- const msgs = messagesResp.data;
935
- if (!msgs || msgs.length === 0)
936
- return false;
937
- const emptyMsg = findEmptyContentMessage(msgs) || failedAssistantMsg;
938
- const messageID = emptyMsg.info?.id;
939
- if (!messageID)
940
- return false;
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 {}
966
+ function readPartsFromStorage(messageID) {
967
+ const partDir = join(PART_STORAGE, messageID);
968
+ if (!existsSync(partDir))
969
+ return [];
970
+ const parts = [];
971
+ for (const file of readdirSync(partDir)) {
972
+ if (!file.endsWith(".json"))
973
+ continue;
974
+ try {
975
+ const content = readFileSync(join(partDir, file), "utf-8");
976
+ parts.push(JSON.parse(content));
977
+ } catch {
978
+ continue;
959
979
  }
960
- const revertTargetID = emptyMsg.info?.parentID || messageID;
961
- await client.session.revert({
962
- path: { id: sessionID },
963
- body: { messageID: revertTargetID },
964
- query: { directory }
965
- });
980
+ }
981
+ return parts;
982
+ }
983
+ function injectTextPartToStorage(sessionID, messageID, text) {
984
+ const partDir = join(PART_STORAGE, messageID);
985
+ if (!existsSync(partDir)) {
986
+ mkdirSync(partDir, { recursive: true });
987
+ }
988
+ const partId = generatePartId();
989
+ const part = {
990
+ id: partId,
991
+ sessionID,
992
+ messageID,
993
+ type: "text",
994
+ text
995
+ };
996
+ try {
997
+ writeFileSync(join(partDir, `${partId}.json`), JSON.stringify(part, null, 2));
966
998
  return true;
967
999
  } catch {
968
1000
  return false;
969
1001
  }
970
1002
  }
1003
+ function findEmptyContentMessageFromStorage(sessionID) {
1004
+ const messages = readMessagesFromStorage(sessionID);
1005
+ for (let i = 0;i < messages.length; i++) {
1006
+ const msg = messages[i];
1007
+ if (msg.role !== "assistant")
1008
+ continue;
1009
+ const isLastMessage = i === messages.length - 1;
1010
+ if (isLastMessage)
1011
+ continue;
1012
+ const parts = readPartsFromStorage(msg.id);
1013
+ const hasContent = parts.some((p) => {
1014
+ if (THINKING_TYPES.has(p.type))
1015
+ return false;
1016
+ if (META_TYPES.has(p.type))
1017
+ return false;
1018
+ if (p.type === "text" && p.text?.trim())
1019
+ return true;
1020
+ if (p.type === "tool_use")
1021
+ return true;
1022
+ if (p.type === "tool_result")
1023
+ return true;
1024
+ return false;
1025
+ });
1026
+ if (!hasContent && parts.length > 0) {
1027
+ return msg.id;
1028
+ }
1029
+ }
1030
+ return null;
1031
+ }
1032
+ async function recoverEmptyContentMessage(_client, sessionID, failedAssistantMsg, _directory) {
1033
+ const emptyMessageID = findEmptyContentMessageFromStorage(sessionID) || failedAssistantMsg.info?.id;
1034
+ if (!emptyMessageID)
1035
+ return false;
1036
+ return injectTextPartToStorage(sessionID, emptyMessageID, "(interrupted)");
1037
+ }
971
1038
  async function fallbackRevertStrategy(client, sessionID, failedAssistantMsg, directory) {
972
1039
  const parentMsgID = failedAssistantMsg.info?.parentID;
973
1040
  const messagesResp = await client.session.messages({
@@ -1093,14 +1160,14 @@ function createSessionRecoveryHook(ctx) {
1093
1160
  // src/hooks/comment-checker/cli.ts
1094
1161
  var {spawn: spawn2 } = globalThis.Bun;
1095
1162
  import { createRequire as createRequire2 } from "module";
1096
- import { dirname, join as join2 } from "path";
1097
- import { existsSync as existsSync2 } from "fs";
1163
+ import { dirname, join as join3 } from "path";
1164
+ import { existsSync as existsSync3 } from "fs";
1098
1165
  import * as fs from "fs";
1099
1166
 
1100
1167
  // src/hooks/comment-checker/downloader.ts
1101
1168
  var {spawn } = globalThis.Bun;
1102
- import { existsSync, mkdirSync, chmodSync, unlinkSync, appendFileSync } from "fs";
1103
- import { join } from "path";
1169
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, chmodSync, unlinkSync, appendFileSync } from "fs";
1170
+ import { join as join2 } from "path";
1104
1171
  import { homedir } from "os";
1105
1172
  import { createRequire } from "module";
1106
1173
  var DEBUG = process.env.COMMENT_CHECKER_DEBUG === "1";
@@ -1121,16 +1188,16 @@ var PLATFORM_MAP = {
1121
1188
  "win32-x64": { os: "windows", arch: "amd64", ext: "zip" }
1122
1189
  };
1123
1190
  function getCacheDir() {
1124
- const xdgCache = process.env.XDG_CACHE_HOME;
1125
- const base = xdgCache || join(homedir(), ".cache");
1126
- return join(base, "oh-my-opencode", "bin");
1191
+ const xdgCache2 = process.env.XDG_CACHE_HOME;
1192
+ const base = xdgCache2 || join2(homedir(), ".cache");
1193
+ return join2(base, "oh-my-opencode", "bin");
1127
1194
  }
1128
1195
  function getBinaryName() {
1129
1196
  return process.platform === "win32" ? "comment-checker.exe" : "comment-checker";
1130
1197
  }
1131
1198
  function getCachedBinaryPath() {
1132
- const binaryPath = join(getCacheDir(), getBinaryName());
1133
- return existsSync(binaryPath) ? binaryPath : null;
1199
+ const binaryPath = join2(getCacheDir(), getBinaryName());
1200
+ return existsSync2(binaryPath) ? binaryPath : null;
1134
1201
  }
1135
1202
  function getPackageVersion() {
1136
1203
  try {
@@ -1177,26 +1244,26 @@ async function downloadCommentChecker() {
1177
1244
  }
1178
1245
  const cacheDir = getCacheDir();
1179
1246
  const binaryName = getBinaryName();
1180
- const binaryPath = join(cacheDir, binaryName);
1181
- if (existsSync(binaryPath)) {
1247
+ const binaryPath = join2(cacheDir, binaryName);
1248
+ if (existsSync2(binaryPath)) {
1182
1249
  debugLog("Binary already cached at:", binaryPath);
1183
1250
  return binaryPath;
1184
1251
  }
1185
1252
  const version = getPackageVersion();
1186
- const { os, arch, ext } = platformInfo;
1187
- const assetName = `comment-checker_v${version}_${os}_${arch}.${ext}`;
1253
+ const { os: os2, arch, ext } = platformInfo;
1254
+ const assetName = `comment-checker_v${version}_${os2}_${arch}.${ext}`;
1188
1255
  const downloadUrl = `https://github.com/${REPO}/releases/download/v${version}/${assetName}`;
1189
1256
  debugLog(`Downloading from: ${downloadUrl}`);
1190
1257
  console.log(`[oh-my-opencode] Downloading comment-checker binary...`);
1191
1258
  try {
1192
- if (!existsSync(cacheDir)) {
1193
- mkdirSync(cacheDir, { recursive: true });
1259
+ if (!existsSync2(cacheDir)) {
1260
+ mkdirSync2(cacheDir, { recursive: true });
1194
1261
  }
1195
1262
  const response = await fetch(downloadUrl, { redirect: "follow" });
1196
1263
  if (!response.ok) {
1197
1264
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1198
1265
  }
1199
- const archivePath = join(cacheDir, assetName);
1266
+ const archivePath = join2(cacheDir, assetName);
1200
1267
  const arrayBuffer = await response.arrayBuffer();
1201
1268
  await Bun.write(archivePath, arrayBuffer);
1202
1269
  debugLog(`Downloaded archive to: ${archivePath}`);
@@ -1205,10 +1272,10 @@ async function downloadCommentChecker() {
1205
1272
  } else {
1206
1273
  await extractZip(archivePath, cacheDir);
1207
1274
  }
1208
- if (existsSync(archivePath)) {
1275
+ if (existsSync2(archivePath)) {
1209
1276
  unlinkSync(archivePath);
1210
1277
  }
1211
- if (process.platform !== "win32" && existsSync(binaryPath)) {
1278
+ if (process.platform !== "win32" && existsSync2(binaryPath)) {
1212
1279
  chmodSync(binaryPath, 493);
1213
1280
  }
1214
1281
  debugLog(`Successfully downloaded binary to: ${binaryPath}`);
@@ -1261,8 +1328,8 @@ function findCommentCheckerPathSync() {
1261
1328
  const require2 = createRequire2(import.meta.url);
1262
1329
  const cliPkgPath = require2.resolve("@code-yeongyu/comment-checker/package.json");
1263
1330
  const cliDir = dirname(cliPkgPath);
1264
- const binaryPath = join2(cliDir, "bin", binaryName);
1265
- if (existsSync2(binaryPath)) {
1331
+ const binaryPath = join3(cliDir, "bin", binaryName);
1332
+ if (existsSync3(binaryPath)) {
1266
1333
  debugLog2("found binary in main package:", binaryPath);
1267
1334
  return binaryPath;
1268
1335
  }
@@ -1275,8 +1342,8 @@ function findCommentCheckerPathSync() {
1275
1342
  const require2 = createRequire2(import.meta.url);
1276
1343
  const pkgPath = require2.resolve(`${platformPkg}/package.json`);
1277
1344
  const pkgDir = dirname(pkgPath);
1278
- const binaryPath = join2(pkgDir, "bin", binaryName);
1279
- if (existsSync2(binaryPath)) {
1345
+ const binaryPath = join3(pkgDir, "bin", binaryName);
1346
+ if (existsSync3(binaryPath)) {
1280
1347
  debugLog2("found binary in platform package:", binaryPath);
1281
1348
  return binaryPath;
1282
1349
  }
@@ -1289,10 +1356,10 @@ function findCommentCheckerPathSync() {
1289
1356
  "/opt/homebrew/bin/comment-checker",
1290
1357
  "/usr/local/bin/comment-checker"
1291
1358
  ];
1292
- for (const path of homebrewPaths) {
1293
- if (existsSync2(path)) {
1294
- debugLog2("found binary via homebrew:", path);
1295
- return path;
1359
+ for (const path2 of homebrewPaths) {
1360
+ if (existsSync3(path2)) {
1361
+ debugLog2("found binary via homebrew:", path2);
1362
+ return path2;
1296
1363
  }
1297
1364
  }
1298
1365
  }
@@ -1315,7 +1382,7 @@ async function getCommentCheckerPath() {
1315
1382
  }
1316
1383
  initPromise = (async () => {
1317
1384
  const syncPath = findCommentCheckerPathSync();
1318
- if (syncPath && existsSync2(syncPath)) {
1385
+ if (syncPath && existsSync3(syncPath)) {
1319
1386
  resolvedCliPath = syncPath;
1320
1387
  debugLog2("using sync-resolved path:", syncPath);
1321
1388
  return syncPath;
@@ -1335,8 +1402,8 @@ async function getCommentCheckerPath() {
1335
1402
  function startBackgroundInit() {
1336
1403
  if (!initPromise) {
1337
1404
  initPromise = getCommentCheckerPath();
1338
- initPromise.then((path) => {
1339
- debugLog2("background init complete:", path || "no binary");
1405
+ initPromise.then((path2) => {
1406
+ debugLog2("background init complete:", path2 || "no binary");
1340
1407
  }).catch((err) => {
1341
1408
  debugLog2("background init error:", err);
1342
1409
  });
@@ -1349,7 +1416,7 @@ async function runCommentChecker(input, cliPath) {
1349
1416
  debugLog2("comment-checker binary not found");
1350
1417
  return { hasComments: false, message: "" };
1351
1418
  }
1352
- if (!existsSync2(binaryPath)) {
1419
+ if (!existsSync3(binaryPath)) {
1353
1420
  debugLog2("comment-checker binary does not exist:", binaryPath);
1354
1421
  return { hasComments: false, message: "" };
1355
1422
  }
@@ -1383,7 +1450,7 @@ async function runCommentChecker(input, cliPath) {
1383
1450
 
1384
1451
  // src/hooks/comment-checker/index.ts
1385
1452
  import * as fs2 from "fs";
1386
- import { existsSync as existsSync3 } from "fs";
1453
+ import { existsSync as existsSync4 } from "fs";
1387
1454
  var DEBUG3 = process.env.COMMENT_CHECKER_DEBUG === "1";
1388
1455
  var DEBUG_FILE3 = "/tmp/comment-checker-debug.log";
1389
1456
  function debugLog3(...args) {
@@ -1409,8 +1476,8 @@ function createCommentCheckerHooks() {
1409
1476
  debugLog3("createCommentCheckerHooks called");
1410
1477
  startBackgroundInit();
1411
1478
  cliPathPromise = getCommentCheckerPath();
1412
- cliPathPromise.then((path) => {
1413
- debugLog3("CLI path resolved:", path || "disabled (no binary)");
1479
+ cliPathPromise.then((path2) => {
1480
+ debugLog3("CLI path resolved:", path2 || "disabled (no binary)");
1414
1481
  }).catch((err) => {
1415
1482
  debugLog3("CLI path resolution error:", err);
1416
1483
  });
@@ -1461,7 +1528,7 @@ function createCommentCheckerHooks() {
1461
1528
  }
1462
1529
  try {
1463
1530
  const cliPath = await cliPathPromise;
1464
- if (!cliPath || !existsSync3(cliPath)) {
1531
+ if (!cliPath || !existsSync4(cliPath)) {
1465
1532
  debugLog3("CLI not available, skipping comment check");
1466
1533
  return;
1467
1534
  }
@@ -1571,6 +1638,9 @@ var SEVERITY_MAP = {
1571
1638
  3: "information",
1572
1639
  4: "hint"
1573
1640
  };
1641
+ var DEFAULT_MAX_REFERENCES = 200;
1642
+ var DEFAULT_MAX_SYMBOLS = 200;
1643
+ var DEFAULT_MAX_DIAGNOSTICS = 200;
1574
1644
  var BUILTIN_SERVERS = {
1575
1645
  typescript: {
1576
1646
  command: ["typescript-language-server", "--stdio"],
@@ -1707,14 +1777,14 @@ var EXT_TO_LANG = {
1707
1777
  ".yml": "yaml"
1708
1778
  };
1709
1779
  // src/tools/lsp/config.ts
1710
- import { existsSync as existsSync4, readFileSync } from "fs";
1711
- import { join as join3 } from "path";
1780
+ import { existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
1781
+ import { join as join4 } from "path";
1712
1782
  import { homedir as homedir2 } from "os";
1713
- function loadJsonFile(path) {
1714
- if (!existsSync4(path))
1783
+ function loadJsonFile(path2) {
1784
+ if (!existsSync5(path2))
1715
1785
  return null;
1716
1786
  try {
1717
- return JSON.parse(readFileSync(path, "utf-8"));
1787
+ return JSON.parse(readFileSync2(path2, "utf-8"));
1718
1788
  } catch {
1719
1789
  return null;
1720
1790
  }
@@ -1722,9 +1792,9 @@ function loadJsonFile(path) {
1722
1792
  function getConfigPaths() {
1723
1793
  const cwd = process.cwd();
1724
1794
  return {
1725
- project: join3(cwd, ".opencode", "oh-my-opencode.json"),
1726
- user: join3(homedir2(), ".config", "opencode", "oh-my-opencode.json"),
1727
- opencode: join3(homedir2(), ".config", "opencode", "opencode.json")
1795
+ project: join4(cwd, ".opencode", "oh-my-opencode.json"),
1796
+ user: join4(homedir2(), ".config", "opencode", "oh-my-opencode.json"),
1797
+ opencode: join4(homedir2(), ".config", "opencode", "opencode.json")
1728
1798
  };
1729
1799
  }
1730
1800
  function loadAllConfigs() {
@@ -1817,7 +1887,7 @@ function isServerInstalled(command) {
1817
1887
  const pathEnv = process.env.PATH || "";
1818
1888
  const paths = pathEnv.split(":");
1819
1889
  for (const p of paths) {
1820
- if (existsSync4(join3(p, cmd))) {
1890
+ if (existsSync5(join4(p, cmd))) {
1821
1891
  return true;
1822
1892
  }
1823
1893
  }
@@ -1867,7 +1937,7 @@ function getAllServers() {
1867
1937
  }
1868
1938
  // src/tools/lsp/client.ts
1869
1939
  var {spawn: spawn3 } = globalThis.Bun;
1870
- import { readFileSync as readFileSync2 } from "fs";
1940
+ import { readFileSync as readFileSync3 } from "fs";
1871
1941
  import { extname, resolve } from "path";
1872
1942
  class LSPServerManager {
1873
1943
  static instance;
@@ -2254,7 +2324,7 @@ ${msg}`);
2254
2324
  const absPath = resolve(filePath);
2255
2325
  if (this.openedFiles.has(absPath))
2256
2326
  return;
2257
- const text = readFileSync2(absPath, "utf-8");
2327
+ const text = readFileSync3(absPath, "utf-8");
2258
2328
  const ext = extname(absPath);
2259
2329
  const languageId = getLanguageId(ext);
2260
2330
  this.notify("textDocument/didOpen", {
@@ -2361,16 +2431,16 @@ ${msg}`);
2361
2431
  }
2362
2432
  // src/tools/lsp/utils.ts
2363
2433
  import { extname as extname2, resolve as resolve2 } from "path";
2364
- import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync } from "fs";
2434
+ import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
2365
2435
  function findWorkspaceRoot(filePath) {
2366
2436
  let dir = resolve2(filePath);
2367
- if (!existsSync5(dir) || !__require("fs").statSync(dir).isDirectory()) {
2437
+ if (!existsSync6(dir) || !__require("fs").statSync(dir).isDirectory()) {
2368
2438
  dir = __require("path").dirname(dir);
2369
2439
  }
2370
2440
  const markers = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "build.gradle"];
2371
2441
  while (dir !== "/") {
2372
2442
  for (const marker of markers) {
2373
- if (existsSync5(__require("path").join(dir, marker))) {
2443
+ if (existsSync6(__require("path").join(dir, marker))) {
2374
2444
  return dir;
2375
2445
  }
2376
2446
  }
@@ -2484,12 +2554,22 @@ function formatPrepareRenameResult(result) {
2484
2554
  if ("defaultBehavior" in result) {
2485
2555
  return result.defaultBehavior ? "Rename supported (using default behavior)" : "Cannot rename at this position";
2486
2556
  }
2487
- const startLine = result.range.start.line + 1;
2488
- const startChar = result.range.start.character;
2489
- const endLine = result.range.end.line + 1;
2490
- const endChar = result.range.end.character;
2491
- const placeholder = result.placeholder ? ` (current: "${result.placeholder}")` : "";
2492
- return `Rename available at ${startLine}:${startChar}-${endLine}:${endChar}${placeholder}`;
2557
+ if ("range" in result && result.range) {
2558
+ const startLine = result.range.start.line + 1;
2559
+ const startChar = result.range.start.character;
2560
+ const endLine = result.range.end.line + 1;
2561
+ const endChar = result.range.end.character;
2562
+ const placeholder = result.placeholder ? ` (current: "${result.placeholder}")` : "";
2563
+ return `Rename available at ${startLine}:${startChar}-${endLine}:${endChar}${placeholder}`;
2564
+ }
2565
+ if ("start" in result && "end" in result) {
2566
+ const startLine = result.start.line + 1;
2567
+ const startChar = result.start.character;
2568
+ const endLine = result.end.line + 1;
2569
+ const endChar = result.end.character;
2570
+ return `Rename available at ${startLine}:${startChar}-${endLine}:${endChar}`;
2571
+ }
2572
+ return "Cannot rename at this position";
2493
2573
  }
2494
2574
  function formatCodeAction(action) {
2495
2575
  let result = `[${action.kind || "action"}] ${action.title}`;
@@ -2518,7 +2598,7 @@ function formatCodeActions(actions) {
2518
2598
  }
2519
2599
  function applyTextEditsToFile(filePath, edits) {
2520
2600
  try {
2521
- let content = readFileSync3(filePath, "utf-8");
2601
+ let content = readFileSync4(filePath, "utf-8");
2522
2602
  const lines = content.split(`
2523
2603
  `);
2524
2604
  const sortedEdits = [...edits].sort((a, b) => {
@@ -2543,7 +2623,7 @@ function applyTextEditsToFile(filePath, edits) {
2543
2623
  `));
2544
2624
  }
2545
2625
  }
2546
- writeFileSync(filePath, lines.join(`
2626
+ writeFileSync2(filePath, lines.join(`
2547
2627
  `), "utf-8");
2548
2628
  return { success: true, editCount: edits.length };
2549
2629
  } catch (err) {
@@ -2574,7 +2654,7 @@ function applyWorkspaceEdit(edit) {
2574
2654
  if (change.kind === "create") {
2575
2655
  try {
2576
2656
  const filePath = change.uri.replace("file://", "");
2577
- writeFileSync(filePath, "", "utf-8");
2657
+ writeFileSync2(filePath, "", "utf-8");
2578
2658
  result.filesModified.push(filePath);
2579
2659
  } catch (err) {
2580
2660
  result.success = false;
@@ -2584,8 +2664,8 @@ function applyWorkspaceEdit(edit) {
2584
2664
  try {
2585
2665
  const oldPath = change.oldUri.replace("file://", "");
2586
2666
  const newPath = change.newUri.replace("file://", "");
2587
- const content = readFileSync3(oldPath, "utf-8");
2588
- writeFileSync(newPath, content, "utf-8");
2667
+ const content = readFileSync4(oldPath, "utf-8");
2668
+ writeFileSync2(newPath, content, "utf-8");
2589
2669
  __require("fs").unlinkSync(oldPath);
2590
2670
  result.filesModified.push(newPath);
2591
2671
  } catch (err) {
@@ -3365,10 +3445,10 @@ function mergeDefs(...defs) {
3365
3445
  function cloneDef(schema) {
3366
3446
  return mergeDefs(schema._zod.def);
3367
3447
  }
3368
- function getElementAtPath(obj, path) {
3369
- if (!path)
3448
+ function getElementAtPath(obj, path2) {
3449
+ if (!path2)
3370
3450
  return obj;
3371
- return path.reduce((acc, key) => acc?.[key], obj);
3451
+ return path2.reduce((acc, key) => acc?.[key], obj);
3372
3452
  }
3373
3453
  function promiseAllObject(promisesObj) {
3374
3454
  const keys = Object.keys(promisesObj);
@@ -3727,11 +3807,11 @@ function aborted(x, startIndex = 0) {
3727
3807
  }
3728
3808
  return false;
3729
3809
  }
3730
- function prefixIssues(path, issues) {
3810
+ function prefixIssues(path2, issues) {
3731
3811
  return issues.map((iss) => {
3732
3812
  var _a;
3733
3813
  (_a = iss).path ?? (_a.path = []);
3734
- iss.path.unshift(path);
3814
+ iss.path.unshift(path2);
3735
3815
  return iss;
3736
3816
  });
3737
3817
  }
@@ -3899,7 +3979,7 @@ function treeifyError(error, _mapper) {
3899
3979
  return issue2.message;
3900
3980
  };
3901
3981
  const result = { errors: [] };
3902
- const processError = (error2, path = []) => {
3982
+ const processError = (error2, path2 = []) => {
3903
3983
  var _a, _b;
3904
3984
  for (const issue2 of error2.issues) {
3905
3985
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -3909,7 +3989,7 @@ function treeifyError(error, _mapper) {
3909
3989
  } else if (issue2.code === "invalid_element") {
3910
3990
  processError({ issues: issue2.issues }, issue2.path);
3911
3991
  } else {
3912
- const fullpath = [...path, ...issue2.path];
3992
+ const fullpath = [...path2, ...issue2.path];
3913
3993
  if (fullpath.length === 0) {
3914
3994
  result.errors.push(mapper(issue2));
3915
3995
  continue;
@@ -3941,8 +4021,8 @@ function treeifyError(error, _mapper) {
3941
4021
  }
3942
4022
  function toDotPath(_path) {
3943
4023
  const segs = [];
3944
- const path = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
3945
- for (const seg of path) {
4024
+ const path2 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
4025
+ for (const seg of path2) {
3946
4026
  if (typeof seg === "number")
3947
4027
  segs.push(`[${seg}]`);
3948
4028
  else if (typeof seg === "symbol")
@@ -15025,7 +15105,14 @@ var lsp_find_references = tool({
15025
15105
  const output2 = "No references found";
15026
15106
  return output2;
15027
15107
  }
15028
- const output = result.map(formatLocation).join(`
15108
+ const total = result.length;
15109
+ const truncated = total > DEFAULT_MAX_REFERENCES;
15110
+ const limited = truncated ? result.slice(0, DEFAULT_MAX_REFERENCES) : result;
15111
+ const lines = limited.map(formatLocation);
15112
+ if (truncated) {
15113
+ lines.unshift(`Found ${total} references (showing first ${DEFAULT_MAX_REFERENCES}):`);
15114
+ }
15115
+ const output = lines.join(`
15029
15116
  `);
15030
15117
  return output;
15031
15118
  } catch (e) {
@@ -15045,18 +15132,23 @@ var lsp_document_symbols = tool({
15045
15132
  return await client.documentSymbols(args.filePath);
15046
15133
  });
15047
15134
  if (!result || result.length === 0) {
15048
- const output2 = "No symbols found";
15049
- return output2;
15135
+ const output = "No symbols found";
15136
+ return output;
15050
15137
  }
15051
- let output;
15052
- if ("range" in result[0]) {
15053
- output = result.map((s) => formatDocumentSymbol(s)).join(`
15054
- `);
15138
+ const total = result.length;
15139
+ const truncated = total > DEFAULT_MAX_SYMBOLS;
15140
+ const limited = truncated ? result.slice(0, DEFAULT_MAX_SYMBOLS) : result;
15141
+ const lines = [];
15142
+ if (truncated) {
15143
+ lines.push(`Found ${total} symbols (showing first ${DEFAULT_MAX_SYMBOLS}):`);
15144
+ }
15145
+ if ("range" in limited[0]) {
15146
+ lines.push(...limited.map((s) => formatDocumentSymbol(s)));
15055
15147
  } else {
15056
- output = result.map(formatSymbolInfo).join(`
15057
- `);
15148
+ lines.push(...limited.map(formatSymbolInfo));
15058
15149
  }
15059
- return output;
15150
+ return lines.join(`
15151
+ `);
15060
15152
  } catch (e) {
15061
15153
  const output = `Error: ${e instanceof Error ? e.message : String(e)}`;
15062
15154
  return output;
@@ -15079,8 +15171,15 @@ var lsp_workspace_symbols = tool({
15079
15171
  const output2 = "No symbols found";
15080
15172
  return output2;
15081
15173
  }
15082
- const limited = args.limit ? result.slice(0, args.limit) : result;
15083
- const output = limited.map(formatSymbolInfo).join(`
15174
+ const total = result.length;
15175
+ const limit = Math.min(args.limit ?? DEFAULT_MAX_SYMBOLS, DEFAULT_MAX_SYMBOLS);
15176
+ const truncated = total > limit;
15177
+ const limited = result.slice(0, limit);
15178
+ const lines = limited.map(formatSymbolInfo);
15179
+ if (truncated) {
15180
+ lines.unshift(`Found ${total} symbols (showing first ${limit}):`);
15181
+ }
15182
+ const output = lines.join(`
15084
15183
  `);
15085
15184
  return output;
15086
15185
  } catch (e) {
@@ -15113,7 +15212,14 @@ var lsp_diagnostics = tool({
15113
15212
  const output2 = "No diagnostics found";
15114
15213
  return output2;
15115
15214
  }
15116
- const output = diagnostics.map(formatDiagnostic).join(`
15215
+ const total = diagnostics.length;
15216
+ const truncated = total > DEFAULT_MAX_DIAGNOSTICS;
15217
+ const limited = truncated ? diagnostics.slice(0, DEFAULT_MAX_DIAGNOSTICS) : diagnostics;
15218
+ const lines = limited.map(formatDiagnostic);
15219
+ if (truncated) {
15220
+ lines.unshift(`Found ${total} diagnostics (showing first ${DEFAULT_MAX_DIAGNOSTICS}):`);
15221
+ }
15222
+ const output = lines.join(`
15117
15223
  `);
15118
15224
  return output;
15119
15225
  } catch (e) {
@@ -15259,13 +15365,13 @@ var lsp_code_action_resolve = tool({
15259
15365
  });
15260
15366
  // src/tools/ast-grep/constants.ts
15261
15367
  import { createRequire as createRequire4 } from "module";
15262
- import { dirname as dirname2, join as join5 } from "path";
15263
- import { existsSync as existsSync7, statSync } from "fs";
15368
+ import { dirname as dirname2, join as join6 } from "path";
15369
+ import { existsSync as existsSync8, statSync } from "fs";
15264
15370
 
15265
15371
  // src/tools/ast-grep/downloader.ts
15266
15372
  var {spawn: spawn4 } = globalThis.Bun;
15267
- import { existsSync as existsSync6, mkdirSync as mkdirSync2, chmodSync as chmodSync2, unlinkSync as unlinkSync2 } from "fs";
15268
- import { join as join4 } from "path";
15373
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, chmodSync as chmodSync2, unlinkSync as unlinkSync2 } from "fs";
15374
+ import { join as join5 } from "path";
15269
15375
  import { homedir as homedir3 } from "os";
15270
15376
  import { createRequire as createRequire3 } from "module";
15271
15377
  var REPO2 = "ast-grep/ast-grep";
@@ -15291,19 +15397,19 @@ var PLATFORM_MAP2 = {
15291
15397
  function getCacheDir2() {
15292
15398
  if (process.platform === "win32") {
15293
15399
  const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
15294
- const base2 = localAppData || join4(homedir3(), "AppData", "Local");
15295
- return join4(base2, "oh-my-opencode", "bin");
15400
+ const base2 = localAppData || join5(homedir3(), "AppData", "Local");
15401
+ return join5(base2, "oh-my-opencode", "bin");
15296
15402
  }
15297
- const xdgCache = process.env.XDG_CACHE_HOME;
15298
- const base = xdgCache || join4(homedir3(), ".cache");
15299
- return join4(base, "oh-my-opencode", "bin");
15403
+ const xdgCache2 = process.env.XDG_CACHE_HOME;
15404
+ const base = xdgCache2 || join5(homedir3(), ".cache");
15405
+ return join5(base, "oh-my-opencode", "bin");
15300
15406
  }
15301
15407
  function getBinaryName3() {
15302
15408
  return process.platform === "win32" ? "sg.exe" : "sg";
15303
15409
  }
15304
15410
  function getCachedBinaryPath2() {
15305
- const binaryPath = join4(getCacheDir2(), getBinaryName3());
15306
- return existsSync6(binaryPath) ? binaryPath : null;
15411
+ const binaryPath = join5(getCacheDir2(), getBinaryName3());
15412
+ return existsSync7(binaryPath) ? binaryPath : null;
15307
15413
  }
15308
15414
  async function extractZip2(archivePath, destDir) {
15309
15415
  const proc = process.platform === "win32" ? spawn4([
@@ -15329,30 +15435,30 @@ async function downloadAstGrep(version2 = DEFAULT_VERSION) {
15329
15435
  }
15330
15436
  const cacheDir = getCacheDir2();
15331
15437
  const binaryName = getBinaryName3();
15332
- const binaryPath = join4(cacheDir, binaryName);
15333
- if (existsSync6(binaryPath)) {
15438
+ const binaryPath = join5(cacheDir, binaryName);
15439
+ if (existsSync7(binaryPath)) {
15334
15440
  return binaryPath;
15335
15441
  }
15336
- const { arch, os } = platformInfo;
15337
- const assetName = `app-${arch}-${os}.zip`;
15442
+ const { arch, os: os2 } = platformInfo;
15443
+ const assetName = `app-${arch}-${os2}.zip`;
15338
15444
  const downloadUrl = `https://github.com/${REPO2}/releases/download/${version2}/${assetName}`;
15339
15445
  console.log(`[oh-my-opencode] Downloading ast-grep binary...`);
15340
15446
  try {
15341
- if (!existsSync6(cacheDir)) {
15342
- mkdirSync2(cacheDir, { recursive: true });
15447
+ if (!existsSync7(cacheDir)) {
15448
+ mkdirSync3(cacheDir, { recursive: true });
15343
15449
  }
15344
15450
  const response = await fetch(downloadUrl, { redirect: "follow" });
15345
15451
  if (!response.ok) {
15346
15452
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
15347
15453
  }
15348
- const archivePath = join4(cacheDir, assetName);
15454
+ const archivePath = join5(cacheDir, assetName);
15349
15455
  const arrayBuffer = await response.arrayBuffer();
15350
15456
  await Bun.write(archivePath, arrayBuffer);
15351
15457
  await extractZip2(archivePath, cacheDir);
15352
- if (existsSync6(archivePath)) {
15458
+ if (existsSync7(archivePath)) {
15353
15459
  unlinkSync2(archivePath);
15354
15460
  }
15355
- if (process.platform !== "win32" && existsSync6(binaryPath)) {
15461
+ if (process.platform !== "win32" && existsSync7(binaryPath)) {
15356
15462
  chmodSync2(binaryPath, 493);
15357
15463
  }
15358
15464
  console.log(`[oh-my-opencode] ast-grep binary ready.`);
@@ -15403,8 +15509,8 @@ function findSgCliPathSync() {
15403
15509
  const require2 = createRequire4(import.meta.url);
15404
15510
  const cliPkgPath = require2.resolve("@ast-grep/cli/package.json");
15405
15511
  const cliDir = dirname2(cliPkgPath);
15406
- const sgPath = join5(cliDir, binaryName);
15407
- if (existsSync7(sgPath) && isValidBinary(sgPath)) {
15512
+ const sgPath = join6(cliDir, binaryName);
15513
+ if (existsSync8(sgPath) && isValidBinary(sgPath)) {
15408
15514
  return sgPath;
15409
15515
  }
15410
15516
  } catch {}
@@ -15415,17 +15521,17 @@ function findSgCliPathSync() {
15415
15521
  const pkgPath = require2.resolve(`${platformPkg}/package.json`);
15416
15522
  const pkgDir = dirname2(pkgPath);
15417
15523
  const astGrepName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep";
15418
- const binaryPath = join5(pkgDir, astGrepName);
15419
- if (existsSync7(binaryPath) && isValidBinary(binaryPath)) {
15524
+ const binaryPath = join6(pkgDir, astGrepName);
15525
+ if (existsSync8(binaryPath) && isValidBinary(binaryPath)) {
15420
15526
  return binaryPath;
15421
15527
  }
15422
15528
  } catch {}
15423
15529
  }
15424
15530
  if (process.platform === "darwin") {
15425
15531
  const homebrewPaths = ["/opt/homebrew/bin/sg", "/usr/local/bin/sg"];
15426
- for (const path of homebrewPaths) {
15427
- if (existsSync7(path) && isValidBinary(path)) {
15428
- return path;
15532
+ for (const path2 of homebrewPaths) {
15533
+ if (existsSync8(path2) && isValidBinary(path2)) {
15534
+ return path2;
15429
15535
  }
15430
15536
  }
15431
15537
  }
@@ -15443,8 +15549,8 @@ function getSgCliPath() {
15443
15549
  }
15444
15550
  return "sg";
15445
15551
  }
15446
- function setSgCliPath(path) {
15447
- resolvedCliPath2 = path;
15552
+ function setSgCliPath(path2) {
15553
+ resolvedCliPath2 = path2;
15448
15554
  }
15449
15555
  var SG_CLI_PATH = getSgCliPath();
15450
15556
  var CLI_LANGUAGES = [
@@ -15475,6 +15581,9 @@ var CLI_LANGUAGES = [
15475
15581
  "yaml"
15476
15582
  ];
15477
15583
  var NAPI_LANGUAGES = ["html", "javascript", "tsx", "css", "typescript"];
15584
+ var DEFAULT_TIMEOUT_MS = 300000;
15585
+ var DEFAULT_MAX_OUTPUT_BYTES = 1 * 1024 * 1024;
15586
+ var DEFAULT_MAX_MATCHES = 500;
15478
15587
  var LANG_EXTENSIONS = {
15479
15588
  bash: [".bash", ".sh", ".zsh", ".bats"],
15480
15589
  c: [".c", ".h"],
@@ -15505,11 +15614,11 @@ var LANG_EXTENSIONS = {
15505
15614
 
15506
15615
  // src/tools/ast-grep/cli.ts
15507
15616
  var {spawn: spawn5 } = globalThis.Bun;
15508
- import { existsSync as existsSync8 } from "fs";
15617
+ import { existsSync as existsSync9 } from "fs";
15509
15618
  var resolvedCliPath3 = null;
15510
15619
  var initPromise2 = null;
15511
15620
  async function getAstGrepPath() {
15512
- if (resolvedCliPath3 !== null && existsSync8(resolvedCliPath3)) {
15621
+ if (resolvedCliPath3 !== null && existsSync9(resolvedCliPath3)) {
15513
15622
  return resolvedCliPath3;
15514
15623
  }
15515
15624
  if (initPromise2) {
@@ -15517,7 +15626,7 @@ async function getAstGrepPath() {
15517
15626
  }
15518
15627
  initPromise2 = (async () => {
15519
15628
  const syncPath = findSgCliPathSync();
15520
- if (syncPath && existsSync8(syncPath)) {
15629
+ if (syncPath && existsSync9(syncPath)) {
15521
15630
  resolvedCliPath3 = syncPath;
15522
15631
  setSgCliPath(syncPath);
15523
15632
  return syncPath;
@@ -15532,16 +15641,6 @@ async function getAstGrepPath() {
15532
15641
  })();
15533
15642
  return initPromise2;
15534
15643
  }
15535
- async function spawnSg(cliPath, args) {
15536
- const proc = spawn5([cliPath, ...args], {
15537
- stdout: "pipe",
15538
- stderr: "pipe"
15539
- });
15540
- const stdout = await new Response(proc.stdout).text();
15541
- const stderr = await new Response(proc.stderr).text();
15542
- const exitCode = await proc.exited;
15543
- return { stdout, stderr, exitCode };
15544
- }
15545
15644
  async function runSg(options) {
15546
15645
  const args = ["run", "-p", options.pattern, "--lang", options.lang, "--json=compact"];
15547
15646
  if (options.rewrite) {
@@ -15561,53 +15660,120 @@ async function runSg(options) {
15561
15660
  const paths = options.paths && options.paths.length > 0 ? options.paths : ["."];
15562
15661
  args.push(...paths);
15563
15662
  let cliPath = getSgCliPath();
15564
- if (!existsSync8(cliPath) && cliPath !== "sg") {
15663
+ if (!existsSync9(cliPath) && cliPath !== "sg") {
15565
15664
  const downloadedPath = await getAstGrepPath();
15566
15665
  if (downloadedPath) {
15567
15666
  cliPath = downloadedPath;
15568
15667
  }
15569
15668
  }
15570
- let result;
15669
+ const timeout = DEFAULT_TIMEOUT_MS;
15670
+ const proc = spawn5([cliPath, ...args], {
15671
+ stdout: "pipe",
15672
+ stderr: "pipe"
15673
+ });
15674
+ const timeoutPromise = new Promise((_, reject) => {
15675
+ const id = setTimeout(() => {
15676
+ proc.kill();
15677
+ reject(new Error(`Search timeout after ${timeout}ms`));
15678
+ }, timeout);
15679
+ proc.exited.then(() => clearTimeout(id));
15680
+ });
15681
+ let stdout;
15682
+ let stderr;
15683
+ let exitCode;
15571
15684
  try {
15572
- result = await spawnSg(cliPath, args);
15685
+ stdout = await Promise.race([new Response(proc.stdout).text(), timeoutPromise]);
15686
+ stderr = await new Response(proc.stderr).text();
15687
+ exitCode = await proc.exited;
15573
15688
  } catch (e) {
15574
15689
  const error45 = e;
15575
- if (error45.code === "ENOENT" || error45.message?.includes("ENOENT") || error45.message?.includes("not found")) {
15690
+ if (error45.message?.includes("timeout")) {
15691
+ return {
15692
+ matches: [],
15693
+ totalMatches: 0,
15694
+ truncated: true,
15695
+ truncatedReason: "timeout",
15696
+ error: error45.message
15697
+ };
15698
+ }
15699
+ const nodeError = e;
15700
+ if (nodeError.code === "ENOENT" || nodeError.message?.includes("ENOENT") || nodeError.message?.includes("not found")) {
15576
15701
  const downloadedPath = await ensureAstGrepBinary();
15577
15702
  if (downloadedPath) {
15578
15703
  resolvedCliPath3 = downloadedPath;
15579
15704
  setSgCliPath(downloadedPath);
15580
- result = await spawnSg(downloadedPath, args);
15705
+ return runSg(options);
15581
15706
  } else {
15582
- throw new Error(`ast-grep CLI binary not found.
15707
+ return {
15708
+ matches: [],
15709
+ totalMatches: 0,
15710
+ truncated: false,
15711
+ error: `ast-grep CLI binary not found.
15583
15712
 
15584
15713
  ` + `Auto-download failed. Manual install options:
15585
15714
  ` + ` bun add -D @ast-grep/cli
15586
15715
  ` + ` cargo install ast-grep --locked
15587
- ` + ` brew install ast-grep`);
15716
+ ` + ` brew install ast-grep`
15717
+ };
15588
15718
  }
15589
- } else {
15590
- throw new Error(`Failed to spawn ast-grep: ${error45.message}`);
15591
15719
  }
15720
+ return {
15721
+ matches: [],
15722
+ totalMatches: 0,
15723
+ truncated: false,
15724
+ error: `Failed to spawn ast-grep: ${error45.message}`
15725
+ };
15592
15726
  }
15593
- const { stdout, stderr, exitCode } = result;
15594
15727
  if (exitCode !== 0 && stdout.trim() === "") {
15595
15728
  if (stderr.includes("No files found")) {
15596
- return [];
15729
+ return { matches: [], totalMatches: 0, truncated: false };
15597
15730
  }
15598
15731
  if (stderr.trim()) {
15599
- throw new Error(stderr.trim());
15732
+ return { matches: [], totalMatches: 0, truncated: false, error: stderr.trim() };
15600
15733
  }
15601
- return [];
15734
+ return { matches: [], totalMatches: 0, truncated: false };
15602
15735
  }
15603
15736
  if (!stdout.trim()) {
15604
- return [];
15737
+ return { matches: [], totalMatches: 0, truncated: false };
15605
15738
  }
15739
+ const outputTruncated = stdout.length >= DEFAULT_MAX_OUTPUT_BYTES;
15740
+ const outputToProcess = outputTruncated ? stdout.substring(0, DEFAULT_MAX_OUTPUT_BYTES) : stdout;
15741
+ let matches = [];
15606
15742
  try {
15607
- return JSON.parse(stdout);
15743
+ matches = JSON.parse(outputToProcess);
15608
15744
  } catch {
15609
- return [];
15745
+ if (outputTruncated) {
15746
+ try {
15747
+ const lastValidIndex = outputToProcess.lastIndexOf("}");
15748
+ if (lastValidIndex > 0) {
15749
+ const bracketIndex = outputToProcess.lastIndexOf("},", lastValidIndex);
15750
+ if (bracketIndex > 0) {
15751
+ const truncatedJson = outputToProcess.substring(0, bracketIndex + 1) + "]";
15752
+ matches = JSON.parse(truncatedJson);
15753
+ }
15754
+ }
15755
+ } catch {
15756
+ return {
15757
+ matches: [],
15758
+ totalMatches: 0,
15759
+ truncated: true,
15760
+ truncatedReason: "max_output_bytes",
15761
+ error: "Output too large and could not be parsed"
15762
+ };
15763
+ }
15764
+ } else {
15765
+ return { matches: [], totalMatches: 0, truncated: false };
15766
+ }
15610
15767
  }
15768
+ const totalMatches = matches.length;
15769
+ const matchesTruncated = totalMatches > DEFAULT_MAX_MATCHES;
15770
+ const finalMatches = matchesTruncated ? matches.slice(0, DEFAULT_MAX_MATCHES) : matches;
15771
+ return {
15772
+ matches: finalMatches,
15773
+ totalMatches,
15774
+ truncated: outputTruncated || matchesTruncated,
15775
+ truncatedReason: outputTruncated ? "max_output_bytes" : matchesTruncated ? "max_matches" : undefined
15776
+ };
15611
15777
  }
15612
15778
 
15613
15779
  // src/tools/ast-grep/napi.ts
@@ -15696,13 +15862,22 @@ function getRootInfo(code, lang) {
15696
15862
  }
15697
15863
 
15698
15864
  // src/tools/ast-grep/utils.ts
15699
- function formatSearchResult(matches) {
15700
- if (matches.length === 0) {
15865
+ function formatSearchResult(result) {
15866
+ if (result.error) {
15867
+ return `Error: ${result.error}`;
15868
+ }
15869
+ if (result.matches.length === 0) {
15701
15870
  return "No matches found";
15702
15871
  }
15703
- const lines = [`Found ${matches.length} match(es):
15704
- `];
15705
- for (const match of matches) {
15872
+ const lines = [];
15873
+ if (result.truncated) {
15874
+ const reason = result.truncatedReason === "max_matches" ? `showing first ${result.matches.length} of ${result.totalMatches}` : result.truncatedReason === "max_output_bytes" ? "output exceeded 1MB limit" : "search timed out";
15875
+ lines.push(`\u26A0\uFE0F Results truncated (${reason})
15876
+ `);
15877
+ }
15878
+ lines.push(`Found ${result.matches.length} match(es)${result.truncated ? ` (truncated from ${result.totalMatches})` : ""}:
15879
+ `);
15880
+ for (const match of result.matches) {
15706
15881
  const loc = `${match.file}:${match.range.start.line + 1}:${match.range.start.column + 1}`;
15707
15882
  lines.push(`${loc}`);
15708
15883
  lines.push(` ${match.lines.trim()}`);
@@ -15711,14 +15886,23 @@ function formatSearchResult(matches) {
15711
15886
  return lines.join(`
15712
15887
  `);
15713
15888
  }
15714
- function formatReplaceResult(matches, isDryRun) {
15715
- if (matches.length === 0) {
15889
+ function formatReplaceResult(result, isDryRun) {
15890
+ if (result.error) {
15891
+ return `Error: ${result.error}`;
15892
+ }
15893
+ if (result.matches.length === 0) {
15716
15894
  return "No matches found to replace";
15717
15895
  }
15718
15896
  const prefix = isDryRun ? "[DRY RUN] " : "";
15719
- const lines = [`${prefix}${matches.length} replacement(s):
15720
- `];
15721
- for (const match of matches) {
15897
+ const lines = [];
15898
+ if (result.truncated) {
15899
+ const reason = result.truncatedReason === "max_matches" ? `showing first ${result.matches.length} of ${result.totalMatches}` : result.truncatedReason === "max_output_bytes" ? "output exceeded 1MB limit" : "search timed out";
15900
+ lines.push(`\u26A0\uFE0F Results truncated (${reason})
15901
+ `);
15902
+ }
15903
+ lines.push(`${prefix}${result.matches.length} replacement(s):
15904
+ `);
15905
+ for (const match of result.matches) {
15722
15906
  const loc = `${match.file}:${match.range.start.line + 1}:${match.range.start.column + 1}`;
15723
15907
  lines.push(`${loc}`);
15724
15908
  lines.push(` ${match.text}`);
@@ -15796,15 +15980,15 @@ var ast_grep_search = tool({
15796
15980
  },
15797
15981
  execute: async (args, context) => {
15798
15982
  try {
15799
- const matches = await runSg({
15983
+ const result = await runSg({
15800
15984
  pattern: args.pattern,
15801
15985
  lang: args.lang,
15802
15986
  paths: args.paths,
15803
15987
  globs: args.globs,
15804
15988
  context: args.context
15805
15989
  });
15806
- let output = formatSearchResult(matches);
15807
- if (matches.length === 0) {
15990
+ let output = formatSearchResult(result);
15991
+ if (result.matches.length === 0 && !result.error) {
15808
15992
  const hint = getEmptyResultHint(args.pattern, args.lang);
15809
15993
  if (hint) {
15810
15994
  output += `
@@ -15833,7 +16017,7 @@ var ast_grep_replace = tool({
15833
16017
  },
15834
16018
  execute: async (args, context) => {
15835
16019
  try {
15836
- const matches = await runSg({
16020
+ const result = await runSg({
15837
16021
  pattern: args.pattern,
15838
16022
  rewrite: args.rewrite,
15839
16023
  lang: args.lang,
@@ -15841,7 +16025,7 @@ var ast_grep_replace = tool({
15841
16025
  globs: args.globs,
15842
16026
  updateAll: args.dryRun === false
15843
16027
  });
15844
- const output = formatReplaceResult(matches, args.dryRun !== false);
16028
+ const output = formatReplaceResult(result, args.dryRun !== false);
15845
16029
  showOutputToUser(context, output);
15846
16030
  return output;
15847
16031
  } catch (e) {
@@ -15921,8 +16105,8 @@ var ast_grep_transform = tool({
15921
16105
  var {spawn: spawn6 } = globalThis.Bun;
15922
16106
 
15923
16107
  // src/tools/safe-grep/constants.ts
15924
- import { existsSync as existsSync9 } from "fs";
15925
- import { join as join6, dirname as dirname3 } from "path";
16108
+ import { existsSync as existsSync10 } from "fs";
16109
+ import { join as join7, dirname as dirname3 } from "path";
15926
16110
  import { spawnSync } from "child_process";
15927
16111
  var cachedCli = null;
15928
16112
  function findExecutable(name) {
@@ -15943,13 +16127,13 @@ function getOpenCodeBundledRg() {
15943
16127
  const isWindows = process.platform === "win32";
15944
16128
  const rgName = isWindows ? "rg.exe" : "rg";
15945
16129
  const candidates = [
15946
- join6(execDir, rgName),
15947
- join6(execDir, "bin", rgName),
15948
- join6(execDir, "..", "bin", rgName),
15949
- join6(execDir, "..", "libexec", rgName)
16130
+ join7(execDir, rgName),
16131
+ join7(execDir, "bin", rgName),
16132
+ join7(execDir, "..", "bin", rgName),
16133
+ join7(execDir, "..", "libexec", rgName)
15950
16134
  ];
15951
16135
  for (const candidate of candidates) {
15952
- if (existsSync9(candidate)) {
16136
+ if (existsSync10(candidate)) {
15953
16137
  return candidate;
15954
16138
  }
15955
16139
  }
@@ -15980,8 +16164,8 @@ var DEFAULT_MAX_DEPTH = 20;
15980
16164
  var DEFAULT_MAX_FILESIZE = "10M";
15981
16165
  var DEFAULT_MAX_COUNT = 500;
15982
16166
  var DEFAULT_MAX_COLUMNS = 1000;
15983
- var DEFAULT_TIMEOUT_MS = 300000;
15984
- var DEFAULT_MAX_OUTPUT_BYTES = 10 * 1024 * 1024;
16167
+ var DEFAULT_TIMEOUT_MS2 = 300000;
16168
+ var DEFAULT_MAX_OUTPUT_BYTES2 = 10 * 1024 * 1024;
15985
16169
  var RG_SAFETY_FLAGS = [
15986
16170
  "--no-follow",
15987
16171
  "--color=never",
@@ -16082,7 +16266,7 @@ function parseOutput(output) {
16082
16266
  async function runRg(options) {
16083
16267
  const cli = resolveGrepCli();
16084
16268
  const args = buildArgs(options, cli.backend);
16085
- const timeout = Math.min(options.timeout ?? DEFAULT_TIMEOUT_MS, DEFAULT_TIMEOUT_MS);
16269
+ const timeout = Math.min(options.timeout ?? DEFAULT_TIMEOUT_MS2, DEFAULT_TIMEOUT_MS2);
16086
16270
  if (cli.backend === "rg") {
16087
16271
  args.push("--", options.pattern);
16088
16272
  } else {
@@ -16105,8 +16289,8 @@ async function runRg(options) {
16105
16289
  const stdout = await Promise.race([new Response(proc.stdout).text(), timeoutPromise]);
16106
16290
  const stderr = await new Response(proc.stderr).text();
16107
16291
  const exitCode = await proc.exited;
16108
- const truncated = stdout.length >= DEFAULT_MAX_OUTPUT_BYTES;
16109
- const outputToProcess = truncated ? stdout.substring(0, DEFAULT_MAX_OUTPUT_BYTES) : stdout;
16292
+ const truncated = stdout.length >= DEFAULT_MAX_OUTPUT_BYTES2;
16293
+ const outputToProcess = truncated ? stdout.substring(0, DEFAULT_MAX_OUTPUT_BYTES2) : stdout;
16110
16294
  if (exitCode > 1 && stderr.trim()) {
16111
16295
  return {
16112
16296
  matches: [],
@@ -16282,11 +16466,11 @@ var OhMyOpenCodeConfigSchema = exports_external.object({
16282
16466
  });
16283
16467
  // src/index.ts
16284
16468
  import * as fs3 from "fs";
16285
- import * as path from "path";
16469
+ import * as path2 from "path";
16286
16470
  function loadPluginConfig(directory) {
16287
16471
  const configPaths = [
16288
- path.join(directory, "oh-my-opencode.json"),
16289
- path.join(directory, ".oh-my-opencode.json")
16472
+ path2.join(directory, "oh-my-opencode.json"),
16473
+ path2.join(directory, ".oh-my-opencode.json")
16290
16474
  ];
16291
16475
  for (const configPath of configPaths) {
16292
16476
  try {
@@ -1,4 +1,4 @@
1
- import type { CliMatch, CliLanguage } from "./types";
1
+ import type { CliLanguage, SgResult } from "./types";
2
2
  export interface RunOptions {
3
3
  pattern: string;
4
4
  lang: CliLanguage;
@@ -10,6 +10,6 @@ export interface RunOptions {
10
10
  }
11
11
  export declare function getAstGrepPath(): Promise<string | null>;
12
12
  export declare function startBackgroundInit(): void;
13
- export declare function runSg(options: RunOptions): Promise<CliMatch[]>;
13
+ export declare function runSg(options: RunOptions): Promise<SgResult>;
14
14
  export declare function isCliAvailable(): boolean;
15
15
  export declare function ensureCliAvailable(): Promise<boolean>;
@@ -4,6 +4,9 @@ export declare function setSgCliPath(path: string): void;
4
4
  export declare const SG_CLI_PATH: string;
5
5
  export declare const CLI_LANGUAGES: readonly ["bash", "c", "cpp", "csharp", "css", "elixir", "go", "haskell", "html", "java", "javascript", "json", "kotlin", "lua", "nix", "php", "python", "ruby", "rust", "scala", "solidity", "swift", "typescript", "tsx", "yaml"];
6
6
  export declare const NAPI_LANGUAGES: readonly ["html", "javascript", "tsx", "css", "typescript"];
7
+ export declare const DEFAULT_TIMEOUT_MS = 300000;
8
+ export declare const DEFAULT_MAX_OUTPUT_BYTES: number;
9
+ export declare const DEFAULT_MAX_MATCHES = 500;
7
10
  export declare const LANG_EXTENSIONS: Record<string, string[]>;
8
11
  export interface EnvironmentCheckResult {
9
12
  cli: {
@@ -49,3 +49,10 @@ export interface TransformResult {
49
49
  transformed: string;
50
50
  editCount: number;
51
51
  }
52
+ export interface SgResult {
53
+ matches: CliMatch[];
54
+ totalMatches: number;
55
+ truncated: boolean;
56
+ truncatedReason?: "max_matches" | "max_output_bytes" | "timeout";
57
+ error?: string;
58
+ }
@@ -1,5 +1,5 @@
1
- import type { CliMatch, AnalyzeResult } from "./types";
2
- export declare function formatSearchResult(matches: CliMatch[]): string;
3
- export declare function formatReplaceResult(matches: CliMatch[], isDryRun: boolean): string;
1
+ import type { AnalyzeResult, SgResult } from "./types";
2
+ export declare function formatSearchResult(result: SgResult): string;
3
+ export declare function formatReplaceResult(result: SgResult, isDryRun: boolean): string;
4
4
  export declare function formatAnalyzeResult(results: AnalyzeResult[], extractedMetaVars: boolean): string;
5
5
  export declare function formatTransformResult(original: string, transformed: string, editCount: number): string;
@@ -1,5 +1,8 @@
1
1
  import type { LSPServerConfig } from "./types";
2
2
  export declare const SYMBOL_KIND_MAP: Record<number, string>;
3
3
  export declare const SEVERITY_MAP: Record<number, string>;
4
+ export declare const DEFAULT_MAX_REFERENCES = 200;
5
+ export declare const DEFAULT_MAX_SYMBOLS = 200;
6
+ export declare const DEFAULT_MAX_DIAGNOSTICS = 200;
4
7
  export declare const BUILTIN_SERVERS: Record<string, Omit<LSPServerConfig, "id">>;
5
8
  export declare const EXT_TO_LANG: Record<string, string>;
@@ -1,5 +1,5 @@
1
1
  import { LSPClient } from "./client";
2
- import type { HoverResult, DocumentSymbol, SymbolInfo, Location, LocationLink, Diagnostic, PrepareRenameResult, PrepareRenameDefaultBehavior, WorkspaceEdit, TextEdit, CodeAction, Command } from "./types";
2
+ import type { HoverResult, DocumentSymbol, SymbolInfo, Location, LocationLink, Diagnostic, PrepareRenameResult, PrepareRenameDefaultBehavior, Range, WorkspaceEdit, TextEdit, CodeAction, Command } from "./types";
3
3
  export declare function findWorkspaceRoot(filePath: string): string;
4
4
  export declare function withLspClient<T>(filePath: string, fn: (client: LSPClient) => Promise<T>): Promise<T>;
5
5
  export declare function formatHoverResult(result: HoverResult | null): string;
@@ -10,7 +10,7 @@ export declare function formatDocumentSymbol(symbol: DocumentSymbol, indent?: nu
10
10
  export declare function formatSymbolInfo(symbol: SymbolInfo): string;
11
11
  export declare function formatDiagnostic(diag: Diagnostic): string;
12
12
  export declare function filterDiagnosticsBySeverity(diagnostics: Diagnostic[], severityFilter?: "error" | "warning" | "information" | "hint" | "all"): Diagnostic[];
13
- export declare function formatPrepareRenameResult(result: PrepareRenameResult | PrepareRenameDefaultBehavior | null): string;
13
+ export declare function formatPrepareRenameResult(result: PrepareRenameResult | PrepareRenameDefaultBehavior | Range | null): string;
14
14
  export declare function formatTextEdit(edit: TextEdit): string;
15
15
  export declare function formatWorkspaceEdit(edit: WorkspaceEdit | null): string;
16
16
  export declare function formatCodeAction(action: CodeAction): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-opencode",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "OpenCode plugin - custom agents (oracle, librarian) and enhanced features",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -46,6 +46,7 @@
46
46
  "@ast-grep/napi": "^0.40.0",
47
47
  "@code-yeongyu/comment-checker": "^0.4.1",
48
48
  "@opencode-ai/plugin": "^1.0.7",
49
+ "xdg-basedir": "^5.1.0",
49
50
  "zod": "^4.1.8"
50
51
  },
51
52
  "devDependencies": {