Skip to content
Open
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
2 changes: 2 additions & 0 deletions apps/server/src/provider/Drivers/ClaudeDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const path = yield* Path.Path;
const httpClient = yield* HttpClient.HttpClient;
const serverConfig = yield* ServerConfig;
const eventLoggers = yield* ProviderEventLoggers;
const processEnv = mergeProviderInstanceEnvironment(environment);
const fallbackContinuationIdentity = defaultProviderContinuationIdentity({
Expand Down Expand Up @@ -148,6 +149,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {
effectiveConfig,
() => Cache.get(capabilitiesProbeCache, capabilitiesCacheKey),
processEnv,
serverConfig.cwd,
).pipe(
Effect.map(stampIdentity),
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
Expand Down
18 changes: 15 additions & 3 deletions apps/server/src/provider/Layers/ClaudeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@ import {
type ServerProviderDraft,
} from "../providerSnapshot.ts";
import { compareCliVersions } from "../cliVersion.ts";
import { makeClaudeEnvironment } from "../Drivers/ClaudeHome.ts";
import { makeClaudeEnvironment, resolveClaudeHomePath } from "../Drivers/ClaudeHome.ts";
import {
discoverClaudeSkills,
mergeProviderSkills,
mergeSkillsIntoSlashCommands,
} from "../SkillDiscovery.ts";

const DEFAULT_CLAUDE_MODEL_CAPABILITIES: ModelCapabilities = createModelCapabilities({
optionDescriptors: [],
Expand Down Expand Up @@ -516,6 +521,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
claudeSettings: ClaudeSettings,
) => Effect.Effect<ClaudeCapabilitiesProbe | undefined>,
environment: NodeJS.ProcessEnv = process.env,
cwd: string = process.cwd(),
): Effect.fn.Return<
ServerProviderDraft,
never,
Expand Down Expand Up @@ -622,14 +628,19 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
: undefined;
const slashCommands = capabilities?.slashCommands ?? [];
const dedupedSlashCommands = dedupeSlashCommands(slashCommands);
const claudeHome = yield* resolveClaudeHomePath(claudeSettings);
const discoveredSkills = yield* discoverClaudeSkills({ cwd, homeDir: claudeHome });
const skills = mergeProviderSkills([], discoveredSkills);
const mergedSlashCommands = mergeSkillsIntoSlashCommands(dedupedSlashCommands, skills);

if (!capabilities) {
return buildServerProvider({
presentation: CLAUDE_PRESENTATION,
enabled: claudeSettings.enabled,
checkedAt,
models,
slashCommands: dedupedSlashCommands,
slashCommands: mergedSlashCommands,
skills,
probe: {
installed: true,
version: parsedVersion,
Expand All @@ -649,7 +660,8 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
enabled: claudeSettings.enabled,
checkedAt,
models,
slashCommands: dedupedSlashCommands,
slashCommands: mergedSlashCommands,
skills,
probe: {
installed: true,
version: parsedVersion,
Expand Down
52 changes: 52 additions & 0 deletions apps/server/src/provider/Layers/CodexProvider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { describe, expect, it } from "vitest";

import { parseCodexSkillsListResponse } from "./CodexProvider.ts";

describe("parseCodexSkillsListResponse", () => {
it("omits app-backed skills from Codex app-server results", () => {
const skills = parseCodexSkillsListResponse(
{
data: [
{
cwd: "/workspace",
errors: [],
skills: [
{
name: "browser-use:browser",
path: "/Users/test/.codex/plugins/cache/openai-bundled/browser-use/skills/browser/SKILL.md",
description: "Drive a browser.",
scope: "user",
enabled: true,
},
{
name: "review-follow-up",
path: "/Users/test/.codex/skills/review-follow-up/SKILL.md",
enabled: true,
description: "Review a follow-up change.",
scope: "user",
},
{
name: "agent-plugin",
path: "C:\\Users\\test\\.agents\\plugins\\cache\\example\\skills\\agent-plugin\\SKILL.md",
description: "Run an app-backed agent skill.",
scope: "user",
enabled: true,
},
],
},
],
},
"/workspace",
);

expect(skills).toEqual([
{
name: "review-follow-up",
path: "/Users/test/.codex/skills/review-follow-up/SKILL.md",
enabled: true,
description: "Review a follow-up change.",
scope: "user",
},
]);
});
});
87 changes: 61 additions & 26 deletions apps/server/src/provider/Layers/CodexProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import type {
import { ServerSettingsError } from "@t3tools/contracts";

import { createModelCapabilities } from "@t3tools/shared/model";
import { isCommandAvailable } from "@t3tools/shared/shell";

import { buildServerProvider, type ServerProviderDraft } from "../providerSnapshot.ts";
import { expandHomePath } from "../../pathExpansion.ts";
import { scopedSafeTeardown } from "./scopedSafeTeardown.ts";
Expand Down Expand Up @@ -168,7 +170,18 @@ function appendCustomCodexModels(
return customEntries.length === 0 ? models : [...models, ...customEntries];
}

function parseCodexSkillsListResponse(
function normalizeSkillPathSeparators(pathValue: string): string {
return pathValue.replaceAll("\\", "/");
}

function isCodexAppBackedSkill(skill: CodexSchema.V2SkillsListResponse__SkillMetadata): boolean {
const normalizedPath = normalizeSkillPathSeparators(skill.path);
return (
normalizedPath.includes("/.codex/plugins/") || normalizedPath.includes("/.agents/plugins/")
);
}

export function parseCodexSkillsListResponse(
response: CodexSchema.V2SkillsListResponse,
cwd: string,
): ReadonlyArray<ServerProviderSkill> {
Expand All @@ -177,31 +190,33 @@ function parseCodexSkillsListResponse(
? matchingEntry.skills
: response.data.flatMap((entry) => entry.skills);

return skills.map((skill) => {
const shortDescription =
skill.shortDescription ?? skill.interface?.shortDescription ?? undefined;

const parsedSkill: Types.Mutable<ServerProviderSkill> = {
name: skill.name,
path: skill.path,
enabled: skill.enabled,
};

if (skill.description) {
parsedSkill.description = skill.description;
}
if (skill.scope) {
parsedSkill.scope = skill.scope;
}
if (skill.interface?.displayName) {
parsedSkill.displayName = skill.interface.displayName;
}
if (shortDescription) {
parsedSkill.shortDescription = shortDescription;
}

return parsedSkill;
});
return skills
.filter((skill) => !isCodexAppBackedSkill(skill))
.map((skill) => {
const shortDescription =
skill.shortDescription ?? skill.interface?.shortDescription ?? undefined;

const parsedSkill: Types.Mutable<ServerProviderSkill> = {
name: skill.name,
path: skill.path,
enabled: skill.enabled,
};

if (skill.description) {
parsedSkill.description = skill.description;
}
if (skill.scope) {
parsedSkill.scope = skill.scope;
}
if (skill.interface?.displayName) {
parsedSkill.displayName = skill.interface.displayName;
}
if (shortDescription) {
parsedSkill.shortDescription = shortDescription;
}

return parsedSkill;
});
}

const requestAllCodexModels = Effect.fn("requestAllCodexModels")(function* (
Expand Down Expand Up @@ -428,6 +443,26 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu
});
}

if (
probe === probeCodexAppServerProvider &&
!isCommandAvailable(codexSettings.binaryPath, { env: environment })
) {
return buildServerProvider({
presentation: CODEX_PRESENTATION,
enabled: codexSettings.enabled,
checkedAt,
models: emptyModels,
skills: [],
probe: {
installed: false,
version: null,
status: "error",
auth: { status: "unknown" },
message: "Codex CLI (`codex`) is not installed or not on PATH.",
},
});
}

const probeResult = yield* probe({
binaryPath: codexSettings.binaryPath,
homePath: codexSettings.homePath,
Expand Down
45 changes: 45 additions & 0 deletions apps/server/src/provider/Layers/OpenCodeProvider.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import assert from "node:assert/strict";
import * as NodeFS from "node:fs";
import * as NodeOS from "node:os";
import * as NodePath from "node:path";

import * as NodeServices from "@effect/platform-node/NodeServices";
import { it } from "@effect/vitest";
Expand Down Expand Up @@ -198,6 +201,48 @@ it.layer(testLayer)("checkOpenCodeProviderStatus", (it) => {
assert.equal(runtimeMock.state.closeCalls, 1);
}),
);

it.effect("includes discovered OpenCode-compatible skills", () => {
const root = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-opencode-skills-"));
const home = NodePath.join(root, "home");
const workspace = NodePath.join(root, "workspace", "apps", "web");
const skillDir = NodePath.join(root, "workspace", ".opencode", "skills", "git-release");
NodeFS.mkdirSync(NodePath.join(root, "workspace", ".git"), { recursive: true });
NodeFS.mkdirSync(workspace, { recursive: true });
NodeFS.mkdirSync(skillDir, { recursive: true });
NodeFS.writeFileSync(
NodePath.join(skillDir, "SKILL.md"),
["---", "name: git-release", "description: Prepare a release.", "---"].join("\n"),
"utf8",
);
runtimeMock.state.inventory = {
providerList: { connected: ["openai"], all: [], default: {} },
agents: [],
};

return Effect.gen(function* () {
const snapshot = yield* checkOpenCodeProviderStatus(makeOpenCodeSettings(), workspace, {
...process.env,
HOME: home,
USERPROFILE: home,
});

assert.deepStrictEqual(snapshot.skills, [
{
name: "git-release",
description: "Prepare a release.",
shortDescription: "Prepare a release.",
displayName: "git-release",
path: NodePath.resolve(NodePath.join(skillDir, "SKILL.md")),
scope: "project",
enabled: true,
invocationPrefix: "$",
},
]);
}).pipe(
Effect.ensuring(Effect.sync(() => NodeFS.rmSync(root, { force: true, recursive: true }))),
);
});
});

it.layer(testLayer)("checkOpenCodeProviderStatus with configured server URL", (it) => {
Expand Down
12 changes: 12 additions & 0 deletions apps/server/src/provider/Layers/OpenCodeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
type ServerProviderModel,
} from "@t3tools/contracts";
import { Cause, Data, Effect } from "effect";
import * as NodeOS from "node:os";

import { createModelCapabilities } from "@t3tools/shared/model";
import {
Expand All @@ -20,6 +21,7 @@ import {
openCodeRuntimeErrorDetail,
type OpenCodeInventory,
} from "../opencodeRuntime.ts";
import { discoverOpenCodeSkills, mergeProviderSkills } from "../SkillDiscovery.ts";
import type { Agent, ProviderListResponse } from "@opencode-ai/sdk/v2";

const PROVIDER = ProviderDriverKind.make("opencode");
Expand Down Expand Up @@ -48,6 +50,10 @@ function normalizeProbeMessage(message: string): string | undefined {
return trimmed;
}

function homeDirFromEnvironment(environment: NodeJS.ProcessEnv): string {
return environment.HOME ?? environment.USERPROFILE ?? NodeOS.homedir();
}

function normalizedErrorMessage(cause: unknown): string | undefined {
if (cause instanceof OpenCodeProbeError) {
return normalizeProbeMessage(cause.detail);
Expand Down Expand Up @@ -445,12 +451,18 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu
customModels,
DEFAULT_OPENCODE_MODEL_CAPABILITIES,
);
const discoveredSkills = yield* discoverOpenCodeSkills({
cwd,
homeDir: homeDirFromEnvironment(environment),
});
const skills = mergeProviderSkills([], discoveredSkills);
const connectedCount = inventoryExit.value.providerList.connected.length;
return buildServerProvider({
presentation: OPENCODE_PRESENTATION,
enabled: true,
checkedAt,
models,
skills,
probe: {
installed: true,
version,
Expand Down
Loading
Loading