Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bed2dd79a | ||
|
|
4c1d534093 | ||
|
|
a04234eaab | ||
|
|
6d0e4c49c2 | ||
|
|
8f39575264 | ||
|
|
2464473731 |
24
AGENTS.md
24
AGENTS.md
@@ -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
|
||||
|
||||
89
README.ko.md
89
README.ko.md
@@ -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
102
README.md
@@ -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" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user