oh-my-opencode 2.3.0 → 2.4.0

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
@@ -156,11 +156,22 @@ Windows から Linux に初めて乗り換えた時のこと、自分の思い
156
156
 
157
157
  ### ステップ 0: セットアップについてユーザーに尋ねる
158
158
 
159
- OpenCode は様々なモデルをサポートしており、当プラグインは様々な最先端 LLM の使用を提案しています。そのため、ユーザーに以下の質問をする必要があります:
159
+ OpenCode は様々なモデルをサポートしており、当プラグインは様々な最先端 LLM の使用を提案しています。そのため、ユーザーに以下の質問をする必要があります。
160
+
161
+ **重要**: ユーザーがどのプロバイダーを持っているか追跡してください。ユーザーが Claude を持っていると確認した場合のみ、`anthropic/claude-opus-4-5` をフォールバックとして使用できます。Claude がない場合は、代わりに `opencode/big-pickle` をフォールバックとして使用してください。
160
162
 
161
163
  1. **Claude Pro/Max のサブスクリプションをお持ちですか?** お持ちでない場合、メインオーケストレーターモデルとして 'opencode/big-pickle' (GLM 4.6 として知られる) を使用することになります (非推奨)。
162
- - ユーザーが**「はい」**と答えた場合、ToDo 追加: "ユーザーの Claude Pro/Max 認証設定"
163
- - ユーザーが**「いいえ」**と答えた場合、`~/.config/opencode/oh-my-opencode.json` に以下を保存:
164
+ - ユーザーが**「はい」**と答えた場合、追加質問: **max20 (20x モード) を使用していますか?**
165
+ - **「はい」 (max20)**: ToDo 追加: "ユーザーの Claude Pro/Max 認証設定" — librarian に Claude Sonnet 4.5 をフル使用。**記憶: Claude がフォールバックとして使用可能。**
166
+ - **「いいえ」 (max20 ではない)**: ToDo 追加: "ユーザーの Claude Pro/Max 認証設定"、そして `~/.config/opencode/oh-my-opencode.json` に以下をマージ。**記憶: Claude がフォールバックとして使用可能。**
167
+ ```json
168
+ {
169
+ "agents": {
170
+ "librarian": { "model": "opencode/big-pickle" }
171
+ }
172
+ }
173
+ ```
174
+ - ユーザーが**「いいえ」** (Claude サブスクリプションなし) と答えた場合、`~/.config/opencode/oh-my-opencode.json` に以下を保存。**記憶: Claude は使用不可 — すべてのフォールバックは `opencode/big-pickle` を使用する必要があります。**
164
175
  ```json
165
176
  {
166
177
  "agents": {
@@ -170,29 +181,49 @@ OpenCode は様々なモデルをサポートしており、当プラグイン
170
181
  }
171
182
  ```
172
183
 
173
- 2. **ChatGPT のサブスクリプションをお持ちですか?** お持ちでない場合、oracle エージェントに 'anthropic/claude-opus-4-5' を使用することになります。
184
+ 2. **ChatGPT のサブスクリプションをお持ちですか?** お持ちでない場合、oracle エージェントにフォールバックモデルを使用することになります。
174
185
  - ユーザーが**「はい」**と答えた場合、ToDo 追加: "ユーザーの ChatGPT 認証設定"
175
- - ユーザーが**「いいえ」**と答えた場合、`oh-my-opencode.json` に以下をマージ:
176
- ```json
177
- {
178
- "agents": {
179
- "oracle": { "model": "anthropic/claude-opus-4-5" }
186
+ - ユーザーが**「いいえ」**と答えた場合:
187
+ - **Claude が使用可能な場合**: `oh-my-opencode.json` に以下をマージ:
188
+ ```json
189
+ {
190
+ "agents": {
191
+ "oracle": { "model": "anthropic/claude-opus-4-5" }
192
+ }
180
193
  }
181
- }
182
- ```
194
+ ```
195
+ - **Claude が使用不可の場合**: `oh-my-opencode.json` に以下をマージ:
196
+ ```json
197
+ {
198
+ "agents": {
199
+ "oracle": { "model": "opencode/big-pickle" }
200
+ }
201
+ }
202
+ ```
183
203
 
184
204
  3. **Gemini モデルを統合しますか?** 統合しない場合、美しいデザインを享受できません。
185
205
  - ユーザーが**「はい」**と答えた場合、ToDo 追加: "Google OAuth 設定"
186
- - ユーザーが**「いいえ」**と答えた場合、`oh-my-opencode.json` に以下をマージ:
187
- ```json
188
- {
189
- "agents": {
190
- "frontend-ui-ux-engineer": { "model": "anthropic/claude-opus-4-5" },
191
- "document-writer": { "model": "anthropic/claude-opus-4-5" },
192
- "multimodal-looker": { "model": "anthropic/claude-opus-4-5" }
206
+ - ユーザーが**「いいえ」**と答えた場合:
207
+ - **Claude が使用可能な場合**: `oh-my-opencode.json` に以下をマージ:
208
+ ```json
209
+ {
210
+ "agents": {
211
+ "frontend-ui-ux-engineer": { "model": "anthropic/claude-opus-4-5" },
212
+ "document-writer": { "model": "anthropic/claude-opus-4-5" },
213
+ "multimodal-looker": { "model": "anthropic/claude-opus-4-5" }
214
+ }
193
215
  }
194
- }
195
- ```
216
+ ```
217
+ - **Claude が使用不可の場合**: `oh-my-opencode.json` に以下をマージ:
218
+ ```json
219
+ {
220
+ "agents": {
221
+ "frontend-ui-ux-engineer": { "model": "opencode/big-pickle" },
222
+ "document-writer": { "model": "opencode/big-pickle" },
223
+ "multimodal-looker": { "model": "opencode/big-pickle" }
224
+ }
225
+ }
226
+ ```
196
227
 
197
228
  ### ステップ 1: OpenCode インストール確認
198
229
 
package/README.ko.md CHANGED
@@ -153,43 +153,74 @@ OpenCode 가 낭만이 사라진것같은 오늘날의 시대에, 당신에게
153
153
 
154
154
  ### 0단계: 사용자에게 설정에 대해 물어보세요.
155
155
 
156
- OpenCode는 다양한 모델을 지원하며, 이 플러그인은 다양한 프론티어 LLM 사용을 제안합니다. 따라서 사용자에게 다음 질문을 해야 합니다:
156
+ OpenCode는 다양한 모델을 지원하며, 이 플러그인은 다양한 프론티어 LLM 사용을 제안합니다. 따라서 사용자에게 다음 질문을 해야 합니다.
157
+
158
+ **중요**: 사용자가 어떤 프로바이더를 가지고 있는지 추적하세요. 사용자가 Claude가 있다고 확인한 경우에만 `anthropic/claude-opus-4-5`를 폴백으로 사용할 수 있습니다. Claude가 없다면 `opencode/big-pickle`을 폴백으로 사용해야 합니다.
157
159
 
158
160
  1. **Claude Pro/Max 구독이 있으신가요?** 그렇지 않다면 메인 오케스트레이터 모델로 'opencode/big-pickle'(GLM 4.6으로 알려짐)을 사용하게 됩니다 (권장하지 않음).
159
- - 사용자가 **'예'**라고 답하면, 추가: "사용자 Claude Pro/Max 인증 설정"
160
- - 사용자가 **'아니오'**라고 답하면, `~/.config/opencode/oh-my-opencode.json`에 다음을 저장:
161
+ - 사용자가 **'예'**라고 답하면, 추가 질문: **max20 (20x 모드)를 사용하고 계신가요?**
162
+ - **'' (max20)**: 할 일 추가: "사용자 Claude Pro/Max 인증 설정" — librarian에 Claude Sonnet 4.5 전체 사용. **기억: Claude를 폴백으로 사용 가능.**
163
+ - **'아니오' (max20 아님)**: 할 일 추가: "사용자 Claude Pro/Max 인증 설정", 그리고 `~/.config/opencode/oh-my-opencode.json`에 다음을 병합. **기억: Claude를 폴백으로 사용 가능.**
164
+ ```json
165
+ {
166
+ "agents": {
167
+ "librarian": { "model": "opencode/big-pickle" }
168
+ }
169
+ }
170
+ ```
171
+ - 사용자가 **'아니오'** (Claude 구독 없음)라고 답하면, `~/.config/opencode/oh-my-opencode.json`에 다음을 저장. **기억: Claude 사용 불가 — 모든 폴백은 `opencode/big-pickle`을 사용해야 함.**
161
172
  ```json
162
173
  {
163
174
  "agents": {
164
- "sisyphus": { "model": "opencode/big-pickle" },
175
+ "Sisyphus": { "model": "opencode/big-pickle" },
165
176
  "librarian": { "model": "opencode/big-pickle" }
166
177
  }
167
178
  }
168
179
  ```
169
180
 
170
- 2. **ChatGPT 구독이 있으신가요?** 그렇지 않다면 oracle 에이전트에 'anthropic/claude-opus-4-5'를 사용하게 됩니다.
181
+ 2. **ChatGPT 구독이 있으신가요?** 그렇지 않다면 oracle 에이전트에 폴백 모델을 사용하게 됩니다.
171
182
  - 사용자가 **'예'**라고 답하면, 할 일 추가: "사용자 ChatGPT 인증 설정"
172
- - 사용자가 **'아니오'**라고 답하면, `oh-my-opencode.json`에 다음을 병합:
173
- ```json
174
- {
175
- "agents": {
176
- "oracle": { "model": "anthropic/claude-opus-4-5" }
183
+ - 사용자가 **'아니오'**라고 답하면:
184
+ - **Claude가 사용 가능한 경우**: `oh-my-opencode.json`에 다음을 병합:
185
+ ```json
186
+ {
187
+ "agents": {
188
+ "oracle": { "model": "anthropic/claude-opus-4-5" }
189
+ }
177
190
  }
178
- }
179
- ```
191
+ ```
192
+ - **Claude가 사용 불가능한 경우**: `oh-my-opencode.json`에 다음을 병합:
193
+ ```json
194
+ {
195
+ "agents": {
196
+ "oracle": { "model": "opencode/big-pickle" }
197
+ }
198
+ }
199
+ ```
180
200
 
181
201
  3. **Gemini 모델을 통합하시겠습니까?** 그렇지 않다면 아름다운 디자인을 즐길 수 없습니다.
182
202
  - 사용자가 **'예'**라고 답하면, 할 일 추가: "Google OAuth 설정"
183
- - 사용자가 **'아니오'**라고 답하면, `oh-my-opencode.json`에 다음을 병합:
184
- ```json
185
- {
186
- "agents": {
187
- "frontend-ui-ux-engineer": { "model": "anthropic/claude-opus-4-5" },
188
- "document-writer": { "model": "anthropic/claude-opus-4-5" },
189
- "multimodal-looker": { "model": "anthropic/claude-opus-4-5" }
203
+ - 사용자가 **'아니오'**라고 답하면:
204
+ - **Claude가 사용 가능한 경우**: `oh-my-opencode.json`에 다음을 병합:
205
+ ```json
206
+ {
207
+ "agents": {
208
+ "frontend-ui-ux-engineer": { "model": "anthropic/claude-opus-4-5" },
209
+ "document-writer": { "model": "anthropic/claude-opus-4-5" },
210
+ "multimodal-looker": { "model": "anthropic/claude-opus-4-5" }
211
+ }
190
212
  }
191
- }
192
- ```
213
+ ```
214
+ - **Claude가 사용 불가능한 경우**: `oh-my-opencode.json`에 다음을 병합:
215
+ ```json
216
+ {
217
+ "agents": {
218
+ "frontend-ui-ux-engineer": { "model": "opencode/big-pickle" },
219
+ "document-writer": { "model": "opencode/big-pickle" },
220
+ "multimodal-looker": { "model": "opencode/big-pickle" }
221
+ }
222
+ }
223
+ ```
193
224
 
194
225
  ### 1단계: OpenCode 설치 확인
195
226
 
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ NOTICE: News regarding oh-my-opencode used to posted on my X account, but since it got suspended innocently, my friend [@_junhoyeo](https://x.com/_junhoyeo) will post on behalf of me. (THE GUY WHO MADE HERO IMAGE)
2
+
1
3
  <!-- <CENTERED SECTION FOR GITHUB DISPLAY> -->
2
4
 
3
5
  <div align="center">
@@ -181,11 +183,22 @@ follow these steps:
181
183
 
182
184
  ### Step 0: Ask user about the setup.
183
185
 
184
- Since OpenCode Supports various models, and our plugin suggests various frontier LLMs. Thus, you must ask user, following questions:
186
+ Since OpenCode supports various models, and our plugin suggests various frontier LLMs, you must ask user the following questions.
187
+
188
+ **IMPORTANT**: Track which providers the user has. Only use `anthropic/claude-opus-4-5` as fallback when the user confirmed they have Claude. If they don't have Claude, use `opencode/big-pickle` as fallback instead.
185
189
 
186
190
  1. **Do you have a Claude Pro/Max Subscription?** Otherwise, we'll use 'opencode/big-pickle' (known for GLM 4.6) as a main orchestrator model (not recommended)
187
- - If user said **yes**, add ToDo: "Setup user Claude Pro/Max Auth"
188
- - If user said **no**, merge the following to `~/.config/opencode/oh-my-opencode.json`:
191
+ - If user said **yes**, ask follow-up: **Are you on max20 (20x mode)?**
192
+ - If **yes (max20)**: Add ToDo: "Setup user Claude Pro/Max Auth" — full Claude Sonnet 4.5 for librarian. **Remember: Claude is available for fallbacks.**
193
+ - If **no (not max20)**: Add ToDo: "Setup user Claude Pro/Max Auth", then merge the following to `~/.config/opencode/oh-my-opencode.json`. **Remember: Claude is available for fallbacks.**
194
+ ```json
195
+ {
196
+ "agents": {
197
+ "librarian": { "model": "opencode/big-pickle" }
198
+ }
199
+ }
200
+ ```
201
+ - If user said **no** (no Claude subscription), merge the following to `~/.config/opencode/oh-my-opencode.json`. **Remember: Claude is NOT available — all fallbacks must use `opencode/big-pickle`.**
189
202
  ```json
190
203
  {
191
204
  "agents": {
@@ -195,29 +208,49 @@ Since OpenCode Supports various models, and our plugin suggests various frontier
195
208
  }
196
209
  ```
197
210
 
198
- 2. **Do you have a ChatGPT Subscription?** Otherwise, we'll use 'anthropic/claude-opus-4-5' with 32k thinking option for oracle agent.
211
+ 2. **Do you have a ChatGPT Subscription?** Otherwise, we'll use a fallback model for oracle agent.
199
212
  - If user said **yes**, add ToDo: "Setup user ChatGPT Auth"
200
- - If user said **no**, merge the following to `oh-my-opencode.json`:
201
- ```json
202
- {
203
- "agents": {
204
- "oracle": { "model": "anthropic/claude-opus-4-5" }
213
+ - If user said **no**:
214
+ - **If Claude is available**: merge the following to `oh-my-opencode.json`:
215
+ ```json
216
+ {
217
+ "agents": {
218
+ "oracle": { "model": "anthropic/claude-opus-4-5" }
219
+ }
205
220
  }
206
- }
207
- ```
221
+ ```
222
+ - **If Claude is NOT available**: merge the following to `oh-my-opencode.json`:
223
+ ```json
224
+ {
225
+ "agents": {
226
+ "oracle": { "model": "opencode/big-pickle" }
227
+ }
228
+ }
229
+ ```
208
230
 
209
231
  3. **Will you integrate Gemini models?** Otherwise, you cannot enjoy the beautiful design.
210
232
  - If user said **yes**, add ToDo: "Setup Google OAuth"
211
- - If user said **no**, merge the following to `oh-my-opencode.json`:
212
- ```json
213
- {
214
- "agents": {
215
- "frontend-ui-ux-engineer": { "model": "anthropic/claude-opus-4-5" },
216
- "document-writer": { "model": "anthropic/claude-opus-4-5" },
217
- "multimodal-looker": { "model": "anthropic/claude-opus-4-5" }
233
+ - If user said **no**:
234
+ - **If Claude is available**: merge the following to `oh-my-opencode.json`:
235
+ ```json
236
+ {
237
+ "agents": {
238
+ "frontend-ui-ux-engineer": { "model": "anthropic/claude-opus-4-5" },
239
+ "document-writer": { "model": "anthropic/claude-opus-4-5" },
240
+ "multimodal-looker": { "model": "anthropic/claude-opus-4-5" }
241
+ }
218
242
  }
219
- }
220
- ```
243
+ ```
244
+ - **If Claude is NOT available**: merge the following to `oh-my-opencode.json`:
245
+ ```json
246
+ {
247
+ "agents": {
248
+ "frontend-ui-ux-engineer": { "model": "opencode/big-pickle" },
249
+ "document-writer": { "model": "opencode/big-pickle" },
250
+ "multimodal-looker": { "model": "opencode/big-pickle" }
251
+ }
252
+ }
253
+ ```
221
254
 
222
255
  ### Step 1: Install OpenCode, if not
223
256
 
package/README.zh-cn.md CHANGED
@@ -162,11 +162,22 @@ Install and configure by following the instructions here https://raw.githubuserc
162
162
 
163
163
  ### 步骤 0:摸清用户底细
164
164
 
165
- OpenCode 支持各种模型,我们这个插件也建议用各种前沿 LLM。所以先问清楚:
165
+ OpenCode 支持各种模型,我们这个插件也建议用各种前沿 LLM。所以先问清楚。
166
+
167
+ **重要**:追踪用户有哪些 provider。只有用户确认有 Claude 的情况下,才能用 `anthropic/claude-opus-4-5` 做 fallback。没有 Claude 就用 `opencode/big-pickle` 做 fallback。
166
168
 
167
169
  1. **有 Claude Pro/Max 订阅吗?** 没有的话,主编排模型只能用 'opencode/big-pickle'(也就是 GLM 4.6),这体验可不太好(不推荐)。
168
- - 用户说**有**,加个待办:"配置用户 Claude Pro/Max 认证"
169
- - 用户说**没有**,把这坨配置写到 `~/.config/opencode/oh-my-opencode.json`:
170
+ - 用户说**有**,继续问:**你用的是 max20 (20x 模式) 吗?**
171
+ - **是 (max20)**:加个待办:"配置用户 Claude Pro/Max 认证" — librarian 用完整的 Claude Sonnet 4.5。**记住:Claude 可以做 fallback。**
172
+ - **不是 (没有 max20)**:加个待办:"配置用户 Claude Pro/Max 认证",然后把这个合并到 `~/.config/opencode/oh-my-opencode.json`。**记住:Claude 可以做 fallback。**
173
+ ```json
174
+ {
175
+ "agents": {
176
+ "librarian": { "model": "opencode/big-pickle" }
177
+ }
178
+ }
179
+ ```
180
+ - 用户说**没有** (没有 Claude 订阅),把这坨配置写到 `~/.config/opencode/oh-my-opencode.json`。**记住:没有 Claude — 所有 fallback 都得用 `opencode/big-pickle`。**
170
181
  ```json
171
182
  {
172
183
  "agents": {
@@ -176,29 +187,49 @@ OpenCode 支持各种模型,我们这个插件也建议用各种前沿 LLM。
176
187
  }
177
188
  ```
178
189
 
179
- 2. **有 ChatGPT 订阅吗?** 没有的话,oracle Agent 就用 'anthropic/claude-opus-4-5' 带 32k thinking 了。
190
+ 2. **有 ChatGPT 订阅吗?** 没有的话,oracle Agent 就用 fallback 模型了。
180
191
  - 用户说**有**,加个待办:"配置用户 ChatGPT 认证"
181
- - 用户说**没有**,把这个合并到 `oh-my-opencode.json`:
182
- ```json
183
- {
184
- "agents": {
185
- "oracle": { "model": "anthropic/claude-opus-4-5" }
192
+ - 用户说**没有**:
193
+ - **有 Claude 的情况下**:把这个合并到 `oh-my-opencode.json`:
194
+ ```json
195
+ {
196
+ "agents": {
197
+ "oracle": { "model": "anthropic/claude-opus-4-5" }
198
+ }
186
199
  }
187
- }
188
- ```
200
+ ```
201
+ - **没有 Claude 的情况下**:把这个合并到 `oh-my-opencode.json`:
202
+ ```json
203
+ {
204
+ "agents": {
205
+ "oracle": { "model": "opencode/big-pickle" }
206
+ }
207
+ }
208
+ ```
189
209
 
190
210
  3. **要接 Gemini 模型吗?** 不接的话,那么漂亮的 UI 设计你就无福消受了。
191
211
  - 用户说**要**,加个待办:"配置 Google OAuth"
192
- - 用户说**不要**,把这个合并到 `oh-my-opencode.json`:
193
- ```json
194
- {
195
- "agents": {
196
- "frontend-ui-ux-engineer": { "model": "anthropic/claude-opus-4-5" },
197
- "document-writer": { "model": "anthropic/claude-opus-4-5" },
198
- "multimodal-looker": { "model": "anthropic/claude-opus-4-5" }
212
+ - 用户说**不要**:
213
+ - **有 Claude 的情况下**:把这个合并到 `oh-my-opencode.json`:
214
+ ```json
215
+ {
216
+ "agents": {
217
+ "frontend-ui-ux-engineer": { "model": "anthropic/claude-opus-4-5" },
218
+ "document-writer": { "model": "anthropic/claude-opus-4-5" },
219
+ "multimodal-looker": { "model": "anthropic/claude-opus-4-5" }
220
+ }
199
221
  }
200
- }
201
- ```
222
+ ```
223
+ - **没有 Claude 的情况下**:把这个合并到 `oh-my-opencode.json`:
224
+ ```json
225
+ {
226
+ "agents": {
227
+ "frontend-ui-ux-engineer": { "model": "opencode/big-pickle" },
228
+ "document-writer": { "model": "opencode/big-pickle" },
229
+ "multimodal-looker": { "model": "opencode/big-pickle" }
230
+ }
231
+ }
232
+ ```
202
233
 
203
234
  ### 步骤 1:确认 OpenCode 装没装
204
235
 
@@ -1092,6 +1092,7 @@ export declare const OhMyOpenCodeConfigSchema: z.ZodObject<{
1092
1092
  empty_message_recovery: z.ZodOptional<z.ZodBoolean>;
1093
1093
  auto_resume: z.ZodOptional<z.ZodBoolean>;
1094
1094
  }, z.core.$strip>>;
1095
+ auto_update: z.ZodOptional<z.ZodBoolean>;
1095
1096
  }, z.core.$strip>;
1096
1097
  export type OhMyOpenCodeConfig = z.infer<typeof OhMyOpenCodeConfigSchema>;
1097
1098
  export type AgentOverrideConfig = z.infer<typeof AgentOverrideConfigSchema>;
@@ -6,8 +6,15 @@ export interface PluginEntryInfo {
6
6
  entry: string;
7
7
  isPinned: boolean;
8
8
  pinnedVersion: string | null;
9
+ configPath: string;
9
10
  }
10
11
  export declare function findPluginEntry(directory: string): PluginEntryInfo | null;
11
12
  export declare function getCachedVersion(): string | null;
13
+ /**
14
+ * Updates a pinned version entry in the config file.
15
+ * Only replaces within the "plugin" array to avoid unintended edits.
16
+ * Preserves JSONC comments and formatting via string replacement.
17
+ */
18
+ export declare function updatePinnedVersion(configPath: string, oldEntry: string, newVersion: string): boolean;
12
19
  export declare function getLatestVersion(): Promise<string | null>;
13
20
  export declare function checkForUpdate(directory: string): Promise<UpdateCheckResult>;
@@ -6,7 +6,7 @@ export declare function createAutoUpdateCheckerHook(ctx: PluginInput, options?:
6
6
  type: string;
7
7
  properties?: unknown;
8
8
  };
9
- }) => Promise<void>;
9
+ }) => void;
10
10
  };
11
11
  export type { UpdateCheckResult, AutoUpdateCheckerOptions } from "./types";
12
12
  export { checkForUpdate } from "./checker";
@@ -21,4 +21,5 @@ export interface UpdateCheckResult {
21
21
  export interface AutoUpdateCheckerOptions {
22
22
  showStartupToast?: boolean;
23
23
  isSisyphusEnabled?: boolean;
24
+ autoUpdate?: boolean;
24
25
  }
@@ -3,7 +3,6 @@ export { createContextWindowMonitorHook } from "./context-window-monitor";
3
3
  export { createSessionNotification } from "./session-notification";
4
4
  export { createSessionRecoveryHook, type SessionRecoveryHook, type SessionRecoveryOptions } from "./session-recovery";
5
5
  export { createCommentCheckerHooks } from "./comment-checker";
6
- export { createGrepOutputTruncatorHook } from "./grep-output-truncator";
7
6
  export { createToolOutputTruncatorHook } from "./tool-output-truncator";
8
7
  export { createDirectoryAgentsInjectorHook } from "./directory-agents-injector";
9
8
  export { createDirectoryReadmeInjectorHook } from "./directory-readme-injector";
package/dist/index.js CHANGED
@@ -3241,6 +3241,15 @@ function createBuiltinAgents(disabledAgents = [], agentOverrides = {}, directory
3241
3241
  import { existsSync as existsSync4, readdirSync as readdirSync2 } from "fs";
3242
3242
  import { join as join6 } from "path";
3243
3243
 
3244
+ // src/features/claude-code-session-state/state.ts
3245
+ var subagentSessions = new Set;
3246
+ var mainSessionID;
3247
+ function setMainSession(id) {
3248
+ mainSessionID = id;
3249
+ }
3250
+ function getMainSessionID() {
3251
+ return mainSessionID;
3252
+ }
3244
3253
  // src/features/hook-message-injector/injector.ts
3245
3254
  import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, readdirSync, writeFileSync } from "fs";
3246
3255
  import { join as join5 } from "path";
@@ -3399,12 +3408,14 @@ function detectInterrupt(error) {
3399
3408
  }
3400
3409
  return false;
3401
3410
  }
3411
+ var COUNTDOWN_SECONDS = 5;
3412
+ var TOAST_DURATION_MS = 900;
3402
3413
  function createTodoContinuationEnforcer(ctx) {
3403
3414
  const remindedSessions = new Set;
3404
3415
  const interruptedSessions = new Set;
3405
3416
  const errorSessions = new Set;
3406
3417
  const recoveringSessions = new Set;
3407
- const pendingTimers = new Map;
3418
+ const pendingCountdowns = new Map;
3408
3419
  const markRecovering = (sessionID) => {
3409
3420
  recoveringSessions.add(sessionID);
3410
3421
  };
@@ -3422,10 +3433,10 @@ function createTodoContinuationEnforcer(ctx) {
3422
3433
  interruptedSessions.add(sessionID);
3423
3434
  }
3424
3435
  log(`[${HOOK_NAME}] session.error received`, { sessionID, isInterrupt, error: props?.error });
3425
- const timer = pendingTimers.get(sessionID);
3426
- if (timer) {
3427
- clearTimeout(timer);
3428
- pendingTimers.delete(sessionID);
3436
+ const countdown = pendingCountdowns.get(sessionID);
3437
+ if (countdown) {
3438
+ clearInterval(countdown.intervalId);
3439
+ pendingCountdowns.delete(sessionID);
3429
3440
  }
3430
3441
  }
3431
3442
  return;
@@ -3435,57 +3446,78 @@ function createTodoContinuationEnforcer(ctx) {
3435
3446
  if (!sessionID)
3436
3447
  return;
3437
3448
  log(`[${HOOK_NAME}] session.idle received`, { sessionID });
3438
- const existingTimer = pendingTimers.get(sessionID);
3439
- if (existingTimer) {
3440
- clearTimeout(existingTimer);
3441
- log(`[${HOOK_NAME}] Cancelled existing timer`, { sessionID });
3442
- }
3443
- const timer = setTimeout(async () => {
3444
- pendingTimers.delete(sessionID);
3445
- log(`[${HOOK_NAME}] Timer fired, checking conditions`, { sessionID });
3446
- if (recoveringSessions.has(sessionID)) {
3447
- log(`[${HOOK_NAME}] Skipped: session in recovery mode`, { sessionID });
3448
- return;
3449
- }
3450
- const shouldBypass = interruptedSessions.has(sessionID) || errorSessions.has(sessionID);
3449
+ const mainSessionID2 = getMainSessionID();
3450
+ if (mainSessionID2 && sessionID !== mainSessionID2) {
3451
+ log(`[${HOOK_NAME}] Skipped: not main session`, { sessionID, mainSessionID: mainSessionID2 });
3452
+ return;
3453
+ }
3454
+ const existingCountdown = pendingCountdowns.get(sessionID);
3455
+ if (existingCountdown) {
3456
+ clearInterval(existingCountdown.intervalId);
3457
+ pendingCountdowns.delete(sessionID);
3458
+ log(`[${HOOK_NAME}] Cancelled existing countdown`, { sessionID });
3459
+ }
3460
+ if (recoveringSessions.has(sessionID)) {
3461
+ log(`[${HOOK_NAME}] Skipped: session in recovery mode`, { sessionID });
3462
+ return;
3463
+ }
3464
+ const shouldBypass = interruptedSessions.has(sessionID) || errorSessions.has(sessionID);
3465
+ if (shouldBypass) {
3451
3466
  interruptedSessions.delete(sessionID);
3452
3467
  errorSessions.delete(sessionID);
3453
- if (shouldBypass) {
3454
- log(`[${HOOK_NAME}] Skipped: error/interrupt bypass`, { sessionID });
3455
- return;
3456
- }
3457
- if (remindedSessions.has(sessionID)) {
3458
- log(`[${HOOK_NAME}] Skipped: already reminded this session`, { sessionID });
3459
- return;
3460
- }
3461
- let todos = [];
3462
- try {
3463
- log(`[${HOOK_NAME}] Fetching todos for session`, { sessionID });
3464
- const response = await ctx.client.session.todo({
3465
- path: { id: sessionID }
3466
- });
3467
- todos = response.data ?? response;
3468
- log(`[${HOOK_NAME}] Todo API response`, { sessionID, todosCount: todos?.length ?? 0 });
3469
- } catch (err) {
3470
- log(`[${HOOK_NAME}] Todo API error`, { sessionID, error: String(err) });
3471
- return;
3472
- }
3473
- if (!todos || todos.length === 0) {
3474
- log(`[${HOOK_NAME}] No todos found`, { sessionID });
3468
+ log(`[${HOOK_NAME}] Skipped: error/interrupt bypass`, { sessionID });
3469
+ return;
3470
+ }
3471
+ if (remindedSessions.has(sessionID)) {
3472
+ log(`[${HOOK_NAME}] Skipped: already reminded this session`, { sessionID });
3473
+ return;
3474
+ }
3475
+ let todos = [];
3476
+ try {
3477
+ log(`[${HOOK_NAME}] Fetching todos for session`, { sessionID });
3478
+ const response = await ctx.client.session.todo({
3479
+ path: { id: sessionID }
3480
+ });
3481
+ todos = response.data ?? response;
3482
+ log(`[${HOOK_NAME}] Todo API response`, { sessionID, todosCount: todos?.length ?? 0 });
3483
+ } catch (err) {
3484
+ log(`[${HOOK_NAME}] Todo API error`, { sessionID, error: String(err) });
3485
+ return;
3486
+ }
3487
+ if (!todos || todos.length === 0) {
3488
+ log(`[${HOOK_NAME}] No todos found`, { sessionID });
3489
+ return;
3490
+ }
3491
+ const incomplete = todos.filter((t) => t.status !== "completed" && t.status !== "cancelled");
3492
+ if (incomplete.length === 0) {
3493
+ log(`[${HOOK_NAME}] All todos completed`, { sessionID, total: todos.length });
3494
+ return;
3495
+ }
3496
+ log(`[${HOOK_NAME}] Found incomplete todos, starting countdown`, { sessionID, incomplete: incomplete.length, total: todos.length });
3497
+ const showCountdownToast = async (seconds) => {
3498
+ await ctx.client.tui.showToast({
3499
+ body: {
3500
+ title: "Todo Continuation",
3501
+ message: `Resuming in ${seconds}s... (${incomplete.length} tasks remaining)`,
3502
+ variant: "warning",
3503
+ duration: TOAST_DURATION_MS
3504
+ }
3505
+ }).catch(() => {});
3506
+ };
3507
+ const executeAfterCountdown = async () => {
3508
+ pendingCountdowns.delete(sessionID);
3509
+ log(`[${HOOK_NAME}] Countdown finished, executing continuation`, { sessionID });
3510
+ if (recoveringSessions.has(sessionID)) {
3511
+ log(`[${HOOK_NAME}] Abort: session entered recovery mode during countdown`, { sessionID });
3475
3512
  return;
3476
3513
  }
3477
- const incomplete = todos.filter((t) => t.status !== "completed" && t.status !== "cancelled");
3478
- if (incomplete.length === 0) {
3479
- log(`[${HOOK_NAME}] All todos completed`, { sessionID, total: todos.length });
3514
+ if (interruptedSessions.has(sessionID) || errorSessions.has(sessionID)) {
3515
+ log(`[${HOOK_NAME}] Abort: error/interrupt occurred during countdown`, { sessionID });
3516
+ interruptedSessions.delete(sessionID);
3517
+ errorSessions.delete(sessionID);
3480
3518
  return;
3481
3519
  }
3482
- log(`[${HOOK_NAME}] Found incomplete todos`, { sessionID, incomplete: incomplete.length, total: todos.length });
3483
3520
  remindedSessions.add(sessionID);
3484
- if (interruptedSessions.has(sessionID) || errorSessions.has(sessionID) || recoveringSessions.has(sessionID)) {
3485
- log(`[${HOOK_NAME}] Abort occurred during delay/fetch`, { sessionID });
3486
- remindedSessions.delete(sessionID);
3487
- return;
3488
- }
3489
3521
  try {
3490
3522
  const messageDir = getMessageDir(sessionID);
3491
3523
  const prevMessage = messageDir ? findNearestMessageWithFields(messageDir) : null;
@@ -3516,19 +3548,37 @@ function createTodoContinuationEnforcer(ctx) {
3516
3548
  log(`[${HOOK_NAME}] Prompt injection failed`, { sessionID, error: String(err) });
3517
3549
  remindedSessions.delete(sessionID);
3518
3550
  }
3519
- }, 5000);
3520
- pendingTimers.set(sessionID, timer);
3551
+ };
3552
+ let secondsRemaining = COUNTDOWN_SECONDS;
3553
+ showCountdownToast(secondsRemaining).catch(() => {});
3554
+ const intervalId = setInterval(() => {
3555
+ secondsRemaining--;
3556
+ if (secondsRemaining <= 0) {
3557
+ clearInterval(intervalId);
3558
+ pendingCountdowns.delete(sessionID);
3559
+ executeAfterCountdown();
3560
+ return;
3561
+ }
3562
+ const countdown = pendingCountdowns.get(sessionID);
3563
+ if (!countdown) {
3564
+ clearInterval(intervalId);
3565
+ return;
3566
+ }
3567
+ countdown.secondsRemaining = secondsRemaining;
3568
+ showCountdownToast(secondsRemaining).catch(() => {});
3569
+ }, 1000);
3570
+ pendingCountdowns.set(sessionID, { secondsRemaining, intervalId });
3521
3571
  }
3522
3572
  if (event.type === "message.updated") {
3523
3573
  const info = props?.info;
3524
3574
  const sessionID = info?.sessionID;
3525
3575
  log(`[${HOOK_NAME}] message.updated received`, { sessionID, role: info?.role });
3526
3576
  if (sessionID && info?.role === "user") {
3527
- const timer = pendingTimers.get(sessionID);
3528
- if (timer) {
3529
- clearTimeout(timer);
3530
- pendingTimers.delete(sessionID);
3531
- log(`[${HOOK_NAME}] Cancelled pending timer on user message`, { sessionID });
3577
+ const countdown = pendingCountdowns.get(sessionID);
3578
+ if (countdown) {
3579
+ clearInterval(countdown.intervalId);
3580
+ pendingCountdowns.delete(sessionID);
3581
+ log(`[${HOOK_NAME}] Cancelled countdown on user message`, { sessionID });
3532
3582
  }
3533
3583
  }
3534
3584
  if (sessionID && info?.role === "assistant" && remindedSessions.has(sessionID)) {
@@ -3543,10 +3593,10 @@ function createTodoContinuationEnforcer(ctx) {
3543
3593
  interruptedSessions.delete(sessionInfo.id);
3544
3594
  errorSessions.delete(sessionInfo.id);
3545
3595
  recoveringSessions.delete(sessionInfo.id);
3546
- const timer = pendingTimers.get(sessionInfo.id);
3547
- if (timer) {
3548
- clearTimeout(timer);
3549
- pendingTimers.delete(sessionInfo.id);
3596
+ const countdown = pendingCountdowns.get(sessionInfo.id);
3597
+ if (countdown) {
3598
+ clearInterval(countdown.intervalId);
3599
+ pendingCountdowns.delete(sessionInfo.id);
3550
3600
  }
3551
3601
  }
3552
3602
  }
@@ -3616,17 +3666,6 @@ ${CONTEXT_REMINDER}
3616
3666
  }
3617
3667
  // src/hooks/session-notification.ts
3618
3668
  import { platform } from "os";
3619
-
3620
- // src/features/claude-code-session-state/state.ts
3621
- var subagentSessions = new Set;
3622
- var mainSessionID;
3623
- function setMainSession(id) {
3624
- mainSessionID = id;
3625
- }
3626
- function getMainSessionID() {
3627
- return mainSessionID;
3628
- }
3629
- // src/hooks/session-notification.ts
3630
3669
  function detectPlatform() {
3631
3670
  const p = platform();
3632
3671
  if (p === "darwin" || p === "linux" || p === "win32")
@@ -4712,6 +4751,8 @@ ${result.message}`;
4712
4751
  }
4713
4752
  // src/hooks/tool-output-truncator.ts
4714
4753
  var TRUNCATABLE_TOOLS = [
4754
+ "grep",
4755
+ "Grep",
4715
4756
  "safe_grep",
4716
4757
  "glob",
4717
4758
  "Glob",
@@ -5708,12 +5749,16 @@ async function executeCompact(sessionID, msg, autoCompactState, client, director
5708
5749
  });
5709
5750
  fallbackState.revertAttempt++;
5710
5751
  fallbackState.lastRevertedMessageID = pair.userMessageID;
5711
- retryState.attempt = 0;
5712
- truncateState.truncateAttempt = 0;
5713
- autoCompactState.compactionInProgress.delete(sessionID);
5714
- setTimeout(() => {
5715
- executeCompact(sessionID, msg, autoCompactState, client, directory, experimental);
5716
- }, 1000);
5752
+ clearSessionState(autoCompactState, sessionID);
5753
+ setTimeout(async () => {
5754
+ try {
5755
+ await client.session.prompt_async({
5756
+ path: { sessionID },
5757
+ body: { parts: [{ type: "text", text: "Continue" }] },
5758
+ query: { directory }
5759
+ });
5760
+ } catch {}
5761
+ }, 500);
5717
5762
  return;
5718
5763
  } catch {}
5719
5764
  } else {
@@ -7502,9 +7547,6 @@ var USER_OPENCODE_CONFIG = path4.join(USER_CONFIG_DIR, "opencode", "opencode.jso
7502
7547
  var USER_OPENCODE_CONFIG_JSONC = path4.join(USER_CONFIG_DIR, "opencode", "opencode.jsonc");
7503
7548
 
7504
7549
  // src/hooks/auto-update-checker/checker.ts
7505
- function isLocalDevMode(directory) {
7506
- return getLocalDevPath(directory) !== null;
7507
- }
7508
7550
  function stripJsonComments(json) {
7509
7551
  return json.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (m, g) => g ? "" : m).replace(/,(\s*[}\]])/g, "$1");
7510
7552
  }
@@ -7586,12 +7628,12 @@ function findPluginEntry(directory) {
7586
7628
  const plugins = config.plugin ?? [];
7587
7629
  for (const entry of plugins) {
7588
7630
  if (entry === PACKAGE_NAME) {
7589
- return { entry, isPinned: false, pinnedVersion: null };
7631
+ return { entry, isPinned: false, pinnedVersion: null, configPath };
7590
7632
  }
7591
7633
  if (entry.startsWith(`${PACKAGE_NAME}@`)) {
7592
7634
  const pinnedVersion = entry.slice(PACKAGE_NAME.length + 1);
7593
7635
  const isPinned = pinnedVersion !== "latest";
7594
- return { entry, isPinned, pinnedVersion: isPinned ? pinnedVersion : null };
7636
+ return { entry, isPinned, pinnedVersion: isPinned ? pinnedVersion : null, configPath };
7595
7637
  }
7596
7638
  }
7597
7639
  } catch {
@@ -7623,6 +7665,48 @@ function getCachedVersion() {
7623
7665
  }
7624
7666
  return null;
7625
7667
  }
7668
+ function updatePinnedVersion(configPath, oldEntry, newVersion) {
7669
+ try {
7670
+ const content = fs4.readFileSync(configPath, "utf-8");
7671
+ const newEntry = `${PACKAGE_NAME}@${newVersion}`;
7672
+ const pluginMatch = content.match(/"plugin"\s*:\s*\[/);
7673
+ if (!pluginMatch || pluginMatch.index === undefined) {
7674
+ log(`[auto-update-checker] No "plugin" array found in ${configPath}`);
7675
+ return false;
7676
+ }
7677
+ const startIdx = pluginMatch.index + pluginMatch[0].length;
7678
+ let bracketCount = 1;
7679
+ let endIdx = startIdx;
7680
+ for (let i = startIdx;i < content.length && bracketCount > 0; i++) {
7681
+ if (content[i] === "[")
7682
+ bracketCount++;
7683
+ else if (content[i] === "]")
7684
+ bracketCount--;
7685
+ endIdx = i;
7686
+ }
7687
+ const before = content.slice(0, startIdx);
7688
+ const pluginArrayContent = content.slice(startIdx, endIdx);
7689
+ const after = content.slice(endIdx);
7690
+ const escapedOldEntry = oldEntry.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7691
+ const regex = new RegExp(`["']${escapedOldEntry}["']`);
7692
+ if (!regex.test(pluginArrayContent)) {
7693
+ log(`[auto-update-checker] Entry "${oldEntry}" not found in plugin array of ${configPath}`);
7694
+ return false;
7695
+ }
7696
+ const updatedPluginArray = pluginArrayContent.replace(regex, `"${newEntry}"`);
7697
+ const updatedContent = before + updatedPluginArray + after;
7698
+ if (updatedContent === content) {
7699
+ log(`[auto-update-checker] No changes made to ${configPath}`);
7700
+ return false;
7701
+ }
7702
+ fs4.writeFileSync(configPath, updatedContent, "utf-8");
7703
+ log(`[auto-update-checker] Updated ${configPath}: ${oldEntry} \u2192 ${newEntry}`);
7704
+ return true;
7705
+ } catch (err) {
7706
+ log(`[auto-update-checker] Failed to update config file ${configPath}:`, err);
7707
+ return false;
7708
+ }
7709
+ }
7626
7710
  async function getLatestVersion() {
7627
7711
  const controller = new AbortController;
7628
7712
  const timeoutId = setTimeout(() => controller.abort(), NPM_FETCH_TIMEOUT);
@@ -7641,44 +7725,45 @@ async function getLatestVersion() {
7641
7725
  clearTimeout(timeoutId);
7642
7726
  }
7643
7727
  }
7644
- async function checkForUpdate(directory) {
7645
- if (isLocalDevMode(directory)) {
7646
- log("[auto-update-checker] Local dev mode detected, skipping update check");
7647
- return { needsUpdate: false, currentVersion: null, latestVersion: null, isLocalDev: true, isPinned: false };
7648
- }
7649
- const pluginInfo = findPluginEntry(directory);
7650
- if (!pluginInfo) {
7651
- log("[auto-update-checker] Plugin not found in config");
7652
- return { needsUpdate: false, currentVersion: null, latestVersion: null, isLocalDev: false, isPinned: false };
7653
- }
7654
- if (pluginInfo.isPinned) {
7655
- log(`[auto-update-checker] Version pinned to ${pluginInfo.pinnedVersion}, skipping update check`);
7656
- return { needsUpdate: false, currentVersion: pluginInfo.pinnedVersion, latestVersion: null, isLocalDev: false, isPinned: true };
7657
- }
7658
- const currentVersion = getCachedVersion();
7659
- if (!currentVersion) {
7660
- log("[auto-update-checker] No cached version found");
7661
- return { needsUpdate: false, currentVersion: null, latestVersion: null, isLocalDev: false, isPinned: false };
7662
- }
7663
- const latestVersion = await getLatestVersion();
7664
- if (!latestVersion) {
7665
- log("[auto-update-checker] Failed to fetch latest version");
7666
- return { needsUpdate: false, currentVersion, latestVersion: null, isLocalDev: false, isPinned: false };
7667
- }
7668
- const needsUpdate = currentVersion !== latestVersion;
7669
- log(`[auto-update-checker] Current: ${currentVersion}, Latest: ${latestVersion}, NeedsUpdate: ${needsUpdate}`);
7670
- return { needsUpdate, currentVersion, latestVersion, isLocalDev: false, isPinned: false };
7671
- }
7672
7728
 
7673
7729
  // src/hooks/auto-update-checker/cache.ts
7674
7730
  import * as fs5 from "fs";
7675
7731
  import * as path6 from "path";
7732
+ function stripTrailingCommas(json) {
7733
+ return json.replace(/,(\s*[}\]])/g, "$1");
7734
+ }
7735
+ function removeFromBunLock(packageName) {
7736
+ const lockPath = path6.join(CACHE_DIR, "bun.lock");
7737
+ if (!fs5.existsSync(lockPath))
7738
+ return false;
7739
+ try {
7740
+ const content = fs5.readFileSync(lockPath, "utf-8");
7741
+ const lock = JSON.parse(stripTrailingCommas(content));
7742
+ let modified = false;
7743
+ if (lock.workspaces?.[""]?.dependencies?.[packageName]) {
7744
+ delete lock.workspaces[""].dependencies[packageName];
7745
+ modified = true;
7746
+ }
7747
+ if (lock.packages?.[packageName]) {
7748
+ delete lock.packages[packageName];
7749
+ modified = true;
7750
+ }
7751
+ if (modified) {
7752
+ fs5.writeFileSync(lockPath, JSON.stringify(lock, null, 2));
7753
+ log(`[auto-update-checker] Removed from bun.lock: ${packageName}`);
7754
+ }
7755
+ return modified;
7756
+ } catch {
7757
+ return false;
7758
+ }
7759
+ }
7676
7760
  function invalidatePackage(packageName = PACKAGE_NAME) {
7677
7761
  try {
7678
7762
  const pkgDir = path6.join(CACHE_DIR, "node_modules", packageName);
7679
7763
  const pkgJsonPath = path6.join(CACHE_DIR, "package.json");
7680
7764
  let packageRemoved = false;
7681
7765
  let dependencyRemoved = false;
7766
+ let lockRemoved = false;
7682
7767
  if (fs5.existsSync(pkgDir)) {
7683
7768
  fs5.rmSync(pkgDir, { recursive: true, force: true });
7684
7769
  log(`[auto-update-checker] Package removed: ${pkgDir}`);
@@ -7694,7 +7779,8 @@ function invalidatePackage(packageName = PACKAGE_NAME) {
7694
7779
  dependencyRemoved = true;
7695
7780
  }
7696
7781
  }
7697
- if (!packageRemoved && !dependencyRemoved) {
7782
+ lockRemoved = removeFromBunLock(packageName);
7783
+ if (!packageRemoved && !dependencyRemoved && !lockRemoved) {
7698
7784
  log(`[auto-update-checker] Package not found, nothing to invalidate: ${packageName}`);
7699
7785
  return false;
7700
7786
  }
@@ -7707,7 +7793,7 @@ function invalidatePackage(packageName = PACKAGE_NAME) {
7707
7793
 
7708
7794
  // src/hooks/auto-update-checker/index.ts
7709
7795
  function createAutoUpdateCheckerHook(ctx, options = {}) {
7710
- const { showStartupToast = true, isSisyphusEnabled = false } = options;
7796
+ const { showStartupToast = true, isSisyphusEnabled = false, autoUpdate = true } = options;
7711
7797
  const getToastMessage = (isUpdate, latestVersion) => {
7712
7798
  if (isSisyphusEnabled) {
7713
7799
  return isUpdate ? `Sisyphus on steroids is steering OpenCode.
@@ -7716,21 +7802,9 @@ v${latestVersion} available. Restart to apply.` : `Sisyphus on steroids is steer
7716
7802
  return isUpdate ? `OpenCode is now on Steroids. oMoMoMoMo...
7717
7803
  v${latestVersion} available. Restart OpenCode to apply.` : `OpenCode is now on Steroids. oMoMoMoMo...`;
7718
7804
  };
7719
- const showVersionToast = async (version) => {
7720
- const displayVersion = version ?? "unknown";
7721
- await ctx.client.tui.showToast({
7722
- body: {
7723
- title: `OhMyOpenCode ${displayVersion}`,
7724
- message: getToastMessage(false),
7725
- variant: "info",
7726
- duration: 5000
7727
- }
7728
- }).catch(() => {});
7729
- log(`[auto-update-checker] Startup toast shown: v${displayVersion}`);
7730
- };
7731
7805
  let hasChecked = false;
7732
7806
  return {
7733
- event: async ({ event }) => {
7807
+ event: ({ event }) => {
7734
7808
  if (event.type !== "session.created")
7735
7809
  return;
7736
7810
  if (hasChecked)
@@ -7739,47 +7813,69 @@ v${latestVersion} available. Restart OpenCode to apply.` : `OpenCode is now on S
7739
7813
  if (props?.info?.parentID)
7740
7814
  return;
7741
7815
  hasChecked = true;
7742
- try {
7743
- const result = await checkForUpdate(ctx.directory);
7744
- if (result.isLocalDev) {
7745
- log("[auto-update-checker] Skipped: local development mode");
7746
- if (showStartupToast) {
7747
- const version = getLocalDevVersion(ctx.directory) ?? getCachedVersion();
7748
- await showVersionToast(version);
7749
- }
7750
- return;
7751
- }
7752
- if (result.isPinned) {
7753
- log(`[auto-update-checker] Skipped: version pinned to ${result.currentVersion}`);
7816
+ setTimeout(() => {
7817
+ const cachedVersion = getCachedVersion();
7818
+ const localDevVersion = getLocalDevVersion(ctx.directory);
7819
+ const displayVersion = localDevVersion ?? cachedVersion;
7820
+ showConfigErrorsIfAny(ctx).catch(() => {});
7821
+ if (localDevVersion) {
7754
7822
  if (showStartupToast) {
7755
- await showVersionToast(result.currentVersion);
7823
+ showLocalDevToast(ctx, displayVersion, isSisyphusEnabled).catch(() => {});
7756
7824
  }
7825
+ log("[auto-update-checker] Local development mode");
7757
7826
  return;
7758
7827
  }
7759
- if (!result.needsUpdate) {
7760
- log("[auto-update-checker] No update needed");
7761
- if (showStartupToast) {
7762
- await showVersionToast(result.currentVersion);
7763
- }
7764
- return;
7828
+ if (showStartupToast) {
7829
+ showVersionToast(ctx, displayVersion, getToastMessage(false)).catch(() => {});
7765
7830
  }
7766
- invalidatePackage(PACKAGE_NAME);
7767
- await ctx.client.tui.showToast({
7768
- body: {
7769
- title: `OhMyOpenCode ${result.latestVersion}`,
7770
- message: getToastMessage(true, result.latestVersion ?? undefined),
7771
- variant: "info",
7772
- duration: 8000
7773
- }
7774
- }).catch(() => {});
7775
- log(`[auto-update-checker] Update notification sent: v${result.currentVersion} \u2192 v${result.latestVersion}`);
7776
- } catch (err) {
7777
- log("[auto-update-checker] Error during update check:", err);
7778
- }
7779
- await showConfigErrorsIfAny(ctx);
7831
+ runBackgroundUpdateCheck(ctx, autoUpdate, getToastMessage).catch((err) => {
7832
+ log("[auto-update-checker] Background update check failed:", err);
7833
+ });
7834
+ }, 0);
7780
7835
  }
7781
7836
  };
7782
7837
  }
7838
+ async function runBackgroundUpdateCheck(ctx, autoUpdate, getToastMessage) {
7839
+ const pluginInfo = findPluginEntry(ctx.directory);
7840
+ if (!pluginInfo) {
7841
+ log("[auto-update-checker] Plugin not found in config");
7842
+ return;
7843
+ }
7844
+ const cachedVersion = getCachedVersion();
7845
+ const currentVersion = cachedVersion ?? pluginInfo.pinnedVersion;
7846
+ if (!currentVersion) {
7847
+ log("[auto-update-checker] No version found (cached or pinned)");
7848
+ return;
7849
+ }
7850
+ const latestVersion = await getLatestVersion();
7851
+ if (!latestVersion) {
7852
+ log("[auto-update-checker] Failed to fetch latest version");
7853
+ return;
7854
+ }
7855
+ if (currentVersion === latestVersion) {
7856
+ log("[auto-update-checker] Already on latest version");
7857
+ return;
7858
+ }
7859
+ log(`[auto-update-checker] Update available: ${currentVersion} \u2192 ${latestVersion}`);
7860
+ if (!autoUpdate) {
7861
+ await showUpdateAvailableToast(ctx, latestVersion, getToastMessage);
7862
+ log("[auto-update-checker] Auto-update disabled, notification only");
7863
+ return;
7864
+ }
7865
+ if (pluginInfo.isPinned) {
7866
+ const updated = updatePinnedVersion(pluginInfo.configPath, pluginInfo.entry, latestVersion);
7867
+ if (updated) {
7868
+ invalidatePackage(PACKAGE_NAME);
7869
+ await showAutoUpdatedToast(ctx, currentVersion, latestVersion);
7870
+ log(`[auto-update-checker] Config updated: ${pluginInfo.entry} \u2192 ${PACKAGE_NAME}@${latestVersion}`);
7871
+ } else {
7872
+ await showUpdateAvailableToast(ctx, latestVersion, getToastMessage);
7873
+ }
7874
+ } else {
7875
+ invalidatePackage(PACKAGE_NAME);
7876
+ await showUpdateAvailableToast(ctx, latestVersion, getToastMessage);
7877
+ }
7878
+ }
7783
7879
  async function showConfigErrorsIfAny(ctx) {
7784
7880
  const errors = getConfigLoadErrors();
7785
7881
  if (errors.length === 0)
@@ -7798,12 +7894,60 @@ ${errorMessages}`,
7798
7894
  log(`[auto-update-checker] Config load errors shown: ${errors.length} error(s)`);
7799
7895
  clearConfigLoadErrors();
7800
7896
  }
7897
+ async function showVersionToast(ctx, version, message) {
7898
+ const displayVersion = version ?? "unknown";
7899
+ await ctx.client.tui.showToast({
7900
+ body: {
7901
+ title: `OhMyOpenCode ${displayVersion}`,
7902
+ message,
7903
+ variant: "info",
7904
+ duration: 5000
7905
+ }
7906
+ }).catch(() => {});
7907
+ log(`[auto-update-checker] Startup toast shown: v${displayVersion}`);
7908
+ }
7909
+ async function showUpdateAvailableToast(ctx, latestVersion, getToastMessage) {
7910
+ await ctx.client.tui.showToast({
7911
+ body: {
7912
+ title: `OhMyOpenCode ${latestVersion}`,
7913
+ message: getToastMessage(true, latestVersion),
7914
+ variant: "info",
7915
+ duration: 8000
7916
+ }
7917
+ }).catch(() => {});
7918
+ log(`[auto-update-checker] Update available toast shown: v${latestVersion}`);
7919
+ }
7920
+ async function showAutoUpdatedToast(ctx, oldVersion, newVersion) {
7921
+ await ctx.client.tui.showToast({
7922
+ body: {
7923
+ title: `OhMyOpenCode Updated!`,
7924
+ message: `v${oldVersion} \u2192 v${newVersion}
7925
+ Restart OpenCode to apply.`,
7926
+ variant: "success",
7927
+ duration: 8000
7928
+ }
7929
+ }).catch(() => {});
7930
+ log(`[auto-update-checker] Auto-updated toast shown: v${oldVersion} \u2192 v${newVersion}`);
7931
+ }
7932
+ async function showLocalDevToast(ctx, version, isSisyphusEnabled) {
7933
+ const displayVersion = version ?? "dev";
7934
+ const message = isSisyphusEnabled ? "Sisyphus running in local development mode." : "Running in local development mode. oMoMoMo...";
7935
+ await ctx.client.tui.showToast({
7936
+ body: {
7937
+ title: `OhMyOpenCode ${displayVersion} (dev)`,
7938
+ message,
7939
+ variant: "warning",
7940
+ duration: 5000
7941
+ }
7942
+ }).catch(() => {});
7943
+ log(`[auto-update-checker] Local dev toast shown: v${displayVersion}`);
7944
+ }
7801
7945
  // src/hooks/agent-usage-reminder/storage.ts
7802
7946
  import {
7803
7947
  existsSync as existsSync21,
7804
7948
  mkdirSync as mkdirSync8,
7805
7949
  readFileSync as readFileSync13,
7806
- writeFileSync as writeFileSync9,
7950
+ writeFileSync as writeFileSync10,
7807
7951
  unlinkSync as unlinkSync7
7808
7952
  } from "fs";
7809
7953
  import { join as join30 } from "path";
@@ -7874,7 +8018,7 @@ function saveAgentUsageState(state2) {
7874
8018
  mkdirSync8(AGENT_USAGE_REMINDER_STORAGE, { recursive: true });
7875
8019
  }
7876
8020
  const filePath = getStoragePath4(state2.sessionID);
7877
- writeFileSync9(filePath, JSON.stringify(state2, null, 2));
8021
+ writeFileSync10(filePath, JSON.stringify(state2, null, 2));
7878
8022
  }
7879
8023
  function clearAgentUsageState(sessionID) {
7880
8024
  const filePath = getStoragePath4(sessionID);
@@ -7980,6 +8124,14 @@ TELL THE USER WHAT AGENTS YOU WILL LEVERAGE NOW TO SATISFY USER'S REQUEST.
7980
8124
  3. Always Use Plan agent with gathered context to create detailed work breakdown
7981
8125
  4. Execute with continuous verification against original requirements
7982
8126
 
8127
+ ## ZERO TOLERANCE FAILURES
8128
+ - **NO Scope Reduction**: Never make "demo", "skeleton", "simplified", "basic" versions - deliver FULL implementation
8129
+ - **NO Partial Completion**: Never stop at 60-80% saying "you can extend this..." - finish 100%
8130
+ - **NO Assumed Shortcuts**: Never skip requirements you deem "optional" or "can be added later"
8131
+ - **NO Premature Stopping**: Never declare done until ALL TODOs are completed and verified
8132
+
8133
+ THE USER ASKED FOR X. DELIVER EXACTLY X. NOT A SUBSET. NOT A DEMO. NOT A STARTING POINT.
8134
+
7983
8135
  </ultrawork-mode>
7984
8136
 
7985
8137
  ---
@@ -7998,18 +8150,17 @@ NEVER stop at first result - be exhaustive.`
7998
8150
  {
7999
8151
  pattern: /\b(analyze|analyse|investigate|examine|research|study|deep[\s-]?dive|inspect|audit|evaluate|assess|review|diagnose|scrutinize|dissect|debug|comprehend|interpret|breakdown|understand)\b|why\s+is|how\s+does|how\s+to|\uBD84\uC11D|\uC870\uC0AC|\uD30C\uC545|\uC5F0\uAD6C|\uAC80\uD1A0|\uC9C4\uB2E8|\uC774\uD574|\uC124\uBA85|\uC6D0\uC778|\uC774\uC720|\uB72F\uC5B4\uBD10|\uB530\uC838\uBD10|\uD3C9\uAC00|\uD574\uC11D|\uB514\uBC84\uAE45|\uB514\uBC84\uADF8|\uC5B4\uB5BB\uAC8C|\uC65C|\uC0B4\uD3B4|\u5206\u6790|\u8ABF\u67FB|\u89E3\u6790|\u691C\u8A0E|\u7814\u7A76|\u8A3A\u65AD|\u7406\u89E3|\u8AAC\u660E|\u691C\u8A3C|\u7CBE\u67FB|\u7A76\u660E|\u30C7\u30D0\u30C3\u30B0|\u306A\u305C|\u3069\u3046|\u4ED5\u7D44\u307F|\u8C03\u67E5|\u68C0\u67E5|\u5256\u6790|\u6DF1\u5165|\u8BCA\u65AD|\u89E3\u91CA|\u8C03\u8BD5|\u4E3A\u4EC0\u4E48|\u539F\u7406|\u641E\u6E05\u695A|\u5F04\u660E\u767D|ph\u00E2n t\u00EDch|\u0111i\u1EC1u tra|nghi\u00EAn c\u1EE9u|ki\u1EC3m tra|xem x\u00E9t|ch\u1EA9n \u0111o\u00E1n|gi\u1EA3i th\u00EDch|t\u00ECm hi\u1EC3u|g\u1EE1 l\u1ED7i|t\u1EA1i sao/i,
8000
8152
  message: `[analyze-mode]
8001
- DEEP ANALYSIS MODE. Execute in phases:
8153
+ ANALYSIS MODE. Gather context before diving deep:
8002
8154
 
8003
- PHASE 1 - GATHER CONTEXT (10+ agents parallel):
8004
- - 3+ explore agents (codebase structure, patterns, implementations)
8005
- - 3+ librarian agents (official docs, best practices, examples)
8006
- - 2+ general agents (different analytical perspectives)
8155
+ CONTEXT GATHERING (parallel):
8156
+ - 1-2 explore agents (codebase patterns, implementations)
8157
+ - 1-2 librarian agents (if external library involved)
8158
+ - Direct tools: Grep, AST-grep, LSP for targeted searches
8007
8159
 
8008
- PHASE 2 - EXPERT CONSULTATION (after Phase 1):
8009
- - 3+ oracle agents in parallel with gathered context
8010
- - Each oracle: different angle (architecture, performance, edge cases)
8160
+ IF COMPLEX (architecture, multi-system, debugging after 2+ failures):
8161
+ - Consult oracle for strategic guidance
8011
8162
 
8012
- SYNTHESIZE: Cross-reference findings, identify consensus & contradictions.`
8163
+ SYNTHESIZE findings before proceeding.`
8013
8164
  }
8014
8165
  ];
8015
8166
 
@@ -8026,9 +8177,16 @@ function extractPromptText2(parts) {
8026
8177
  }
8027
8178
 
8028
8179
  // src/hooks/keyword-detector/index.ts
8180
+ var sessionFirstMessageProcessed2 = new Set;
8029
8181
  function createKeywordDetectorHook() {
8030
8182
  return {
8031
8183
  "chat.message": async (input, output) => {
8184
+ const isFirstMessage = !sessionFirstMessageProcessed2.has(input.sessionID);
8185
+ sessionFirstMessageProcessed2.add(input.sessionID);
8186
+ if (isFirstMessage) {
8187
+ log("Skipping keyword detection on first message for title generation", { sessionID: input.sessionID });
8188
+ return;
8189
+ }
8032
8190
  const promptText = extractPromptText2(output.parts);
8033
8191
  const messages = detectKeywords(promptText);
8034
8192
  if (messages.length === 0) {
@@ -8058,7 +8216,13 @@ var NON_INTERACTIVE_ENV = {
8058
8216
  DEBIAN_FRONTEND: "noninteractive",
8059
8217
  GIT_TERMINAL_PROMPT: "0",
8060
8218
  GCM_INTERACTIVE: "never",
8061
- HOMEBREW_NO_AUTO_UPDATE: "1"
8219
+ HOMEBREW_NO_AUTO_UPDATE: "1",
8220
+ GIT_EDITOR: "true",
8221
+ EDITOR: "true",
8222
+ VISUAL: "true",
8223
+ GIT_SEQUENCE_EDITOR: "true",
8224
+ GIT_PAGER: "cat",
8225
+ PAGER: "cat"
8062
8226
  };
8063
8227
 
8064
8228
  // src/hooks/non-interactive-env/index.ts
@@ -8088,7 +8252,7 @@ import {
8088
8252
  existsSync as existsSync22,
8089
8253
  mkdirSync as mkdirSync9,
8090
8254
  readFileSync as readFileSync14,
8091
- writeFileSync as writeFileSync10,
8255
+ writeFileSync as writeFileSync11,
8092
8256
  unlinkSync as unlinkSync8
8093
8257
  } from "fs";
8094
8258
  import { join as join32 } from "path";
@@ -8136,7 +8300,7 @@ function saveInteractiveBashSessionState(state2) {
8136
8300
  tmuxSessions: Array.from(state2.tmuxSessions),
8137
8301
  updatedAt: state2.updatedAt
8138
8302
  };
8139
- writeFileSync10(filePath, JSON.stringify(serialized, null, 2));
8303
+ writeFileSync11(filePath, JSON.stringify(serialized, null, 2));
8140
8304
  }
8141
8305
  function clearInteractiveBashSessionState(sessionID) {
8142
8306
  const filePath = getStoragePath5(sessionID);
@@ -10525,6 +10689,10 @@ var BUILTIN_SERVERS = {
10525
10689
  command: ["astro-ls", "--stdio"],
10526
10690
  extensions: [".astro"]
10527
10691
  },
10692
+ "bash-ls": {
10693
+ command: ["bash-language-server", "start"],
10694
+ extensions: [".sh", ".bash", ".zsh", ".ksh"]
10695
+ },
10528
10696
  jdtls: {
10529
10697
  command: ["jdtls"],
10530
10698
  extensions: [".java"]
@@ -11311,7 +11479,7 @@ ${msg}`);
11311
11479
  }
11312
11480
  // src/tools/lsp/utils.ts
11313
11481
  import { extname as extname2, resolve as resolve6 } from "path";
11314
- import { existsSync as existsSync28, readFileSync as readFileSync20, writeFileSync as writeFileSync11 } from "fs";
11482
+ import { existsSync as existsSync28, readFileSync as readFileSync20, writeFileSync as writeFileSync12 } from "fs";
11315
11483
  function findWorkspaceRoot(filePath) {
11316
11484
  let dir = resolve6(filePath);
11317
11485
  if (!existsSync28(dir) || !__require("fs").statSync(dir).isDirectory()) {
@@ -11503,7 +11671,7 @@ function applyTextEditsToFile(filePath, edits) {
11503
11671
  `));
11504
11672
  }
11505
11673
  }
11506
- writeFileSync11(filePath, lines.join(`
11674
+ writeFileSync12(filePath, lines.join(`
11507
11675
  `), "utf-8");
11508
11676
  return { success: true, editCount: edits.length };
11509
11677
  } catch (err) {
@@ -11534,7 +11702,7 @@ function applyWorkspaceEdit(edit) {
11534
11702
  if (change.kind === "create") {
11535
11703
  try {
11536
11704
  const filePath = change.uri.replace("file://", "");
11537
- writeFileSync11(filePath, "", "utf-8");
11705
+ writeFileSync12(filePath, "", "utf-8");
11538
11706
  result.filesModified.push(filePath);
11539
11707
  } catch (err) {
11540
11708
  result.success = false;
@@ -11545,7 +11713,7 @@ function applyWorkspaceEdit(edit) {
11545
11713
  const oldPath = change.oldUri.replace("file://", "");
11546
11714
  const newPath = change.newUri.replace("file://", "");
11547
11715
  const content = readFileSync20(oldPath, "utf-8");
11548
- writeFileSync11(newPath, content, "utf-8");
11716
+ writeFileSync12(newPath, content, "utf-8");
11549
11717
  __require("fs").unlinkSync(oldPath);
11550
11718
  result.filesModified.push(newPath);
11551
11719
  } catch (err) {
@@ -26799,7 +26967,8 @@ var OhMyOpenCodeConfigSchema = exports_external.object({
26799
26967
  claude_code: ClaudeCodeConfigSchema.optional(),
26800
26968
  google_auth: exports_external.boolean().optional(),
26801
26969
  sisyphus_agent: SisyphusAgentConfigSchema.optional(),
26802
- experimental: ExperimentalConfigSchema.optional()
26970
+ experimental: ExperimentalConfigSchema.optional(),
26971
+ auto_update: exports_external.boolean().optional()
26803
26972
  });
26804
26973
  // src/agents/plan-prompt.ts
26805
26974
  var PLAN_SYSTEM_PROMPT = `<system-reminder>
@@ -27018,7 +27187,8 @@ var OhMyOpenCodePlugin = async (ctx) => {
27018
27187
  const rulesInjector = isHookEnabled("rules-injector") ? createRulesInjectorHook(ctx) : null;
27019
27188
  const autoUpdateChecker = isHookEnabled("auto-update-checker") ? createAutoUpdateCheckerHook(ctx, {
27020
27189
  showStartupToast: isHookEnabled("startup-toast"),
27021
- isSisyphusEnabled: pluginConfig.sisyphus_agent?.disabled !== true
27190
+ isSisyphusEnabled: pluginConfig.sisyphus_agent?.disabled !== true,
27191
+ autoUpdate: pluginConfig.auto_update ?? true
27022
27192
  }) : null;
27023
27193
  const keywordDetector = isHookEnabled("keyword-detector") ? createKeywordDetectorHook() : null;
27024
27194
  const agentUsageReminder = isHookEnabled("agent-usage-reminder") ? createAgentUsageReminderHook(ctx) : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-opencode",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
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",
@@ -24,7 +24,8 @@
24
24
  "build:schema": "bun run script/build-schema.ts",
25
25
  "clean": "rm -rf dist",
26
26
  "prepublishOnly": "bun run clean && bun run build",
27
- "typecheck": "tsc --noEmit"
27
+ "typecheck": "tsc --noEmit",
28
+ "test": "bun test"
28
29
  },
29
30
  "keywords": [
30
31
  "opencode",
@@ -1,12 +0,0 @@
1
- import type { PluginInput } from "@opencode-ai/plugin";
2
- export declare function createGrepOutputTruncatorHook(ctx: PluginInput): {
3
- "tool.execute.after": (input: {
4
- tool: string;
5
- sessionID: string;
6
- callID: string;
7
- }, output: {
8
- title: string;
9
- output: string;
10
- metadata: unknown;
11
- }) => Promise<void>;
12
- };