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 +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.zh-cn.md +1 -1
- package/dist/cli/index.js +3 -2
- package/dist/config/schema.d.ts +2 -0
- package/dist/features/background-agent/manager.d.ts +1 -0
- package/dist/hooks/edit-error-recovery/index.d.ts +31 -0
- package/dist/hooks/edit-error-recovery/index.test.d.ts +1 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/non-interactive-env/index.test.d.ts +1 -0
- package/dist/hooks/ralph-loop/types.d.ts +1 -0
- package/dist/hooks/session-recovery/index.d.ts +2 -0
- package/dist/hooks/session-recovery/index.test.d.ts +1 -0
- package/dist/index.js +146 -45
- package/dist/tools/ast-grep/utils.d.ts +1 -1
- package/package.json +1 -1
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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.
|
|
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"
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -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";
|
|
@@ -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 {};
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -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 {};
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
19598
|
-
|
|
19599
|
-
|
|
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
|
-
|
|
19606
|
-
|
|
19607
|
-
|
|
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}]
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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 ?
|
|
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 ?
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 ?
|
|
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
|
|
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 ?
|
|
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(
|
|
5
|
+
export declare function formatTransformResult(_original: string, transformed: string, editCount: number): string;
|