oh-my-parallel-agent-opencode 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -0
- package/__tests__/agents.test.ts +107 -0
- package/__tests__/config-handler.test.ts +198 -0
- package/__tests__/dynamic-agent.test.ts +68 -0
- package/__tests__/schema.test.ts +149 -0
- package/__tests__/setup.test.ts +16 -0
- package/bun.lock +29 -0
- package/oh-my-parallel-agent-opencode.example.json +7 -0
- package/package.json +19 -0
- package/src/agents/explore.ts +117 -0
- package/src/agents/index.ts +69 -0
- package/src/agents/librarian.ts +302 -0
- package/src/agents/metis.ts +341 -0
- package/src/agents/momus.ts +237 -0
- package/src/agents/types.ts +95 -0
- package/src/config/schema.ts +15 -0
- package/src/index.ts +116 -0
- package/src/plugin-handlers/config-handler.ts +129 -0
- package/src/utils/dynamic-agent.ts +37 -0
- package/tsconfig.json +19 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { Plugin, PluginContext } from "@opencode-ai/plugin"
|
|
2
|
+
import { ParallelAgentConfigSchema } from "./config/schema"
|
|
3
|
+
import { createDynamicAgent, type SupportedAgentName } from "./agents"
|
|
4
|
+
import { resolveBaseAgent } from "./utils/dynamic-agent"
|
|
5
|
+
import * as fs from "fs"
|
|
6
|
+
import * as path from "path"
|
|
7
|
+
|
|
8
|
+
const CONFIG_FILE_NAME = "oh-my-parallel-agent.json"
|
|
9
|
+
|
|
10
|
+
function loadConfig(directory: string) {
|
|
11
|
+
const configPath_var = path.join(directory, CONFIG_FILE_NAME)
|
|
12
|
+
|
|
13
|
+
if (!fs.existsSync(configPath_var)) {
|
|
14
|
+
return null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const content_var = fs.readFileSync(configPath_var, "utf-8")
|
|
19
|
+
const parsed_var = JSON.parse(content_var)
|
|
20
|
+
const result_var = ParallelAgentConfigSchema.safeParse(parsed_var)
|
|
21
|
+
|
|
22
|
+
if (!result_var.success) {
|
|
23
|
+
console.error(`[oh-my-parallel-agent] 설정 파일 파싱 오류:`, result_var.error.message)
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return result_var.data
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error(`[oh-my-parallel-agent] 설정 파일 읽기 오류:`, error)
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Oh My Parallel Agent 플러그인
|
|
36
|
+
*
|
|
37
|
+
* 설정 파일(oh-my-parallel-agent.json)에 정의된 동적 에이전트들을
|
|
38
|
+
* OpenCode에 등록합니다.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* {
|
|
42
|
+
* "agents": {
|
|
43
|
+
* "momus-1": { "model": "openai/gpt-5.2" },
|
|
44
|
+
* "momus-2": { "model": "anthropic/claude-sonnet-4-20250514" }
|
|
45
|
+
* }
|
|
46
|
+
* }
|
|
47
|
+
*/
|
|
48
|
+
const OhMyParallelAgentPlugin: Plugin = async (ctx: PluginContext) => {
|
|
49
|
+
const pluginConfig_var = loadConfig(ctx.directory)
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
name: "oh-my-parallel-agent-opencode",
|
|
53
|
+
|
|
54
|
+
config: async (config: Record<string, unknown>) => {
|
|
55
|
+
if (!pluginConfig_var) {
|
|
56
|
+
return config
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const agentEntries_var = pluginConfig_var.agents
|
|
60
|
+
if (!agentEntries_var || Object.keys(agentEntries_var).length === 0) {
|
|
61
|
+
return config
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const existingAgents_var = (config.agent as Record<string, unknown>) ?? {}
|
|
65
|
+
const newAgents_var: Record<string, unknown> = {}
|
|
66
|
+
|
|
67
|
+
for (const [agentName_var, agentConfig_var] of Object.entries(agentEntries_var)) {
|
|
68
|
+
const resolved_var = resolveBaseAgent(agentName_var)
|
|
69
|
+
|
|
70
|
+
if (!resolved_var) {
|
|
71
|
+
console.warn(`[oh-my-parallel-agent] 지원하지 않는 에이전트 이름: ${agentName_var}`)
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const { baseAgent, variant } = resolved_var
|
|
76
|
+
const modelToUse_var = agentConfig_var.model ?? "anthropic/claude-sonnet-4-20250514"
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const dynamicAgent_var = createDynamicAgent(
|
|
80
|
+
baseAgent as SupportedAgentName,
|
|
81
|
+
modelToUse_var,
|
|
82
|
+
{
|
|
83
|
+
model: agentConfig_var.model,
|
|
84
|
+
variant: agentConfig_var.variant,
|
|
85
|
+
prompt_append: agentConfig_var.prompt_append,
|
|
86
|
+
temperature: agentConfig_var.temperature,
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if (variant && dynamicAgent_var.description) {
|
|
91
|
+
dynamicAgent_var.description = `${dynamicAgent_var.description} (variant ${variant})`
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
dynamicAgent_var.mode = "subagent"
|
|
95
|
+
|
|
96
|
+
newAgents_var[agentName_var] = dynamicAgent_var
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error(`[oh-my-parallel-agent] 에이전트 생성 실패 (${agentName_var}):`, error)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
config.agent = {
|
|
103
|
+
...existingAgents_var,
|
|
104
|
+
...newAgents_var,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return config
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export default OhMyParallelAgentPlugin
|
|
113
|
+
|
|
114
|
+
export type { ParallelAgentConfig, AgentOverrideConfig } from "./config/schema"
|
|
115
|
+
export type { SupportedAgentName } from "./agents"
|
|
116
|
+
export { resolveBaseAgent, isDynamicAgentName } from "./utils/dynamic-agent"
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs"
|
|
2
|
+
import { join } from "path"
|
|
3
|
+
import type { AgentConfig } from "@opencode-ai/sdk"
|
|
4
|
+
import { ParallelAgentConfigSchema, type ParallelAgentConfig } from "../config/schema"
|
|
5
|
+
import { resolveBaseAgent } from "../utils/dynamic-agent"
|
|
6
|
+
import { createDynamicAgent, type SupportedAgentName } from "../agents"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 설정 파일 경로를 찾는 함수
|
|
10
|
+
*
|
|
11
|
+
* 우선순위:
|
|
12
|
+
* 1. 프로젝트 루트의 .opencode/ 디렉토리
|
|
13
|
+
* 2. 사용자 홈의 .config/opencode/ 디렉토리
|
|
14
|
+
*
|
|
15
|
+
* @returns 설정 파일 전체 경로 또는 null
|
|
16
|
+
*/
|
|
17
|
+
export function findConfigPath(): string | null {
|
|
18
|
+
const configFileName = "oh-my-parallel-agent-opencode.json"
|
|
19
|
+
|
|
20
|
+
// 1. 프로젝트 레벨 설정 (.opencode/)
|
|
21
|
+
const projectConfigPath = join(process.cwd(), ".opencode", configFileName)
|
|
22
|
+
if (existsSync(projectConfigPath)) {
|
|
23
|
+
return projectConfigPath
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 2. 글로벌 레벨 설정 (~/.config/opencode/)
|
|
27
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE
|
|
28
|
+
if (homeDir) {
|
|
29
|
+
const globalConfigPath = join(homeDir, ".config", "opencode", configFileName)
|
|
30
|
+
if (existsSync(globalConfigPath)) {
|
|
31
|
+
return globalConfigPath
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 설정 파일을 로드하고 파싱하는 함수
|
|
40
|
+
*
|
|
41
|
+
* @param configPath - 설정 파일 경로 (optional, 없으면 자동 탐색)
|
|
42
|
+
* @returns 파싱된 ParallelAgentConfig 또는 null
|
|
43
|
+
* @throws Zod validation error if config is invalid
|
|
44
|
+
*/
|
|
45
|
+
export function loadParallelAgentConfig(configPath?: string): ParallelAgentConfig | null {
|
|
46
|
+
const resolvedPath = configPath || findConfigPath()
|
|
47
|
+
|
|
48
|
+
if (!resolvedPath) {
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!existsSync(resolvedPath)) {
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const fileContent = readFileSync(resolvedPath, "utf-8")
|
|
58
|
+
const jsonData = JSON.parse(fileContent)
|
|
59
|
+
|
|
60
|
+
const validatedConfig = ParallelAgentConfigSchema.parse(jsonData)
|
|
61
|
+
return validatedConfig
|
|
62
|
+
} catch (error) {
|
|
63
|
+
throw new Error(`Failed to load config from ${resolvedPath}: ${error}`)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 설정으로부터 동적 에이전트들을 생성하는 함수
|
|
69
|
+
*
|
|
70
|
+
* @param config - ParallelAgentConfig 객체
|
|
71
|
+
* @returns 에이전트 이름 -> AgentConfig 매핑
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* const config = { agents: { "momus-2": { model: "openai/gpt-5.2" } } }
|
|
75
|
+
* const agents = createParallelAgents(config)
|
|
76
|
+
* // agents["momus-2"] = AgentConfig for momus with gpt-5.2
|
|
77
|
+
*/
|
|
78
|
+
export function createParallelAgents(config: ParallelAgentConfig): Record<string, AgentConfig> {
|
|
79
|
+
const result: Record<string, AgentConfig> = {}
|
|
80
|
+
|
|
81
|
+
for (const [agentName, override] of Object.entries(config.agents)) {
|
|
82
|
+
// 1. 에이전트 이름 파싱 (예: "momus-2" -> { baseAgent: "momus", variant: "2" })
|
|
83
|
+
const resolved = resolveBaseAgent(agentName)
|
|
84
|
+
|
|
85
|
+
if (!resolved) {
|
|
86
|
+
// 지원하지 않는 에이전트 이름은 스킵
|
|
87
|
+
console.warn(`[oh-my-parallel-agent-opencode] Skipping unsupported agent name: ${agentName}`)
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 2. 모델 결정 (override.model이 없으면 "default" 사용)
|
|
92
|
+
const modelToUse = override?.model ?? "default"
|
|
93
|
+
|
|
94
|
+
// 3. 동적 에이전트 생성
|
|
95
|
+
try {
|
|
96
|
+
const agentConfig = createDynamicAgent(
|
|
97
|
+
resolved.baseAgent as SupportedAgentName,
|
|
98
|
+
modelToUse,
|
|
99
|
+
override ?? undefined
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
result[agentName] = agentConfig
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error(`[oh-my-parallel-agent-opencode] Failed to create agent ${agentName}:`, error)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return result
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 메인 설정 핸들러 함수
|
|
113
|
+
*
|
|
114
|
+
* 설정 파일을 로드하고 동적 에이전트들을 생성합니다.
|
|
115
|
+
* oh-my-opencode의 createBuiltinAgents와 유사한 역할을 합니다.
|
|
116
|
+
*
|
|
117
|
+
* @param configPath - 선택적 설정 파일 경로
|
|
118
|
+
* @returns 생성된 에이전트 레코드
|
|
119
|
+
*/
|
|
120
|
+
export function createParallelAgentsFromConfig(configPath?: string): Record<string, AgentConfig> {
|
|
121
|
+
const config = loadParallelAgentConfig(configPath)
|
|
122
|
+
|
|
123
|
+
if (!config) {
|
|
124
|
+
// 설정 파일이 없으면 빈 객체 반환
|
|
125
|
+
return {}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return createParallelAgents(config)
|
|
129
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const SUPPORTED_BASE_AGENTS = ["librarian", "explore", "metis", "momus"] as const;
|
|
2
|
+
|
|
3
|
+
export interface ResolvedAgent {
|
|
4
|
+
baseAgent: string;
|
|
5
|
+
variant?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function resolveBaseAgent(agentName: string): ResolvedAgent | null {
|
|
9
|
+
if (!agentName || agentName.trim() === "") {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const pattern = /^(librarian|explore|metis|momus)(-(\d+))?$/;
|
|
14
|
+
const match = agentName.match(pattern);
|
|
15
|
+
|
|
16
|
+
if (!match) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const baseAgent = match[1];
|
|
21
|
+
const variant = match[3];
|
|
22
|
+
|
|
23
|
+
if (variant) {
|
|
24
|
+
return {
|
|
25
|
+
baseAgent,
|
|
26
|
+
variant,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
baseAgent,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isDynamicAgentName(name: string): boolean {
|
|
36
|
+
return resolveBaseAgent(name) !== null;
|
|
37
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"rootDir": "src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"lib": ["ESNext"],
|
|
15
|
+
"types": ["bun-types"]
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*", "__tests__/**/*"],
|
|
18
|
+
"exclude": ["node_modules", "dist"]
|
|
19
|
+
}
|