oh-my-opencode 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ko.md CHANGED
@@ -332,6 +332,7 @@ OpenCode 는 아주 확장가능하고 아주 커스터마이저블합니다.
332
332
  - Use camelCase for function names
333
333
  ```
334
334
  - **Think Mode**: 확장된 사고(Extended Thinking)가 필요한 상황을 자동으로 감지하고 모드를 전환합니다. 사용자가 깊은 사고를 요청하는 표현(예: "think deeply", "ultrathink")을 감지하면, 추론 능력을 극대화하도록 모델 설정을 동적으로 조정합니다.
335
+ - **Ultrawork Mode**: 사용자가 "ultrawork" 또는 "ulw" 키워드를 입력하면 자동으로 에이전트 오케스트레이션 가이드를 주입합니다. 메인 에이전트가 모든 가용한 전문 에이전트(탐색, 사서, 계획, UI)를 백그라운드 작업을 통해 병렬로 최대한 활용하도록 강제하며, 엄격한 TODO 추적 및 검증 프로토콜을 따르게 합니다.
335
336
  - **Anthropic Auto Compact**: Anthropic 모델 사용 시 컨텍스트 한계에 도달하면 대화 기록을 자동으로 압축하여 효율적으로 관리합니다.
336
337
  - **Empty Task Response Detector**: 서브 에이전트가 수행한 작업이 비어있거나 무의미한 응답을 반환하는 경우를 감지하여, 오류 없이 우아하게 처리합니다.
337
338
  - **Grep Output Truncator**: Grep 검색 결과가 너무 길어 컨텍스트를 장악해버리는 것을 방지하기 위해, 과도한 출력을 자동으로 자릅니다.
@@ -344,7 +345,7 @@ OpenCode 는 아주 확장가능하고 아주 커스터마이저블합니다.
344
345
  }
345
346
  ```
346
347
 
347
- 사용 가능한 훅: `todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `grep-output-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `anthropic-auto-compact`, `rules-injector`, `background-notification`, `auto-update-checker`
348
+ 사용 가능한 훅: `todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `grep-output-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `ultrawork-mode`, `anthropic-auto-compact`, `rules-injector`, `background-notification`, `auto-update-checker`
348
349
 
349
350
  > **참고**: `disabled_hooks`는 Oh My OpenCode의 내장 훅을 제어합니다. Claude Code의 `settings.json` 훅을 비활성화하려면 `claude_code.hooks: false`를 대신 사용하세요 ([호환성 토글](#호환성-토글) 참고).
350
351
 
package/README.md CHANGED
@@ -330,6 +330,7 @@ Example workflow:
330
330
  - Use camelCase for function names
331
331
  ```
332
332
  - **Think Mode**: Automatic extended thinking detection and mode switching. Detects when user requests deep thinking (e.g., "think deeply", "ultrathink") and dynamically adjusts model settings for enhanced reasoning.
333
+ - **Ultrawork Mode**: When user triggers "ultrawork" or "ulw" keywords, automatically injects agent orchestration guidance. Forces the main agent to leverage all available specialized agents (exploration, librarian, planning, UI) via background tasks in parallel, with strict TODO tracking and verification protocols.
333
334
  - **Anthropic Auto Compact**: Automatically compacts conversation history when approaching context limits for Anthropic models.
334
335
  - **Empty Task Response Detector**: Detects when subagent tasks return empty or meaningless responses and handles gracefully.
335
336
  - **Grep Output Truncator**: Prevents grep output from overwhelming the context by truncating excessively long results.
@@ -342,7 +343,7 @@ You can disable specific built-in hooks using `disabled_hooks` in `~/.config/ope
342
343
  }
343
344
  ```
344
345
 
345
- Available hooks: `todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `grep-output-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `anthropic-auto-compact`, `rules-injector`, `background-notification`, `auto-update-checker`
346
+ Available hooks: `todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `grep-output-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `ultrawork-mode`, `anthropic-auto-compact`, `rules-injector`, `background-notification`, `auto-update-checker`
346
347
 
347
348
  > **Note**: `disabled_hooks` controls Oh My OpenCode's built-in hooks. To disable Claude Code's `settings.json` hooks, use `claude_code.hooks: false` instead (see [Compatibility Toggles](#compatibility-toggles)).
348
349
 
@@ -1,6 +1,7 @@
1
1
  export interface ThinkModeState {
2
2
  requested: boolean;
3
3
  modelSwitched: boolean;
4
+ thinkingConfigInjected: boolean;
4
5
  providerID?: string;
5
6
  modelID?: string;
6
7
  }
@@ -4,13 +4,13 @@ export * from "./types";
4
4
  export declare function clearUltraworkModeState(sessionID: string): void;
5
5
  export declare function createUltraworkModeHook(): {
6
6
  /**
7
- * chat.message hook - detect ultrawork/ulw keywords, inject context
7
+ * chat.message hook - detect ultrawork/ulw keywords, inject context via history
8
8
  *
9
9
  * Execution timing: AFTER claudeCodeHooks["chat.message"]
10
10
  * Behavior:
11
11
  * 1. Extract text from user prompt
12
12
  * 2. Detect ultrawork/ulw keywords (excluding code blocks)
13
- * 3. If detected, prepend ULTRAWORK_CONTEXT to first text part
13
+ * 3. If detected, inject ULTRAWORK_CONTEXT via injectHookMessage (history injection)
14
14
  */
15
15
  "chat.message": (input: {
16
16
  sessionID: string;
package/dist/index.js CHANGED
@@ -2586,13 +2586,42 @@ function log(message, data) {
2586
2586
  fs.appendFileSync(logFile, logEntry);
2587
2587
  } catch {}
2588
2588
  }
2589
+ // src/shared/deep-merge.ts
2590
+ var DANGEROUS_KEYS = new Set(["__proto__", "constructor", "prototype"]);
2591
+ var MAX_DEPTH = 50;
2592
+ function isPlainObject(value) {
2593
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.prototype.toString.call(value) === "[object Object]";
2594
+ }
2595
+ function deepMerge(base, override, depth = 0) {
2596
+ if (!base && !override)
2597
+ return;
2598
+ if (!base)
2599
+ return override;
2600
+ if (!override)
2601
+ return base;
2602
+ if (depth > MAX_DEPTH)
2603
+ return override ?? base;
2604
+ const result = { ...base };
2605
+ for (const key of Object.keys(override)) {
2606
+ if (DANGEROUS_KEYS.has(key))
2607
+ continue;
2608
+ const baseValue = base[key];
2609
+ const overrideValue = override[key];
2610
+ if (overrideValue === undefined)
2611
+ continue;
2612
+ if (isPlainObject(baseValue) && isPlainObject(overrideValue)) {
2613
+ result[key] = deepMerge(baseValue, overrideValue, depth + 1);
2614
+ } else {
2615
+ result[key] = overrideValue;
2616
+ }
2617
+ }
2618
+ return result;
2619
+ }
2620
+
2589
2621
  // src/shared/snake-case.ts
2590
2622
  function camelToSnake(str) {
2591
2623
  return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
2592
2624
  }
2593
- function isPlainObject(value) {
2594
- return typeof value === "object" && value !== null && !Array.isArray(value);
2595
- }
2596
2625
  function objectToSnakeCase(obj, deep = true) {
2597
2626
  const result = {};
2598
2627
  for (const [key, value] of Object.entries(obj)) {
@@ -2665,36 +2694,22 @@ function isHookDisabled(config, hookType) {
2665
2694
  }
2666
2695
  return false;
2667
2696
  }
2668
- // src/shared/deep-merge.ts
2669
- var DANGEROUS_KEYS = new Set(["__proto__", "constructor", "prototype"]);
2670
- var MAX_DEPTH = 50;
2671
- function isPlainObject2(value) {
2672
- return typeof value === "object" && value !== null && !Array.isArray(value) && Object.prototype.toString.call(value) === "[object Object]";
2697
+ // src/shared/file-utils.ts
2698
+ import { lstatSync, readlinkSync } from "fs";
2699
+ import { resolve } from "path";
2700
+ function isMarkdownFile(entry) {
2701
+ return !entry.name.startsWith(".") && entry.name.endsWith(".md") && entry.isFile();
2673
2702
  }
2674
- function deepMerge(base, override, depth = 0) {
2675
- if (!base && !override)
2676
- return;
2677
- if (!base)
2678
- return override;
2679
- if (!override)
2680
- return base;
2681
- if (depth > MAX_DEPTH)
2682
- return override ?? base;
2683
- const result = { ...base };
2684
- for (const key of Object.keys(override)) {
2685
- if (DANGEROUS_KEYS.has(key))
2686
- continue;
2687
- const baseValue = base[key];
2688
- const overrideValue = override[key];
2689
- if (overrideValue === undefined)
2690
- continue;
2691
- if (isPlainObject2(baseValue) && isPlainObject2(overrideValue)) {
2692
- result[key] = deepMerge(baseValue, overrideValue, depth + 1);
2693
- } else {
2694
- result[key] = overrideValue;
2703
+ function resolveSymlink(filePath) {
2704
+ try {
2705
+ const stats = lstatSync(filePath, { throwIfNoEntry: false });
2706
+ if (stats?.isSymbolicLink()) {
2707
+ return resolve(filePath, "..", readlinkSync(filePath));
2695
2708
  }
2709
+ return filePath;
2710
+ } catch {
2711
+ return filePath;
2696
2712
  }
2697
- return result;
2698
2713
  }
2699
2714
  // src/agents/utils.ts
2700
2715
  var allBuiltinAgents = {
@@ -4042,7 +4057,7 @@ function createGrepOutputTruncatorHook(ctx) {
4042
4057
  }
4043
4058
  // src/hooks/directory-agents-injector/index.ts
4044
4059
  import { existsSync as existsSync8, readFileSync as readFileSync4 } from "fs";
4045
- import { dirname as dirname2, join as join9, resolve } from "path";
4060
+ import { dirname as dirname2, join as join9, resolve as resolve2 } from "path";
4046
4061
 
4047
4062
  // src/hooks/directory-agents-injector/storage.ts
4048
4063
  import {
@@ -4108,7 +4123,7 @@ function createDirectoryAgentsInjectorHook(ctx) {
4108
4123
  return null;
4109
4124
  if (title.startsWith("/"))
4110
4125
  return title;
4111
- return resolve(ctx.directory, title);
4126
+ return resolve2(ctx.directory, title);
4112
4127
  }
4113
4128
  function findAgentsMdUp(startDir) {
4114
4129
  const found = [];
@@ -4183,7 +4198,7 @@ ${content}`;
4183
4198
  }
4184
4199
  // src/hooks/directory-readme-injector/index.ts
4185
4200
  import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
4186
- import { dirname as dirname3, join as join12, resolve as resolve2 } from "path";
4201
+ import { dirname as dirname3, join as join12, resolve as resolve3 } from "path";
4187
4202
 
4188
4203
  // src/hooks/directory-readme-injector/storage.ts
4189
4204
  import {
@@ -4249,7 +4264,7 @@ function createDirectoryReadmeInjectorHook(ctx) {
4249
4264
  return null;
4250
4265
  if (title.startsWith("/"))
4251
4266
  return title;
4252
- return resolve2(ctx.directory, title);
4267
+ return resolve3(ctx.directory, title);
4253
4268
  }
4254
4269
  function findReadmeMdUp(startDir) {
4255
4270
  const found = [];
@@ -4817,6 +4832,46 @@ var ALREADY_HIGH = new Set([
4817
4832
  "gpt-5.2-chat-latest-high",
4818
4833
  "gpt-5.2-pro-high"
4819
4834
  ]);
4835
+ var THINKING_CONFIGS = {
4836
+ anthropic: {
4837
+ thinking: {
4838
+ type: "enabled",
4839
+ budgetTokens: 64000
4840
+ },
4841
+ maxTokens: 128000
4842
+ },
4843
+ "amazon-bedrock": {
4844
+ reasoningConfig: {
4845
+ type: "enabled",
4846
+ budgetTokens: 32000
4847
+ },
4848
+ maxTokens: 64000
4849
+ },
4850
+ google: {
4851
+ providerOptions: {
4852
+ google: {
4853
+ thinkingConfig: {
4854
+ thinkingLevel: "HIGH"
4855
+ }
4856
+ }
4857
+ }
4858
+ },
4859
+ "google-vertex": {
4860
+ providerOptions: {
4861
+ "google-vertex": {
4862
+ thinkingConfig: {
4863
+ thinkingLevel: "HIGH"
4864
+ }
4865
+ }
4866
+ }
4867
+ }
4868
+ };
4869
+ var THINKING_CAPABLE_MODELS = {
4870
+ anthropic: ["claude-sonnet-4", "claude-opus-4", "claude-3"],
4871
+ "amazon-bedrock": ["claude", "anthropic"],
4872
+ google: ["gemini-2", "gemini-3"],
4873
+ "google-vertex": ["gemini-2", "gemini-3"]
4874
+ };
4820
4875
  function getHighVariant(modelID) {
4821
4876
  if (ALREADY_HIGH.has(modelID)) {
4822
4877
  return null;
@@ -4826,6 +4881,19 @@ function getHighVariant(modelID) {
4826
4881
  function isAlreadyHighVariant(modelID) {
4827
4882
  return ALREADY_HIGH.has(modelID) || modelID.endsWith("-high");
4828
4883
  }
4884
+ function getThinkingConfig(providerID, modelID) {
4885
+ if (isAlreadyHighVariant(modelID)) {
4886
+ return null;
4887
+ }
4888
+ const config = THINKING_CONFIGS[providerID];
4889
+ const capablePatterns = THINKING_CAPABLE_MODELS[providerID];
4890
+ if (!config || !capablePatterns) {
4891
+ return null;
4892
+ }
4893
+ const modelLower = modelID.toLowerCase();
4894
+ const isCapable = capablePatterns.some((pattern) => modelLower.includes(pattern.toLowerCase()));
4895
+ return isCapable ? config : null;
4896
+ }
4829
4897
 
4830
4898
  // src/hooks/think-mode/index.ts
4831
4899
  var thinkModeState = new Map;
@@ -4835,7 +4903,8 @@ function createThinkModeHook() {
4835
4903
  const promptText = extractPromptText(output.parts);
4836
4904
  const state = {
4837
4905
  requested: false,
4838
- modelSwitched: false
4906
+ modelSwitched: false,
4907
+ thinkingConfigInjected: false
4839
4908
  };
4840
4909
  if (!detectThinkKeyword(promptText)) {
4841
4910
  thinkModeState.set(sessionID, state);
@@ -4854,15 +4923,28 @@ function createThinkModeHook() {
4854
4923
  return;
4855
4924
  }
4856
4925
  const highVariant = getHighVariant(currentModel.modelID);
4857
- if (!highVariant) {
4858
- thinkModeState.set(sessionID, state);
4859
- return;
4926
+ const thinkingConfig = getThinkingConfig(currentModel.providerID, currentModel.modelID);
4927
+ if (highVariant) {
4928
+ output.message.model = {
4929
+ providerID: currentModel.providerID,
4930
+ modelID: highVariant
4931
+ };
4932
+ state.modelSwitched = true;
4933
+ log("Think mode: model switched to high variant", {
4934
+ sessionID,
4935
+ from: currentModel.modelID,
4936
+ to: highVariant
4937
+ });
4938
+ }
4939
+ if (thinkingConfig) {
4940
+ Object.assign(output.message, thinkingConfig);
4941
+ state.thinkingConfigInjected = true;
4942
+ log("Think mode: thinking config injected", {
4943
+ sessionID,
4944
+ provider: currentModel.providerID,
4945
+ config: thinkingConfig
4946
+ });
4860
4947
  }
4861
- output.message.model = {
4862
- providerID: currentModel.providerID,
4863
- modelID: highVariant
4864
- };
4865
- state.modelSwitched = true;
4866
4948
  thinkModeState.set(sessionID, state);
4867
4949
  },
4868
4950
  event: async ({ event }) => {
@@ -5907,7 +5989,7 @@ ${result.message}`;
5907
5989
  // src/hooks/rules-injector/index.ts
5908
5990
  import { readFileSync as readFileSync9 } from "fs";
5909
5991
  import { homedir as homedir7 } from "os";
5910
- import { relative as relative3, resolve as resolve3 } from "path";
5992
+ import { relative as relative3, resolve as resolve4 } from "path";
5911
5993
 
5912
5994
  // src/hooks/rules-injector/finder.ts
5913
5995
  import {
@@ -6264,7 +6346,7 @@ function createRulesInjectorHook(ctx) {
6264
6346
  return null;
6265
6347
  if (title.startsWith("/"))
6266
6348
  return title;
6267
- return resolve3(ctx.directory, title);
6349
+ return resolve4(ctx.directory, title);
6268
6350
  }
6269
6351
  const toolExecuteAfter = async (input, output) => {
6270
6352
  if (!TRACKED_TOOLS.includes(input.tool.toLowerCase()))
@@ -6586,12 +6668,18 @@ function createUltraworkModeHook() {
6586
6668
  }
6587
6669
  state.detected = true;
6588
6670
  log("Ultrawork keyword detected", { sessionID: input.sessionID });
6589
- const parts = output.parts;
6590
- const idx = parts.findIndex((p) => p.type === "text" && p.text);
6591
- if (idx >= 0) {
6592
- parts[idx].text = `${ULTRAWORK_CONTEXT}${parts[idx].text ?? ""}`;
6671
+ const message = output.message;
6672
+ const success = injectHookMessage(input.sessionID, ULTRAWORK_CONTEXT, {
6673
+ agent: message.agent,
6674
+ model: message.model,
6675
+ path: message.path,
6676
+ tools: message.tools
6677
+ });
6678
+ if (success) {
6593
6679
  state.injected = true;
6594
- log("Ultrawork context injected", { sessionID: input.sessionID });
6680
+ log("Ultrawork context injected via history", { sessionID: input.sessionID });
6681
+ } else {
6682
+ log("Ultrawork context injection failed", { sessionID: input.sessionID });
6595
6683
  }
6596
6684
  ultraworkModeState.set(input.sessionID, state);
6597
6685
  },
@@ -6854,8 +6942,8 @@ function startCallbackServer(timeoutMs = 5 * 60 * 1000) {
6854
6942
  });
6855
6943
  const actualPort = server.port;
6856
6944
  const waitForCallback = () => {
6857
- return new Promise((resolve4, reject) => {
6858
- resolveCallback = resolve4;
6945
+ return new Promise((resolve5, reject) => {
6946
+ resolveCallback = resolve5;
6859
6947
  rejectCallback = reject;
6860
6948
  timeoutId = setTimeout(() => {
6861
6949
  cleanup();
@@ -7801,7 +7889,7 @@ async function attemptFetch(options) {
7801
7889
  if (attempt < maxPermissionRetries) {
7802
7890
  const delay = calculateRetryDelay2(attempt);
7803
7891
  debugLog6(`[RETRY] GCP permission error, retry ${attempt + 1}/${maxPermissionRetries} after ${delay}ms`);
7804
- await new Promise((resolve4) => setTimeout(resolve4, delay));
7892
+ await new Promise((resolve5) => setTimeout(resolve5, delay));
7805
7893
  continue;
7806
7894
  }
7807
7895
  debugLog6(`[RETRY] GCP permission error, max retries exceeded`);
@@ -8116,9 +8204,6 @@ async function createGoogleAntigravityAuthPlugin({
8116
8204
  import { existsSync as existsSync19, readdirSync as readdirSync4, readFileSync as readFileSync11 } from "fs";
8117
8205
  import { homedir as homedir9 } from "os";
8118
8206
  import { join as join24, basename } from "path";
8119
- function isMarkdownFile(entry) {
8120
- return !entry.name.startsWith(".") && entry.name.endsWith(".md") && entry.isFile();
8121
- }
8122
8207
  function loadCommandsFromDir(commandsDir, scope) {
8123
8208
  if (!existsSync19(commandsDir)) {
8124
8209
  return [];
@@ -8190,9 +8275,9 @@ function loadOpencodeProjectCommands() {
8190
8275
  return commandsToRecord(commands);
8191
8276
  }
8192
8277
  // src/features/claude-code-skill-loader/loader.ts
8193
- import { existsSync as existsSync20, readdirSync as readdirSync5, readFileSync as readFileSync12, statSync as statSync3, readlinkSync } from "fs";
8278
+ import { existsSync as existsSync20, readdirSync as readdirSync5, readFileSync as readFileSync12 } from "fs";
8194
8279
  import { homedir as homedir10 } from "os";
8195
- import { join as join25, resolve as resolve4 } from "path";
8280
+ import { join as join25 } from "path";
8196
8281
  function loadSkillsFromDir(skillsDir, scope) {
8197
8282
  if (!existsSync20(skillsDir)) {
8198
8283
  return [];
@@ -8205,10 +8290,7 @@ function loadSkillsFromDir(skillsDir, scope) {
8205
8290
  const skillPath = join25(skillsDir, entry.name);
8206
8291
  if (!entry.isDirectory() && !entry.isSymbolicLink())
8207
8292
  continue;
8208
- let resolvedPath = skillPath;
8209
- if (statSync3(skillPath, { throwIfNoEntry: false })?.isSymbolicLink()) {
8210
- resolvedPath = resolve4(skillPath, "..", readlinkSync(skillPath));
8211
- }
8293
+ const resolvedPath = resolveSymlink(skillPath);
8212
8294
  const skillMdPath = join25(resolvedPath, "SKILL.md");
8213
8295
  if (!existsSync20(skillMdPath))
8214
8296
  continue;
@@ -8275,9 +8357,6 @@ function parseToolsConfig(toolsStr) {
8275
8357
  }
8276
8358
  return result;
8277
8359
  }
8278
- function isMarkdownFile2(entry) {
8279
- return !entry.name.startsWith(".") && entry.name.endsWith(".md") && entry.isFile();
8280
- }
8281
8360
  function loadAgentsFromDir(agentsDir, scope) {
8282
8361
  if (!existsSync21(agentsDir)) {
8283
8362
  return [];
@@ -8285,7 +8364,7 @@ function loadAgentsFromDir(agentsDir, scope) {
8285
8364
  const entries = readdirSync6(agentsDir, { withFileTypes: true });
8286
8365
  const agents = [];
8287
8366
  for (const entry of entries) {
8288
- if (!isMarkdownFile2(entry))
8367
+ if (!isMarkdownFile(entry))
8289
8368
  continue;
8290
8369
  const agentPath = join26(agentsDir, entry.name);
8291
8370
  const agentName = basename2(entry.name, ".md");
@@ -10248,7 +10327,7 @@ __export(exports_util, {
10248
10327
  jsonStringifyReplacer: () => jsonStringifyReplacer,
10249
10328
  joinValues: () => joinValues,
10250
10329
  issue: () => issue,
10251
- isPlainObject: () => isPlainObject3,
10330
+ isPlainObject: () => isPlainObject2,
10252
10331
  isObject: () => isObject,
10253
10332
  hexToUint8Array: () => hexToUint8Array,
10254
10333
  getSizableOrigin: () => getSizableOrigin,
@@ -10430,7 +10509,7 @@ var allowsEval = cached(() => {
10430
10509
  return false;
10431
10510
  }
10432
10511
  });
10433
- function isPlainObject3(o) {
10512
+ function isPlainObject2(o) {
10434
10513
  if (isObject(o) === false)
10435
10514
  return false;
10436
10515
  const ctor = o.constructor;
@@ -10445,7 +10524,7 @@ function isPlainObject3(o) {
10445
10524
  return true;
10446
10525
  }
10447
10526
  function shallowClone(o) {
10448
- if (isPlainObject3(o))
10527
+ if (isPlainObject2(o))
10449
10528
  return { ...o };
10450
10529
  if (Array.isArray(o))
10451
10530
  return [...o];
@@ -10628,7 +10707,7 @@ function omit(schema, mask) {
10628
10707
  return clone(schema, def);
10629
10708
  }
10630
10709
  function extend(schema, shape) {
10631
- if (!isPlainObject3(shape)) {
10710
+ if (!isPlainObject2(shape)) {
10632
10711
  throw new Error("Invalid input to extend: expected a plain object");
10633
10712
  }
10634
10713
  const checks = schema._zod.def.checks;
@@ -10647,7 +10726,7 @@ function extend(schema, shape) {
10647
10726
  return clone(schema, def);
10648
10727
  }
10649
10728
  function safeExtend(schema, shape) {
10650
- if (!isPlainObject3(shape)) {
10729
+ if (!isPlainObject2(shape)) {
10651
10730
  throw new Error("Invalid input to safeExtend: expected a plain object");
10652
10731
  }
10653
10732
  const def = {
@@ -12797,7 +12876,7 @@ function mergeValues(a, b) {
12797
12876
  if (a instanceof Date && b instanceof Date && +a === +b) {
12798
12877
  return { valid: true, data: a };
12799
12878
  }
12800
- if (isPlainObject3(a) && isPlainObject3(b)) {
12879
+ if (isPlainObject2(a) && isPlainObject2(b)) {
12801
12880
  const bKeys = Object.keys(b);
12802
12881
  const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);
12803
12882
  const newObj = { ...a, ...b };
@@ -12927,7 +13006,7 @@ var $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
12927
13006
  $ZodType.init(inst, def);
12928
13007
  inst._zod.parse = (payload, ctx) => {
12929
13008
  const input = payload.value;
12930
- if (!isPlainObject3(input)) {
13009
+ if (!isPlainObject2(input)) {
12931
13010
  payload.issues.push({
12932
13011
  expected: "record",
12933
13012
  code: "invalid_type",
@@ -22308,7 +22387,7 @@ var lsp_code_action_resolve = tool({
22308
22387
  // src/tools/ast-grep/constants.ts
22309
22388
  import { createRequire as createRequire4 } from "module";
22310
22389
  import { dirname as dirname5, join as join30 } from "path";
22311
- import { existsSync as existsSync26, statSync as statSync4 } from "fs";
22390
+ import { existsSync as existsSync26, statSync as statSync3 } from "fs";
22312
22391
 
22313
22392
  // src/tools/ast-grep/downloader.ts
22314
22393
  var {spawn: spawn5 } = globalThis.Bun;
@@ -22422,7 +22501,7 @@ async function ensureAstGrepBinary() {
22422
22501
  // src/tools/ast-grep/constants.ts
22423
22502
  function isValidBinary(filePath) {
22424
22503
  try {
22425
- return statSync4(filePath).size > 1e4;
22504
+ return statSync3(filePath).size > 1e4;
22426
22505
  } catch {
22427
22506
  return false;
22428
22507
  }
@@ -23274,11 +23353,7 @@ function discoverCommandsFromDir(commandsDir, scope) {
23274
23353
  const entries = readdirSync7(commandsDir, { withFileTypes: true });
23275
23354
  const commands = [];
23276
23355
  for (const entry of entries) {
23277
- if (entry.name.startsWith("."))
23278
- continue;
23279
- if (!entry.name.endsWith(".md"))
23280
- continue;
23281
- if (!entry.isFile())
23356
+ if (!isMarkdownFile(entry))
23282
23357
  continue;
23283
23358
  const commandPath = join32(commandsDir, entry.name);
23284
23359
  const commandName = basename3(entry.name, ".md");
@@ -23442,9 +23517,9 @@ var SkillFrontmatterSchema = exports_external.object({
23442
23517
  metadata: exports_external.record(exports_external.string(), exports_external.string()).optional()
23443
23518
  });
23444
23519
  // src/tools/skill/tools.ts
23445
- import { existsSync as existsSync30, readdirSync as readdirSync8, statSync as statSync5, readlinkSync as readlinkSync2, readFileSync as readFileSync18 } from "fs";
23520
+ import { existsSync as existsSync30, readdirSync as readdirSync8, readFileSync as readFileSync18 } from "fs";
23446
23521
  import { homedir as homedir16 } from "os";
23447
- import { join as join33, resolve as resolve7, basename as basename4 } from "path";
23522
+ import { join as join33, basename as basename4 } from "path";
23448
23523
  function parseSkillFrontmatter(data) {
23449
23524
  return {
23450
23525
  name: typeof data.name === "string" ? data.name : "",
@@ -23465,15 +23540,7 @@ function discoverSkillsFromDir(skillsDir, scope) {
23465
23540
  continue;
23466
23541
  const skillPath = join33(skillsDir, entry.name);
23467
23542
  if (entry.isDirectory() || entry.isSymbolicLink()) {
23468
- let resolvedPath = skillPath;
23469
- try {
23470
- const stats = statSync5(skillPath, { throwIfNoEntry: false });
23471
- if (stats?.isSymbolicLink()) {
23472
- resolvedPath = resolve7(skillPath, "..", readlinkSync2(skillPath));
23473
- }
23474
- } catch {
23475
- continue;
23476
- }
23543
+ const resolvedPath = resolveSymlink(skillPath);
23477
23544
  const skillMdPath = join33(resolvedPath, "SKILL.md");
23478
23545
  if (!existsSync30(skillMdPath))
23479
23546
  continue;
@@ -23502,17 +23569,6 @@ function discoverSkillsSync() {
23502
23569
  var availableSkills = discoverSkillsSync();
23503
23570
  var skillListForDescription = availableSkills.map((s) => `- ${s.name}: ${s.description} (${s.scope})`).join(`
23504
23571
  `);
23505
- function resolveSymlink(skillPath) {
23506
- try {
23507
- const stats = statSync5(skillPath, { throwIfNoEntry: false });
23508
- if (stats?.isSymbolicLink()) {
23509
- return resolve7(skillPath, "..", readlinkSync2(skillPath));
23510
- }
23511
- return skillPath;
23512
- } catch {
23513
- return skillPath;
23514
- }
23515
- }
23516
23572
  async function parseSkillMd(skillPath) {
23517
23573
  const resolvedPath = resolveSymlink(skillPath);
23518
23574
  const skillMdPath = join33(resolvedPath, "SKILL.md");
@@ -23784,7 +23840,7 @@ Use \`background_output\` tool with task_id="${task.id}" to check progress:
23784
23840
  });
23785
23841
  }
23786
23842
  function delay(ms) {
23787
- return new Promise((resolve8) => setTimeout(resolve8, ms));
23843
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
23788
23844
  }
23789
23845
  function truncateText(text, maxLength) {
23790
23846
  if (text.length <= maxLength)
@@ -1,3 +1,4 @@
1
+ export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
1
2
  /**
2
3
  * Deep merges two objects, with override values taking precedence.
3
4
  * - Objects are recursively merged
@@ -0,0 +1,6 @@
1
+ export declare function isMarkdownFile(entry: {
2
+ name: string;
3
+ isFile: () => boolean;
4
+ }): boolean;
5
+ export declare function isSymbolicLink(filePath: string): boolean;
6
+ export declare function resolveSymlink(filePath: string): string;
@@ -8,3 +8,4 @@ export * from "./tool-name";
8
8
  export * from "./pattern-matcher";
9
9
  export * from "./hook-disabled";
10
10
  export * from "./deep-merge";
11
+ export * from "./file-utils";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-opencode",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "OpenCode plugin - custom agents (oracle, librarian) and enhanced features",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",