Compare commits

..

6 Commits

Author SHA1 Message Date
github-actions[bot]
9bed2dd79a release: v0.1.16 2025-12-05 06:55:27 +00:00
YeonGyu-Kim
4c1d534093 docs: simplify agent configuration, add OpenCode doc links 2025-12-05 15:52:04 +09:00
github-actions[bot]
a04234eaab release: v0.1.15 2025-12-05 06:33:29 +00:00
YeonGyu-Kim
6d0e4c49c2 docs: add deployment workflow documentation to AGENTS.md 2025-12-05 15:32:09 +09:00
YeonGyu-Kim
8f39575264 fix(session-recovery): handle empty content from interrupted reasoning (#6)
* fix(publish): make git operations idempotent

- Check for staged changes before commit
- Check if tag exists before creating
- Check if release exists before creating

* fix(session-recovery): handle empty content from interrupted reasoning

- Add THINKING_TYPES set including 'reasoning' type (OpenCode's thinking)
- Add hasNonEmptyOutput() to detect messages with only thinking/meta parts
- Add findEmptyContentMessage() to scan all messages for empty content
- Handle step-start/step-finish meta parts in empty content detection
- Patch interrupted messages with '(interrupted)' text before falling back to revert
2025-12-05 15:28:22 +09:00
YeonGyu-Kim
2464473731 fix(ast-grep): add validation for incomplete function declaration patterns (#5)
* fix(publish): make git operations idempotent

- Check for staged changes before commit
- Check if tag exists before creating
- Check if release exists before creating

* fix(ast-grep): add validation for incomplete function declaration patterns

- Add validatePatternForCli function to detect incomplete patterns like
  'export async function $METHOD' (missing params and body)
- Only validates JS/TS languages (javascript, typescript, tsx)
- Provides helpful error message with correct pattern examples
- Update tool description to clarify complete AST nodes required

This fixes the issue where incomplete patterns would fail silently
with no results instead of providing actionable feedback.
2025-12-05 15:17:42 +09:00
7 changed files with 193 additions and 184 deletions

View File

@@ -84,9 +84,31 @@ bun run build
bun run rebuild
```
## DEPLOYMENT
**배포는 GitHub Actions workflow_dispatch로만 진행**
1. package.json 버전은 수정하지 않음 (워크플로우에서 자동 bump)
2. 변경사항 커밋 & 푸시
3. GitHub Actions에서 `publish` 워크플로우 수동 실행
- `bump`: major | minor | patch 선택
- `version`: (선택) 특정 버전 지정 가능
```bash
# 워크플로우 실행 (CLI)
gh workflow run publish -f bump=patch
# 워크플로우 상태 확인
gh run list --workflow=publish
```
**주의사항**:
- `bun publish` 직접 실행 금지 (OIDC provenance 문제)
- 로컬에서 버전 bump 하지 말 것
## NOTES
- **No tests**: Test framework not configured
- **No CI/CD**: GitHub workflows not present
- **CI/CD**: GitHub Actions publish workflow 사용
- **Version requirement**: OpenCode >= 1.0.132 (earlier versions have config bugs)
- **Multi-language docs**: README.md, README.en.md, README.ko.md

View File

@@ -96,76 +96,9 @@ OpenCode 가 낭만이 사라진것같은 오늘날의 시대에, 당신에게
### Agent 설정
에이전트의 모델, 프롬프트, 권한 등을 세밀하게 조정할 수 있.
`agents` 옵션으로 에이전트의 모델, 프롬프트, 권한 등을 세밀하게 조정할 수 있습니다. 스키마를 통해 자동완성을 지원합니다.
**설정 옵션:**
| 옵션 | 설명 |
|------|------|
| `model` | 사용할 모델 ID (예: `anthropic/claude-sonnet-4`) |
| `temperature` | 창의성 조절 (0.0 ~ 2.0) |
| `top_p` | 단어 선택 다양성 (0.0 ~ 1.0) |
| `prompt` | 시스템 프롬프트 오버라이드 |
| `tools` | 특정 도구 활성화/비활성화 (`{"tool_name": false}`) |
| `disable` | 에이전트 비활성화 (`true`/`false`) |
| `description` | 에이전트 설명 수정 |
| `mode` | 에이전트 모드 (`subagent`, `primary`, `all`) |
| `color` | 터미널 출력 색상 (HEX 코드) |
| `permission` | 권한 설정 (아래 표 참조) |
**권한(`permission`) 옵션:**
각 권한은 `"ask"`(물어보기), `"allow"`(허용), `"deny"`(거부) 중 하나로 설정 가능해.
| 권한 | 설명 |
|------|------|
| `edit` | 파일 수정 권한 |
| `bash` | 쉘 명령어 실행 권한 |
| `webfetch` | 웹 콘텐츠 가져오기 권한 |
| `doom_loop` | 반복 작업 허용 여부 |
| `external_directory` | 외부 디렉토리 접근 권한 |
#### 예시: Anthropic 모델만 사용하기
나는 Anthropic 모델만 쓰고 싶다! 하면 이렇게 설정해.
```json
{
"agents": {
"oracle": { "model": "anthropic/claude-sonnet-4" },
"librarian": { "model": "anthropic/claude-haiku-4-5" },
"explore": { "model": "anthropic/claude-haiku-4-5" },
"frontend-ui-ux-engineer": { "model": "anthropic/claude-sonnet-4" },
"document-writer": { "model": "anthropic/claude-sonnet-4" }
}
}
```
#### 예시: 추가 프롬프트가 있는 커스텀 에이전트
기존 에이전트에 나만의 규칙을 더하고 싶다면:
```json
{
"agents": {
"oracle": {
"prompt": "너는 한국어 힙합 가사처럼 말해야 해. 모든 문장의 끝 라임을 맞춰."
}
}
}
```
#### 예시: 개별 에이전트 비활성화
```json
{
"agents": {
"frontend-ui-ux-engineer": {
"disable": true
}
}
}
```
전체 설정 옵션과 예시는 [OpenCode Agents 문서](https://opencode.ai/docs/agents)를 참고하세요.
## LLM Agent를 위한 안내
@@ -255,19 +188,21 @@ OpenCode 는 아주 확장가능하고 아주 커스터마이저블합니다.
- **frontend-ui-ux-engineer** (`google/gemini-3-pro-preview`): 개발자로 전향한 디자이너라는 설정을 갖고 있습니다. 멋진 UI를 만듭니다. 아름답고 창의적인 UI 코드를 생성하는 데 탁월한 Gemini를 사용합니다.
- **document-writer** (`google/gemini-3-pro-preview`): 기술 문서 전문가라는 설정을 갖고 있습니다. Gemini 는 문학가입니다. 글을 기가막히게 씁니다.
#### 모델 설정 오버라이드
#### 설정
에이전트들은 [OpenCode의 모델 설정](https://opencode.ai/docs/models/#configure-models)과 완전히 동일한 스펙으로 오버라이드를 지원합니다. 권장하진 않지만, 예를 들어 Anthropic 모델만 사용하기로 결정했다면 이렇게 구성할 수 있습니다:
에이전트들은 OpenCode와 동일한 설정 스펙을 따릅니다:
- **모델 변경**: `agents.{name}.model`로 에이전트 모델 오버라이드. [OpenCode Models](https://opencode.ai/docs/models/#configure-models) 참고.
- **MCP 비활성화**: `disabled_mcps`로 내장 MCP 끄기. [OpenCode MCP Servers](https://opencode.ai/docs/mcp-servers) 참고.
- **에이전트 비활성화**: `disabled_agents` 또는 `agents.{name}.disable` 사용. [OpenCode Agents](https://opencode.ai/docs/agents) 참고.
권장하진 않지만(이 플러그인은 멀티 모델 오케스트레이션용), Anthropic만 사용하는 경우 예시:
```json
{
"agents": {
"explore": {
"model": "anthropic/claude-haiku-4-5"
},
"frontend-ui-ux-engineer": {
"model": "anthropic/claude-opus-4"
}
"explore": { "model": "anthropic/claude-haiku-4-5" },
"frontend-ui-ux-engineer": { "model": "anthropic/claude-opus-4" }
}
}
```

102
README.md
View File

@@ -146,89 +146,9 @@ Available agents: `oracle`, `librarian`, `explore`, `frontend-ui-ux-engineer`, `
### Agent Configuration
You can override the configuration of any built-in agent using the `agents` option. This allows you to change models, adjust creativity, modify permissions, or disable agents individually.
You can override any built-in agent's model, prompt, permissions, and more using the `agents` option. Configuration uses autocomplete via schema.
#### Configuration Options
| Option | Type | Description |
|--------|------|-------------|
| `model` | string | Override the default model (e.g., "anthropic/claude-sonnet-4") |
| `temperature` | number (0-2) | Controls randomness (0 = deterministic, 2 = creative) |
| `top_p` | number (0-1) | Nucleus sampling parameter |
| `prompt` | string | Additional system prompt to append |
| `tools` | object | Enable/disable specific tools (e.g., `{"websearch_exa": false}`) |
| `disable` | boolean | Completely disable the agent |
| `description` | string | Override agent description |
| `mode` | "subagent" | "primary" | "all" | When agent is available |
| `color` | string | Hex color code for terminal output (e.g., "#FF0000") |
| `permission` | object | Permission settings for sensitive operations |
#### Permission Options
| Option | Values | Description |
|--------|--------|-------------|
| `edit` | "ask" | "allow" | "deny" | File modification permissions |
| `bash` | "ask" | "allow" | "deny" | object | Shell command execution permissions |
| `webfetch` | "ask" | "allow" | "deny" | Web access permissions |
| `doom_loop` | "ask" | "allow" | "deny" | Infinite loop prevention |
| `external_directory` | "ask" | "allow" | "deny" | Access outside project root |
#### Examples
**Using Only Anthropic Models**
This configuration forces all agents to use Anthropic models, suitable for users with only Anthropic API access.
```json
{
"agents": {
"oracle": {
"model": "anthropic/claude-sonnet-4"
},
"librarian": {
"model": "anthropic/claude-haiku-4-5"
},
"explore": {
"model": "anthropic/claude-haiku-4-5"
},
"frontend-ui-ux-engineer": {
"model": "anthropic/claude-sonnet-4"
},
"document-writer": {
"model": "anthropic/claude-sonnet-4"
}
}
}
```
**Custom Agent with Additional Prompt**
Inject custom instructions into an agent's system prompt.
```json
{
"agents": {
"frontend-ui-ux-engineer": {
"prompt": "ALWAYS use Tailwind CSS. NEVER use inline styles. Prefer dark mode defaults.",
"temperature": 0.8
}
}
}
```
**Disable Agents Individually**
You can also disable agents using the `disable` property within the agent config.
```json
{
"agents": {
"explore": {
"disable": true
}
}
}
```
For full configuration options and examples, see the [OpenCode Agents documentation](https://opencode.ai/docs/agents).
## Why OpenCode & Why Oh My OpenCode
@@ -265,19 +185,21 @@ I believe in the right tool for the job. For your wallet's sake, use CLIProxyAPI
- **frontend-ui-ux-engineer** (`google/gemini-3-pro-preview`): A designer turned developer. Creates stunning UIs. Uses Gemini because its creativity and UI code generation are superior.
- **document-writer** (`google/gemini-3-pro-preview`): A technical writing expert. Gemini is a wordsmith; it writes prose that flows naturally.
#### Model Configuration Override
#### Configuration
Agents follow the exact same model configuration spec as [OpenCode's model configuration](https://opencode.ai/docs/models/#configure-models). While not generally recommended, if you decide to use only Anthropic models, you could configure like this:
Agents follow the same configuration spec as OpenCode:
- **Change models**: Override any agent's model via `agents.{name}.model`. See [OpenCode Models](https://opencode.ai/docs/models/#configure-models).
- **Disable MCPs**: Use `disabled_mcps` to turn off built-in MCPs. See [OpenCode MCP Servers](https://opencode.ai/docs/mcp-servers).
- **Disable agents**: Use `disabled_agents` or `agents.{name}.disable`. See [OpenCode Agents](https://opencode.ai/docs/agents).
While not generally recommended (this plugin is designed for multi-model orchestration), here's an example for Anthropic-only users:
```json
{
"agents": {
"explore": {
"model": "anthropic/claude-haiku-4-5"
},
"frontend-ui-ux-engineer": {
"model": "anthropic/claude-opus-4"
}
"explore": { "model": "anthropic/claude-haiku-4-5" },
"frontend-ui-ux-engineer": { "model": "anthropic/claude-opus-4" }
}
}
```

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode",
"version": "0.1.14",
"version": "0.1.16",
"description": "OpenCode plugin - custom agents (oracle, librarian) and enhanced features",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@@ -78,13 +78,35 @@ async function gitTagAndRelease(newVersion: string, changelog: string): Promise<
await $`git config user.email "github-actions[bot]@users.noreply.github.com"`
await $`git config user.name "github-actions[bot]"`
await $`git add package.json`
await $`git commit -m "release: v${newVersion}"`
await $`git tag v${newVersion}`
// Commit only if there are staged changes (idempotent)
const hasStagedChanges = await $`git diff --cached --quiet`.nothrow()
if (hasStagedChanges.exitCode !== 0) {
await $`git commit -m "release: v${newVersion}"`
} else {
console.log("No changes to commit (version already updated)")
}
// Tag only if it doesn't exist (idempotent)
const tagExists = await $`git rev-parse v${newVersion}`.nothrow()
if (tagExists.exitCode !== 0) {
await $`git tag v${newVersion}`
} else {
console.log(`Tag v${newVersion} already exists`)
}
// Push (idempotent - git push is already idempotent)
await $`git push origin HEAD --tags`
// Create release only if it doesn't exist (idempotent)
console.log("\nCreating GitHub release...")
const releaseNotes = changelog || "No notable changes"
await $`gh release create v${newVersion} --title "v${newVersion}" --notes ${releaseNotes}`
const releaseExists = await $`gh release view v${newVersion}`.nothrow()
if (releaseExists.exitCode !== 0) {
await $`gh release create v${newVersion} --title "v${newVersion}" --notes ${releaseNotes}`
} else {
console.log(`Release v${newVersion} already exists`)
}
}
async function checkVersionExists(version: string): Promise<boolean> {

View File

@@ -211,29 +211,93 @@ async function recoverThinkingDisabledViolation(
return false
}
const THINKING_TYPES = new Set(["thinking", "redacted_thinking", "reasoning"])
function hasNonEmptyOutput(msg: MessageData): boolean {
const parts = msg.parts
if (!parts || parts.length === 0) return false
return parts.some((p) => {
if (THINKING_TYPES.has(p.type)) return false
if (p.type === "step-start" || p.type === "step-finish") return false
if (p.type === "text" && p.text && p.text.trim()) return true
if (p.type === "tool_use" && p.id) return true
if (p.type === "tool_result") return true
return false
})
}
function findEmptyContentMessage(msgs: MessageData[]): MessageData | null {
for (let i = 0; i < msgs.length; i++) {
const msg = msgs[i]
const isLastMessage = i === msgs.length - 1
const isAssistant = msg.info?.role === "assistant"
if (isLastMessage && isAssistant) continue
if (!hasNonEmptyOutput(msg)) {
return msg
}
}
return null
}
async function recoverEmptyContentMessage(
client: Client,
sessionID: string,
failedAssistantMsg: MessageData,
directory: string
): Promise<boolean> {
const messageID = failedAssistantMsg.info?.id
const parentMsgID = failedAssistantMsg.info?.parentID
if (!messageID) {
return false
}
// Revert to parent message (delete the empty message)
const revertTargetID = parentMsgID || messageID
try {
const messagesResp = await client.session.messages({
path: { id: sessionID },
query: { directory },
})
const msgs = (messagesResp as { data?: MessageData[] }).data
if (!msgs || msgs.length === 0) return false
const emptyMsg = findEmptyContentMessage(msgs) || failedAssistantMsg
const messageID = emptyMsg.info?.id
if (!messageID) return false
const existingParts = emptyMsg.parts || []
const hasOnlyThinkingOrMeta = existingParts.length > 0 && existingParts.every(
(p) => THINKING_TYPES.has(p.type) || p.type === "step-start" || p.type === "step-finish"
)
if (hasOnlyThinkingOrMeta) {
const strippedParts: MessagePart[] = [{ type: "text", text: "(interrupted)" }]
try {
// @ts-expect-error - Experimental API
await client.message?.update?.({
path: { id: messageID },
body: { parts: strippedParts },
})
return true
} catch {
// message.update not available
}
try {
// @ts-expect-error - Experimental API
await client.session.patch?.({
path: { id: sessionID },
body: { messageID, parts: strippedParts },
})
return true
} catch {
// session.patch not available
}
}
const revertTargetID = emptyMsg.info?.parentID || messageID
await client.session.revert({
path: { id: sessionID },
body: { messageID: revertTargetID },
query: { directory },
})
return true
} catch {
return false

View File

@@ -10,13 +10,55 @@ function showOutputToUser(context: unknown, output: string): void {
ctx.metadata?.({ metadata: { output } })
}
/**
* JS/TS languages that require complete function declaration patterns
*/
const JS_TS_LANGUAGES = ["javascript", "typescript", "tsx"] as const
/**
* Validates AST pattern for common incomplete patterns that will fail silently.
* Only validates JS/TS languages where function declarations require body.
*
* @throws Error with helpful message if pattern is incomplete
*/
function validatePatternForCli(pattern: string, lang: CliLanguage): void {
if (!JS_TS_LANGUAGES.includes(lang as (typeof JS_TS_LANGUAGES)[number])) {
return
}
const src = pattern.trim()
// Detect incomplete function declarations:
// - "function $NAME" (no params/body)
// - "export function $NAME" (no params/body)
// - "export async function $NAME" (no params/body)
// - "export default function $NAME" (no params/body)
// Pattern: ends with $METAVAR (uppercase, underscore, digits) without ( or {
const incompleteFunctionDecl =
/^(export\s+)?(default\s+)?(async\s+)?function\s+\$[A-Z_][A-Z0-9_]*\s*$/i.test(src)
if (incompleteFunctionDecl) {
throw new Error(
`Incomplete AST pattern for ${lang}: "${pattern}"\n\n` +
`ast-grep requires complete AST nodes. Function declarations must include parameters and body.\n\n` +
`Examples of correct patterns:\n` +
` - "export async function $NAME($$$) { $$$ }" (matches export async functions)\n` +
` - "function $NAME($$$) { $$$ }" (matches all function declarations)\n` +
` - "async function $NAME($$$) { $$$ }" (matches async functions)\n\n` +
`Your pattern "${pattern}" is missing the parameter list and body.`
)
}
}
export const ast_grep_search = tool({
description:
"Search code patterns across filesystem using AST-aware matching. Supports 25 languages. " +
"Use meta-variables: $VAR (single node), $$$ (multiple nodes). " +
"IMPORTANT: Patterns must be complete AST nodes (valid code). " +
"For functions, include params and body: 'export async function $NAME($$$) { $$$ }' not 'export async function $NAME'. " +
"Examples: 'console.log($MSG)', 'def $FUNC($$$):', 'async function $NAME($$$)'",
args: {
pattern: tool.schema.string().describe("AST pattern with meta-variables ($VAR, $$$)"),
pattern: tool.schema.string().describe("AST pattern with meta-variables ($VAR, $$$). Must be complete AST node."),
lang: tool.schema.enum(CLI_LANGUAGES).describe("Target language"),
paths: tool.schema.array(tool.schema.string()).optional().describe("Paths to search (default: ['.'])"),
globs: tool.schema.array(tool.schema.string()).optional().describe("Include/exclude globs (prefix ! to exclude)"),
@@ -24,6 +66,8 @@ export const ast_grep_search = tool({
},
execute: async (args, context) => {
try {
validatePatternForCli(args.pattern, args.lang as CliLanguage)
const matches = await runSg({
pattern: args.pattern,
lang: args.lang as CliLanguage,