Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
72 changes: 72 additions & 0 deletions apps/server/src/provider/Layers/OpenCodeProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const runtimeMock = {
inventory: {
providerList: { connected: [] as string[], all: [] as unknown[], default: {} },
agents: [] as unknown[],
skills: [] as unknown[],
} as unknown,
},
reset() {
Expand All @@ -48,6 +49,7 @@ const runtimeMock = {
this.state.inventory = {
providerList: { connected: [], all: [] as unknown[], default: {} },
agents: [] as unknown[],
skills: [] as unknown[],
};
},
};
Expand Down Expand Up @@ -194,6 +196,76 @@ it.layer(testLayer)("checkOpenCodeProviderStatus", (it) => {
}),
);

it.effect("includes OpenCode skills in the provider snapshot", () =>
Effect.gen(function* () {
runtimeMock.state.inventory = {
providerList: {
connected: ["openai"],
all: [
{
id: "openai",
name: "OpenAI",
models: {
"gpt-5.4": {
id: "gpt-5.4",
name: "GPT-5.4",
variants: {},
},
},
},
],
default: {},
},
agents: [],
skills: [
{
name: "openclaw-review",
description: "Review OpenClaw workflow changes.",
location: "/Users/test/.agents/skills/openclaw-review/SKILL.md",
content: "---\nname: openclaw-review\n---\n",
},
{
name: "openclaw-triage",
description: "Triage OpenClaw routing issues.",
location: "/Users/test/.agents/skills/openclaw-triage/SKILL.md",
content: "---\nname: openclaw-triage\n---\n",
},
{
name: "missing-location",
description: "This incomplete SDK row should be skipped.",
location: "",
content: "---\nname: missing-location\n---\n",
},
],
};

const snapshot = yield* checkOpenCodeProviderStatus(makeOpenCodeSettings(), process.cwd());

assert.deepEqual(
snapshot.skills.map((skill) => ({
name: skill.name,
path: skill.path,
enabled: skill.enabled,
shortDescription: skill.shortDescription,
})),
[
{
name: "openclaw-review",
path: "/Users/test/.agents/skills/openclaw-review/SKILL.md",
enabled: true,
shortDescription: "Review OpenClaw workflow changes.",
},
{
name: "openclaw-triage",
path: "/Users/test/.agents/skills/openclaw-triage/SKILL.md",
enabled: true,
shortDescription: "Triage OpenClaw routing issues.",
},
],
);
}),
);

it.effect("closes the local OpenCode server scope after provider refresh", () =>
Effect.gen(function* () {
yield* checkOpenCodeProviderStatus(makeOpenCodeSettings(), process.cwd());
Expand Down
29 changes: 29 additions & 0 deletions apps/server/src/provider/Layers/OpenCodeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
type ModelCapabilities,
type OpenCodeSettings,
type ServerProviderModel,
type ServerProviderSkill,
} from "@t3tools/contracts";
import * as Cause from "effect/Cause";
import * as Data from "effect/Data";
Expand Down Expand Up @@ -252,6 +253,32 @@ function flattenOpenCodeModels(input: OpenCodeInventory): ReadonlyArray<ServerPr
return models.toSorted((left, right) => left.name.localeCompare(right.name));
}

function trimOptional(value: string | null | undefined): string | undefined {
const trimmed = value?.trim();
return trimmed && trimmed.length > 0 ? trimmed : undefined;
}

function flattenOpenCodeSkills(input: OpenCodeInventory): ReadonlyArray<ServerProviderSkill> {
const skills: ServerProviderSkill[] = [];
for (const skill of input.skills ?? []) {
const name = trimOptional(skill.name);
const path = trimOptional(skill.location);
if (!name || !path) {
continue;
}

const description = trimOptional(skill.description);
skills.push({
name,
path,
enabled: true,
...(description ? { description, shortDescription: description } : {}),
});
}

return skills.toSorted((left, right) => left.name.localeCompare(right.name));
}

export const makePendingOpenCodeProvider = (
openCodeSettings: OpenCodeSettings,
): Effect.Effect<ServerProviderDraft> =>
Expand Down Expand Up @@ -442,12 +469,14 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu
customModels,
DEFAULT_OPENCODE_MODEL_CAPABILITIES,
);
const skills = flattenOpenCodeSkills(inventoryExit.value);
const connectedCount = inventoryExit.value.providerList.connected.length;
return buildServerProvider({
presentation: OPENCODE_PRESENTATION,
enabled: true,
checkedAt,
models,
skills,
probe: {
installed: true,
version,
Expand Down
19 changes: 16 additions & 3 deletions apps/server/src/provider/opencodeRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,21 @@ export interface OpenCodeCommandResult {
export interface OpenCodeInventory {
readonly providerList: ProviderListResponse;
readonly agents: ReadonlyArray<Agent>;
readonly skills: ReadonlyArray<OpenCodeSkill>;
}

export interface ParsedOpenCodeModelSlug {
readonly providerID: string;
readonly modelID: string;
}

export interface OpenCodeSkill {
readonly name?: string | null;
readonly description?: string | null;
readonly location?: string | null;
readonly content?: string | null;
}

export interface OpenCodeRuntimeShape {
/**
* Spawns a local OpenCode server process. Its lifetime is bound to the caller's
Expand Down Expand Up @@ -537,11 +545,16 @@ const makeOpenCodeRuntime = Effect.gen(function* () {
Effect.map((result) => result.data ?? []),
);

const loadOpenCodeInventory: OpenCodeRuntimeShape["loadOpenCodeInventory"] = (client) =>
Effect.all([loadProviders(client), loadAgents(client)], { concurrency: "unbounded" }).pipe(
Effect.map(([providerList, agents]) => ({ providerList, agents })),
const loadSkills = (client: OpencodeClient) =>
runOpenCodeSdk("app.skills", () => client.app.skills()).pipe(
Effect.map((result) => (result.data ?? []) as ReadonlyArray<OpenCodeSkill>),
);

const loadOpenCodeInventory: OpenCodeRuntimeShape["loadOpenCodeInventory"] = (client) =>
Effect.all([loadProviders(client), loadAgents(client), loadSkills(client)], {
concurrency: "unbounded",
}).pipe(Effect.map(([providerList, agents, skills]) => ({ providerList, agents, skills })));

return {
startOpenCodeServerProcess,
connectToOpenCodeServer,
Expand Down
Loading