oh-my-opencode 0.1.24 → 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
  }
@@ -1710,14 +1777,14 @@ var EXT_TO_LANG = {
1710
1777
  ".yml": "yaml"
1711
1778
  };
1712
1779
  // src/tools/lsp/config.ts
1713
- import { existsSync as existsSync4, readFileSync } from "fs";
1714
- import { join as join3 } from "path";
1780
+ import { existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
1781
+ import { join as join4 } from "path";
1715
1782
  import { homedir as homedir2 } from "os";
1716
- function loadJsonFile(path) {
1717
- if (!existsSync4(path))
1783
+ function loadJsonFile(path2) {
1784
+ if (!existsSync5(path2))
1718
1785
  return null;
1719
1786
  try {
1720
- return JSON.parse(readFileSync(path, "utf-8"));
1787
+ return JSON.parse(readFileSync2(path2, "utf-8"));
1721
1788
  } catch {
1722
1789
  return null;
1723
1790
  }
@@ -1725,9 +1792,9 @@ function loadJsonFile(path) {
1725
1792
  function getConfigPaths() {
1726
1793
  const cwd = process.cwd();
1727
1794
  return {
1728
- project: join3(cwd, ".opencode", "oh-my-opencode.json"),
1729
- user: join3(homedir2(), ".config", "opencode", "oh-my-opencode.json"),
1730
- 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")
1731
1798
  };
1732
1799
  }
1733
1800
  function loadAllConfigs() {
@@ -1820,7 +1887,7 @@ function isServerInstalled(command) {
1820
1887
  const pathEnv = process.env.PATH || "";
1821
1888
  const paths = pathEnv.split(":");
1822
1889
  for (const p of paths) {
1823
- if (existsSync4(join3(p, cmd))) {
1890
+ if (existsSync5(join4(p, cmd))) {
1824
1891
  return true;
1825
1892
  }
1826
1893
  }
@@ -1870,7 +1937,7 @@ function getAllServers() {
1870
1937
  }
1871
1938
  // src/tools/lsp/client.ts
1872
1939
  var {spawn: spawn3 } = globalThis.Bun;
1873
- import { readFileSync as readFileSync2 } from "fs";
1940
+ import { readFileSync as readFileSync3 } from "fs";
1874
1941
  import { extname, resolve } from "path";
1875
1942
  class LSPServerManager {
1876
1943
  static instance;
@@ -2257,7 +2324,7 @@ ${msg}`);
2257
2324
  const absPath = resolve(filePath);
2258
2325
  if (this.openedFiles.has(absPath))
2259
2326
  return;
2260
- const text = readFileSync2(absPath, "utf-8");
2327
+ const text = readFileSync3(absPath, "utf-8");
2261
2328
  const ext = extname(absPath);
2262
2329
  const languageId = getLanguageId(ext);
2263
2330
  this.notify("textDocument/didOpen", {
@@ -2364,16 +2431,16 @@ ${msg}`);
2364
2431
  }
2365
2432
  // src/tools/lsp/utils.ts
2366
2433
  import { extname as extname2, resolve as resolve2 } from "path";
2367
- import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync } from "fs";
2434
+ import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
2368
2435
  function findWorkspaceRoot(filePath) {
2369
2436
  let dir = resolve2(filePath);
2370
- if (!existsSync5(dir) || !__require("fs").statSync(dir).isDirectory()) {
2437
+ if (!existsSync6(dir) || !__require("fs").statSync(dir).isDirectory()) {
2371
2438
  dir = __require("path").dirname(dir);
2372
2439
  }
2373
2440
  const markers = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "build.gradle"];
2374
2441
  while (dir !== "/") {
2375
2442
  for (const marker of markers) {
2376
- if (existsSync5(__require("path").join(dir, marker))) {
2443
+ if (existsSync6(__require("path").join(dir, marker))) {
2377
2444
  return dir;
2378
2445
  }
2379
2446
  }
@@ -2487,12 +2554,22 @@ function formatPrepareRenameResult(result) {
2487
2554
  if ("defaultBehavior" in result) {
2488
2555
  return result.defaultBehavior ? "Rename supported (using default behavior)" : "Cannot rename at this position";
2489
2556
  }
2490
- const startLine = result.range.start.line + 1;
2491
- const startChar = result.range.start.character;
2492
- const endLine = result.range.end.line + 1;
2493
- const endChar = result.range.end.character;
2494
- const placeholder = result.placeholder ? ` (current: "${result.placeholder}")` : "";
2495
- 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";
2496
2573
  }
2497
2574
  function formatCodeAction(action) {
2498
2575
  let result = `[${action.kind || "action"}] ${action.title}`;
@@ -2521,7 +2598,7 @@ function formatCodeActions(actions) {
2521
2598
  }
2522
2599
  function applyTextEditsToFile(filePath, edits) {
2523
2600
  try {
2524
- let content = readFileSync3(filePath, "utf-8");
2601
+ let content = readFileSync4(filePath, "utf-8");
2525
2602
  const lines = content.split(`
2526
2603
  `);
2527
2604
  const sortedEdits = [...edits].sort((a, b) => {
@@ -2546,7 +2623,7 @@ function applyTextEditsToFile(filePath, edits) {
2546
2623
  `));
2547
2624
  }
2548
2625
  }
2549
- writeFileSync(filePath, lines.join(`
2626
+ writeFileSync2(filePath, lines.join(`
2550
2627
  `), "utf-8");
2551
2628
  return { success: true, editCount: edits.length };
2552
2629
  } catch (err) {
@@ -2577,7 +2654,7 @@ function applyWorkspaceEdit(edit) {
2577
2654
  if (change.kind === "create") {
2578
2655
  try {
2579
2656
  const filePath = change.uri.replace("file://", "");
2580
- writeFileSync(filePath, "", "utf-8");
2657
+ writeFileSync2(filePath, "", "utf-8");
2581
2658
  result.filesModified.push(filePath);
2582
2659
  } catch (err) {
2583
2660
  result.success = false;
@@ -2587,8 +2664,8 @@ function applyWorkspaceEdit(edit) {
2587
2664
  try {
2588
2665
  const oldPath = change.oldUri.replace("file://", "");
2589
2666
  const newPath = change.newUri.replace("file://", "");
2590
- const content = readFileSync3(oldPath, "utf-8");
2591
- writeFileSync(newPath, content, "utf-8");
2667
+ const content = readFileSync4(oldPath, "utf-8");
2668
+ writeFileSync2(newPath, content, "utf-8");
2592
2669
  __require("fs").unlinkSync(oldPath);
2593
2670
  result.filesModified.push(newPath);
2594
2671
  } catch (err) {
@@ -3368,10 +3445,10 @@ function mergeDefs(...defs) {
3368
3445
  function cloneDef(schema) {
3369
3446
  return mergeDefs(schema._zod.def);
3370
3447
  }
3371
- function getElementAtPath(obj, path) {
3372
- if (!path)
3448
+ function getElementAtPath(obj, path2) {
3449
+ if (!path2)
3373
3450
  return obj;
3374
- return path.reduce((acc, key) => acc?.[key], obj);
3451
+ return path2.reduce((acc, key) => acc?.[key], obj);
3375
3452
  }
3376
3453
  function promiseAllObject(promisesObj) {
3377
3454
  const keys = Object.keys(promisesObj);
@@ -3730,11 +3807,11 @@ function aborted(x, startIndex = 0) {
3730
3807
  }
3731
3808
  return false;
3732
3809
  }
3733
- function prefixIssues(path, issues) {
3810
+ function prefixIssues(path2, issues) {
3734
3811
  return issues.map((iss) => {
3735
3812
  var _a;
3736
3813
  (_a = iss).path ?? (_a.path = []);
3737
- iss.path.unshift(path);
3814
+ iss.path.unshift(path2);
3738
3815
  return iss;
3739
3816
  });
3740
3817
  }
@@ -3902,7 +3979,7 @@ function treeifyError(error, _mapper) {
3902
3979
  return issue2.message;
3903
3980
  };
3904
3981
  const result = { errors: [] };
3905
- const processError = (error2, path = []) => {
3982
+ const processError = (error2, path2 = []) => {
3906
3983
  var _a, _b;
3907
3984
  for (const issue2 of error2.issues) {
3908
3985
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -3912,7 +3989,7 @@ function treeifyError(error, _mapper) {
3912
3989
  } else if (issue2.code === "invalid_element") {
3913
3990
  processError({ issues: issue2.issues }, issue2.path);
3914
3991
  } else {
3915
- const fullpath = [...path, ...issue2.path];
3992
+ const fullpath = [...path2, ...issue2.path];
3916
3993
  if (fullpath.length === 0) {
3917
3994
  result.errors.push(mapper(issue2));
3918
3995
  continue;
@@ -3944,8 +4021,8 @@ function treeifyError(error, _mapper) {
3944
4021
  }
3945
4022
  function toDotPath(_path) {
3946
4023
  const segs = [];
3947
- const path = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
3948
- for (const seg of path) {
4024
+ const path2 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
4025
+ for (const seg of path2) {
3949
4026
  if (typeof seg === "number")
3950
4027
  segs.push(`[${seg}]`);
3951
4028
  else if (typeof seg === "symbol")
@@ -15288,13 +15365,13 @@ var lsp_code_action_resolve = tool({
15288
15365
  });
15289
15366
  // src/tools/ast-grep/constants.ts
15290
15367
  import { createRequire as createRequire4 } from "module";
15291
- import { dirname as dirname2, join as join5 } from "path";
15292
- 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";
15293
15370
 
15294
15371
  // src/tools/ast-grep/downloader.ts
15295
15372
  var {spawn: spawn4 } = globalThis.Bun;
15296
- import { existsSync as existsSync6, mkdirSync as mkdirSync2, chmodSync as chmodSync2, unlinkSync as unlinkSync2 } from "fs";
15297
- 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";
15298
15375
  import { homedir as homedir3 } from "os";
15299
15376
  import { createRequire as createRequire3 } from "module";
15300
15377
  var REPO2 = "ast-grep/ast-grep";
@@ -15320,19 +15397,19 @@ var PLATFORM_MAP2 = {
15320
15397
  function getCacheDir2() {
15321
15398
  if (process.platform === "win32") {
15322
15399
  const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
15323
- const base2 = localAppData || join4(homedir3(), "AppData", "Local");
15324
- return join4(base2, "oh-my-opencode", "bin");
15400
+ const base2 = localAppData || join5(homedir3(), "AppData", "Local");
15401
+ return join5(base2, "oh-my-opencode", "bin");
15325
15402
  }
15326
- const xdgCache = process.env.XDG_CACHE_HOME;
15327
- const base = xdgCache || join4(homedir3(), ".cache");
15328
- 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");
15329
15406
  }
15330
15407
  function getBinaryName3() {
15331
15408
  return process.platform === "win32" ? "sg.exe" : "sg";
15332
15409
  }
15333
15410
  function getCachedBinaryPath2() {
15334
- const binaryPath = join4(getCacheDir2(), getBinaryName3());
15335
- return existsSync6(binaryPath) ? binaryPath : null;
15411
+ const binaryPath = join5(getCacheDir2(), getBinaryName3());
15412
+ return existsSync7(binaryPath) ? binaryPath : null;
15336
15413
  }
15337
15414
  async function extractZip2(archivePath, destDir) {
15338
15415
  const proc = process.platform === "win32" ? spawn4([
@@ -15358,30 +15435,30 @@ async function downloadAstGrep(version2 = DEFAULT_VERSION) {
15358
15435
  }
15359
15436
  const cacheDir = getCacheDir2();
15360
15437
  const binaryName = getBinaryName3();
15361
- const binaryPath = join4(cacheDir, binaryName);
15362
- if (existsSync6(binaryPath)) {
15438
+ const binaryPath = join5(cacheDir, binaryName);
15439
+ if (existsSync7(binaryPath)) {
15363
15440
  return binaryPath;
15364
15441
  }
15365
- const { arch, os } = platformInfo;
15366
- const assetName = `app-${arch}-${os}.zip`;
15442
+ const { arch, os: os2 } = platformInfo;
15443
+ const assetName = `app-${arch}-${os2}.zip`;
15367
15444
  const downloadUrl = `https://github.com/${REPO2}/releases/download/${version2}/${assetName}`;
15368
15445
  console.log(`[oh-my-opencode] Downloading ast-grep binary...`);
15369
15446
  try {
15370
- if (!existsSync6(cacheDir)) {
15371
- mkdirSync2(cacheDir, { recursive: true });
15447
+ if (!existsSync7(cacheDir)) {
15448
+ mkdirSync3(cacheDir, { recursive: true });
15372
15449
  }
15373
15450
  const response = await fetch(downloadUrl, { redirect: "follow" });
15374
15451
  if (!response.ok) {
15375
15452
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
15376
15453
  }
15377
- const archivePath = join4(cacheDir, assetName);
15454
+ const archivePath = join5(cacheDir, assetName);
15378
15455
  const arrayBuffer = await response.arrayBuffer();
15379
15456
  await Bun.write(archivePath, arrayBuffer);
15380
15457
  await extractZip2(archivePath, cacheDir);
15381
- if (existsSync6(archivePath)) {
15458
+ if (existsSync7(archivePath)) {
15382
15459
  unlinkSync2(archivePath);
15383
15460
  }
15384
- if (process.platform !== "win32" && existsSync6(binaryPath)) {
15461
+ if (process.platform !== "win32" && existsSync7(binaryPath)) {
15385
15462
  chmodSync2(binaryPath, 493);
15386
15463
  }
15387
15464
  console.log(`[oh-my-opencode] ast-grep binary ready.`);
@@ -15432,8 +15509,8 @@ function findSgCliPathSync() {
15432
15509
  const require2 = createRequire4(import.meta.url);
15433
15510
  const cliPkgPath = require2.resolve("@ast-grep/cli/package.json");
15434
15511
  const cliDir = dirname2(cliPkgPath);
15435
- const sgPath = join5(cliDir, binaryName);
15436
- if (existsSync7(sgPath) && isValidBinary(sgPath)) {
15512
+ const sgPath = join6(cliDir, binaryName);
15513
+ if (existsSync8(sgPath) && isValidBinary(sgPath)) {
15437
15514
  return sgPath;
15438
15515
  }
15439
15516
  } catch {}
@@ -15444,17 +15521,17 @@ function findSgCliPathSync() {
15444
15521
  const pkgPath = require2.resolve(`${platformPkg}/package.json`);
15445
15522
  const pkgDir = dirname2(pkgPath);
15446
15523
  const astGrepName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep";
15447
- const binaryPath = join5(pkgDir, astGrepName);
15448
- if (existsSync7(binaryPath) && isValidBinary(binaryPath)) {
15524
+ const binaryPath = join6(pkgDir, astGrepName);
15525
+ if (existsSync8(binaryPath) && isValidBinary(binaryPath)) {
15449
15526
  return binaryPath;
15450
15527
  }
15451
15528
  } catch {}
15452
15529
  }
15453
15530
  if (process.platform === "darwin") {
15454
15531
  const homebrewPaths = ["/opt/homebrew/bin/sg", "/usr/local/bin/sg"];
15455
- for (const path of homebrewPaths) {
15456
- if (existsSync7(path) && isValidBinary(path)) {
15457
- return path;
15532
+ for (const path2 of homebrewPaths) {
15533
+ if (existsSync8(path2) && isValidBinary(path2)) {
15534
+ return path2;
15458
15535
  }
15459
15536
  }
15460
15537
  }
@@ -15472,8 +15549,8 @@ function getSgCliPath() {
15472
15549
  }
15473
15550
  return "sg";
15474
15551
  }
15475
- function setSgCliPath(path) {
15476
- resolvedCliPath2 = path;
15552
+ function setSgCliPath(path2) {
15553
+ resolvedCliPath2 = path2;
15477
15554
  }
15478
15555
  var SG_CLI_PATH = getSgCliPath();
15479
15556
  var CLI_LANGUAGES = [
@@ -15537,11 +15614,11 @@ var LANG_EXTENSIONS = {
15537
15614
 
15538
15615
  // src/tools/ast-grep/cli.ts
15539
15616
  var {spawn: spawn5 } = globalThis.Bun;
15540
- import { existsSync as existsSync8 } from "fs";
15617
+ import { existsSync as existsSync9 } from "fs";
15541
15618
  var resolvedCliPath3 = null;
15542
15619
  var initPromise2 = null;
15543
15620
  async function getAstGrepPath() {
15544
- if (resolvedCliPath3 !== null && existsSync8(resolvedCliPath3)) {
15621
+ if (resolvedCliPath3 !== null && existsSync9(resolvedCliPath3)) {
15545
15622
  return resolvedCliPath3;
15546
15623
  }
15547
15624
  if (initPromise2) {
@@ -15549,7 +15626,7 @@ async function getAstGrepPath() {
15549
15626
  }
15550
15627
  initPromise2 = (async () => {
15551
15628
  const syncPath = findSgCliPathSync();
15552
- if (syncPath && existsSync8(syncPath)) {
15629
+ if (syncPath && existsSync9(syncPath)) {
15553
15630
  resolvedCliPath3 = syncPath;
15554
15631
  setSgCliPath(syncPath);
15555
15632
  return syncPath;
@@ -15583,7 +15660,7 @@ async function runSg(options) {
15583
15660
  const paths = options.paths && options.paths.length > 0 ? options.paths : ["."];
15584
15661
  args.push(...paths);
15585
15662
  let cliPath = getSgCliPath();
15586
- if (!existsSync8(cliPath) && cliPath !== "sg") {
15663
+ if (!existsSync9(cliPath) && cliPath !== "sg") {
15587
15664
  const downloadedPath = await getAstGrepPath();
15588
15665
  if (downloadedPath) {
15589
15666
  cliPath = downloadedPath;
@@ -16028,8 +16105,8 @@ var ast_grep_transform = tool({
16028
16105
  var {spawn: spawn6 } = globalThis.Bun;
16029
16106
 
16030
16107
  // src/tools/safe-grep/constants.ts
16031
- import { existsSync as existsSync9 } from "fs";
16032
- 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";
16033
16110
  import { spawnSync } from "child_process";
16034
16111
  var cachedCli = null;
16035
16112
  function findExecutable(name) {
@@ -16050,13 +16127,13 @@ function getOpenCodeBundledRg() {
16050
16127
  const isWindows = process.platform === "win32";
16051
16128
  const rgName = isWindows ? "rg.exe" : "rg";
16052
16129
  const candidates = [
16053
- join6(execDir, rgName),
16054
- join6(execDir, "bin", rgName),
16055
- join6(execDir, "..", "bin", rgName),
16056
- join6(execDir, "..", "libexec", rgName)
16130
+ join7(execDir, rgName),
16131
+ join7(execDir, "bin", rgName),
16132
+ join7(execDir, "..", "bin", rgName),
16133
+ join7(execDir, "..", "libexec", rgName)
16057
16134
  ];
16058
16135
  for (const candidate of candidates) {
16059
- if (existsSync9(candidate)) {
16136
+ if (existsSync10(candidate)) {
16060
16137
  return candidate;
16061
16138
  }
16062
16139
  }
@@ -16389,11 +16466,11 @@ var OhMyOpenCodeConfigSchema = exports_external.object({
16389
16466
  });
16390
16467
  // src/index.ts
16391
16468
  import * as fs3 from "fs";
16392
- import * as path from "path";
16469
+ import * as path2 from "path";
16393
16470
  function loadPluginConfig(directory) {
16394
16471
  const configPaths = [
16395
- path.join(directory, "oh-my-opencode.json"),
16396
- path.join(directory, ".oh-my-opencode.json")
16472
+ path2.join(directory, "oh-my-opencode.json"),
16473
+ path2.join(directory, ".oh-my-opencode.json")
16397
16474
  ];
16398
16475
  for (const configPath of configPaths) {
16399
16476
  try {
@@ -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.24",
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": {