ring/platforms/opencode/plugin/ring-unified.ts
Gandalf f85aa61440
feat: add OpenCode platform integration (consolidate ring-for-opencode)
Move OpenCode runtime plugin and installer into Ring monorepo under platforms/opencode/.
The installer reads skills, agents, and commands directly from the Ring monorepo's
canonical directories (default/, dev-team/, pm-team/, etc.) — zero asset duplication.

What's included:
- installer.sh: reads from Ring dirs, applies frontmatter/tool transforms, installs to ~/.config/opencode/
- plugin/: TypeScript runtime (RingUnifiedPlugin) with hooks, lifecycle, loaders
- src/: CLI (doctor, config-manager)
- prompts/: session-start and context-injection templates
- standards/: coding standards (from dev-team/docs/)
- ring.jsonc: default config with full 86-skill/35-agent/33-command inventory

What's NOT included (intentionally):
- assets/ directory: eliminated, content comes from Ring monorepo
- scripts/codereview/: eliminated, replaced by mithril
- using-ring-opencode skill: uses canonical using-ring instead

Transforms applied by installer:
- Agent: type→mode, strip version/changelog/output_schema/input_schema
- Skill: keep name+description frontmatter, body unchanged
- Command: strip argument-hint (unsupported by OpenCode)
- All: normalize tool names (Bash→bash, Read→read, etc.)
- All: strip Model Requirement sections from agents

Replaces: LerianStudio/ring-for-opencode repository
Generated-by: Gandalf
AI-Model: claude-opus-4
2026-03-07 22:46:47 -03:00

185 lines
5.7 KiB
TypeScript

/**
* Ring Unified Plugin
*
* Single entry point that matches oh-my-opencode's registration pattern.
* Combines all Ring functionality into one Plugin export.
*
* Features:
* - Config handler: Injects agents, skills, commands
* - Tool registration: Custom Ring tools
* - Event routing: Lifecycle events to hooks
* - System transform: Context injection
* - Compaction: Context preservation
*/
import type { Plugin, PluginInput } from "@opencode-ai/plugin"
import type { Config as OpenCodeSdkConfig } from "@opencode-ai/sdk"
import type { RingConfig } from "./config/index.js"
// Config
import { createConfigHandler, loadConfig } from "./config/index.js"
import { createContextInjectionHook, createSessionStartHook } from "./hooks/factories/index.js"
// Hooks
import { hookRegistry } from "./hooks/index.js"
import type { HookContext, HookOutput } from "./hooks/types.js"
// Lifecycle
import { createLifecycleRouter } from "./lifecycle/index.js"
// Tools
import { createRingTools } from "./tools/index.js"
// Utils
import { getSessionId } from "./utils/state.js"
/**
* Initialize all hooks based on configuration.
*/
function initializeHooks(config: RingConfig): void {
hookRegistry.clear()
hookRegistry.setDisabledHooks(config.disabled_hooks)
const isDisabled = (name: string) => config.disabled_hooks?.includes(name as never) ?? false
if (!isDisabled("session-start")) {
hookRegistry.register(createSessionStartHook(config.hooks?.["session-start"]))
}
if (!isDisabled("context-injection")) {
hookRegistry.register(createContextInjectionHook(config.hooks?.["context-injection"]))
}
}
/**
* Build a HookContext from available context data.
*/
function buildHookContext(
sessionId: string,
directory: string,
lifecycle: HookContext["lifecycle"],
event?: { type: string; properties?: Record<string, unknown> },
): HookContext {
return {
sessionId,
directory,
lifecycle,
event,
}
}
/**
* Ring Unified Plugin
*
* Matches oh-my-opencode's Plugin signature:
* Plugin = (input: PluginInput) => Promise<Hooks>
*
* Note: Return type is inferred to allow custom properties
* that extend the base Hooks interface for Ring-specific functionality.
*/
export const RingUnifiedPlugin: Plugin = async (ctx: PluginInput) => {
const { directory } = ctx
const projectRoot = directory
// Load Ring configuration
const config = loadConfig(projectRoot)
const debug = process.env.RING_DEBUG === "true"
if (debug) {
console.debug("[ring] Initializing unified plugin")
}
// Initialize hooks
initializeHooks(config)
// Create config handler for OpenCode config injection
const configHandler = createConfigHandler({
projectRoot,
ringConfig: config,
})
// Create lifecycle router (state side-effects only)
const lifecycleRouter = createLifecycleRouter({
projectRoot,
ringConfig: config,
})
const sessionId = getSessionId()
const ringTools = createRingTools(directory)
return {
// Register Ring tools
tool: ringTools,
// Config handler - inject agents, skills, commands
// Type assertion needed: our handler modifies a subset of config properties
// that are compatible with the SDK's Config type at runtime
config: configHandler as unknown as (input: OpenCodeSdkConfig) => Promise<void>,
// Event handler - lifecycle routing + hook execution
event: async ({ event }) => {
// Route to lifecycle router
await lifecycleRouter({ event })
// Build output object for hooks to modify
const output: HookOutput = {}
// Extract sessionID from event properties (may be present in various event types)
const props = event.properties as Record<string, unknown> | undefined
const eventSessionId = (props?.sessionID as string) ?? sessionId
// Build normalized event for hook context
const normalizedEvent = {
type: event.type,
properties: props,
}
// Execute hooks based on event type
if (event.type === "session.created") {
const hookCtx = buildHookContext(
eventSessionId,
directory,
"session.created",
normalizedEvent,
)
await hookRegistry.executeLifecycle("session.created", hookCtx, output)
}
if (event.type === "session.idle") {
const hookCtx = buildHookContext(eventSessionId, directory, "session.idle", normalizedEvent)
await hookRegistry.executeLifecycle("session.idle", hookCtx, output)
}
if (event.type === "session.error") {
const hookCtx = buildHookContext(
eventSessionId,
directory,
"session.error",
normalizedEvent,
)
await hookRegistry.executeLifecycle("session.error", hookCtx, output)
}
},
// System prompt transformation
"experimental.chat.system.transform": async (
_input: Record<string, unknown>,
output: { system: string[] },
) => {
if (!output?.system || !Array.isArray(output.system)) return
const hookCtx = buildHookContext(sessionId, directory, "chat.params")
const hookOutput: HookOutput = { system: output.system }
await hookRegistry.executeLifecycle("chat.params", hookCtx, hookOutput)
},
// Compaction context injection
"experimental.session.compacting": async (
input: { sessionID: string },
output: { context: string[] },
) => {
if (!output?.context || !Array.isArray(output.context)) return
const hookCtx = buildHookContext(input.sessionID, directory, "session.compacting")
const hookOutput: HookOutput = { context: output.context }
await hookRegistry.executeLifecycle("session.compacting", hookCtx, hookOutput)
},
}
}
export default RingUnifiedPlugin