Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/create-managers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { TmuxSessionManager } from "./features/tmux-subagent"
import * as openclawRuntimeDispatch from "./openclaw/runtime-dispatch"
import { registerManagerForCleanup } from "./features/background-agent/process-cleanup"
import { createConfigHandler } from "./plugin-handlers"
import { createHostSkillConfigStore } from "./shared/host-skill-config"
import { log } from "./shared"
import { markServerRunningInProcess } from "./shared/tmux/tmux-utils/server-health"

Expand Down Expand Up @@ -38,6 +39,7 @@ export type Managers = {
backgroundManager: BackgroundManager
skillMcpManager: SkillMcpManager
configHandler: ReturnType<typeof createConfigHandler>
hostSkillConfigStore: ReturnType<typeof createHostSkillConfigStore>
}

export function createManagers(args: {
Expand Down Expand Up @@ -113,17 +115,20 @@ export function createManagers(args: {
deps.initTaskToastManagerFn(ctx.client)

const skillMcpManager = new deps.SkillMcpManagerClass()
const hostSkillConfigStore = createHostSkillConfigStore()

const configHandler = deps.createConfigHandlerFn({
ctx: { directory: ctx.directory, client: ctx.client },
pluginConfig,
modelCacheState,
hostSkillConfigStore,
})

return {
tmuxSessionManager,
backgroundManager,
skillMcpManager,
configHandler,
hostSkillConfigStore,
}
}
2 changes: 1 addition & 1 deletion src/create-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type CreateToolsResult = {
export async function createTools(args: {
ctx: PluginContext
pluginConfig: OhMyOpenCodeConfig
managers: Pick<Managers, "backgroundManager" | "tmuxSessionManager" | "skillMcpManager">
managers: Pick<Managers, "backgroundManager" | "tmuxSessionManager" | "skillMcpManager" | "hostSkillConfigStore">
}): Promise<CreateToolsResult> {
const { ctx, pluginConfig, managers } = args

Expand Down
31 changes: 31 additions & 0 deletions src/plugin-handlers/agent-config-handler-agents-skills.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,35 @@ describe("applyAgentConfig .agents skills", () => {
expect(discoveredSkills.map(skill => skill.name)).toContain("project-agent-skill")
expect(discoveredSkills.map(skill => skill.name)).toContain("global-agent-skill")
})

test("passes host config skills declared in opencode config.skills.paths to builtin agent creation", async () => {
// given
discoverConfigSourceSkillsSpy
.mockResolvedValueOnce([])
.mockResolvedValueOnce([
{
name: "host-config-skill",
definition: { name: "host-config-skill", template: "host-template" },
scope: "config",
},
])

// when
await applyAgentConfig({
config: {
model: "anthropic/claude-opus-4-6",
agent: {},
skills: {
paths: ["/host/skills"],
},
},
pluginConfig: createPluginConfig(),
ctx: { directory: "/tmp/project" },
pluginComponents: createPluginComponents(),
})

// then
const discoveredSkills = createBuiltinAgentsSpy.mock.calls[0]?.[6] as Array<{ name: string }>
expect(discoveredSkills.map(skill => skill.name)).toContain("host-config-skill")
})
})
8 changes: 8 additions & 0 deletions src/plugin-handlers/agent-config-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from "./agent-override-protection";
import { buildPrometheusAgentConfig } from "./prometheus-agent-config-builder";
import { buildPlanDemoteConfig } from "./plan-model-inheritance";
import { adaptHostSkillConfig } from "../shared/host-skill-config";

type AgentConfigRecord = Record<string, Record<string, unknown> | undefined> & {
build?: Record<string, unknown>;
Expand Down Expand Up @@ -51,8 +52,10 @@ export async function applyAgentConfig(params: {
) as typeof params.pluginConfig.disabled_agents;

const includeClaudeSkillsForAwareness = params.pluginConfig.claude_code?.skills ?? true;
const hostSkillConfig = adaptHostSkillConfig(params.config.skills)
const [
discoveredConfigSourceSkills,
discoveredHostConfigSourceSkills,
discoveredUserSkills,
discoveredProjectSkills,
discoveredProjectAgentsSkills,
Expand All @@ -64,6 +67,10 @@ export async function applyAgentConfig(params: {
config: params.pluginConfig.skills,
configDir: params.ctx.directory,
}),
discoverConfigSourceSkills({
config: hostSkillConfig,
configDir: params.ctx.directory,
}),
includeClaudeSkillsForAwareness ? discoverUserClaudeSkills() : Promise.resolve([]),
includeClaudeSkillsForAwareness
? discoverProjectClaudeSkills(params.ctx.directory)
Expand All @@ -78,6 +85,7 @@ export async function applyAgentConfig(params: {

const allDiscoveredSkills = [
...discoveredConfigSourceSkills,
...discoveredHostConfigSourceSkills,
...discoveredOpencodeProjectSkills,
...discoveredProjectSkills,
...discoveredProjectAgentsSkills,
Expand Down
35 changes: 35 additions & 0 deletions src/plugin-handlers/command-config-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,39 @@ describe("applyCommandConfig", () => {
const commandConfig = config.command as Record<string, { agent?: string }>;
expect(commandConfig["start-work"]?.agent).toBe(getAgentListDisplayName("atlas"));
});

test("includes host config skills declared in opencode config.skills.paths", async () => {
// given
discoverConfigSourceSkillsSpy
.mockResolvedValueOnce([])
.mockResolvedValueOnce([
{
name: "host-config-skill",
definition: {
name: "host-config-skill",
description: "Host config skill",
template: "template",
},
scope: "config",
},
]);
const config: Record<string, unknown> = {
command: {},
skills: {
paths: ["/host/skills"],
},
};

// when
await applyCommandConfig({
config,
pluginConfig: createPluginConfig(),
ctx: { directory: "/tmp" },
pluginComponents: createPluginComponents(),
});

// then
const commandConfig = config.command as Record<string, { description?: string }>;
expect(commandConfig["host-config-skill"]?.description).toContain("Host config skill");
});
});
8 changes: 8 additions & 0 deletions src/plugin-handlers/command-config-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
log,
} from "../shared";
import type { PluginComponents } from "./plugin-components-loader";
import { adaptHostSkillConfig } from "../shared/host-skill-config";

export async function applyCommandConfig(params: {
config: Record<string, unknown>;
Expand All @@ -40,6 +41,7 @@ export async function applyCommandConfig(params: {

const includeClaudeCommands = params.pluginConfig.claude_code?.commands ?? true;
const includeClaudeSkills = params.pluginConfig.claude_code?.skills ?? true;
const hostSkillConfig = adaptHostSkillConfig(params.config.skills);

const externalSkillPlugin = detectExternalSkillPlugin(params.ctx.directory);
if (includeClaudeSkills && externalSkillPlugin.detected) {
Expand All @@ -48,6 +50,7 @@ export async function applyCommandConfig(params: {

const [
configSourceSkills,
hostConfigSourceSkills,
userCommands,
projectCommands,
opencodeGlobalCommands,
Expand All @@ -63,6 +66,10 @@ export async function applyCommandConfig(params: {
config: params.pluginConfig.skills,
configDir: params.ctx.directory,
}),
discoverConfigSourceSkills({
config: hostSkillConfig,
configDir: params.ctx.directory,
}),
includeClaudeCommands ? loadUserCommands() : Promise.resolve({}),
includeClaudeCommands ? loadProjectCommands(params.ctx.directory) : Promise.resolve({}),
loadOpencodeGlobalCommands(),
Expand All @@ -78,6 +85,7 @@ export async function applyCommandConfig(params: {
params.config.command = {
...builtinCommands,
...skillsToCommandDefinitionRecord(configSourceSkills),
...skillsToCommandDefinitionRecord(hostConfigSourceSkills),
...userCommands,
...userSkills,
...globalAgentsSkills,
Expand Down
5 changes: 4 additions & 1 deletion src/plugin-handlers/config-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,24 @@ import { applyProviderConfig } from "./provider-config-handler";
import { loadPluginComponents } from "./plugin-components-loader";
import { applyToolConfig } from "./tool-config-handler";
import { clearFormatterCache } from "../tools/hashline-edit/formatter-trigger"
import type { createHostSkillConfigStore } from "../shared/host-skill-config"

export { resolveCategoryConfig } from "./category-config-resolver";

export interface ConfigHandlerDeps {
ctx: { directory: string; client?: any };
pluginConfig: OhMyOpenCodeConfig;
modelCacheState: ModelCacheState;
hostSkillConfigStore: ReturnType<typeof createHostSkillConfigStore>;
}

export function createConfigHandler(deps: ConfigHandlerDeps) {
const { ctx, pluginConfig, modelCacheState } = deps;
const { ctx, pluginConfig, modelCacheState, hostSkillConfigStore } = deps;

return async (config: Record<string, unknown>) => {
const formatterConfig = config.formatter;

hostSkillConfigStore.set(config.skills)
setAdditionalAllowedMcpEnvVars(pluginConfig.mcp_env_allowlist ?? [])
applyProviderConfig({ config, modelCacheState });
clearFormatterCache()
Expand Down
4 changes: 3 additions & 1 deletion src/plugin/tool-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export function trimToolsToCap(filteredTools: ToolsRecord, maxTools: number): vo
export function createToolRegistry(args: {
ctx: PluginContext
pluginConfig: OhMyOpenCodeConfig
managers: Pick<Managers, "backgroundManager" | "tmuxSessionManager" | "skillMcpManager">
managers: Pick<Managers, "backgroundManager" | "tmuxSessionManager" | "skillMcpManager" | "hostSkillConfigStore">
skillContext: SkillContext
availableCategories: AvailableCategory[]
interactiveBashEnabled?: boolean
Expand Down Expand Up @@ -241,6 +241,8 @@ export function createToolRegistry(args: {
getSessionID: getSessionIDForMcp,
gitMasterConfig: pluginConfig.git_master,
browserProvider: skillContext.browserProvider,
directory: ctx.directory,
hostConfigSkills: () => managers.hostSkillConfigStore.get(),
nativeSkills: "skills" in ctx ? (ctx as { skills: SkillLoadOptions["nativeSkills"] }).skills : undefined,
})

Expand Down
38 changes: 38 additions & 0 deletions src/shared/host-skill-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { SkillsConfig } from "../config/schema/skills"

type HostSkillConfig = {
paths?: unknown
urls?: unknown
}

function toStringArray(value: unknown): string[] {
if (!Array.isArray(value)) return []
return value.filter((item): item is string => typeof item === "string")
Comment thread
EnochLi15 marked this conversation as resolved.
Outdated
}

export function adaptHostSkillConfig(value: unknown): SkillsConfig | undefined {
if (!value || typeof value !== "object") return undefined

const hostSkillConfig = value as HostSkillConfig
const sources = [
...toStringArray(hostSkillConfig.paths),
...toStringArray(hostSkillConfig.urls),
]

if (sources.length === 0) return undefined

return { sources } as SkillsConfig
}

export function createHostSkillConfigStore() {
let current: SkillsConfig | undefined

return {
get(): SkillsConfig | undefined {
return current
},
set(value: unknown): void {
current = adaptHostSkillConfig(value)
},
}
}
28 changes: 21 additions & 7 deletions src/tools/skill/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { SkillArgs, SkillLoadOptions } from "./types"
import type { LoadedSkill } from "../../features/opencode-skill-loader"
import { getAllSkills, clearSkillCache } from "../../features/opencode-skill-loader/skill-content"
import { injectGitMasterConfig } from "../../features/opencode-skill-loader/skill-content"
import { discoverConfigSourceSkills } from "../../features/opencode-skill-loader"
import { discoverCommandsSync } from "../slashcommand/command-discovery"
import type { CommandInfo } from "../slashcommand/types"
import { formatLoadedCommand } from "../slashcommand/command-output-formatter"
Expand All @@ -29,16 +30,29 @@ export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition

const getSkills = async (): Promise<LoadedSkill[]> => {
clearSkillCache()
const discovered = await getAllSkills({
disabledSkills: options?.disabledSkills,
browserProvider: options?.browserProvider,
})
const [discovered, hostConfigSourceSkills] = await Promise.all([
getAllSkills({
disabledSkills: options?.disabledSkills,
browserProvider: options?.browserProvider,
directory: options?.directory,
}),
discoverConfigSourceSkills({
config: options.hostConfigSkills?.(),
configDir: options?.directory ?? process.cwd(),
}),
])
const discoveredWithHostConfig = [
...discovered,
...hostConfigSourceSkills.filter(
(skill) => !new Set(discovered.map((discoveredSkill) => discoveredSkill.name)).has(skill.name),
),
]
const allSkills = !options.skills
? discovered
? discoveredWithHostConfig
: [
...discovered,
...discoveredWithHostConfig,
...options.skills.filter(
(skill) => !new Set(discovered.map((discoveredSkill) => discoveredSkill.name)).has(skill.name)
(skill) => !new Set(discoveredWithHostConfig.map((discoveredSkill) => discoveredSkill.name)).has(skill.name)
),
]

Expand Down
6 changes: 5 additions & 1 deletion src/tools/skill/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { SkillScope, LoadedSkill } from "../../features/opencode-skill-loader/types"
import type { SkillMcpManager } from "../../features/skill-mcp-manager"
import type { BrowserAutomationProvider, GitMasterConfig } from "../../config/schema"
import type { BrowserAutomationProvider, GitMasterConfig, SkillsConfig } from "../../config/schema"
import type { CommandInfo } from "../slashcommand/types"

export interface SkillArgs {
Expand Down Expand Up @@ -32,9 +32,13 @@ export interface SkillLoadOptions {
getSessionID?: () => string | undefined
/** Git master configuration for watermark/co-author settings */
gitMasterConfig?: GitMasterConfig
/** Project directory used when discovering config-based skill sources */
directory?: string
disabledSkills?: Set<string>
/** Browser automation provider for provider-gated skill filtering */
browserProvider?: BrowserAutomationProvider
/** Host opencode config skill sources mirrored from config.skills.{paths,urls} */
hostConfigSkills?: () => SkillsConfig | undefined
/** Include Claude marketplace plugin commands in discovery (default: true) */
pluginsEnabled?: boolean
/** Override plugin enablement from Claude settings by plugin key */
Expand Down
Loading
Loading