oh-my-opencode 2.12.0 → 2.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ja.md CHANGED
@@ -4,7 +4,7 @@
4
4
  >
5
5
  > 一緒に歩みましょう!
6
6
  >
7
- > | [<img alt="Discord link" src="https://img.shields.io/discord/1452487457085063218?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square" width="156px" />](https://discord.gg/PWpXmbhF) | [Discordコミュニティ](https://discord.gg/PWpXmbhF)に参加して、コントリビューターや`oh-my-opencode`仲間とつながりましょう。 |
7
+ > | [<img alt="Discord link" src="https://img.shields.io/discord/1452487457085063218?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square" width="156px" />](https://discord.gg/aSfGzWtYxM) | [Discordコミュニティ](https://discord.gg/aSfGzWtYxM)に参加して、コントリビューターや`oh-my-opencode`仲間とつながりましょう。 |
8
8
  > | :-----| :----- |
9
9
  > | [<img alt="X link" src="https://img.shields.io/badge/Follow-%40justsisyphus-00CED1?style=flat-square&logo=x&labelColor=black" width="156px" />](https://x.com/justsisyphus) | `oh-my-opencode`に関するニュースは私のXアカウントで投稿していましたが、無実の罪で凍結されたため、<br />[@justsisyphus](https://x.com/justsisyphus)が代わりに更新を投稿しています。 |
10
10
  > | [<img alt="Sponsor" src="https://img.shields.io/badge/Sponsor-❤-ff69b4?style=flat-square&logo=github-sponsors&labelColor=black" width="156px" />](https://github.com/sponsors/code-yeongyu) | [スポンサーになって](https://github.com/sponsors/code-yeongyu) `oh-my-opencode` の開発を応援してください。皆さまのご支援がこのプロジェクトを成長させます。 |
package/README.ko.md CHANGED
@@ -4,7 +4,7 @@
4
4
  >
5
5
  > 함께해주세요!
6
6
  >
7
- > | [<img alt="Discord link" src="https://img.shields.io/discord/1452487457085063218?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square" width="156px" />](https://discord.gg/PWpXmbhF) | [Discord 커뮤니티](https://discord.gg/PWpXmbhF)에서 기여자들과 `oh-my-opencode` 사용자들을 만나보세요. |
7
+ > | [<img alt="Discord link" src="https://img.shields.io/discord/1452487457085063218?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square" width="156px" />](https://discord.gg/aSfGzWtYxM) | [Discord 커뮤니티](https://discord.gg/aSfGzWtYxM)에서 기여자들과 `oh-my-opencode` 사용자들을 만나보세요. |
8
8
  > | :-----| :----- |
9
9
  > | [<img alt="X link" src="https://img.shields.io/badge/Follow-%40justsisyphus-00CED1?style=flat-square&logo=x&labelColor=black" width="156px" />](https://x.com/justsisyphus) | `oh-my-opencode` 관련 소식은 제 X 계정에서 올렸었는데, 억울하게 정지당해서 <br />[@justsisyphus](https://x.com/justsisyphus)가 대신 소식을 전하고 있습니다. |
10
10
  > | [<img alt="Sponsor" src="https://img.shields.io/badge/Sponsor-❤-ff69b4?style=flat-square&logo=github-sponsors&labelColor=black" width="156px" />](https://github.com/sponsors/code-yeongyu) | [스폰서가 되어](https://github.com/sponsors/code-yeongyu) `oh-my-opencode` 개발을 응원해주세요. 여러분의 후원이 이 프로젝트를 계속 성장시킵니다. |
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  >
8
8
  > Be with us!
9
9
  >
10
- > | [<img alt="Discord link" src="https://img.shields.io/discord/1452487457085063218?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square" width="156px" />](https://discord.gg/PWpXmbhF) | Join our [Discord community](https://discord.gg/PWpXmbhF) to connect with contributors and fellow `oh-my-opencode` users. |
10
+ > | [<img alt="Discord link" src="https://img.shields.io/discord/1452487457085063218?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square" width="156px" />](https://discord.gg/aSfGzWtYxM) | Join our [Discord community](https://discord.gg/aSfGzWtYxM) to connect with contributors and fellow `oh-my-opencode` users. |
11
11
  > | :-----| :----- |
12
12
  > | [<img alt="X link" src="https://img.shields.io/badge/Follow-%40justsisyphus-00CED1?style=flat-square&logo=x&labelColor=black" width="156px" />](https://x.com/justsisyphus) | News and updates for `oh-my-opencode` used to be posted on my X account. <br /> Since it was suspended mistakenly, [@justsisyphus](https://x.com/justsisyphus) now posts updates on my behalf. |
13
13
  > | [<img alt="Sponsor" src="https://img.shields.io/badge/Sponsor-❤-ff69b4?style=flat-square&logo=github-sponsors&labelColor=black" width="156px" />](https://github.com/sponsors/code-yeongyu) | Support the development of `oh-my-opencode` by [becoming a sponsor](https://github.com/sponsors/code-yeongyu). Your contribution helps keep this project alive and growing. |
package/README.zh-cn.md CHANGED
@@ -4,7 +4,7 @@
4
4
  >
5
5
  > 与我们同行!
6
6
  >
7
- > | [<img alt="Discord link" src="https://img.shields.io/discord/1452487457085063218?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square" width="156px" />](https://discord.gg/PWpXmbhF) | 加入我们的 [Discord 社区](https://discord.gg/PWpXmbhF),和贡献者们、`oh-my-opencode` 用户们一起交流。 |
7
+ > | [<img alt="Discord link" src="https://img.shields.io/discord/1452487457085063218?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square" width="156px" />](https://discord.gg/aSfGzWtYxM) | 加入我们的 [Discord 社区](https://discord.gg/aSfGzWtYxM),和贡献者们、`oh-my-opencode` 用户们一起交流。 |
8
8
  > | :-----| :----- |
9
9
  > | [<img alt="X link" src="https://img.shields.io/badge/Follow-%40justsisyphus-00CED1?style=flat-square&logo=x&labelColor=black" width="156px" />](https://x.com/justsisyphus) | `oh-my-opencode` 的消息之前在我的 X 账号发,但账号被无辜封了,<br />现在 [@justsisyphus](https://x.com/justsisyphus) 替我发更新。 |
10
10
  > | [<img alt="Sponsor" src="https://img.shields.io/badge/Sponsor-❤-ff69b4?style=flat-square&logo=github-sponsors&labelColor=black" width="156px" />](https://github.com/sponsors/code-yeongyu) | [成为赞助者](https://github.com/sponsors/code-yeongyu),支持 `oh-my-opencode` 的开发。您的支持让这个项目持续成长。 |
package/dist/cli/index.js CHANGED
@@ -2657,7 +2657,7 @@ var require_napi = __commonJS((exports, module) => {
2657
2657
  var require_package = __commonJS((exports, module) => {
2658
2658
  module.exports = {
2659
2659
  name: "oh-my-opencode",
2660
- version: "2.11.0",
2660
+ version: "2.12.1",
2661
2661
  description: "OpenCode plugin - custom agents (oracle, librarian) and enhanced features",
2662
2662
  main: "dist/index.js",
2663
2663
  types: "dist/index.d.ts",
@@ -22575,7 +22575,8 @@ var HookNameSchema = exports_external.enum([
22575
22575
  "preemptive-compaction",
22576
22576
  "compaction-context-injector",
22577
22577
  "claude-code-hooks",
22578
- "auto-slash-command"
22578
+ "auto-slash-command",
22579
+ "edit-error-recovery"
22579
22580
  ]);
22580
22581
  var BuiltinCommandNameSchema = exports_external.enum([
22581
22582
  "init-deep"
@@ -61,6 +61,7 @@ export declare const HookNameSchema: z.ZodEnum<{
61
61
  "compaction-context-injector": "compaction-context-injector";
62
62
  "claude-code-hooks": "claude-code-hooks";
63
63
  "auto-slash-command": "auto-slash-command";
64
+ "edit-error-recovery": "edit-error-recovery";
64
65
  }>;
65
66
  export declare const BuiltinCommandNameSchema: z.ZodEnum<{
66
67
  "init-deep": "init-deep";
@@ -816,6 +817,7 @@ export declare const OhMyOpenCodeConfigSchema: z.ZodObject<{
816
817
  "compaction-context-injector": "compaction-context-injector";
817
818
  "claude-code-hooks": "claude-code-hooks";
818
819
  "auto-slash-command": "auto-slash-command";
820
+ "edit-error-recovery": "edit-error-recovery";
819
821
  }>>>;
820
822
  disabled_commands: z.ZodOptional<z.ZodArray<z.ZodEnum<{
821
823
  "init-deep": "init-deep";
@@ -35,6 +35,7 @@ export declare class BackgroundManager {
35
35
  private notifyParentSession;
36
36
  private formatDuration;
37
37
  private hasRunningTasks;
38
+ private pruneStaleTasksAndNotifications;
38
39
  private pollRunningTasks;
39
40
  }
40
41
  export {};
@@ -0,0 +1,31 @@
1
+ import type { PluginInput } from "@opencode-ai/plugin";
2
+ /**
3
+ * Known Edit tool error patterns that indicate the AI made a mistake
4
+ */
5
+ export declare const EDIT_ERROR_PATTERNS: readonly ["oldString and newString must be different", "oldString not found", "oldString found multiple times"];
6
+ /**
7
+ * System reminder injected when Edit tool fails due to AI mistake
8
+ * Short, direct, and commanding - forces immediate corrective action
9
+ */
10
+ export declare const EDIT_ERROR_REMINDER = "\n[EDIT ERROR - IMMEDIATE ACTION REQUIRED]\n\nYou made an Edit mistake. STOP and do this NOW:\n\n1. READ the file immediately to see its ACTUAL current state\n2. VERIFY what the content really looks like (your assumption was wrong)\n3. APOLOGIZE briefly to the user for the error\n4. CONTINUE with corrected action based on the real file content\n\nDO NOT attempt another edit until you've read and verified the file state.\n";
11
+ /**
12
+ * Detects Edit tool errors caused by AI mistakes and injects a recovery reminder
13
+ *
14
+ * This hook catches common Edit tool failures:
15
+ * - oldString and newString must be different (trying to "edit" to same content)
16
+ * - oldString not found (wrong assumption about file content)
17
+ * - oldString found multiple times (ambiguous match, need more context)
18
+ *
19
+ * @see https://github.com/sst/opencode/issues/4718
20
+ */
21
+ export declare function createEditErrorRecoveryHook(_ctx: PluginInput): {
22
+ "tool.execute.after": (input: {
23
+ tool: string;
24
+ sessionID: string;
25
+ callID: string;
26
+ }, output: {
27
+ title: string;
28
+ output: string;
29
+ metadata: unknown;
30
+ }) => Promise<void>;
31
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -23,3 +23,4 @@ export { createEmptyMessageSanitizerHook } from "./empty-message-sanitizer";
23
23
  export { createThinkingBlockValidatorHook } from "./thinking-block-validator";
24
24
  export { createRalphLoopHook, type RalphLoopHook } from "./ralph-loop";
25
25
  export { createAutoSlashCommandHook } from "./auto-slash-command";
26
+ export { createEditErrorRecoveryHook } from "./edit-error-recovery";
@@ -0,0 +1 @@
1
+ export {};
@@ -11,4 +11,5 @@ export interface RalphLoopState {
11
11
  export interface RalphLoopOptions {
12
12
  config?: RalphLoopConfig;
13
13
  getTranscriptPath?: (sessionId: string) => string;
14
+ apiTimeout?: number;
14
15
  }
@@ -3,6 +3,7 @@ import type { ExperimentalConfig } from "../../config";
3
3
  export interface SessionRecoveryOptions {
4
4
  experimental?: ExperimentalConfig;
5
5
  }
6
+ type RecoveryErrorType = "tool_result_missing" | "thinking_block_order" | "thinking_disabled_violation" | null;
6
7
  interface MessageInfo {
7
8
  id?: string;
8
9
  role?: string;
@@ -10,6 +11,7 @@ interface MessageInfo {
10
11
  parentID?: string;
11
12
  error?: unknown;
12
13
  }
14
+ export declare function detectErrorType(error: unknown): RecoveryErrorType;
13
15
  export interface SessionRecoveryHook {
14
16
  handleSessionRecovery: (info: MessageInfo) => Promise<boolean>;
15
17
  isRecoverableError: (error: unknown) => boolean;
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js CHANGED
@@ -8557,6 +8557,7 @@ Incomplete tasks remain in your todo list. Continue working on the next pending
8557
8557
  - Do not stop until all tasks are done`;
8558
8558
  var COUNTDOWN_SECONDS = 2;
8559
8559
  var TOAST_DURATION_MS = 900;
8560
+ var COUNTDOWN_GRACE_PERIOD_MS = 500;
8560
8561
  function getMessageDir(sessionID) {
8561
8562
  if (!existsSync2(MESSAGE_STORAGE))
8562
8563
  return null;
@@ -8616,6 +8617,7 @@ function createTodoContinuationEnforcer(ctx, options = {}) {
8616
8617
  clearInterval(state2.countdownInterval);
8617
8618
  state2.countdownInterval = undefined;
8618
8619
  }
8620
+ state2.countdownStartedAt = undefined;
8619
8621
  }
8620
8622
  function cleanup(sessionID) {
8621
8623
  cancelCountdown(sessionID);
@@ -8705,6 +8707,7 @@ function createTodoContinuationEnforcer(ctx, options = {}) {
8705
8707
  cancelCountdown(sessionID);
8706
8708
  let secondsRemaining = COUNTDOWN_SECONDS;
8707
8709
  showCountdownToast(secondsRemaining, incompleteCount);
8710
+ state2.countdownStartedAt = Date.now();
8708
8711
  state2.countdownInterval = setInterval(() => {
8709
8712
  secondsRemaining--;
8710
8713
  if (secondsRemaining > 0) {
@@ -8788,6 +8791,13 @@ function createTodoContinuationEnforcer(ctx, options = {}) {
8788
8791
  state2.lastEventWasAbortError = false;
8789
8792
  }
8790
8793
  if (role === "user") {
8794
+ if (state2?.countdownStartedAt) {
8795
+ const elapsed = Date.now() - state2.countdownStartedAt;
8796
+ if (elapsed < COUNTDOWN_GRACE_PERIOD_MS) {
8797
+ log(`[${HOOK_NAME}] Ignoring user message in grace period`, { sessionID, elapsed });
8798
+ return;
8799
+ }
8800
+ }
8791
8801
  cancelCountdown(sessionID);
8792
8802
  log(`[${HOOK_NAME}] User message: cleared abort state`, { sessionID });
8793
8803
  }
@@ -9640,7 +9650,7 @@ function detectErrorType(error) {
9640
9650
  if (message.includes("tool_use") && message.includes("tool_result")) {
9641
9651
  return "tool_result_missing";
9642
9652
  }
9643
- if (message.includes("thinking") && (message.includes("first block") || message.includes("must start with") || message.includes("preceeding") || message.includes("expected") && message.includes("found"))) {
9653
+ if (message.includes("thinking") && (message.includes("first block") || message.includes("must start with") || message.includes("preceeding") || message.includes("final block") || message.includes("cannot be thinking") || message.includes("expected") && message.includes("found"))) {
9644
9654
  return "thinking_block_order";
9645
9655
  }
9646
9656
  if (message.includes("thinking is disabled") && message.includes("cannot contain")) {
@@ -11844,7 +11854,7 @@ async function executeCompact(sessionID, msg, autoCompactState, client, director
11844
11854
  setTimeout(async () => {
11845
11855
  try {
11846
11856
  await client.session.prompt_async({
11847
- path: { sessionID },
11857
+ path: { id: sessionID },
11848
11858
  body: { parts: [{ type: "text", text: "Continue" }] },
11849
11859
  query: { directory }
11850
11860
  });
@@ -11910,7 +11920,7 @@ async function executeCompact(sessionID, msg, autoCompactState, client, director
11910
11920
  setTimeout(async () => {
11911
11921
  try {
11912
11922
  await client.session.prompt_async({
11913
- path: { sessionID },
11923
+ path: { id: sessionID },
11914
11924
  body: { parts: [{ type: "text", text: "Continue" }] },
11915
11925
  query: { directory }
11916
11926
  });
@@ -19039,6 +19049,17 @@ function detectBannedCommand(command) {
19039
19049
  }
19040
19050
  return;
19041
19051
  }
19052
+ function shellEscape(value) {
19053
+ if (value === "")
19054
+ return "''";
19055
+ if (/[^a-zA-Z0-9_\-.:\/]/.test(value)) {
19056
+ return `'${value.replace(/'/g, "'\\''")}'`;
19057
+ }
19058
+ return value;
19059
+ }
19060
+ function buildEnvPrefix(env) {
19061
+ return Object.entries(env).map(([key, value]) => `${key}=${shellEscape(value)}`).join(" \\\n");
19062
+ }
19042
19063
  function createNonInteractiveEnvHook(_ctx) {
19043
19064
  return {
19044
19065
  "tool.execute.before": async (input, output) => {
@@ -19049,18 +19070,19 @@ function createNonInteractiveEnvHook(_ctx) {
19049
19070
  if (!command) {
19050
19071
  return;
19051
19072
  }
19052
- output.args.env = {
19053
- ...process.env,
19054
- ...output.args.env,
19055
- ...NON_INTERACTIVE_ENV
19056
- };
19057
19073
  const bannedCmd = detectBannedCommand(command);
19058
19074
  if (bannedCmd) {
19059
19075
  output.message = `\u26A0\uFE0F Warning: '${bannedCmd}' is an interactive command that may hang in non-interactive environments.`;
19060
19076
  }
19061
- log(`[${HOOK_NAME2}] Set non-interactive environment variables`, {
19077
+ const isGitCommand = /\bgit\b/.test(command);
19078
+ if (!isGitCommand) {
19079
+ return;
19080
+ }
19081
+ const envPrefix = buildEnvPrefix(NON_INTERACTIVE_ENV);
19082
+ output.args.command = `${envPrefix} ${command}`;
19083
+ log(`[${HOOK_NAME2}] Prepended non-interactive env vars to git command`, {
19062
19084
  sessionID: input.sessionID,
19063
- env: NON_INTERACTIVE_ENV
19085
+ envPrefix
19064
19086
  });
19065
19087
  }
19066
19088
  };
@@ -19563,11 +19585,13 @@ IMPORTANT:
19563
19585
 
19564
19586
  Original task:
19565
19587
  {{PROMPT}}`;
19588
+ var DEFAULT_API_TIMEOUT = 3000;
19566
19589
  function createRalphLoopHook(ctx, options) {
19567
19590
  const sessions = new Map;
19568
19591
  const config = options?.config;
19569
19592
  const stateDir = config?.state_dir;
19570
19593
  const getTranscriptPath2 = options?.getTranscriptPath ?? getTranscriptPath;
19594
+ const apiTimeout = options?.apiTimeout ?? DEFAULT_API_TIMEOUT;
19571
19595
  function getSessionState(sessionID) {
19572
19596
  let state2 = sessions.get(sessionID);
19573
19597
  if (!state2) {
@@ -19594,28 +19618,26 @@ function createRalphLoopHook(ctx, options) {
19594
19618
  }
19595
19619
  async function detectCompletionInSessionMessages(sessionID, promise) {
19596
19620
  try {
19597
- const response = await ctx.client.session.messages({
19598
- path: { id: sessionID },
19599
- query: { directory: ctx.directory }
19600
- });
19621
+ const response = await Promise.race([
19622
+ ctx.client.session.messages({
19623
+ path: { id: sessionID },
19624
+ query: { directory: ctx.directory }
19625
+ }),
19626
+ new Promise((_, reject) => setTimeout(() => reject(new Error("API timeout")), apiTimeout))
19627
+ ]);
19601
19628
  const messages = response.data ?? [];
19602
19629
  if (!Array.isArray(messages))
19603
19630
  return false;
19631
+ const assistantMessages = messages.filter((msg) => msg.info?.role === "assistant");
19632
+ const lastAssistant = assistantMessages[assistantMessages.length - 1];
19633
+ if (!lastAssistant?.parts)
19634
+ return false;
19604
19635
  const pattern = new RegExp(`<promise>\\s*${escapeRegex(promise)}\\s*</promise>`, "is");
19605
- for (const msg of messages) {
19606
- if (msg.info?.role !== "assistant")
19607
- continue;
19608
- for (const part of msg.parts || []) {
19609
- if (part.type === "text" && part.text) {
19610
- if (pattern.test(part.text)) {
19611
- return true;
19612
- }
19613
- }
19614
- }
19615
- }
19616
- return false;
19636
+ const responseText = lastAssistant.parts.filter((p) => p.type === "text").map((p) => p.text ?? "").join(`
19637
+ `);
19638
+ return pattern.test(responseText);
19617
19639
  } catch (err) {
19618
- log(`[${HOOK_NAME3}] Failed to fetch session messages`, { sessionID, error: String(err) });
19640
+ log(`[${HOOK_NAME3}] Session messages check failed`, { sessionID, error: String(err) });
19619
19641
  return false;
19620
19642
  }
19621
19643
  }
@@ -19673,15 +19695,15 @@ function createRalphLoopHook(ctx, options) {
19673
19695
  if (state2.session_id && state2.session_id !== sessionID) {
19674
19696
  return;
19675
19697
  }
19676
- const completionDetectedViaApi = await detectCompletionInSessionMessages(sessionID, state2.completion_promise);
19677
19698
  const transcriptPath = getTranscriptPath2(sessionID);
19678
19699
  const completionDetectedViaTranscript = detectCompletionPromise(transcriptPath, state2.completion_promise);
19679
- if (completionDetectedViaApi || completionDetectedViaTranscript) {
19700
+ const completionDetectedViaApi = completionDetectedViaTranscript ? false : await detectCompletionInSessionMessages(sessionID, state2.completion_promise);
19701
+ if (completionDetectedViaTranscript || completionDetectedViaApi) {
19680
19702
  log(`[${HOOK_NAME3}] Completion detected!`, {
19681
19703
  sessionID,
19682
19704
  iteration: state2.iteration,
19683
19705
  promise: state2.completion_promise,
19684
- detectedVia: completionDetectedViaApi ? "session_messages_api" : "transcript_file"
19706
+ detectedVia: completionDetectedViaTranscript ? "transcript_file" : "session_messages_api"
19685
19707
  });
19686
19708
  clearState(ctx.directory, stateDir);
19687
19709
  await ctx.client.tui.showToast({
@@ -19758,6 +19780,18 @@ function createRalphLoopHook(ctx, options) {
19758
19780
  }
19759
19781
  if (event2.type === "session.error") {
19760
19782
  const sessionID = props?.sessionID;
19783
+ const error = props?.error;
19784
+ if (error?.name === "MessageAbortedError") {
19785
+ if (sessionID) {
19786
+ const state2 = readState(ctx.directory, stateDir);
19787
+ if (state2?.session_id === sessionID) {
19788
+ clearState(ctx.directory, stateDir);
19789
+ log(`[${HOOK_NAME3}] User aborted, loop cleared`, { sessionID });
19790
+ }
19791
+ sessions.delete(sessionID);
19792
+ }
19793
+ return;
19794
+ }
19761
19795
  if (sessionID) {
19762
19796
  const sessionState = getSessionState(sessionID);
19763
19797
  sessionState.isRecovering = true;
@@ -19828,7 +19862,7 @@ function extractPromptText3(parts) {
19828
19862
 
19829
19863
  // src/hooks/auto-slash-command/executor.ts
19830
19864
  import { existsSync as existsSync35, readdirSync as readdirSync12, readFileSync as readFileSync24 } from "fs";
19831
- import { join as join43, basename as basename2, dirname as dirname9 } from "path";
19865
+ import { join as join43, basename as basename2, dirname as dirname8 } from "path";
19832
19866
  import { homedir as homedir13 } from "os";
19833
19867
  // src/features/opencode-skill-loader/loader.ts
19834
19868
  import { existsSync as existsSync33, readdirSync as readdirSync11, readFileSync as readFileSync22 } from "fs";
@@ -20030,7 +20064,7 @@ function discoverOpencodeProjectSkills() {
20030
20064
  }
20031
20065
  // src/features/opencode-skill-loader/merger.ts
20032
20066
  import { readFileSync as readFileSync23, existsSync as existsSync34 } from "fs";
20033
- import { dirname as dirname8, resolve as resolve5, isAbsolute as isAbsolute2 } from "path";
20067
+ import { dirname as dirname7, resolve as resolve5, isAbsolute as isAbsolute2 } from "path";
20034
20068
  import { homedir as homedir12 } from "os";
20035
20069
  var SCOPE_PRIORITY = {
20036
20070
  builtin: 1,
@@ -20103,7 +20137,7 @@ function configEntryToLoaded(name, entry, configDir) {
20103
20137
  return null;
20104
20138
  }
20105
20139
  const description = entry.description || fileMetadata.description || "";
20106
- const resolvedPath = entry.from ? dirname8(resolveFilePath2(entry.from, configDir)) : configDir || process.cwd();
20140
+ const resolvedPath = entry.from ? dirname7(resolveFilePath2(entry.from, configDir)) : configDir || process.cwd();
20107
20141
  const wrappedTemplate = `<skill-instruction>
20108
20142
  Base directory for this skill: ${resolvedPath}/
20109
20143
  File references (@path) in this skill are relative to this directory.
@@ -20333,7 +20367,7 @@ async function formatCommandTemplate(cmd, args) {
20333
20367
  `);
20334
20368
  sections.push(`## Command Instructions
20335
20369
  `);
20336
- const commandDir = cmd.path ? dirname9(cmd.path) : process.cwd();
20370
+ const commandDir = cmd.path ? dirname8(cmd.path) : process.cwd();
20337
20371
  const withFileRefs = await resolveFileReferencesInText(cmd.content || "", commandDir);
20338
20372
  const resolvedContent = await resolveCommandsInText(withFileRefs);
20339
20373
  sections.push(resolvedContent.trim());
@@ -20424,6 +20458,38 @@ ${AUTO_SLASH_COMMAND_TAG_CLOSE}`;
20424
20458
  }
20425
20459
  };
20426
20460
  }
20461
+ // src/hooks/edit-error-recovery/index.ts
20462
+ var EDIT_ERROR_PATTERNS = [
20463
+ "oldString and newString must be different",
20464
+ "oldString not found",
20465
+ "oldString found multiple times"
20466
+ ];
20467
+ var EDIT_ERROR_REMINDER = `
20468
+ [EDIT ERROR - IMMEDIATE ACTION REQUIRED]
20469
+
20470
+ You made an Edit mistake. STOP and do this NOW:
20471
+
20472
+ 1. READ the file immediately to see its ACTUAL current state
20473
+ 2. VERIFY what the content really looks like (your assumption was wrong)
20474
+ 3. APOLOGIZE briefly to the user for the error
20475
+ 4. CONTINUE with corrected action based on the real file content
20476
+
20477
+ DO NOT attempt another edit until you've read and verified the file state.
20478
+ `;
20479
+ function createEditErrorRecoveryHook(_ctx) {
20480
+ return {
20481
+ "tool.execute.after": async (input, output) => {
20482
+ if (input.tool.toLowerCase() !== "edit")
20483
+ return;
20484
+ const outputLower = output.output.toLowerCase();
20485
+ const hasEditError = EDIT_ERROR_PATTERNS.some((pattern) => outputLower.includes(pattern.toLowerCase()));
20486
+ if (hasEditError) {
20487
+ output.output += `
20488
+ ${EDIT_ERROR_REMINDER}`;
20489
+ }
20490
+ }
20491
+ };
20492
+ }
20427
20493
  // src/auth/antigravity/constants.ts
20428
20494
  var ANTIGRAVITY_CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com";
20429
20495
  var ANTIGRAVITY_CLIENT_SECRET = "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";
@@ -36458,7 +36524,7 @@ var lsp_code_action_resolve = tool({
36458
36524
  });
36459
36525
  // src/tools/ast-grep/constants.ts
36460
36526
  import { createRequire as createRequire4 } from "module";
36461
- import { dirname as dirname10, join as join47 } from "path";
36527
+ import { dirname as dirname9, join as join47 } from "path";
36462
36528
  import { existsSync as existsSync40, statSync as statSync4 } from "fs";
36463
36529
 
36464
36530
  // src/tools/ast-grep/downloader.ts
@@ -36601,7 +36667,7 @@ function findSgCliPathSync() {
36601
36667
  try {
36602
36668
  const require2 = createRequire4(import.meta.url);
36603
36669
  const cliPkgPath = require2.resolve("@ast-grep/cli/package.json");
36604
- const cliDir = dirname10(cliPkgPath);
36670
+ const cliDir = dirname9(cliPkgPath);
36605
36671
  const sgPath = join47(cliDir, binaryName);
36606
36672
  if (existsSync40(sgPath) && isValidBinary(sgPath)) {
36607
36673
  return sgPath;
@@ -36612,7 +36678,7 @@ function findSgCliPathSync() {
36612
36678
  try {
36613
36679
  const require2 = createRequire4(import.meta.url);
36614
36680
  const pkgPath = require2.resolve(`${platformPkg}/package.json`);
36615
- const pkgDir = dirname10(pkgPath);
36681
+ const pkgDir = dirname9(pkgPath);
36616
36682
  const astGrepName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep";
36617
36683
  const binaryPath = join47(pkgDir, astGrepName);
36618
36684
  if (existsSync40(binaryPath) && isValidBinary(binaryPath)) {
@@ -36989,7 +37055,7 @@ var {spawn: spawn9 } = globalThis.Bun;
36989
37055
 
36990
37056
  // src/tools/grep/constants.ts
36991
37057
  import { existsSync as existsSync43 } from "fs";
36992
- import { join as join49, dirname as dirname11 } from "path";
37058
+ import { join as join49, dirname as dirname10 } from "path";
36993
37059
  import { spawnSync } from "child_process";
36994
37060
 
36995
37061
  // src/tools/grep/downloader.ts
@@ -37154,7 +37220,7 @@ function findExecutable(name) {
37154
37220
  }
37155
37221
  function getOpenCodeBundledRg() {
37156
37222
  const execPath = process.execPath;
37157
- const execDir = dirname11(execPath);
37223
+ const execDir = dirname10(execPath);
37158
37224
  const isWindows2 = process.platform === "win32";
37159
37225
  const rgName = isWindows2 ? "rg.exe" : "rg";
37160
37226
  const candidates = [
@@ -37619,7 +37685,7 @@ var glob = tool({
37619
37685
  });
37620
37686
  // src/tools/slashcommand/tools.ts
37621
37687
  import { existsSync as existsSync44, readdirSync as readdirSync14, readFileSync as readFileSync29 } from "fs";
37622
- import { join as join50, basename as basename3, dirname as dirname12 } from "path";
37688
+ import { join as join50, basename as basename3, dirname as dirname11 } from "path";
37623
37689
  function discoverCommandsFromDir2(commandsDir, scope) {
37624
37690
  if (!existsSync44(commandsDir)) {
37625
37691
  return [];
@@ -37725,7 +37791,7 @@ async function formatLoadedCommand(cmd) {
37725
37791
  `);
37726
37792
  sections.push(`## Command Instructions
37727
37793
  `);
37728
- const commandDir = cmd.path ? dirname12(cmd.path) : process.cwd();
37794
+ const commandDir = cmd.path ? dirname11(cmd.path) : process.cwd();
37729
37795
  const withFileRefs = await resolveFileReferencesInText(cmd.content || "", commandDir);
37730
37796
  const resolvedContent = await resolveCommandsInText(withFileRefs);
37731
37797
  sections.push(resolvedContent.trim());
@@ -38511,7 +38577,7 @@ var TOOL_DESCRIPTION_PREFIX = `Load a skill to get detailed instructions for a s
38511
38577
  Skills provide specialized knowledge and step-by-step guidance.
38512
38578
  Use this when a task matches an available skill's description.`;
38513
38579
  // src/tools/skill/tools.ts
38514
- import { dirname as dirname13 } from "path";
38580
+ import { dirname as dirname12 } from "path";
38515
38581
  import { readFileSync as readFileSync30 } from "fs";
38516
38582
  function loadedSkillToInfo(skill) {
38517
38583
  return {
@@ -38633,7 +38699,7 @@ function createSkillTool(options = {}) {
38633
38699
  throw new Error(`Skill "${args.name}" not found. Available skills: ${available || "none"}`);
38634
38700
  }
38635
38701
  const body = extractSkillBody(skill);
38636
- const dir = skill.path ? dirname13(skill.path) : skill.resolvedPath || process.cwd();
38702
+ const dir = skill.path ? dirname12(skill.path) : skill.resolvedPath || process.cwd();
38637
38703
  const output = [
38638
38704
  `## Skill: ${skill.name}`,
38639
38705
  "",
@@ -39405,6 +39471,7 @@ var builtinTools = {
39405
39471
  // src/features/background-agent/manager.ts
39406
39472
  import { existsSync as existsSync47, readdirSync as readdirSync17 } from "fs";
39407
39473
  import { join as join54 } from "path";
39474
+ var TASK_TTL_MS = 30 * 60 * 1000;
39408
39475
  function getMessageDir11(sessionID) {
39409
39476
  if (!existsSync47(MESSAGE_STORAGE))
39410
39477
  return null;
@@ -39670,11 +39737,11 @@ class BackgroundManager {
39670
39737
  },
39671
39738
  query: { directory: this.directory }
39672
39739
  });
39673
- this.clearNotificationsForTask(taskId);
39674
39740
  log("[background-agent] Successfully sent prompt to parent session:", { parentSessionID: task.parentSessionID });
39675
39741
  } catch (error45) {
39676
39742
  log("[background-agent] prompt failed:", String(error45));
39677
39743
  } finally {
39744
+ this.clearNotificationsForTask(taskId);
39678
39745
  this.tasks.delete(taskId);
39679
39746
  log("[background-agent] Removed completed task from memory:", taskId);
39680
39747
  }
@@ -39699,7 +39766,38 @@ class BackgroundManager {
39699
39766
  }
39700
39767
  return false;
39701
39768
  }
39769
+ pruneStaleTasksAndNotifications() {
39770
+ const now = Date.now();
39771
+ for (const [taskId, task] of this.tasks.entries()) {
39772
+ const age = now - task.startedAt.getTime();
39773
+ if (age > TASK_TTL_MS) {
39774
+ log("[background-agent] Pruning stale task:", { taskId, age: Math.round(age / 1000) + "s" });
39775
+ task.status = "error";
39776
+ task.error = "Task timed out after 30 minutes";
39777
+ task.completedAt = new Date;
39778
+ this.clearNotificationsForTask(taskId);
39779
+ this.tasks.delete(taskId);
39780
+ subagentSessions.delete(task.sessionID);
39781
+ }
39782
+ }
39783
+ for (const [sessionID, notifications] of this.notifications.entries()) {
39784
+ if (notifications.length === 0) {
39785
+ this.notifications.delete(sessionID);
39786
+ continue;
39787
+ }
39788
+ const validNotifications = notifications.filter((task) => {
39789
+ const age = now - task.startedAt.getTime();
39790
+ return age <= TASK_TTL_MS;
39791
+ });
39792
+ if (validNotifications.length === 0) {
39793
+ this.notifications.delete(sessionID);
39794
+ } else if (validNotifications.length !== notifications.length) {
39795
+ this.notifications.set(sessionID, validNotifications);
39796
+ }
39797
+ }
39798
+ }
39702
39799
  async pollRunningTasks() {
39800
+ this.pruneStaleTasksAndNotifications();
39703
39801
  const statusResult = await this.client.session.status();
39704
39802
  const allStatuses = statusResult.data ?? {};
39705
39803
  for (const task of this.tasks.values()) {
@@ -42511,7 +42609,8 @@ var HookNameSchema = exports_external.enum([
42511
42609
  "preemptive-compaction",
42512
42610
  "compaction-context-injector",
42513
42611
  "claude-code-hooks",
42514
- "auto-slash-command"
42612
+ "auto-slash-command",
42613
+ "edit-error-recovery"
42515
42614
  ]);
42516
42615
  var BuiltinCommandNameSchema = exports_external.enum([
42517
42616
  "init-deep"
@@ -45646,6 +45745,7 @@ var OhMyOpenCodePlugin = async (ctx) => {
45646
45745
  const thinkingBlockValidator = isHookEnabled("thinking-block-validator") ? createThinkingBlockValidatorHook() : null;
45647
45746
  const ralphLoop = isHookEnabled("ralph-loop") ? createRalphLoopHook(ctx, { config: pluginConfig.ralph_loop }) : null;
45648
45747
  const autoSlashCommand = isHookEnabled("auto-slash-command") ? createAutoSlashCommandHook() : null;
45748
+ const editErrorRecovery = isHookEnabled("edit-error-recovery") ? createEditErrorRecoveryHook(ctx) : null;
45649
45749
  const backgroundManager = new BackgroundManager(ctx);
45650
45750
  const todoContinuationEnforcer = isHookEnabled("todo-continuation-enforcer") ? createTodoContinuationEnforcer(ctx, { backgroundManager }) : null;
45651
45751
  if (sessionRecovery && todoContinuationEnforcer) {
@@ -45840,6 +45940,7 @@ var OhMyOpenCodePlugin = async (ctx) => {
45840
45940
  await emptyTaskResponseDetector?.["tool.execute.after"](input, output);
45841
45941
  await agentUsageReminder?.["tool.execute.after"](input, output);
45842
45942
  await interactiveBashSession?.["tool.execute.after"](input, output);
45943
+ await editErrorRecovery?.["tool.execute.after"](input, output);
45843
45944
  }
45844
45945
  };
45845
45946
  };
@@ -2,4 +2,4 @@ import type { AnalyzeResult, SgResult } from "./types";
2
2
  export declare function formatSearchResult(result: SgResult): string;
3
3
  export declare function formatReplaceResult(result: SgResult, isDryRun: boolean): string;
4
4
  export declare function formatAnalyzeResult(results: AnalyzeResult[], extractedMetaVars: boolean): string;
5
- export declare function formatTransformResult(original: string, transformed: string, editCount: number): string;
5
+ export declare function formatTransformResult(_original: string, transformed: string, editCount: number): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-opencode",
3
- "version": "2.12.0",
3
+ "version": "2.12.2",
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",