Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14ff86c547 | ||
|
|
e4036185f0 | ||
|
|
d34154bc68 | ||
|
|
9e00be91af | ||
|
|
40d4673201 | ||
|
|
cf33fc5da1 | ||
|
|
407786978a | ||
|
|
15454f1d81 | ||
|
|
56160d17f8 | ||
|
|
61bbbcb577 | ||
|
|
adabace02d | ||
|
|
41f93c9f8b | ||
|
|
8102d178cb | ||
|
|
4f019f8fe5 | ||
|
|
7b19177c8a | ||
|
|
e8f59cbbf8 |
29
README.ko.md
29
README.ko.md
@@ -106,7 +106,19 @@ opencode --version # 1.0.132 이상이어야 함
|
||||
cat ~/.config/opencode/opencode.json # plugin 배열에 "oh-my-opencode"가 있어야 함
|
||||
```
|
||||
|
||||
> **참고**: 모델 인증이 필요하면, `opencode` 실행 후 `/connect` 명령어를 사용하거나, tmux를 통해 `opencode auth login`을 안내하세요.
|
||||
### 4단계: 인증정보 설정
|
||||
|
||||
plugin 에 oh my opencode 를 추가했다면, tmux 를 사용하여 다음의 내용을 진행주세요.
|
||||
|
||||
1. tmux 를 사용하여 셸 열기
|
||||
2. opencode auth login > Provider Anthropic > Login method Claude Pro/Max > 사용자에게 oauth 화면에서 인증하라고 안내
|
||||
3. 사용자에게 macOS 일 경우 VibeProxy, 아닐 경우 CLIProxyAPI 를 사용하여 OpenAI / Google 인증을 완료하라고 안내
|
||||
|
||||
### ⚠️ 주의
|
||||
|
||||
**사용자가 명시적으로 요청하지 않는 한, `oh-my-opencode.json` 설정 파일을 생성하거나 수정하지 마세요.**
|
||||
|
||||
플러그인은 설정 파일 없이도 기본값으로 완벽하게 동작합니다. 설정 파일은 사용자가 커스터마이징을 원할 때만 필요합니다.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -154,6 +166,7 @@ OpenCode 는 아주 확장가능하고 아주 커스터마이저블합니다.
|
||||
│ └── Button.tsx # 이 파일을 읽으면 위 3개 AGENTS.md 모두 주입
|
||||
```
|
||||
`Button.tsx`를 읽으면 순서대로 주입됩니다: `project/AGENTS.md` → `src/AGENTS.md` → `components/AGENTS.md`. 각 디렉토리의 컨텍스트는 세션당 한 번만 주입됩니다. Claude Code의 CLAUDE.md 기능에서 영감을 받았습니다.
|
||||
- **Directory README.md Injector**: 파일을 읽을 때 `README.md` 내용을 자동으로 주입합니다. AGENTS.md Injector와 동일하게 동작하며, 파일 디렉토리부터 프로젝트 루트까지 탐색합니다. LLM 에이전트에게 프로젝트 문서 컨텍스트를 제공합니다. 각 디렉토리의 README는 세션당 한 번만 주입됩니다.
|
||||
- **Think Mode**: 확장된 사고(Extended Thinking)가 필요한 상황을 자동으로 감지하고 모드를 전환합니다. 사용자가 깊은 사고를 요청하는 표현(예: "think deeply", "ultrathink")을 감지하면, 추론 능력을 극대화하도록 모델 설정을 동적으로 조정합니다.
|
||||
- **Anthropic Auto Compact**: Anthropic 모델 사용 시 컨텍스트 한계에 도달하면 대화 기록을 자동으로 압축하여 효율적으로 관리합니다.
|
||||
- **Empty Task Response Detector**: 서브 에이전트가 수행한 작업이 비어있거나 무의미한 응답을 반환하는 경우를 감지하여, 오류 없이 우아하게 처리합니다.
|
||||
@@ -326,7 +339,7 @@ Schema 자동 완성이 지원됩니다:
|
||||
|
||||
각 에이전트에서 지원하는 옵션: `model`, `temperature`, `top_p`, `prompt`, `tools`, `disable`, `description`, `mode`, `color`, `permission`.
|
||||
|
||||
또는 `disabled_agents`로 비활성화할 수 있습니다:
|
||||
또는 ~/.config/opencode/oh-my-opencode.json 혹은 .opencode/oh-my-opencode.json 의 `disabled_agents` 를 사용하여 비활성화할 수 있습니다:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -338,7 +351,9 @@ Schema 자동 완성이 지원됩니다:
|
||||
|
||||
### MCPs
|
||||
|
||||
내장된 MCP를 비활성화합니다:
|
||||
기본적으로 Context7, Exa MCP 를 지원합니다.
|
||||
|
||||
이것이 마음에 들지 않는다면, ~/.config/opencode/oh-my-opencode.json 혹은 .opencode/oh-my-opencode.json 의 `disabled_mcps` 를 사용하여 비활성화할 수 있습니다:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -346,13 +361,13 @@ Schema 자동 완성이 지원됩니다:
|
||||
}
|
||||
```
|
||||
|
||||
더 자세한 내용은 [OpenCode MCP Servers](https://opencode.ai/docs/mcp-servers)를 참조하세요.
|
||||
|
||||
### LSP
|
||||
|
||||
Oh My OpenCode의 LSP 도구는 오직 **리팩토링(이름 변경, 코드 액션)만을 위한 것**입니다. 분석용 LSP는 OpenCode 자체에서 처리합니다.
|
||||
OpenCode 는 분석을 위해 LSP 도구를 제공합니다.
|
||||
Oh My OpenCode 에서는 LSP 의 리팩토링(이름 변경, 코드 액션) 도구를 제공합니다.
|
||||
OpenCode 에서 지원하는 모든 LSP 구성 및 커스텀 설정 (opencode.json 에 설정 된 것) 을 그대로 지원하고, Oh My OpenCode 만을 위한 추가적인 설정도 아래와 같이 설정 할 수 있습니다.
|
||||
|
||||
`lsp` 옵션을 통해 LSP 서버를 설정합니다:
|
||||
~/.config/opencode/oh-my-opencode.json 혹은 .opencode/oh-my-opencode.json 의 `lsp` 옵션을 통해 LSP 서버를 추가로 설정 할 수 있습니다:
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
44
README.md
44
README.md
@@ -28,11 +28,12 @@ Oh My OpenCode
|
||||
oMoMoMoMoMo···
|
||||
|
||||
If you work in tech, you likely appreciated [Claude Code](https://www.claude.com/product/claude-code).
|
||||
If you are a hacker, you will fucking falling in love with [OpenCode](https://github.com/sst/opencode).
|
||||
If you are a hacker, you will fucking fall in love with [OpenCode](https://github.com/sst/opencode).
|
||||
|
||||
You don't write code just for a paycheck? You write because you genuinely love it?
|
||||
|
||||
To you, OpenCode will feel like the paradigm shift from Windows to Linux. Not you? It's still worth the investment. Give it 10 minutes. Your work and life will improve. I promise.
|
||||
Do you remember the first time you switched from Windows to Linux? The excitement of configuring everything just the way you wanted?
|
||||
In an era where that romance feels lost, OpenCode will be that project for you.
|
||||
If you love coding and computers, OpenCode will feel exactly like that moment you first touched Linux after years of Windows.
|
||||
Even if you don't, invest a little time. Boost your skills and productivity by multipliers.
|
||||
|
||||
## TL;DR
|
||||
|
||||
@@ -42,7 +43,7 @@ To you, OpenCode will feel like the paradigm shift from Windows to Linux. Not yo
|
||||
- For OpenAI models, I recommend the [OpenCode-OpenAI-Codex-Auth plugin](https://github.com/numman-ali/opencode-openai-codex-auth).
|
||||
- For Gemini and GPT, use [VibeProxy (macOS Only)](https://github.com/automazeio/vibeproxy) or [CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI).
|
||||
- **Truth be told, the rest is optional. Just install it. Once set up, no further learning is required. It simply evolves.**
|
||||
- **Paste this document into Claude Code, OpenCode, or Cursor, instruct it to install, and ask for assistance if needed. It works seamlessly.**
|
||||
- **Paste this document into Claude Code, OpenCode, or Cursor, instruct it to install, and ask for assistance if needed. It just installs.**
|
||||
- If you are an LLM Agent, install the required plugins and dependencies, then run `opencode auth login` via tmux to assist the user with configuration.
|
||||
|
||||
## Installation
|
||||
@@ -104,7 +105,19 @@ opencode --version # Should be >= 1.0.132
|
||||
cat ~/.config/opencode/opencode.json # Should contain "oh-my-opencode" in plugin array
|
||||
```
|
||||
|
||||
> **Note**: If model authentication is needed, run `opencode` and use `/connect` command, or assist the user with `opencode auth login` via tmux.
|
||||
### Step 4: Configure Authentication
|
||||
|
||||
Once `oh-my-opencode` is added to the plugin list, use `tmux` to proceed with the following:
|
||||
|
||||
1. Open a shell using `tmux`.
|
||||
2. Run `opencode auth login` > Select Provider `Anthropic` > Select Login method `Claude Pro/Max` > Instruct the user to authenticate via the OAuth screen.
|
||||
3. Recommend the setup: [VibeProxy](https://github.com/automazeio/vibeproxy) for macOS users, or [CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI) for others.
|
||||
|
||||
### ⚠️ Warning
|
||||
|
||||
**Do NOT create or modify the `oh-my-opencode.json` configuration file unless the user explicitly requests it.**
|
||||
|
||||
The plugin works perfectly with default settings without any configuration file. The configuration file is only needed when the user wants customization.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -115,7 +128,7 @@ OpenCode is limitlessly extensible and customizable. Zero screen flicker.
|
||||
You can mix and orchestrate models to your exact specifications.
|
||||
It is feature-rich. It is elegant. It handles the terminal without hesitation. It is high-performance.
|
||||
|
||||
But here is the catch: the learning curve is steep. There is a lot to master.
|
||||
But here is the catch: the learning curve is steep. There is a lot to master. And your time is expensive.
|
||||
|
||||
Inspired by [AmpCode](https://ampcode.com) and [Claude Code](https://code.claude.com/docs/en/overview), I have implemented their features here—often with superior execution.
|
||||
Because this is OpenCode.
|
||||
@@ -150,6 +163,7 @@ I believe in the right tool for the job. For your wallet's sake, use CLIProxyAPI
|
||||
│ └── Button.tsx # Reading this injects ALL 3 AGENTS.md files
|
||||
```
|
||||
When reading `Button.tsx`, the hook injects contexts in order: `project/AGENTS.md` → `src/AGENTS.md` → `components/AGENTS.md`. Each directory's context is injected only once per session. Inspired by Claude Code's CLAUDE.md feature.
|
||||
- **Directory README.md Injector**: Automatically injects `README.md` contents when reading files. Works identically to the AGENTS.md Injector, searching upward from the file's directory to project root. Provides project documentation context to the LLM agent. Each directory's README is injected only once per session.
|
||||
- **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.
|
||||
- **Anthropic Auto Compact**: Automatically compacts conversation history when approaching context limits for Anthropic models.
|
||||
- **Empty Task Response Detector**: Detects when subagent tasks return empty or meaningless responses and handles gracefully.
|
||||
@@ -323,7 +337,7 @@ Override built-in agent settings:
|
||||
|
||||
Each agent supports: `model`, `temperature`, `top_p`, `prompt`, `tools`, `disable`, `description`, `mode`, `color`, `permission`.
|
||||
|
||||
Or disable agents via `disabled_agents`:
|
||||
Or you can disable them using `disabled_agents` in `~/.config/opencode/oh-my-opencode.json` or `.opencode/oh-my-opencode.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -335,7 +349,9 @@ Available agents: `oracle`, `librarian`, `explore`, `frontend-ui-ux-engineer`, `
|
||||
|
||||
### MCPs
|
||||
|
||||
Disable built-in MCPs:
|
||||
By default, Context7 and Exa MCP are supported.
|
||||
|
||||
If you don't want these, you can disable them using `disabled_mcps` in `~/.config/opencode/oh-my-opencode.json` or `.opencode/oh-my-opencode.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -343,13 +359,13 @@ Disable built-in MCPs:
|
||||
}
|
||||
```
|
||||
|
||||
See [OpenCode MCP Servers](https://opencode.ai/docs/mcp-servers) for more.
|
||||
|
||||
### LSP
|
||||
|
||||
Oh My OpenCode's LSP tools are for **refactoring only** (rename, code actions). Analysis LSP is handled by OpenCode itself.
|
||||
OpenCode provides LSP tools for analysis.
|
||||
Oh My OpenCode provides LSP tools for refactoring (rename, code actions).
|
||||
It supports all LSP configurations and custom settings supported by OpenCode (those configured in opencode.json), and you can also configure additional settings specifically for Oh My OpenCode as shown below.
|
||||
|
||||
Configure LSP servers via `lsp` option:
|
||||
You can configure additional LSP servers via the `lsp` option in `~/.config/opencode/oh-my-opencode.json` or `.opencode/oh-my-opencode.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -389,7 +405,7 @@ If this sounds arrogant and you have a superior solution, send a PR. You are wel
|
||||
|
||||
As of now, I have no affiliation with any of the projects or models mentioned here. This plugin is purely based on personal experimentation and preference.
|
||||
|
||||
I constructed 99% of this project using OpenCode. I focused on functional verification. This documentation has been personally reviewed and comprehensively rewritten, so you can rely on it with confidence.
|
||||
I constructed 99% of this project using OpenCode. I focused on functional verification, and honestly, I don't know how to write proper TypeScript. **But I personally reviewed and comprehensively rewritten this documentation, so you can rely on it with confidence.**
|
||||
## Warnings
|
||||
|
||||
- If you are on [1.0.132](https://github.com/sst/opencode/releases/tag/v1.0.132) or lower, OpenCode has a bug that might break config.
|
||||
|
||||
845
notepad.md
845
notepad.md
@@ -1,845 +0,0 @@
|
||||
# MCP Loader Plugin - Orchestration Notepad
|
||||
|
||||
## Task Started
|
||||
All tasks execution STARTED: Thu Dec 4 16:52:57 KST 2025
|
||||
|
||||
---
|
||||
|
||||
## Orchestration Overview
|
||||
|
||||
**Todo List File**: ./tool-search-tool-plan.md
|
||||
**Total Tasks**: 5 (Phase 1-5)
|
||||
**Target Files**:
|
||||
- `~/.config/opencode/plugin/mcp-loader.ts` - Main plugin
|
||||
- `~/.config/opencode/mcp-loader.json` - Global config example
|
||||
- `~/.config/opencode/plugin/mcp-loader.test.ts` - Unit tests
|
||||
|
||||
---
|
||||
|
||||
## Accumulated Wisdom
|
||||
|
||||
(To be populated by executors)
|
||||
|
||||
---
|
||||
|
||||
## Task Progress
|
||||
|
||||
| Task | Description | Status |
|
||||
|------|-------------|--------|
|
||||
| 1 | Plugin skeleton + config loader | pending |
|
||||
| 2 | MCP server registry + lifecycle | pending |
|
||||
| 3 | mcp_search + mcp_status tools | pending |
|
||||
| 4 | mcp_call tool | pending |
|
||||
| 5 | Documentation | pending |
|
||||
|
||||
---
|
||||
|
||||
|
||||
## 2025-12-04 16:58 - Task 1 Completed
|
||||
|
||||
### Summary
|
||||
- Created `~/.config/opencode/plugin/mcp-loader.ts` - Plugin skeleton with config loader
|
||||
- Created `~/.config/opencode/plugin/mcp-loader.test.ts` - 14 unit tests
|
||||
|
||||
### Key Implementation Details
|
||||
- Config merge: project overrides global for same server names, merges different
|
||||
- Env var substitution: `{env:VAR}` → `process.env.VAR`
|
||||
- Validation: type required, local needs command, remote needs url
|
||||
- Empty config returns `{ servers: {} }` (not error)
|
||||
|
||||
### Test Results
|
||||
- 14 tests passed
|
||||
- substituteEnvVars: 4 tests
|
||||
- substituteHeaderEnvVars: 1 test
|
||||
- loadConfig: 9 tests
|
||||
|
||||
### Files Created
|
||||
- `~/.config/opencode/plugin/mcp-loader.ts`
|
||||
- `~/.config/opencode/plugin/mcp-loader.test.ts`
|
||||
|
||||
---
|
||||
|
||||
## [2025-12-08 18:56] - Task 1: Remove unused import formatWorkspaceEdit from LSP tools
|
||||
|
||||
### DISCOVERED ISSUES
|
||||
- None - simple import cleanup task
|
||||
|
||||
### IMPLEMENTATION DECISIONS
|
||||
- Removed only `formatWorkspaceEdit` from import list at line 17
|
||||
- Kept all other imports intact (formatCodeActions, applyWorkspaceEdit, formatApplyResult remain)
|
||||
- Verified the function exists in utils.ts:212 but is truly unused in tools.ts
|
||||
|
||||
### PROBLEMS FOR NEXT TASKS
|
||||
- None identified for remaining tasks
|
||||
|
||||
### VERIFICATION RESULTS
|
||||
- Ran: `bun run typecheck` → exit 0, no errors
|
||||
- Ran: `bun run build` → exit 0, bundled 200 modules
|
||||
- Ran: `rg "formatWorkspaceEdit" src/tools/lsp/tools.ts` → no matches (confirmed removal)
|
||||
|
||||
### LEARNINGS
|
||||
- Convention: This project uses `bun run typecheck` (tsc --noEmit) and `bun run build` for verification
|
||||
- The `formatWorkspaceEdit` function still exists in utils.ts - it's exported but just not used in tools.ts
|
||||
|
||||
소요 시간: ~2분
|
||||
|
||||
---
|
||||
|
||||
## [2025-12-08 19:00] - Task 2: Remove unused ThinkingPart interface and fallbackRevertStrategy function
|
||||
|
||||
### DISCOVERED ISSUES
|
||||
- None - both items were genuinely unused (no callers found)
|
||||
|
||||
### IMPLEMENTATION DECISIONS
|
||||
- Removed `ThinkingPart` interface (lines 37-40) - defined but never referenced
|
||||
- Removed `fallbackRevertStrategy` function (lines 189-244) - defined but never called
|
||||
- Added comment explaining removal reason as per task requirements
|
||||
- Kept `ThinkingPartType`, `prependThinkingPart`, `stripThinkingParts` - these are different items and ARE used
|
||||
|
||||
### PROBLEMS FOR NEXT TASKS
|
||||
- None identified
|
||||
|
||||
### VERIFICATION RESULTS
|
||||
- Ran: `bun run typecheck` → exit 0, no errors
|
||||
- Ran: `bun run build` → exit 0, bundled 200 modules
|
||||
- Ran: `rg "ThinkingPart" src/hooks/session-recovery/` → only related types/functions found, interface removed
|
||||
- Ran: `rg "fallbackRevertStrategy" src/hooks/session-recovery/` → only comment found, function removed
|
||||
- Ran: `rg "createSessionRecoveryHook" src/hooks/` → exports intact
|
||||
|
||||
### LEARNINGS
|
||||
- `ThinkingPart` interface vs `ThinkingPartType` type vs `prependThinkingPart` function - different entities, verify before removing
|
||||
- `fallbackRevertStrategy` was likely a planned feature that never got integrated into the recovery flow
|
||||
|
||||
소요 시간: ~2분
|
||||
|
||||
---
|
||||
|
||||
## [2025-12-08 19:04] - Task 3: Remove unused builtinMcps export from MCP module
|
||||
|
||||
### DISCOVERED ISSUES
|
||||
- None - `builtinMcps` export was genuinely unused (no external importers)
|
||||
|
||||
### IMPLEMENTATION DECISIONS
|
||||
- Removed `export const builtinMcps = allBuiltinMcps` from line 24
|
||||
- Kept `allBuiltinMcps` const - used internally by `createBuiltinMcps` function
|
||||
- Kept `createBuiltinMcps` function - actively used in src/index.ts:89
|
||||
|
||||
### PROBLEMS FOR NEXT TASKS
|
||||
- None identified
|
||||
|
||||
### VERIFICATION RESULTS
|
||||
- Ran: `bun run typecheck` → exit 0, no errors
|
||||
- Ran: `bun run build` → exit 0, bundled 200 modules
|
||||
- Ran: `rg "builtinMcps" src/mcp/index.ts` → no matches (export removed)
|
||||
- Ran: `rg "createBuiltinMcps" src/mcp/index.ts` → function still exists
|
||||
|
||||
### LEARNINGS
|
||||
- `createBuiltinMcps` function vs `builtinMcps` export - function is used, direct export is not
|
||||
- Internal const `allBuiltinMcps` should be kept since it's referenced by the function
|
||||
|
||||
소요 시간: ~2분
|
||||
|
||||
---
|
||||
|
||||
## [2025-12-09 16:13] - Task 1: Add file-based logger to shared module
|
||||
|
||||
### DISCOVERED ISSUES
|
||||
- None - straightforward file copy and modification task
|
||||
|
||||
### IMPLEMENTATION DECISIONS
|
||||
- Copied logger.ts from opencode-cc-plugin source
|
||||
- Changed log file path from `opencode-cc-plugin.log` to `oh-my-opencode.log`
|
||||
- Added barrel export from `src/shared/index.ts`
|
||||
- Kept original comment for module description
|
||||
|
||||
### PROBLEMS FOR NEXT TASKS
|
||||
- None identified - logger is now available for use in all new loaders
|
||||
|
||||
### VERIFICATION RESULTS
|
||||
- Ran: `bun run typecheck` → exit 0, no errors
|
||||
- Log file path verified: `/tmp/oh-my-opencode.log`
|
||||
- Exports verified: `log()` and `getLogFilePath()` now accessible via `src/shared`
|
||||
|
||||
### LEARNINGS
|
||||
- Source file location: `~/local-workspaces/opencode-cc-plugin/src/shared/logger.ts`
|
||||
- Logger uses `fs.appendFileSync` for synchronous file writing
|
||||
- Empty catch block intentionally swallows errors to prevent logging from breaking main operations
|
||||
|
||||
소요 시간: ~1분
|
||||
|
||||
---
|
||||
|
||||
## [2025-12-09 16:14] - Task 3: Rename skill-loader to claude-code-skill-loader
|
||||
|
||||
### DISCOVERED ISSUES
|
||||
- None - straightforward directory rename task
|
||||
|
||||
### IMPLEMENTATION DECISIONS
|
||||
- Used `mv` command to rename `src/features/skill-loader/` → `src/features/claude-code-skill-loader/`
|
||||
- Updated import path in `src/index.ts` (lines 19-22)
|
||||
- Did NOT modify internal imports (`../command-loader/types`) since command-loader still exists (Task 2 runs in parallel)
|
||||
|
||||
### PROBLEMS FOR NEXT TASKS
|
||||
- If Task 2 renames command-loader to claude-code-command-loader, the internal imports in claude-code-skill-loader will need to be updated as part of Task 2's scope
|
||||
- The skill-loader's loader.ts:6 and types.ts:1 import from `../command-loader/types`
|
||||
|
||||
### VERIFICATION RESULTS
|
||||
- Ran: `bun run typecheck` → exit 0, no errors
|
||||
- Ran: `bun run build` → exit 0, succeeded
|
||||
- Directory structure verified: `skill-loader/` deleted, `claude-code-skill-loader/` exists
|
||||
|
||||
### LEARNINGS
|
||||
- This project uses `mv` for directory rename (acceptable per ANTI-PATTERNS - file creation is forbidden, not rename)
|
||||
- Command: `bun run typecheck` for type check, `bun run build` for build
|
||||
- skill-loader internal imports use relative paths (`../command-loader/types`) which remain valid after rename
|
||||
|
||||
소요 시간: ~2분
|
||||
|
||||
---
|
||||
|
||||
## [2025-12-09 16:16] - Task 2: Rename command-loader to claude-code-command-loader
|
||||
|
||||
### DISCOVERED ISSUES
|
||||
- skill-loader (now claude-code-skill-loader) was importing `CommandDefinition` from `../command-loader/types`
|
||||
- After renaming command-loader, these references also needed updating
|
||||
|
||||
### IMPLEMENTATION DECISIONS
|
||||
- Used `mv` command: `src/features/command-loader/` → `src/features/claude-code-command-loader/`
|
||||
- Updated import path in `src/index.ts` (lines 13-18)
|
||||
- Also updated `claude-code-skill-loader/loader.ts:6` and `types.ts:1` to reference new path
|
||||
|
||||
### PROBLEMS FOR NEXT TASKS
|
||||
- None identified - all dependent imports updated
|
||||
|
||||
### VERIFICATION RESULTS
|
||||
- Ran: `bun run typecheck` → exit 0, no errors
|
||||
- Directory structure verified: `command-loader/` deleted, `claude-code-command-loader/` exists
|
||||
- All imports updated: src/index.ts, claude-code-skill-loader/loader.ts, claude-code-skill-loader/types.ts
|
||||
|
||||
### LEARNINGS
|
||||
- skill-loader depends on command-loader's `CommandDefinition` type via relative import
|
||||
- When renaming shared modules, must update ALL dependent modules' imports
|
||||
- Task 2 and Task 3 have an implicit dependency through the type import
|
||||
|
||||
소요 시간: ~2분
|
||||
|
||||
---
|
||||
|
||||
## [2025-12-09 16:24] - Task 4: Add claude-code-agent-loader feature
|
||||
|
||||
### DISCOVERED ISSUES
|
||||
- None - straightforward file copy task
|
||||
|
||||
### IMPLEMENTATION DECISIONS
|
||||
- Copied 3 files from opencode-cc-plugin: `index.ts`, `loader.ts`, `types.ts`
|
||||
- Import path `../../shared/frontmatter` unchanged - already compatible with oh-my-opencode structure
|
||||
- No `log()` usage in source files - no logger integration needed
|
||||
|
||||
### PROBLEMS FOR NEXT TASKS
|
||||
- None identified - agent-loader is self-contained
|
||||
|
||||
### VERIFICATION RESULTS
|
||||
- Ran: `bun run typecheck` → exit 0, no errors
|
||||
- Directory structure verified: `claude-code-agent-loader/` created with 3 files
|
||||
- Functions exported: `loadUserAgents()`, `loadProjectAgents()`
|
||||
|
||||
### LEARNINGS
|
||||
- Source location: `~/local-workspaces/opencode-cc-plugin/src/features/agent-loader/`
|
||||
- Agent loader uses `parseFrontmatter` from shared module
|
||||
- Agent configs loaded from `~/.claude/agents/` (user) and `.claude/agents/` (project)
|
||||
- Scope is appended to description: `(user)` or `(project)`
|
||||
|
||||
소요 시간: ~1분
|
||||
|
||||
---
|
||||
|
||||
## [2025-12-09 16:25] - Task 5: Add claude-code-mcp-loader feature
|
||||
|
||||
### DISCOVERED ISSUES
|
||||
- None - straightforward file copy task
|
||||
|
||||
### IMPLEMENTATION DECISIONS
|
||||
- Copied 5 files from opencode-cc-plugin: `index.ts`, `loader.ts`, `transformer.ts`, `env-expander.ts`, `types.ts`
|
||||
- Import path `../../shared/logger` unchanged - already compatible with oh-my-opencode structure
|
||||
- Kept `Bun.file()` usage - oh-my-opencode targets Bun runtime
|
||||
- Environment variable expansion supports `${VAR}` and `${VAR:-default}` syntax
|
||||
|
||||
### PROBLEMS FOR NEXT TASKS
|
||||
- None identified - mcp-loader is self-contained
|
||||
- Does NOT conflict with src/mcp/ (builtin MCPs are separate)
|
||||
|
||||
### VERIFICATION RESULTS
|
||||
- Ran: `bun run typecheck` → exit 0, no errors
|
||||
- Directory structure verified: `claude-code-mcp-loader/` created with 5 files
|
||||
- Functions exported: `loadMcpConfigs()`, `formatLoadedServersForToast()`, `transformMcpServer()`, `expandEnvVars()`, `expandEnvVarsInObject()`
|
||||
|
||||
### LEARNINGS
|
||||
- Source location: `~/local-workspaces/opencode-cc-plugin/src/features/mcp-loader/`
|
||||
- MCP configs loaded from:
|
||||
- `~/.claude/.mcp.json` (user scope)
|
||||
- `.mcp.json` (project scope)
|
||||
- `.claude/.mcp.json` (local scope)
|
||||
- Later scope overrides earlier scope for same server name
|
||||
- Supports stdio, http, and sse server types
|
||||
|
||||
소요 시간: ~1분
|
||||
|
||||
---
|
||||
|
||||
## [2025-12-09 16:24] - Task 6: Add claude-code-session-state feature
|
||||
|
||||
### DISCOVERED ISSUES
|
||||
- None - straightforward file copy task
|
||||
|
||||
### IMPLEMENTATION DECISIONS
|
||||
- Copied 4 files from opencode-cc-plugin: `types.ts`, `state.ts`, `detector.ts`, `index.ts`
|
||||
- No import path changes needed - files are completely self-contained
|
||||
- No external dependencies - types are defined locally
|
||||
|
||||
### PROBLEMS FOR NEXT TASKS
|
||||
- Task 7 should import from `./features/claude-code-session-state` in src/index.ts
|
||||
- Task 7 should remove local session variables and use the module's getter/setters
|
||||
|
||||
### VERIFICATION RESULTS
|
||||
- Directory created: `src/features/claude-code-session-state/` (4 files confirmed)
|
||||
- Exports available: sessionErrorState, sessionInterruptState, subagentSessions, sessionFirstMessageProcessed (Maps/Sets)
|
||||
- Exports available: currentSessionID, currentSessionTitle, mainSessionID (state vars)
|
||||
- Exports available: setCurrentSession(), setMainSession(), getCurrentSessionID(), getCurrentSessionTitle(), getMainSessionID() (getters/setters)
|
||||
- Exports available: detectInterrupt() function
|
||||
|
||||
### LEARNINGS
|
||||
- Session state module is completely self-contained - no external dependencies
|
||||
- Uses barrel export pattern: index.ts re-exports everything from types, state, detector
|
||||
- Source directory: `~/local-workspaces/opencode-cc-plugin/src/features/session-state/`
|
||||
|
||||
소요 시간: ~1분
|
||||
|
||||
---
|
||||
|
||||
## [2025-12-09 16:32] - Task 7: Integrate new features into src/index.ts
|
||||
|
||||
### DISCOVERED ISSUES
|
||||
- None - integration task with well-defined API from previous tasks
|
||||
|
||||
### IMPLEMENTATION DECISIONS
|
||||
- Added imports for new modules:
|
||||
- `loadUserAgents`, `loadProjectAgents` from `./features/claude-code-agent-loader`
|
||||
- `loadMcpConfigs` from `./features/claude-code-mcp-loader`
|
||||
- `setCurrentSession`, `setMainSession`, `getMainSessionID`, `getCurrentSessionTitle` from `./features/claude-code-session-state`
|
||||
- `log` from `./shared/logger`
|
||||
- Removed local session variables (lines 77-79): `mainSessionID`, `currentSessionID`, `currentSessionTitle`
|
||||
- Replaced direct session assignments with setter functions:
|
||||
- `mainSessionID = x` → `setMainSession(x)`
|
||||
- `currentSessionID = x; currentSessionTitle = y` → `setCurrentSession(x, y)`
|
||||
- Replaced session variable reads with getter functions:
|
||||
- `mainSessionID` comparisons → `getMainSessionID()`
|
||||
- `currentSessionTitle` reads → `getCurrentSessionTitle()`
|
||||
- Added agent loading in config hook: `loadUserAgents()`, `loadProjectAgents()`
|
||||
- Added MCP loading in config hook: `await loadMcpConfigs()` (async)
|
||||
- Replaced `console.error` with `log()` for config validation errors
|
||||
- Renamed local variable `agents` to `builtinAgents` to distinguish from loaded agents
|
||||
|
||||
### PROBLEMS FOR NEXT TASKS
|
||||
- Task 8 (README update) should document the new Agent Loader and MCP Loader features
|
||||
- Should explain the `claude-code-*` naming convention
|
||||
|
||||
### VERIFICATION RESULTS
|
||||
- Ran: `bun run typecheck` → exit 0, no errors
|
||||
- Ran: `bun run build` → exit 0, successful build
|
||||
- Session tracking verified: all event handlers use getter/setter functions
|
||||
- Agent loading verified: config.agent merges builtin + user + project agents
|
||||
- MCP loading verified: config.mcp merges builtin MCPs + loaded MCP servers
|
||||
|
||||
### LEARNINGS
|
||||
- `setCurrentSession(id, title)` sets both ID and title atomically
|
||||
- `loadMcpConfigs()` is async - must use `await` in config hook
|
||||
- MCP result has `.servers` property that returns the server configs
|
||||
- Order matters in spread: later values override earlier (projectAgents > userAgents > builtinAgents)
|
||||
|
||||
소요 시간: ~4분
|
||||
|
||||
---
|
||||
|
||||
## [2025-12-09 16:35] - Task 8: Update README.md documentation
|
||||
|
||||
### DISCOVERED ISSUES
|
||||
- None - documentation update task
|
||||
|
||||
### IMPLEMENTATION DECISIONS
|
||||
- Added Agent Loader feature documentation under "Other Features" section (line 235-238)
|
||||
- User scope: `~/.claude/agents/`
|
||||
- Project scope: `./.claude/agents/`
|
||||
- Format: `*.md` files with YAML frontmatter
|
||||
- Added MCP Loader feature documentation (line 239-243)
|
||||
- User scope: `~/.claude/.mcp.json`
|
||||
- Project scope: `./.mcp.json`
|
||||
- Local scope: `./.claude/.mcp.json`
|
||||
- Environment variable expansion (`${VAR}` syntax)
|
||||
- Added `claude-code-*` naming convention explanation as a blockquote note (line 245)
|
||||
- Explains features migrated from Claude Code
|
||||
- Lists examples: claude-code-command-loader, skill-loader, agent-loader, mcp-loader
|
||||
|
||||
### PROBLEMS FOR NEXT TASKS
|
||||
- None - this is the final task
|
||||
|
||||
### VERIFICATION RESULTS
|
||||
- README.md updated with new documentation
|
||||
- Style matches existing documentation (bullet points, code blocks for paths)
|
||||
- No sections removed or modified (only additions)
|
||||
|
||||
### LEARNINGS
|
||||
- README.md "Other Features" section is at line 224
|
||||
- Existing features: Terminal Title, Command Loader, Skill Loader
|
||||
- Documentation style: bold feature name, bullet points for scopes/details
|
||||
|
||||
소요 시간: ~1분
|
||||
|
||||
---
|
||||
|
||||
## [2025-12-09 17:24] - Task 0: Shared Utilities 포팅
|
||||
|
||||
### DISCOVERED ISSUES
|
||||
- command-executor.ts already existed but had minor whitespace differences (indentation inconsistency)
|
||||
- pattern-matcher.ts and hook-disabled.ts import from `../claude-compat/types` which doesn't exist yet in oh-my-opencode
|
||||
- Types will be created in Task 1 at `src/hooks/claude-code-hooks/types.ts`
|
||||
|
||||
### IMPLEMENTATION DECISIONS
|
||||
- Created snake-case.ts and tool-name.ts (no dependencies) - exact copy from source
|
||||
- Created temporary stub types at `src/hooks/claude-code-hooks/types.ts` with minimal definitions needed for shared utilities
|
||||
- Created pattern-matcher.ts with adjusted import: `../claude-compat/types` → `../hooks/claude-code-hooks/types`
|
||||
- Created hook-disabled.ts with adjusted import to point to stub types
|
||||
- Added all new utilities to `src/shared/index.ts` using barrel export pattern
|
||||
- Stub types include: HookCommand, HookMatcher, ClaudeHooksConfig, ClaudeHookEvent, PluginConfig
|
||||
|
||||
### PROBLEMS FOR NEXT TASKS
|
||||
- Task 1 will replace stub types with full implementation from opencode-cc-plugin
|
||||
- Stub types in `src/hooks/claude-code-hooks/types.ts` are marked with comments indicating they're temporary
|
||||
- The real PluginConfig will likely be different - current stub only supports `disabledHooks` field
|
||||
|
||||
### VERIFICATION RESULTS
|
||||
- Ran: `bun run typecheck` → exit 0, no errors
|
||||
- All 5 functions exported: executeHookCommand, objectToSnakeCase, transformToolName, findMatchingHooks, isHookDisabled
|
||||
- Import paths verified: pattern-matcher.ts and hook-disabled.ts successfully import from stub types
|
||||
|
||||
### LEARNINGS
|
||||
- Import paths must be adjusted when porting between different project structures
|
||||
- opencode-cc-plugin structure: `src/claude-compat/` → oh-my-opencode structure: `src/hooks/claude-code-hooks/`
|
||||
- Stub types strategy allows Task 0 to complete and typecheck to pass before Task 1 implements full types
|
||||
- command-executor.ts in oh-my-opencode had indentation inconsistency (not 100% identical to source)
|
||||
|
||||
소요 시간: ~5분
|
||||
|
||||
---
|
||||
|
||||
## [2025-12-09 17:34] - Task 1: types.ts 포팅
|
||||
|
||||
### DISCOVERED ISSUES
|
||||
- Stub types.ts had `PluginConfig` interface needed by hook-disabled.ts (from Task 0)
|
||||
- Full types.ts from opencode-cc-plugin did NOT have `PluginConfig`
|
||||
- Typecheck initially failed: Module has no exported member 'PluginConfig'
|
||||
|
||||
### IMPLEMENTATION DECISIONS
|
||||
- Copied full types.ts (181 lines) from opencode-cc-plugin → oh-my-opencode
|
||||
- Preserved ALL types: ClaudeHooksConfig, HookMatcher, PreToolUseInput/Output, PostToolUseInput/Output
|
||||
- Preserved deprecated decision fields: `decision?: "allow" | "deny" | "approve" | "block" | "ask"`
|
||||
- Added `PluginConfig` interface at end (oh-my-opencode specific type needed by hook-disabled.ts)
|
||||
- Kept line 150 comment (`// "pending" | "in_progress" | "completed"`) - existing source comment
|
||||
|
||||
### PROBLEMS FOR NEXT TASKS
|
||||
- PluginConfig is now available for all subsequent tasks
|
||||
- Full type definitions ready for Task 2, 3, 4+ to use
|
||||
|
||||
### VERIFICATION RESULTS
|
||||
- Ran: `bun run typecheck` → exit 0, no errors
|
||||
- Verified: ClaudeHooksConfig, HookMatcher, HookCommand types exist
|
||||
- Verified: PreToolUseInput/Output, PostToolUseInput/Output types exist
|
||||
- Verified: deprecated decision field (approve/block) included in PreToolUseOutput
|
||||
- Verified: PluginConfig export added (fixes hook-disabled.ts import)
|
||||
|
||||
### LEARNINGS
|
||||
- opencode-cc-plugin types.ts: 181 lines, no PluginConfig
|
||||
- oh-my-opencode requires PluginConfig for hook disabling functionality
|
||||
- Stub-to-full replacement pattern works: stub allows Task 0 typecheck, Task 1 replaces with full implementation
|
||||
- Must preserve project-specific types (PluginConfig) when porting from different codebases
|
||||
|
||||
소요 시간: ~2분
|
||||
|
||||
---
|
||||
|
||||
## [2025-12-09 17:39] - Task 3: tool-input-cache.ts 포팅
|
||||
|
||||
### DISCOVERED ISSUES
|
||||
- None - straightforward file copy task
|
||||
|
||||
### IMPLEMENTATION DECISIONS
|
||||
- Copied tool-input-cache.ts (48 lines) from opencode-cc-plugin → oh-my-opencode
|
||||
- Preserved cache structure:
|
||||
* Key format: `${sessionId}:${toolName}:${invocationId}`
|
||||
* TTL: 60000ms (1 minute) as CACHE_TTL constant
|
||||
* Periodic cleanup: setInterval every CACHE_TTL (60000ms)
|
||||
- Preserved original comments from source file (lines 12, 39)
|
||||
- Functions: cacheToolInput(), getToolInput()
|
||||
- Cache behavior: getToolInput() deletes entry immediately after retrieval (single-use cache)
|
||||
|
||||
### PROBLEMS FOR NEXT TASKS
|
||||
- Task 4 (pre-tool-use.ts) will call cacheToolInput() to store tool inputs
|
||||
- Task 5 (post-tool-use.ts) will call getToolInput() to retrieve cached inputs for transcript building
|
||||
- No import path changes needed - this file has no external dependencies
|
||||
|
||||
### VERIFICATION RESULTS
|
||||
- File created: `src/hooks/claude-code-hooks/tool-input-cache.ts` (48 lines)
|
||||
- Functions exported: cacheToolInput(), getToolInput()
|
||||
- TTL verified: CACHE_TTL = 60000 (1 minute)
|
||||
- Cleanup interval verified: setInterval(cleanup, CACHE_TTL)
|
||||
|
||||
### LEARNINGS
|
||||
- Tool input cache is a temporary storage for PreToolUse → PostToolUse communication
|
||||
- Single-use pattern: getToolInput() deletes entry after first retrieval (line 33)
|
||||
- TTL check happens after deletion, so expired entries still return null
|
||||
- setInterval runs in background for periodic cleanup of abandoned entries
|
||||
- Source location: `~/local-workspaces/opencode-cc-plugin/src/claude-compat/hooks/tool-input-cache.ts`
|
||||
|
||||
소요 시간: ~2분
|
||||
|
||||
---
|
||||
|
||||
## [2025-12-09 17:39] - Task 2: config.ts + transcript.ts + todo.ts 포팅
|
||||
|
||||
### DISCOVERED ISSUES
|
||||
- transcript.ts had unused imports (ClaudeCodeMessage, ClaudeCodeContent) - same as source file
|
||||
- LSP warned about unused types - removed from import to clean up
|
||||
|
||||
### IMPLEMENTATION DECISIONS
|
||||
- Copied config.ts (101 lines) - no import path changes needed (only uses `./types` and Node.js builtins)
|
||||
- Copied transcript.ts (256 lines) - changed import path:
|
||||
* Line 10: `../shared/tool-name` → `../../shared/tool-name` (opencode-cc-plugin depth 1, oh-my-opencode depth 2)
|
||||
- Copied todo.ts (78 lines) - no import path changes needed (only uses `./types` and Node.js builtins)
|
||||
- Removed unused imports from transcript.ts: ClaudeCodeMessage, ClaudeCodeContent (not used in function bodies)
|
||||
- Preserved ALL original comments from source files - these are pre-existing comments
|
||||
|
||||
### PROBLEMS FOR NEXT TASKS
|
||||
- Task 3 will import cacheToolInput/getToolInput for cache functionality
|
||||
- Task 4 will import loadClaudeHooksConfig, buildTranscriptFromSession
|
||||
- Task 5 will import transcript building functions for PostToolUse hook
|
||||
|
||||
### VERIFICATION RESULTS
|
||||
- Ran: `bun run typecheck` → exit 0, no errors
|
||||
- Files created: config.ts (101 lines), transcript.ts (256 lines), todo.ts (78 lines)
|
||||
- Functions available: loadClaudeHooksConfig(), buildTranscriptFromSession(), appendTranscriptEntry(), loadTodoFile(), saveTodoFile()
|
||||
- Import paths verified: transcript.ts successfully imports transformToolName from ../../shared
|
||||
|
||||
### LEARNINGS
|
||||
- Import path depth difference: opencode-cc-plugin `src/claude-compat/` (1 level up) → oh-my-opencode `src/hooks/claude-code-hooks/` (2 levels up)
|
||||
- transcript.ts unused imports were present in original source - cleaning them is optional but improves code hygiene
|
||||
- config.ts uses Bun.file() for async file reading - compatible with oh-my-opencode's Bun runtime
|
||||
- Bun.file().text() automatically handles encoding
|
||||
|
||||
소요 시간: ~3분
|
||||
|
||||
---
|
||||
|
||||
## [2025-12-09 17:48] - Task 4: pre-tool-use.ts 포팅 (+ plugin-config.ts, config-loader.ts)
|
||||
|
||||
### DISCOVERED ISSUES
|
||||
- pre-tool-use.ts depends on DEFAULT_CONFIG and isHookCommandDisabled which weren't created yet
|
||||
- Plan document listed plugin-config.ts and config-loader.ts as separate task (Section 4), but not mentioned in Task 4 instructions
|
||||
- These dependency files needed to be created before pre-tool-use.ts could compile
|
||||
|
||||
### IMPLEMENTATION DECISIONS
|
||||
- Created plugin-config.ts (9 lines) with DEFAULT_CONFIG containing forceZsh and zshPath settings
|
||||
* Minimal version - only fields used by pre-tool-use.ts (not full opencode-cc-plugin config)
|
||||
* forceZsh: true, zshPath: "/bin/zsh"
|
||||
- Created config-loader.ts (105 lines) - full copy from opencode-cc-plugin
|
||||
* Changed import: `../claude-compat/types` → `./types`
|
||||
* Changed import: `../shared/logger` → `../../shared/logger`
|
||||
* Functions: loadPluginExtendedConfig(), isHookCommandDisabled()
|
||||
* Supports regex patterns for disabling specific hook commands
|
||||
- Created pre-tool-use.ts (172 lines) - full copy with adjusted imports:
|
||||
* `../types` → `./types`
|
||||
* `../../shared` → `../../shared` (unchanged)
|
||||
* `../../config` → `./plugin-config` (NEW file)
|
||||
* `../../config-loader` → `./config-loader` (NEW file)
|
||||
- Preserved ALL exit code logic:
|
||||
* exitCode === 2 → decision = "deny"
|
||||
* exitCode === 1 → decision = "ask"
|
||||
* exitCode === 0 → parse JSON for decision
|
||||
- Preserved ALL deprecated field support:
|
||||
* decision: "approve" → "allow"
|
||||
* decision: "block" → "deny"
|
||||
- Original comments from source preserved (backward compat, spec references)
|
||||
|
||||
### PROBLEMS FOR NEXT TASKS
|
||||
- Task 5 (post-tool-use.ts) can now import executePreToolUseHooks if needed
|
||||
- plugin-config.ts and config-loader.ts are now available for all subsequent hook implementations
|
||||
- isHookCommandDisabled pattern can be reused in PostToolUse, UserPromptSubmit, Stop hooks
|
||||
|
||||
### VERIFICATION RESULTS
|
||||
- Ran: `bun run typecheck` → exit 0, no errors
|
||||
- Ran: `bun run build` → exit 0, successful
|
||||
- Committed: 530c4d6 "feat(hooks): add PreToolUse hook executor"
|
||||
* 4 files: tool-input-cache.ts (Task 3), plugin-config.ts, config-loader.ts, pre-tool-use.ts (Task 4)
|
||||
* 333 insertions total
|
||||
- Functions available: executePreToolUseHooks(), isHookCommandDisabled(), loadPluginExtendedConfig()
|
||||
- Exit code mapping verified: lines 96-116 check exitCode === 2/1/0
|
||||
- Deprecated field mapping verified: lines 132-141 check decision === "approve"/"block"
|
||||
|
||||
### LEARNINGS
|
||||
- Pre-tool-use.ts depends on plugin configuration that wasn't part of oh-my-opencode's original structure
|
||||
- plugin-config.ts only needs subset of opencode-cc-plugin's config.ts (forceZsh, zshPath for executeHookCommand)
|
||||
- config-loader.ts provides hook command filtering via regex patterns (disabledHooks config)
|
||||
- executeHookCommand from shared/ accepts ExecuteHookOptions{ forceZsh, zshPath } parameter
|
||||
- Task 3 + Task 4 grouped in single commit per plan requirement
|
||||
- Source: `/Users/yeongyu/local-workspaces/opencode-cc-plugin/src/claude-compat/hooks/pre-tool-use.ts` (173 lines)
|
||||
|
||||
소요 시간: ~5분
|
||||
|
||||
---
|
||||
|
||||
## [2025-12-09 17:52] - Task 5: post-tool-use.ts 포팅
|
||||
|
||||
### DISCOVERED ISSUES
|
||||
- None - straightforward file copy with import path adjustments
|
||||
|
||||
### IMPLEMENTATION DECISIONS
|
||||
- Copied post-tool-use.ts (200 lines) from opencode-cc-plugin → oh-my-opencode
|
||||
- Import path adjustments:
|
||||
* `../types` → `./types`
|
||||
* `../../shared` → `../../shared` (unchanged)
|
||||
* `../../config` → `./plugin-config`
|
||||
* `../transcript` → `./transcript`
|
||||
* `../../config-loader` → `./config-loader`
|
||||
- Preserved ALL transcript logic:
|
||||
* buildTranscriptFromSession() call with client.session.messages() API
|
||||
* Temp file creation in try block
|
||||
* deleteTempTranscript() cleanup in finally block
|
||||
- Preserved ALL exit code handling:
|
||||
* exitCode === 2 → warning (continue)
|
||||
* exitCode === 0 → parse JSON for decision: "block"
|
||||
* Non-zero, non-2 → parse JSON for decision: "block"
|
||||
- Preserved ALL output fields: block, reason, message, warnings, elapsedMs, additionalContext, continue, stopReason, suppressOutput, systemMessage
|
||||
- Original comments from source preserved (PORT FROM DISABLED, cleanup explanation)
|
||||
|
||||
### PROBLEMS FOR NEXT TASKS
|
||||
- Task 6 (user-prompt-submit.ts, stop.ts) can use similar pattern for hook execution
|
||||
- plugin-config.ts, config-loader.ts, transcript.ts dependencies already in place
|
||||
|
||||
### VERIFICATION RESULTS
|
||||
- Ran: `bun run typecheck` → exit 0, no errors
|
||||
- Ran: `bun run build` → exit 0, successful
|
||||
- File created: `src/hooks/claude-code-hooks/post-tool-use.ts` (200 lines)
|
||||
- Functions available: executePostToolUseHooks()
|
||||
- Transcript integration verified: buildTranscriptFromSession() imported from ./transcript
|
||||
- Cleanup mechanism verified: deleteTempTranscript() in finally block (line 196)
|
||||
|
||||
### LEARNINGS
|
||||
- PostToolUse differs from PreToolUse: no permission decision (allow/deny/ask), only block/continue
|
||||
- PostToolUse provides hook results via message/warnings/additionalContext (observability, not control)
|
||||
- Exit code 2 in PostToolUse = warning (not block), collected in warnings array
|
||||
- Transcript temp file pattern: create in try, cleanup in finally (prevents disk accumulation)
|
||||
- Source: `/Users/yeongyu/local-workspaces/opencode-cc-plugin/src/claude-compat/hooks/post-tool-use.ts` (200 lines)
|
||||
|
||||
소요 시간: ~5분
|
||||
|
||||
---
|
||||
|
||||
## [2025-12-09 17:58] - Task 6: user-prompt-submit.ts + stop.ts 포팅
|
||||
|
||||
### DISCOVERED ISSUES
|
||||
- None - straightforward file copy with import path adjustments
|
||||
|
||||
### IMPLEMENTATION DECISIONS
|
||||
- Copied user-prompt-submit.ts (118 lines) from opencode-cc-plugin → oh-my-opencode
|
||||
- Copied stop.ts (119 lines) from opencode-cc-plugin → oh-my-opencode
|
||||
- Import path adjustments (both files):
|
||||
* `../types` → `./types`
|
||||
* `../../shared` → `../../shared` (unchanged)
|
||||
* `../../config` → `./plugin-config`
|
||||
* `../../config-loader` → `./config-loader`
|
||||
* `../todo` → `./todo` (stop.ts only)
|
||||
- Preserved recursion prevention logic in user-prompt-submit.ts:
|
||||
* Tags: `<user-prompt-submit-hook>` (open/close)
|
||||
* Check if prompt already contains tags → return early
|
||||
* Wrap hook stdout with tags to prevent infinite recursion
|
||||
- Preserved inject_prompt support:
|
||||
* user-prompt-submit: messages array collection for injection
|
||||
* stop: injectPrompt field in result (from output.inject_prompt or output.reason)
|
||||
- Preserved stopHookActiveState management in stop.ts:
|
||||
* Module-level Map<string, boolean> for per-session state
|
||||
* setStopHookActive(), getStopHookActive() exported
|
||||
* State persists across hook invocations
|
||||
- Preserved exit code handling:
|
||||
* stop.ts: exitCode === 2 → block with reason
|
||||
* user-prompt-submit.ts: exitCode !== 0 → check JSON for decision: "block"
|
||||
|
||||
### PROBLEMS FOR NEXT TASKS
|
||||
- Task 7 (hook-message-injector) will use the message injection pattern
|
||||
- Task 8 (Factory + Integration) will wire these hooks to OpenCode lifecycle events
|
||||
|
||||
### VERIFICATION RESULTS
|
||||
- Ran: `bun run typecheck` → exit 0, no errors
|
||||
- Ran: `bun run build` → exit 0, successful
|
||||
- Files created:
|
||||
* `src/hooks/claude-code-hooks/user-prompt-submit.ts` (115 lines)
|
||||
* `src/hooks/claude-code-hooks/stop.ts` (119 lines)
|
||||
- Functions available:
|
||||
* executeUserPromptSubmitHooks() with UserPromptSubmitContext → UserPromptSubmitResult
|
||||
* executeStopHooks() with StopContext → StopResult
|
||||
* setStopHookActive(), getStopHookActive()
|
||||
- Recursion prevention verified: lines 47-52 check for tag presence
|
||||
- inject_prompt field verified: stop.ts line 102 sets injectPrompt from output
|
||||
|
||||
### LEARNINGS
|
||||
- user-prompt-submit uses tag wrapping pattern to prevent infinite hook loops
|
||||
- stop hook can inject prompts into session via injectPrompt result field
|
||||
- stopHookActiveState Map persists across hook invocations (module-level state)
|
||||
- getTodoPath() from ./todo provides todo file path for Stop hook context
|
||||
- Source files:
|
||||
* `/Users/yeongyu/local-workspaces/opencode-cc-plugin/src/claude-compat/hooks/user-prompt-submit.ts` (118 lines)
|
||||
* `/Users/yeongyu/local-workspaces/opencode-cc-plugin/src/claude-compat/hooks/stop.ts` (119 lines)
|
||||
|
||||
소요 시간: ~3분
|
||||
|
||||
---
|
||||
|
||||
## [2025-12-09 17:58] - Task 7: hook-message-injector 포팅
|
||||
|
||||
### DISCOVERED ISSUES
|
||||
- None - straightforward file copy task
|
||||
|
||||
### IMPLEMENTATION DECISIONS
|
||||
- Created `src/features/hook-message-injector/` directory
|
||||
- Copied 4 files from opencode-cc-plugin → oh-my-opencode:
|
||||
* constants.ts (9 lines): XDG-based path definitions (MESSAGE_STORAGE, PART_STORAGE)
|
||||
* types.ts (46 lines): MessageMeta, OriginalMessageContext, TextPart interfaces
|
||||
* injector.ts (142 lines): injectHookMessage() implementation with message/part storage
|
||||
* index.ts (3 lines): Barrel export
|
||||
- No import path changes needed - module is self-contained
|
||||
- Preserved XDG_DATA_HOME environment variable support
|
||||
- Preserved message fallback logic: finds nearest message with agent/model/tools if not provided
|
||||
|
||||
### PROBLEMS FOR NEXT TASKS
|
||||
- Task 8 (Factory + Integration) will import injectHookMessage from this module
|
||||
- Hook executors (user-prompt-submit, stop) can use injectHookMessage to store hook messages
|
||||
|
||||
### VERIFICATION RESULTS
|
||||
- Ran: `bun run typecheck` → exit 0, no errors
|
||||
- Files created: src/features/hook-message-injector/ (4 files)
|
||||
- Functions exported: injectHookMessage()
|
||||
- Types exported: MessageMeta, OriginalMessageContext, TextPart
|
||||
- Constants exported: MESSAGE_STORAGE, PART_STORAGE (XDG-based paths)
|
||||
|
||||
### LEARNINGS
|
||||
- Message injector uses XDG_DATA_HOME for storage (~/.local/share/opencode/storage/)
|
||||
- Message storage structure: sessionID → messageID.json (meta) + partID.json (content)
|
||||
- Fallback logic: searches recent messages for agent/model/tools if originalMessage is incomplete
|
||||
- Part-based storage allows incremental message building
|
||||
- Source: `/Users/yeongyu/local-workspaces/opencode-cc-plugin/src/features/hook-message-injector/`
|
||||
|
||||
소요 시간: ~2분
|
||||
|
||||
---
|
||||
|
||||
## [2025-12-09 18:08] - Task 8: Factory 생성 + 통합
|
||||
|
||||
### DISCOVERED ISSUES
|
||||
- None - final integration task with well-defined hook executors from previous tasks
|
||||
|
||||
### IMPLEMENTATION DECISIONS
|
||||
- Created `src/hooks/claude-code-hooks/index.ts` (146 lines) with createClaudeCodeHooksHook() factory
|
||||
- Factory returns hook handler object with 3 handlers:
|
||||
* `tool.execute.before`: Executes executePreToolUseHooks()
|
||||
- Loads config dynamically (async) on each invocation
|
||||
- Maps OpenCode input → PreToolUseContext
|
||||
- Caches tool input for PostToolUse
|
||||
- Handles deny/ask decisions (deny throws error, ask logs warning)
|
||||
* `tool.execute.after`: Executes executePostToolUseHooks()
|
||||
- Retrieves cached tool input via getToolInput()
|
||||
- Maps OpenCode input → PostToolUseContext with client wrapper
|
||||
- Appends hook message to output if provided
|
||||
- Throws error if block decision returned
|
||||
* `event`: Executes executeStopHooks() for session.idle
|
||||
- Filters event.type === "session.idle"
|
||||
- Maps OpenCode event → StopContext
|
||||
- Injects prompt via ctx.client.session.prompt() if injectPrompt returned
|
||||
- Updated `src/hooks/index.ts`: Added createClaudeCodeHooksHook export
|
||||
- Updated `src/index.ts`:
|
||||
* Imported createClaudeCodeHooksHook
|
||||
* Created claudeCodeHooks instance
|
||||
* Registered handlers in tool.execute.before, tool.execute.after, event hooks
|
||||
* Claude hooks run FIRST in execution order (before other hooks)
|
||||
- Config loading: Async loadClaudeHooksConfig() and loadPluginExtendedConfig() called in each handler (not cached)
|
||||
- Transcript path: Uses getTranscriptPath() function (not buildTranscriptPath which doesn't exist)
|
||||
|
||||
### PROBLEMS FOR NEXT TASKS
|
||||
- None - this is the final task (Task 8)
|
||||
- All Claude Code Hooks now integrated into oh-my-opencode plugin system
|
||||
|
||||
### VERIFICATION RESULTS
|
||||
- Ran: `bun run typecheck` → exit 0, no errors
|
||||
- Ran: `bun run build` → exit 0, successful build
|
||||
- Files modified:
|
||||
* src/hooks/claude-code-hooks/index.ts (created)
|
||||
* src/hooks/index.ts (export added)
|
||||
* src/index.ts (hook registration)
|
||||
- Hook handler registration verified: claudeCodeHooks handlers called in all 3 hook points
|
||||
- Execution order verified: Claude hooks run before existing hooks in tool.execute.*
|
||||
|
||||
### LEARNINGS
|
||||
- OpenCode Plugin API: Factory pattern createXxxHook(ctx: PluginInput) → handlers object
|
||||
- OpenCode does NOT have chat.params hook → UserPromptSubmit not implemented in factory
|
||||
- Config loading must be async → call loadClaudeHooksConfig() in each handler, not once during initialization
|
||||
- Tool input cache is module-level state → cacheToolInput/getToolInput work across handlers
|
||||
- Stop hook only triggers on session.idle event → filter event.type
|
||||
- Import path: getTranscriptPath (exists), not buildTranscriptPath (doesn't exist)
|
||||
|
||||
소요 시간: ~6분
|
||||
|
||||
---
|
||||
|
||||
## [2025-12-09 18:16] - hook-message-injector 사용 패턴 크로스체크
|
||||
|
||||
### DISCOVERED ISSUES
|
||||
- **CRITICAL**: UserPromptSubmit hooks가 oh-my-opencode에 완전히 누락됨
|
||||
- opencode-cc-plugin에서는 chat.message hook으로 UserPromptSubmit 처리
|
||||
- oh-my-opencode에는 chat.message hook이 구현되지 않음
|
||||
- user-prompt-submit.ts는 정의만 있고 실제 사용처 없음
|
||||
|
||||
### IMPLEMENTATION DECISIONS
|
||||
- PostToolUse는 이미 올바르게 구현됨:
|
||||
* opencode-cc-plugin: result.message를 tool output에 append
|
||||
* oh-my-opencode: 동일한 방식 사용 (claude-code-hooks/index.ts:95-97)
|
||||
* injectHookMessage() 불필요
|
||||
- UserPromptSubmit 구현 추가:
|
||||
* chat.message hook handler 추가 (claude-code-hooks/index.ts)
|
||||
* executeUserPromptSubmitHooks() 호출
|
||||
* sessionFirstMessageProcessed Set으로 첫 메시지 skip (title generation)
|
||||
* result.messages가 있으면 injectHookMessage() 호출
|
||||
* src/index.ts에 hook 등록
|
||||
- Import 추가:
|
||||
* executeUserPromptSubmitHooks, UserPromptSubmitContext, MessagePart from ./user-prompt-submit
|
||||
* injectHookMessage from ../../features/hook-message-injector
|
||||
|
||||
### PROBLEMS FOR NEXT TASKS
|
||||
- None - UserPromptSubmit 통합 완료
|
||||
|
||||
### VERIFICATION RESULTS
|
||||
- Ran: `bunx tsc --noEmit` → exit 0, no errors
|
||||
- Ran: `bun run build` → exit 0, successful build
|
||||
- Files modified:
|
||||
* src/hooks/claude-code-hooks/index.ts (chat.message handler 추가)
|
||||
* src/index.ts (chat.message hook 등록)
|
||||
- Verified OpenCode Plugin API: chat.message hook 공식 지원 확인 (@opencode-ai/plugin/dist/index.d.ts:112-123)
|
||||
|
||||
### LEARNINGS
|
||||
- OpenCode Plugin API에 chat.message hook 존재:
|
||||
* input: sessionID, agent?, model?, messageID?
|
||||
* output: message, parts[]
|
||||
- PostToolUse는 tool output에 직접 append (injectHookMessage 불필요)
|
||||
- UserPromptSubmit는 file system injection 사용 (injectHookMessage 필수)
|
||||
- opencode-cc-plugin 구조: src/plugin/chat-handler.ts → handleChatMessage()
|
||||
- oh-my-opencode 구조: src/hooks/claude-code-hooks/index.ts → createClaudeCodeHooksHook()
|
||||
- 첫 메시지 skip 로직: title generation을 위해 UserPromptSubmit hooks 실행 안 함
|
||||
|
||||
소요 시간: ~8분
|
||||
|
||||
---
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.2",
|
||||
"description": "OpenCode plugin - custom agents (oracle, librarian) and enhanced features",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -7,16 +7,8 @@ const CONTEXT_WARNING_THRESHOLD = 0.70
|
||||
const CONTEXT_REMINDER = `[SYSTEM REMINDER - 1M Context Window]
|
||||
|
||||
You are using Anthropic Claude with 1M context window.
|
||||
Current usage has exceeded 75%.
|
||||
|
||||
RECOMMENDATIONS:
|
||||
- Consider compacting the session if available
|
||||
- Break complex tasks into smaller, focused sessions
|
||||
- Be concise in your responses
|
||||
- Avoid redundant file reads
|
||||
|
||||
You have access to 1M tokens - use them wisely. Do NOT rush or skip tasks.
|
||||
Complete your work thoroughly despite the context usage warning.`
|
||||
You have plenty of context remaining - do NOT rush or skip tasks.
|
||||
Complete your work thoroughly and methodically.`
|
||||
|
||||
interface AssistantMessageInfo {
|
||||
role: "assistant"
|
||||
|
||||
9
src/hooks/directory-readme-injector/constants.ts
Normal file
9
src/hooks/directory-readme-injector/constants.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { join } from "node:path";
|
||||
import { xdgData } from "xdg-basedir";
|
||||
|
||||
export const OPENCODE_STORAGE = join(xdgData ?? "", "opencode", "storage");
|
||||
export const README_INJECTOR_STORAGE = join(
|
||||
OPENCODE_STORAGE,
|
||||
"directory-readme",
|
||||
);
|
||||
export const README_FILENAME = "README.md";
|
||||
126
src/hooks/directory-readme-injector/index.ts
Normal file
126
src/hooks/directory-readme-injector/index.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import type { PluginInput } from "@opencode-ai/plugin";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import {
|
||||
loadInjectedPaths,
|
||||
saveInjectedPaths,
|
||||
clearInjectedPaths,
|
||||
} from "./storage";
|
||||
import { README_FILENAME } from "./constants";
|
||||
|
||||
interface ToolExecuteInput {
|
||||
tool: string;
|
||||
sessionID: string;
|
||||
callID: string;
|
||||
}
|
||||
|
||||
interface ToolExecuteOutput {
|
||||
title: string;
|
||||
output: string;
|
||||
metadata: unknown;
|
||||
}
|
||||
|
||||
interface EventInput {
|
||||
event: {
|
||||
type: string;
|
||||
properties?: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
export function createDirectoryReadmeInjectorHook(ctx: PluginInput) {
|
||||
const sessionCaches = new Map<string, Set<string>>();
|
||||
|
||||
function getSessionCache(sessionID: string): Set<string> {
|
||||
if (!sessionCaches.has(sessionID)) {
|
||||
sessionCaches.set(sessionID, loadInjectedPaths(sessionID));
|
||||
}
|
||||
return sessionCaches.get(sessionID)!;
|
||||
}
|
||||
|
||||
function resolveFilePath(title: string): string | null {
|
||||
if (!title) return null;
|
||||
if (title.startsWith("/")) return title;
|
||||
return resolve(ctx.directory, title);
|
||||
}
|
||||
|
||||
function findReadmeMdUp(startDir: string): string[] {
|
||||
const found: string[] = [];
|
||||
let current = startDir;
|
||||
|
||||
while (true) {
|
||||
const readmePath = join(current, README_FILENAME);
|
||||
if (existsSync(readmePath)) {
|
||||
found.push(readmePath);
|
||||
}
|
||||
|
||||
if (current === ctx.directory) break;
|
||||
const parent = dirname(current);
|
||||
if (parent === current) break;
|
||||
if (!parent.startsWith(ctx.directory)) break;
|
||||
current = parent;
|
||||
}
|
||||
|
||||
return found.reverse();
|
||||
}
|
||||
|
||||
const toolExecuteAfter = async (
|
||||
input: ToolExecuteInput,
|
||||
output: ToolExecuteOutput,
|
||||
) => {
|
||||
if (input.tool.toLowerCase() !== "read") return;
|
||||
|
||||
const filePath = resolveFilePath(output.title);
|
||||
if (!filePath) return;
|
||||
|
||||
const dir = dirname(filePath);
|
||||
const cache = getSessionCache(input.sessionID);
|
||||
const readmePaths = findReadmeMdUp(dir);
|
||||
|
||||
const toInject: { path: string; content: string }[] = [];
|
||||
|
||||
for (const readmePath of readmePaths) {
|
||||
const readmeDir = dirname(readmePath);
|
||||
if (cache.has(readmeDir)) continue;
|
||||
|
||||
try {
|
||||
const content = readFileSync(readmePath, "utf-8");
|
||||
toInject.push({ path: readmePath, content });
|
||||
cache.add(readmeDir);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (toInject.length === 0) return;
|
||||
|
||||
for (const { path, content } of toInject) {
|
||||
output.output += `\n\n[Project README: ${path}]\n${content}`;
|
||||
}
|
||||
|
||||
saveInjectedPaths(input.sessionID, cache);
|
||||
};
|
||||
|
||||
const eventHandler = async ({ event }: EventInput) => {
|
||||
const props = event.properties as Record<string, unknown> | undefined;
|
||||
|
||||
if (event.type === "session.deleted") {
|
||||
const sessionInfo = props?.info as { id?: string } | undefined;
|
||||
if (sessionInfo?.id) {
|
||||
sessionCaches.delete(sessionInfo.id);
|
||||
clearInjectedPaths(sessionInfo.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type === "session.compacted") {
|
||||
const sessionID = (props?.sessionID ??
|
||||
(props?.info as { id?: string } | undefined)?.id) as string | undefined;
|
||||
if (sessionID) {
|
||||
sessionCaches.delete(sessionID);
|
||||
clearInjectedPaths(sessionID);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
"tool.execute.after": toolExecuteAfter,
|
||||
event: eventHandler,
|
||||
};
|
||||
}
|
||||
48
src/hooks/directory-readme-injector/storage.ts
Normal file
48
src/hooks/directory-readme-injector/storage.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
writeFileSync,
|
||||
unlinkSync,
|
||||
} from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { README_INJECTOR_STORAGE } from "./constants";
|
||||
import type { InjectedPathsData } from "./types";
|
||||
|
||||
function getStoragePath(sessionID: string): string {
|
||||
return join(README_INJECTOR_STORAGE, `${sessionID}.json`);
|
||||
}
|
||||
|
||||
export function loadInjectedPaths(sessionID: string): Set<string> {
|
||||
const filePath = getStoragePath(sessionID);
|
||||
if (!existsSync(filePath)) return new Set();
|
||||
|
||||
try {
|
||||
const content = readFileSync(filePath, "utf-8");
|
||||
const data: InjectedPathsData = JSON.parse(content);
|
||||
return new Set(data.injectedPaths);
|
||||
} catch {
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
|
||||
export function saveInjectedPaths(sessionID: string, paths: Set<string>): void {
|
||||
if (!existsSync(README_INJECTOR_STORAGE)) {
|
||||
mkdirSync(README_INJECTOR_STORAGE, { recursive: true });
|
||||
}
|
||||
|
||||
const data: InjectedPathsData = {
|
||||
sessionID,
|
||||
injectedPaths: [...paths],
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
|
||||
writeFileSync(getStoragePath(sessionID), JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
export function clearInjectedPaths(sessionID: string): void {
|
||||
const filePath = getStoragePath(sessionID);
|
||||
if (existsSync(filePath)) {
|
||||
unlinkSync(filePath);
|
||||
}
|
||||
}
|
||||
5
src/hooks/directory-readme-injector/types.ts
Normal file
5
src/hooks/directory-readme-injector/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface InjectedPathsData {
|
||||
sessionID: string;
|
||||
injectedPaths: string[];
|
||||
updatedAt: number;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ export { createSessionRecoveryHook } from "./session-recovery";
|
||||
export { createCommentCheckerHooks } from "./comment-checker";
|
||||
export { createGrepOutputTruncatorHook } from "./grep-output-truncator";
|
||||
export { createDirectoryAgentsInjectorHook } from "./directory-agents-injector";
|
||||
export { createDirectoryReadmeInjectorHook } from "./directory-readme-injector";
|
||||
export { createEmptyTaskResponseDetectorHook } from "./empty-task-response-detector";
|
||||
export { createAnthropicAutoCompactHook } from "./anthropic-auto-compact";
|
||||
export { createThinkModeHook } from "./think-mode";
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { PluginInput } from "@opencode-ai/plugin"
|
||||
import type { createOpencodeClient } from "@opencode-ai/sdk"
|
||||
import {
|
||||
findEmptyMessages,
|
||||
findEmptyMessageByIndex,
|
||||
findMessagesWithOrphanThinking,
|
||||
findMessagesWithThinkingBlocks,
|
||||
injectTextPart,
|
||||
@@ -54,6 +55,12 @@ function getErrorMessage(error: unknown): string {
|
||||
return (errorObj.data?.message || errorObj.error?.message || errorObj.message || "").toLowerCase()
|
||||
}
|
||||
|
||||
function extractMessageIndex(error: unknown): number | null {
|
||||
const message = getErrorMessage(error)
|
||||
const match = message.match(/messages\.(\d+)/)
|
||||
return match ? parseInt(match[1], 10) : null
|
||||
}
|
||||
|
||||
function detectErrorType(error: unknown): RecoveryErrorType {
|
||||
const message = getErrorMessage(error)
|
||||
|
||||
@@ -161,16 +168,26 @@ async function recoverEmptyContentMessage(
|
||||
_client: Client,
|
||||
sessionID: string,
|
||||
failedAssistantMsg: MessageData,
|
||||
_directory: string
|
||||
_directory: string,
|
||||
error: unknown
|
||||
): Promise<boolean> {
|
||||
const emptyMessageIDs = findEmptyMessages(sessionID)
|
||||
const targetIndex = extractMessageIndex(error)
|
||||
const failedID = failedAssistantMsg.info?.id
|
||||
|
||||
if (emptyMessageIDs.length === 0) {
|
||||
const fallbackID = failedAssistantMsg.info?.id
|
||||
if (!fallbackID) return false
|
||||
return injectTextPart(sessionID, fallbackID, "(interrupted)")
|
||||
if (targetIndex !== null) {
|
||||
const targetMessageID = findEmptyMessageByIndex(sessionID, targetIndex)
|
||||
if (targetMessageID) {
|
||||
return injectTextPart(sessionID, targetMessageID, "(interrupted)")
|
||||
}
|
||||
}
|
||||
|
||||
if (failedID) {
|
||||
if (injectTextPart(sessionID, failedID, "(interrupted)")) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const emptyMessageIDs = findEmptyMessages(sessionID)
|
||||
let anySuccess = false
|
||||
for (const messageID of emptyMessageIDs) {
|
||||
if (injectTextPart(sessionID, messageID, "(interrupted)")) {
|
||||
@@ -262,15 +279,16 @@ export function createSessionRecoveryHook(ctx: PluginInput) {
|
||||
} else if (errorType === "thinking_disabled_violation") {
|
||||
success = await recoverThinkingDisabledViolation(ctx.client, sessionID, failedMsg)
|
||||
} else if (errorType === "empty_content_message") {
|
||||
success = await recoverEmptyContentMessage(ctx.client, sessionID, failedMsg, ctx.directory)
|
||||
success = await recoverEmptyContentMessage(ctx.client, sessionID, failedMsg, ctx.directory, info.error)
|
||||
}
|
||||
|
||||
return success
|
||||
} catch {
|
||||
return false
|
||||
} finally {
|
||||
processingErrors.delete(assistantMsgID)
|
||||
}
|
||||
return success
|
||||
} catch (err) {
|
||||
console.error("[session-recovery] Recovery failed:", err)
|
||||
return false
|
||||
} finally {
|
||||
processingErrors.delete(assistantMsgID)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -42,7 +42,12 @@ export function readMessages(sessionID: string): StoredMessageMeta[] {
|
||||
}
|
||||
}
|
||||
|
||||
return messages.sort((a, b) => a.id.localeCompare(b.id))
|
||||
return messages.sort((a, b) => {
|
||||
const aTime = a.time?.created ?? 0
|
||||
const bTime = b.time?.created ?? 0
|
||||
if (aTime !== bTime) return aTime - bTime
|
||||
return a.id.localeCompare(b.id)
|
||||
})
|
||||
}
|
||||
|
||||
export function readParts(messageID: string): StoredPart[] {
|
||||
@@ -117,13 +122,9 @@ export function findEmptyMessages(sessionID: string): string[] {
|
||||
const messages = readMessages(sessionID)
|
||||
const emptyIds: string[] = []
|
||||
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const msg = messages[i]
|
||||
for (const msg of messages) {
|
||||
if (msg.role !== "assistant") continue
|
||||
|
||||
const isLastMessage = i === messages.length - 1
|
||||
if (isLastMessage) continue
|
||||
|
||||
if (!messageHasContent(msg.id)) {
|
||||
emptyIds.push(msg.id)
|
||||
}
|
||||
@@ -132,6 +133,18 @@ export function findEmptyMessages(sessionID: string): string[] {
|
||||
return emptyIds
|
||||
}
|
||||
|
||||
export function findEmptyMessageByIndex(sessionID: string, targetIndex: number): string | null {
|
||||
const messages = readMessages(sessionID)
|
||||
|
||||
if (targetIndex < 0 || targetIndex >= messages.length) return null
|
||||
|
||||
const targetMsg = messages[targetIndex]
|
||||
if (targetMsg.role !== "assistant") return null
|
||||
if (messageHasContent(targetMsg.id)) return null
|
||||
|
||||
return targetMsg.id
|
||||
}
|
||||
|
||||
export function findFirstEmptyMessage(sessionID: string): string | null {
|
||||
const emptyIds = findEmptyMessages(sessionID)
|
||||
return emptyIds.length > 0 ? emptyIds[0] : null
|
||||
|
||||
@@ -36,6 +36,7 @@ export function createTodoContinuationEnforcer(ctx: PluginInput) {
|
||||
const remindedSessions = new Set<string>()
|
||||
const interruptedSessions = new Set<string>()
|
||||
const errorSessions = new Set<string>()
|
||||
const pendingTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
|
||||
return async ({ event }: { event: { type: string; properties?: unknown } }) => {
|
||||
const props = event.properties as Record<string, unknown> | undefined
|
||||
@@ -47,6 +48,13 @@ export function createTodoContinuationEnforcer(ctx: PluginInput) {
|
||||
if (detectInterrupt(props?.error)) {
|
||||
interruptedSessions.add(sessionID)
|
||||
}
|
||||
|
||||
// Cancel pending continuation if error occurs
|
||||
const timer = pendingTimers.get(sessionID)
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
pendingTimers.delete(sessionID)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -55,68 +63,78 @@ export function createTodoContinuationEnforcer(ctx: PluginInput) {
|
||||
const sessionID = props?.sessionID as string | undefined
|
||||
if (!sessionID) return
|
||||
|
||||
// Wait for potential session.error events to be processed first
|
||||
await new Promise(resolve => setTimeout(resolve, 150))
|
||||
|
||||
const shouldBypass = interruptedSessions.has(sessionID) || errorSessions.has(sessionID)
|
||||
|
||||
interruptedSessions.delete(sessionID)
|
||||
errorSessions.delete(sessionID)
|
||||
|
||||
if (shouldBypass) {
|
||||
return
|
||||
// Cancel any existing timer to debounce
|
||||
const existingTimer = pendingTimers.get(sessionID)
|
||||
if (existingTimer) {
|
||||
clearTimeout(existingTimer)
|
||||
}
|
||||
|
||||
if (remindedSessions.has(sessionID)) {
|
||||
return
|
||||
}
|
||||
// Schedule continuation check
|
||||
const timer = setTimeout(async () => {
|
||||
pendingTimers.delete(sessionID)
|
||||
|
||||
let todos: Todo[] = []
|
||||
try {
|
||||
const response = await ctx.client.session.todo({
|
||||
path: { id: sessionID },
|
||||
})
|
||||
todos = (response.data ?? response) as Todo[]
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
const shouldBypass = interruptedSessions.has(sessionID) || errorSessions.has(sessionID)
|
||||
|
||||
interruptedSessions.delete(sessionID)
|
||||
errorSessions.delete(sessionID)
|
||||
|
||||
if (!todos || todos.length === 0) {
|
||||
return
|
||||
}
|
||||
if (shouldBypass) {
|
||||
return
|
||||
}
|
||||
|
||||
const incomplete = todos.filter(
|
||||
(t) => t.status !== "completed" && t.status !== "cancelled"
|
||||
)
|
||||
if (remindedSessions.has(sessionID)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (incomplete.length === 0) {
|
||||
return
|
||||
}
|
||||
let todos: Todo[] = []
|
||||
try {
|
||||
const response = await ctx.client.session.todo({
|
||||
path: { id: sessionID },
|
||||
})
|
||||
todos = (response.data ?? response) as Todo[]
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
remindedSessions.add(sessionID)
|
||||
if (!todos || todos.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// Re-check if abort occurred during the delay
|
||||
if (interruptedSessions.has(sessionID) || errorSessions.has(sessionID)) {
|
||||
remindedSessions.delete(sessionID)
|
||||
return
|
||||
}
|
||||
const incomplete = todos.filter(
|
||||
(t) => t.status !== "completed" && t.status !== "cancelled"
|
||||
)
|
||||
|
||||
try {
|
||||
await ctx.client.session.prompt({
|
||||
path: { id: sessionID },
|
||||
body: {
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: `${CONTINUATION_PROMPT}\n\n[Status: ${todos.length - incomplete.length}/${todos.length} completed, ${incomplete.length} remaining]`,
|
||||
},
|
||||
],
|
||||
},
|
||||
query: { directory: ctx.directory },
|
||||
})
|
||||
} catch {
|
||||
remindedSessions.delete(sessionID)
|
||||
}
|
||||
if (incomplete.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
remindedSessions.add(sessionID)
|
||||
|
||||
// Re-check if abort occurred during the delay/fetch
|
||||
if (interruptedSessions.has(sessionID) || errorSessions.has(sessionID)) {
|
||||
remindedSessions.delete(sessionID)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await ctx.client.session.prompt({
|
||||
path: { id: sessionID },
|
||||
body: {
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: `${CONTINUATION_PROMPT}\n\n[Status: ${todos.length - incomplete.length}/${todos.length} completed, ${incomplete.length} remaining]`,
|
||||
},
|
||||
],
|
||||
},
|
||||
query: { directory: ctx.directory },
|
||||
})
|
||||
} catch {
|
||||
remindedSessions.delete(sessionID)
|
||||
}
|
||||
}, 200)
|
||||
|
||||
pendingTimers.set(sessionID, timer)
|
||||
}
|
||||
|
||||
if (event.type === "message.updated") {
|
||||
@@ -124,6 +142,13 @@ export function createTodoContinuationEnforcer(ctx: PluginInput) {
|
||||
const sessionID = info?.sessionID as string | undefined
|
||||
if (sessionID && info?.role === "user") {
|
||||
remindedSessions.delete(sessionID)
|
||||
|
||||
// Cancel pending continuation on user interaction
|
||||
const timer = pendingTimers.get(sessionID)
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
pendingTimers.delete(sessionID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,6 +158,13 @@ export function createTodoContinuationEnforcer(ctx: PluginInput) {
|
||||
remindedSessions.delete(sessionInfo.id)
|
||||
interruptedSessions.delete(sessionInfo.id)
|
||||
errorSessions.delete(sessionInfo.id)
|
||||
|
||||
// Cancel pending continuation
|
||||
const timer = pendingTimers.get(sessionInfo.id)
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
pendingTimers.delete(sessionInfo.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
89
src/index.ts
89
src/index.ts
@@ -7,9 +7,11 @@ import {
|
||||
createCommentCheckerHooks,
|
||||
createGrepOutputTruncatorHook,
|
||||
createDirectoryAgentsInjectorHook,
|
||||
createDirectoryReadmeInjectorHook,
|
||||
createEmptyTaskResponseDetectorHook,
|
||||
createThinkModeHook,
|
||||
createClaudeCodeHooksHook,
|
||||
createAnthropicAutoCompactHook,
|
||||
} from "./hooks";
|
||||
import {
|
||||
loadUserCommands,
|
||||
@@ -39,33 +41,77 @@ import { OhMyOpenCodeConfigSchema, type OhMyOpenCodeConfig } from "./config";
|
||||
import { log } from "./shared/logger";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as os from "os";
|
||||
|
||||
function loadConfigFromPath(configPath: string): OhMyOpenCodeConfig | null {
|
||||
try {
|
||||
if (fs.existsSync(configPath)) {
|
||||
const content = fs.readFileSync(configPath, "utf-8");
|
||||
const rawConfig = JSON.parse(content);
|
||||
const result = OhMyOpenCodeConfigSchema.safeParse(rawConfig);
|
||||
|
||||
if (!result.success) {
|
||||
log(`Config validation error in ${configPath}:`, result.error.issues);
|
||||
return null;
|
||||
}
|
||||
|
||||
log(`Config loaded from ${configPath}`, { agents: result.data.agents });
|
||||
return result.data;
|
||||
}
|
||||
} catch (err) {
|
||||
log(`Error loading config from ${configPath}:`, err);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function mergeConfigs(base: OhMyOpenCodeConfig, override: OhMyOpenCodeConfig): OhMyOpenCodeConfig {
|
||||
return {
|
||||
...base,
|
||||
...override,
|
||||
agents: override.agents !== undefined
|
||||
? { ...(base.agents ?? {}), ...override.agents }
|
||||
: base.agents,
|
||||
disabled_agents: [
|
||||
...new Set([...(base.disabled_agents ?? []), ...(override.disabled_agents ?? [])])
|
||||
],
|
||||
disabled_mcps: [
|
||||
...new Set([...(base.disabled_mcps ?? []), ...(override.disabled_mcps ?? [])])
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function loadPluginConfig(directory: string): OhMyOpenCodeConfig {
|
||||
const configPaths = [
|
||||
path.join(directory, "oh-my-opencode.json"),
|
||||
path.join(directory, ".oh-my-opencode.json"),
|
||||
// User-level config paths
|
||||
const userConfigPaths = [
|
||||
path.join(os.homedir(), ".config", "opencode", "oh-my-opencode.json"),
|
||||
];
|
||||
|
||||
for (const configPath of configPaths) {
|
||||
try {
|
||||
if (fs.existsSync(configPath)) {
|
||||
const content = fs.readFileSync(configPath, "utf-8");
|
||||
const rawConfig = JSON.parse(content);
|
||||
const result = OhMyOpenCodeConfigSchema.safeParse(rawConfig);
|
||||
// Project-level config paths (higher precedence)
|
||||
const projectConfigPaths = [
|
||||
path.join(directory, ".opencode", "oh-my-opencode.json"),
|
||||
];
|
||||
|
||||
if (!result.success) {
|
||||
log(`Config validation error in ${configPath}:`, result.error.issues);
|
||||
return {};
|
||||
}
|
||||
|
||||
return result.data;
|
||||
}
|
||||
} catch {
|
||||
// Ignore parse errors, use defaults
|
||||
// Load user config first
|
||||
let config: OhMyOpenCodeConfig = {};
|
||||
for (const configPath of userConfigPaths) {
|
||||
const userConfig = loadConfigFromPath(configPath);
|
||||
if (userConfig) {
|
||||
config = userConfig;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
// Override with project config
|
||||
for (const configPath of projectConfigPaths) {
|
||||
const projectConfig = loadConfigFromPath(configPath);
|
||||
if (projectConfig) {
|
||||
config = mergeConfigs(config, projectConfig);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
log("Final merged config", { agents: config.agents, disabled_agents: config.disabled_agents, disabled_mcps: config.disabled_mcps });
|
||||
return config;
|
||||
}
|
||||
|
||||
const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
@@ -77,9 +123,11 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
const commentChecker = createCommentCheckerHooks();
|
||||
const grepOutputTruncator = createGrepOutputTruncatorHook(ctx);
|
||||
const directoryAgentsInjector = createDirectoryAgentsInjectorHook(ctx);
|
||||
const directoryReadmeInjector = createDirectoryReadmeInjectorHook(ctx);
|
||||
const emptyTaskResponseDetector = createEmptyTaskResponseDetectorHook(ctx);
|
||||
const thinkMode = createThinkModeHook();
|
||||
const claudeCodeHooks = createClaudeCodeHooksHook(ctx, {});
|
||||
const anthropicAutoCompact = createAnthropicAutoCompactHook(ctx);
|
||||
|
||||
updateTerminalTitle({ sessionId: "main" });
|
||||
|
||||
@@ -139,7 +187,9 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
await todoContinuationEnforcer(input);
|
||||
await contextWindowMonitor.event(input);
|
||||
await directoryAgentsInjector.event(input);
|
||||
await directoryReadmeInjector.event(input);
|
||||
await thinkMode.event(input);
|
||||
await anthropicAutoCompact.event(input);
|
||||
|
||||
const { event } = input;
|
||||
const props = event.properties as Record<string, unknown> | undefined;
|
||||
@@ -256,6 +306,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
await contextWindowMonitor["tool.execute.after"](input, output);
|
||||
await commentChecker["tool.execute.after"](input, output);
|
||||
await directoryAgentsInjector["tool.execute.after"](input, output);
|
||||
await directoryReadmeInjector["tool.execute.after"](input, output);
|
||||
await emptyTaskResponseDetector["tool.execute.after"](input, output);
|
||||
|
||||
if (input.sessionID === getMainSessionID()) {
|
||||
|
||||
@@ -2,8 +2,23 @@ import { tool } from "@opencode-ai/plugin"
|
||||
import { existsSync, readdirSync, statSync, readlinkSync, readFileSync } from "fs"
|
||||
import { homedir } from "os"
|
||||
import { join, resolve, basename } from "path"
|
||||
import { z } from "zod/v4"
|
||||
import { parseFrontmatter, resolveCommandsInText } from "../../shared"
|
||||
import type { SkillScope, SkillMetadata, SkillInfo, LoadedSkill } from "./types"
|
||||
import { SkillFrontmatterSchema } from "./types"
|
||||
import type { SkillScope, SkillMetadata, SkillInfo, LoadedSkill, SkillFrontmatter } from "./types"
|
||||
|
||||
function parseSkillFrontmatter(data: Record<string, unknown>): SkillFrontmatter {
|
||||
return {
|
||||
name: typeof data.name === "string" ? data.name : "",
|
||||
description: typeof data.description === "string" ? data.description : "",
|
||||
license: typeof data.license === "string" ? data.license : undefined,
|
||||
"allowed-tools": Array.isArray(data["allowed-tools"]) ? data["allowed-tools"] : undefined,
|
||||
metadata:
|
||||
typeof data.metadata === "object" && data.metadata !== null
|
||||
? (data.metadata as Record<string, string>)
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
function discoverSkillsFromDir(
|
||||
skillsDir: string,
|
||||
@@ -93,10 +108,14 @@ async function parseSkillMd(skillPath: string): Promise<SkillInfo | null> {
|
||||
content = await resolveCommandsInText(content)
|
||||
const { data, body } = parseFrontmatter(content)
|
||||
|
||||
const frontmatter = parseSkillFrontmatter(data)
|
||||
|
||||
const metadata: SkillMetadata = {
|
||||
name: data.name || basename(skillPath),
|
||||
description: data.description || "",
|
||||
license: data.license,
|
||||
name: frontmatter.name || basename(skillPath),
|
||||
description: frontmatter.description,
|
||||
license: frontmatter.license,
|
||||
allowedTools: frontmatter["allowed-tools"],
|
||||
metadata: frontmatter.metadata,
|
||||
}
|
||||
|
||||
const referencesDir = join(resolvedPath, "references")
|
||||
@@ -118,6 +137,7 @@ async function parseSkillMd(skillPath: string): Promise<SkillInfo | null> {
|
||||
return {
|
||||
name: metadata.name,
|
||||
path: resolvedPath,
|
||||
basePath: resolvedPath,
|
||||
metadata,
|
||||
content: body,
|
||||
references,
|
||||
@@ -202,6 +222,7 @@ async function loadSkillWithReferences(
|
||||
content = await resolveCommandsInText(content)
|
||||
referencesLoaded.push({ path: ref, content })
|
||||
} catch {
|
||||
// Skip unreadable references
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -209,6 +230,7 @@ async function loadSkillWithReferences(
|
||||
return {
|
||||
name: skill.name,
|
||||
metadata: skill.metadata,
|
||||
basePath: skill.basePath,
|
||||
body: skill.content,
|
||||
referencesLoaded,
|
||||
}
|
||||
@@ -234,31 +256,24 @@ function formatLoadedSkills(loadedSkills: LoadedSkill[]): string {
|
||||
return "No skills loaded."
|
||||
}
|
||||
|
||||
const sections: string[] = ["# Loaded Skills\n"]
|
||||
const skill = loadedSkills[0]
|
||||
const sections: string[] = []
|
||||
|
||||
for (const skill of loadedSkills) {
|
||||
sections.push(`## ${skill.metadata.name}\n`)
|
||||
sections.push(`**Description**: ${skill.metadata.description || "(no description)"}\n`)
|
||||
sections.push("### Skill Instructions\n")
|
||||
sections.push(skill.body.trim())
|
||||
sections.push(`Base directory for this skill: ${skill.basePath}/`)
|
||||
sections.push("")
|
||||
sections.push(skill.body.trim())
|
||||
|
||||
if (skill.referencesLoaded.length > 0) {
|
||||
sections.push("\n### Loaded References\n")
|
||||
for (const ref of skill.referencesLoaded) {
|
||||
sections.push(`#### ${ref.path}\n`)
|
||||
sections.push("```")
|
||||
sections.push(ref.content.trim())
|
||||
sections.push("```\n")
|
||||
}
|
||||
if (skill.referencesLoaded.length > 0) {
|
||||
sections.push("\n---\n### Loaded References\n")
|
||||
for (const ref of skill.referencesLoaded) {
|
||||
sections.push(`#### ${ref.path}\n`)
|
||||
sections.push("```")
|
||||
sections.push(ref.content.trim())
|
||||
sections.push("```\n")
|
||||
}
|
||||
|
||||
sections.push("\n---\n")
|
||||
}
|
||||
|
||||
const skillNames = loadedSkills.map((s) => s.metadata.name).join(", ")
|
||||
sections.push(`**Skills loaded**: ${skillNames}`)
|
||||
sections.push(`**Total**: ${loadedSkills.length} skill(s)`)
|
||||
sections.push("\nPlease confirm these skills match your needs before proceeding.")
|
||||
sections.push(`\n---\n**Launched skill**: ${skill.metadata.name}`)
|
||||
|
||||
return sections.join("\n")
|
||||
}
|
||||
@@ -266,25 +281,7 @@ function formatLoadedSkills(loadedSkills: LoadedSkill[]): string {
|
||||
export const skill = tool({
|
||||
description: `Execute a skill within the main conversation.
|
||||
|
||||
When users ask you to perform tasks, check if any of the available skills below can help complete the task more effectively. Skills provide specialized capabilities and domain knowledge.
|
||||
|
||||
How to use skills:
|
||||
- Invoke skills using this tool with the skill name only (no arguments)
|
||||
- When you invoke a skill, the skill's prompt will expand and provide detailed instructions on how to complete the task
|
||||
|
||||
Important:
|
||||
- Only use skills listed in Available Skills below
|
||||
- Do not invoke a skill that is already running
|
||||
|
||||
Skills are loaded from:
|
||||
- ~/.claude/skills/ (user scope - global skills)
|
||||
- ./.claude/skills/ (project scope - project-specific skills)
|
||||
|
||||
Each skill contains:
|
||||
- SKILL.md: Main instructions with YAML frontmatter (name, description)
|
||||
- references/: Documentation files loaded into context as needed
|
||||
- scripts/: Executable code for deterministic operations
|
||||
- assets/: Files used in output (templates, icons, etc.)
|
||||
When you invoke a skill, the skill's prompt will expand and provide detailed instructions on how to complete the task.
|
||||
|
||||
Available Skills:
|
||||
${skillListForDescription}`,
|
||||
|
||||
@@ -1,14 +1,36 @@
|
||||
import { z } from "zod/v4"
|
||||
|
||||
export type SkillScope = "user" | "project"
|
||||
|
||||
/**
|
||||
* Zod schema for skill frontmatter validation
|
||||
* Following Anthropic Agent Skills Specification v1.0
|
||||
*/
|
||||
export const SkillFrontmatterSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.regex(/^[a-z0-9-]+$/, "Name must be lowercase alphanumeric with hyphens only")
|
||||
.min(1, "Name cannot be empty"),
|
||||
description: z.string().min(20, "Description must be at least 20 characters for discoverability"),
|
||||
license: z.string().optional(),
|
||||
"allowed-tools": z.array(z.string()).optional(),
|
||||
metadata: z.record(z.string(), z.string()).optional(),
|
||||
})
|
||||
|
||||
export type SkillFrontmatter = z.infer<typeof SkillFrontmatterSchema>
|
||||
|
||||
export interface SkillMetadata {
|
||||
name: string
|
||||
description: string
|
||||
license?: string
|
||||
allowedTools?: string[]
|
||||
metadata?: Record<string, string>
|
||||
}
|
||||
|
||||
export interface SkillInfo {
|
||||
name: string
|
||||
path: string
|
||||
basePath: string
|
||||
metadata: SkillMetadata
|
||||
content: string
|
||||
references: string[]
|
||||
@@ -19,6 +41,7 @@ export interface SkillInfo {
|
||||
export interface LoadedSkill {
|
||||
name: string
|
||||
metadata: SkillMetadata
|
||||
basePath: string
|
||||
body: string
|
||||
referencesLoaded: Array<{ path: string; content: string }>
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
## Root Level Rules
|
||||
|
||||
- Root rule 1
|
||||
- Root rule 2
|
||||
@@ -1,3 +0,0 @@
|
||||
export const config = {
|
||||
strict: true
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
## Nested Level Rules
|
||||
|
||||
- Nested rule 1 (더 specific)
|
||||
- Nested rule 2
|
||||
@@ -1 +0,0 @@
|
||||
export const deep = true
|
||||
@@ -1,3 +0,0 @@
|
||||
export function greet(name: string): string {
|
||||
return `Hello, ${name}!`
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
id: test-rule
|
||||
message: Test rule
|
||||
severity: info
|
||||
language: JavaScript
|
||||
rule:
|
||||
pattern: console
|
||||
Reference in New Issue
Block a user