oh-my-opencode 2.3.1 → 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
  }
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")
@@ -5710,12 +5749,16 @@ async function executeCompact(sessionID, msg, autoCompactState, client, director
5710
5749
  });
5711
5750
  fallbackState.revertAttempt++;
5712
5751
  fallbackState.lastRevertedMessageID = pair.userMessageID;
5713
- retryState.attempt = 0;
5714
- truncateState.truncateAttempt = 0;
5715
- autoCompactState.compactionInProgress.delete(sessionID);
5716
- setTimeout(() => {
5717
- executeCompact(sessionID, msg, autoCompactState, client, directory, experimental);
5718
- }, 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);
5719
5762
  return;
5720
5763
  } catch {}
5721
5764
  } else {
@@ -7504,9 +7547,6 @@ var USER_OPENCODE_CONFIG = path4.join(USER_CONFIG_DIR, "opencode", "opencode.jso
7504
7547
  var USER_OPENCODE_CONFIG_JSONC = path4.join(USER_CONFIG_DIR, "opencode", "opencode.jsonc");
7505
7548
 
7506
7549
  // src/hooks/auto-update-checker/checker.ts
7507
- function isLocalDevMode(directory) {
7508
- return getLocalDevPath(directory) !== null;
7509
- }
7510
7550
  function stripJsonComments(json) {
7511
7551
  return json.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (m, g) => g ? "" : m).replace(/,(\s*[}\]])/g, "$1");
7512
7552
  }
@@ -7588,12 +7628,12 @@ function findPluginEntry(directory) {
7588
7628
  const plugins = config.plugin ?? [];
7589
7629
  for (const entry of plugins) {
7590
7630
  if (entry === PACKAGE_NAME) {
7591
- return { entry, isPinned: false, pinnedVersion: null };
7631
+ return { entry, isPinned: false, pinnedVersion: null, configPath };
7592
7632
  }
7593
7633
  if (entry.startsWith(`${PACKAGE_NAME}@`)) {
7594
7634
  const pinnedVersion = entry.slice(PACKAGE_NAME.length + 1);
7595
7635
  const isPinned = pinnedVersion !== "latest";
7596
- return { entry, isPinned, pinnedVersion: isPinned ? pinnedVersion : null };
7636
+ return { entry, isPinned, pinnedVersion: isPinned ? pinnedVersion : null, configPath };
7597
7637
  }
7598
7638
  }
7599
7639
  } catch {
@@ -7625,6 +7665,48 @@ function getCachedVersion() {
7625
7665
  }
7626
7666
  return null;
7627
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
+ }
7628
7710
  async function getLatestVersion() {
7629
7711
  const controller = new AbortController;
7630
7712
  const timeoutId = setTimeout(() => controller.abort(), NPM_FETCH_TIMEOUT);
@@ -7643,34 +7725,6 @@ async function getLatestVersion() {
7643
7725
  clearTimeout(timeoutId);
7644
7726
  }
7645
7727
  }
7646
- async function checkForUpdate(directory) {
7647
- if (isLocalDevMode(directory)) {
7648
- log("[auto-update-checker] Local dev mode detected, skipping update check");
7649
- return { needsUpdate: false, currentVersion: null, latestVersion: null, isLocalDev: true, isPinned: false };
7650
- }
7651
- const pluginInfo = findPluginEntry(directory);
7652
- if (!pluginInfo) {
7653
- log("[auto-update-checker] Plugin not found in config");
7654
- return { needsUpdate: false, currentVersion: null, latestVersion: null, isLocalDev: false, isPinned: false };
7655
- }
7656
- if (pluginInfo.isPinned) {
7657
- log(`[auto-update-checker] Version pinned to ${pluginInfo.pinnedVersion}, skipping update check`);
7658
- return { needsUpdate: false, currentVersion: pluginInfo.pinnedVersion, latestVersion: null, isLocalDev: false, isPinned: true };
7659
- }
7660
- const currentVersion = getCachedVersion();
7661
- if (!currentVersion) {
7662
- log("[auto-update-checker] No cached version found");
7663
- return { needsUpdate: false, currentVersion: null, latestVersion: null, isLocalDev: false, isPinned: false };
7664
- }
7665
- const latestVersion = await getLatestVersion();
7666
- if (!latestVersion) {
7667
- log("[auto-update-checker] Failed to fetch latest version");
7668
- return { needsUpdate: false, currentVersion, latestVersion: null, isLocalDev: false, isPinned: false };
7669
- }
7670
- const needsUpdate = currentVersion !== latestVersion;
7671
- log(`[auto-update-checker] Current: ${currentVersion}, Latest: ${latestVersion}, NeedsUpdate: ${needsUpdate}`);
7672
- return { needsUpdate, currentVersion, latestVersion, isLocalDev: false, isPinned: false };
7673
- }
7674
7728
 
7675
7729
  // src/hooks/auto-update-checker/cache.ts
7676
7730
  import * as fs5 from "fs";
@@ -7739,7 +7793,7 @@ function invalidatePackage(packageName = PACKAGE_NAME) {
7739
7793
 
7740
7794
  // src/hooks/auto-update-checker/index.ts
7741
7795
  function createAutoUpdateCheckerHook(ctx, options = {}) {
7742
- const { showStartupToast = true, isSisyphusEnabled = false } = options;
7796
+ const { showStartupToast = true, isSisyphusEnabled = false, autoUpdate = true } = options;
7743
7797
  const getToastMessage = (isUpdate, latestVersion) => {
7744
7798
  if (isSisyphusEnabled) {
7745
7799
  return isUpdate ? `Sisyphus on steroids is steering OpenCode.
@@ -7748,21 +7802,9 @@ v${latestVersion} available. Restart to apply.` : `Sisyphus on steroids is steer
7748
7802
  return isUpdate ? `OpenCode is now on Steroids. oMoMoMoMo...
7749
7803
  v${latestVersion} available. Restart OpenCode to apply.` : `OpenCode is now on Steroids. oMoMoMoMo...`;
7750
7804
  };
7751
- const showVersionToast = async (version) => {
7752
- const displayVersion = version ?? "unknown";
7753
- await ctx.client.tui.showToast({
7754
- body: {
7755
- title: `OhMyOpenCode ${displayVersion}`,
7756
- message: getToastMessage(false),
7757
- variant: "info",
7758
- duration: 5000
7759
- }
7760
- }).catch(() => {});
7761
- log(`[auto-update-checker] Startup toast shown: v${displayVersion}`);
7762
- };
7763
7805
  let hasChecked = false;
7764
7806
  return {
7765
- event: async ({ event }) => {
7807
+ event: ({ event }) => {
7766
7808
  if (event.type !== "session.created")
7767
7809
  return;
7768
7810
  if (hasChecked)
@@ -7771,47 +7813,69 @@ v${latestVersion} available. Restart OpenCode to apply.` : `OpenCode is now on S
7771
7813
  if (props?.info?.parentID)
7772
7814
  return;
7773
7815
  hasChecked = true;
7774
- try {
7775
- const result = await checkForUpdate(ctx.directory);
7776
- if (result.isLocalDev) {
7777
- log("[auto-update-checker] Skipped: local development mode");
7816
+ setTimeout(() => {
7817
+ const cachedVersion = getCachedVersion();
7818
+ const localDevVersion = getLocalDevVersion(ctx.directory);
7819
+ const displayVersion = localDevVersion ?? cachedVersion;
7820
+ showConfigErrorsIfAny(ctx).catch(() => {});
7821
+ if (localDevVersion) {
7778
7822
  if (showStartupToast) {
7779
- const version = getLocalDevVersion(ctx.directory) ?? getCachedVersion();
7780
- await showVersionToast(version);
7823
+ showLocalDevToast(ctx, displayVersion, isSisyphusEnabled).catch(() => {});
7781
7824
  }
7825
+ log("[auto-update-checker] Local development mode");
7782
7826
  return;
7783
7827
  }
7784
- if (result.isPinned) {
7785
- log(`[auto-update-checker] Skipped: version pinned to ${result.currentVersion}`);
7786
- if (showStartupToast) {
7787
- await showVersionToast(result.currentVersion);
7788
- }
7789
- return;
7828
+ if (showStartupToast) {
7829
+ showVersionToast(ctx, displayVersion, getToastMessage(false)).catch(() => {});
7790
7830
  }
7791
- if (!result.needsUpdate) {
7792
- log("[auto-update-checker] No update needed");
7793
- if (showStartupToast) {
7794
- await showVersionToast(result.currentVersion);
7795
- }
7796
- return;
7797
- }
7798
- invalidatePackage(PACKAGE_NAME);
7799
- await ctx.client.tui.showToast({
7800
- body: {
7801
- title: `OhMyOpenCode ${result.latestVersion}`,
7802
- message: getToastMessage(true, result.latestVersion ?? undefined),
7803
- variant: "info",
7804
- duration: 8000
7805
- }
7806
- }).catch(() => {});
7807
- log(`[auto-update-checker] Update notification sent: v${result.currentVersion} \u2192 v${result.latestVersion}`);
7808
- } catch (err) {
7809
- log("[auto-update-checker] Error during update check:", err);
7810
- }
7811
- await showConfigErrorsIfAny(ctx);
7831
+ runBackgroundUpdateCheck(ctx, autoUpdate, getToastMessage).catch((err) => {
7832
+ log("[auto-update-checker] Background update check failed:", err);
7833
+ });
7834
+ }, 0);
7812
7835
  }
7813
7836
  };
7814
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
+ }
7815
7879
  async function showConfigErrorsIfAny(ctx) {
7816
7880
  const errors = getConfigLoadErrors();
7817
7881
  if (errors.length === 0)
@@ -7830,12 +7894,60 @@ ${errorMessages}`,
7830
7894
  log(`[auto-update-checker] Config load errors shown: ${errors.length} error(s)`);
7831
7895
  clearConfigLoadErrors();
7832
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
+ }
7833
7945
  // src/hooks/agent-usage-reminder/storage.ts
7834
7946
  import {
7835
7947
  existsSync as existsSync21,
7836
7948
  mkdirSync as mkdirSync8,
7837
7949
  readFileSync as readFileSync13,
7838
- writeFileSync as writeFileSync9,
7950
+ writeFileSync as writeFileSync10,
7839
7951
  unlinkSync as unlinkSync7
7840
7952
  } from "fs";
7841
7953
  import { join as join30 } from "path";
@@ -7906,7 +8018,7 @@ function saveAgentUsageState(state2) {
7906
8018
  mkdirSync8(AGENT_USAGE_REMINDER_STORAGE, { recursive: true });
7907
8019
  }
7908
8020
  const filePath = getStoragePath4(state2.sessionID);
7909
- writeFileSync9(filePath, JSON.stringify(state2, null, 2));
8021
+ writeFileSync10(filePath, JSON.stringify(state2, null, 2));
7910
8022
  }
7911
8023
  function clearAgentUsageState(sessionID) {
7912
8024
  const filePath = getStoragePath4(sessionID);
@@ -8012,6 +8124,14 @@ TELL THE USER WHAT AGENTS YOU WILL LEVERAGE NOW TO SATISFY USER'S REQUEST.
8012
8124
  3. Always Use Plan agent with gathered context to create detailed work breakdown
8013
8125
  4. Execute with continuous verification against original requirements
8014
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
+
8015
8135
  </ultrawork-mode>
8016
8136
 
8017
8137
  ---
@@ -8030,18 +8150,17 @@ NEVER stop at first result - be exhaustive.`
8030
8150
  {
8031
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,
8032
8152
  message: `[analyze-mode]
8033
- DEEP ANALYSIS MODE. Execute in phases:
8153
+ ANALYSIS MODE. Gather context before diving deep:
8034
8154
 
8035
- PHASE 1 - GATHER CONTEXT (10+ agents parallel):
8036
- - 3+ explore agents (codebase structure, patterns, implementations)
8037
- - 3+ librarian agents (official docs, best practices, examples)
8038
- - 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
8039
8159
 
8040
- PHASE 2 - EXPERT CONSULTATION (after Phase 1):
8041
- - 3+ oracle agents in parallel with gathered context
8042
- - Each oracle: different angle (architecture, performance, edge cases)
8160
+ IF COMPLEX (architecture, multi-system, debugging after 2+ failures):
8161
+ - Consult oracle for strategic guidance
8043
8162
 
8044
- SYNTHESIZE: Cross-reference findings, identify consensus & contradictions.`
8163
+ SYNTHESIZE findings before proceeding.`
8045
8164
  }
8046
8165
  ];
8047
8166
 
@@ -8058,9 +8177,16 @@ function extractPromptText2(parts) {
8058
8177
  }
8059
8178
 
8060
8179
  // src/hooks/keyword-detector/index.ts
8180
+ var sessionFirstMessageProcessed2 = new Set;
8061
8181
  function createKeywordDetectorHook() {
8062
8182
  return {
8063
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
+ }
8064
8190
  const promptText = extractPromptText2(output.parts);
8065
8191
  const messages = detectKeywords(promptText);
8066
8192
  if (messages.length === 0) {
@@ -8090,7 +8216,13 @@ var NON_INTERACTIVE_ENV = {
8090
8216
  DEBIAN_FRONTEND: "noninteractive",
8091
8217
  GIT_TERMINAL_PROMPT: "0",
8092
8218
  GCM_INTERACTIVE: "never",
8093
- 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"
8094
8226
  };
8095
8227
 
8096
8228
  // src/hooks/non-interactive-env/index.ts
@@ -8120,7 +8252,7 @@ import {
8120
8252
  existsSync as existsSync22,
8121
8253
  mkdirSync as mkdirSync9,
8122
8254
  readFileSync as readFileSync14,
8123
- writeFileSync as writeFileSync10,
8255
+ writeFileSync as writeFileSync11,
8124
8256
  unlinkSync as unlinkSync8
8125
8257
  } from "fs";
8126
8258
  import { join as join32 } from "path";
@@ -8168,7 +8300,7 @@ function saveInteractiveBashSessionState(state2) {
8168
8300
  tmuxSessions: Array.from(state2.tmuxSessions),
8169
8301
  updatedAt: state2.updatedAt
8170
8302
  };
8171
- writeFileSync10(filePath, JSON.stringify(serialized, null, 2));
8303
+ writeFileSync11(filePath, JSON.stringify(serialized, null, 2));
8172
8304
  }
8173
8305
  function clearInteractiveBashSessionState(sessionID) {
8174
8306
  const filePath = getStoragePath5(sessionID);
@@ -11347,7 +11479,7 @@ ${msg}`);
11347
11479
  }
11348
11480
  // src/tools/lsp/utils.ts
11349
11481
  import { extname as extname2, resolve as resolve6 } from "path";
11350
- 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";
11351
11483
  function findWorkspaceRoot(filePath) {
11352
11484
  let dir = resolve6(filePath);
11353
11485
  if (!existsSync28(dir) || !__require("fs").statSync(dir).isDirectory()) {
@@ -11539,7 +11671,7 @@ function applyTextEditsToFile(filePath, edits) {
11539
11671
  `));
11540
11672
  }
11541
11673
  }
11542
- writeFileSync11(filePath, lines.join(`
11674
+ writeFileSync12(filePath, lines.join(`
11543
11675
  `), "utf-8");
11544
11676
  return { success: true, editCount: edits.length };
11545
11677
  } catch (err) {
@@ -11570,7 +11702,7 @@ function applyWorkspaceEdit(edit) {
11570
11702
  if (change.kind === "create") {
11571
11703
  try {
11572
11704
  const filePath = change.uri.replace("file://", "");
11573
- writeFileSync11(filePath, "", "utf-8");
11705
+ writeFileSync12(filePath, "", "utf-8");
11574
11706
  result.filesModified.push(filePath);
11575
11707
  } catch (err) {
11576
11708
  result.success = false;
@@ -11581,7 +11713,7 @@ function applyWorkspaceEdit(edit) {
11581
11713
  const oldPath = change.oldUri.replace("file://", "");
11582
11714
  const newPath = change.newUri.replace("file://", "");
11583
11715
  const content = readFileSync20(oldPath, "utf-8");
11584
- writeFileSync11(newPath, content, "utf-8");
11716
+ writeFileSync12(newPath, content, "utf-8");
11585
11717
  __require("fs").unlinkSync(oldPath);
11586
11718
  result.filesModified.push(newPath);
11587
11719
  } catch (err) {
@@ -26835,7 +26967,8 @@ var OhMyOpenCodeConfigSchema = exports_external.object({
26835
26967
  claude_code: ClaudeCodeConfigSchema.optional(),
26836
26968
  google_auth: exports_external.boolean().optional(),
26837
26969
  sisyphus_agent: SisyphusAgentConfigSchema.optional(),
26838
- experimental: ExperimentalConfigSchema.optional()
26970
+ experimental: ExperimentalConfigSchema.optional(),
26971
+ auto_update: exports_external.boolean().optional()
26839
26972
  });
26840
26973
  // src/agents/plan-prompt.ts
26841
26974
  var PLAN_SYSTEM_PROMPT = `<system-reminder>
@@ -27054,7 +27187,8 @@ var OhMyOpenCodePlugin = async (ctx) => {
27054
27187
  const rulesInjector = isHookEnabled("rules-injector") ? createRulesInjectorHook(ctx) : null;
27055
27188
  const autoUpdateChecker = isHookEnabled("auto-update-checker") ? createAutoUpdateCheckerHook(ctx, {
27056
27189
  showStartupToast: isHookEnabled("startup-toast"),
27057
- isSisyphusEnabled: pluginConfig.sisyphus_agent?.disabled !== true
27190
+ isSisyphusEnabled: pluginConfig.sisyphus_agent?.disabled !== true,
27191
+ autoUpdate: pluginConfig.auto_update ?? true
27058
27192
  }) : null;
27059
27193
  const keywordDetector = isHookEnabled("keyword-detector") ? createKeywordDetectorHook() : null;
27060
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.1",
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",