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
13 changes: 11 additions & 2 deletions apps/server/src/provider/Drivers/OpenCodeDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ import * as Duration from "effect/Duration";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import * as Layer from "effect/Layer";
import * as Schema from "effect/Schema";
import * as Stream from "effect/Stream";
import { HttpClient } from "effect/unstable/http";
import { ChildProcessSpawner } from "effect/unstable/process";
import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem";

import { makeOpenCodeTextGeneration } from "../../textGeneration/OpenCodeTextGeneration.ts";
import { ServerConfig } from "../../config.ts";
Expand Down Expand Up @@ -138,15 +140,22 @@ export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv>
effectiveConfig,
serverConfig.cwd,
processEnv,
).pipe(Effect.map(stampIdentity), Effect.provideService(OpenCodeRuntime, openCodeRuntime));
).pipe(
Effect.map(stampIdentity),
Effect.provideService(OpenCodeRuntime, openCodeRuntime),
Effect.provide(Layer.merge(Path.layer, NodeFileSystem.layer)),
);

const snapshot = yield* makeManagedServerProvider<OpenCodeSettings>({
maintenanceCapabilities,
getSettings: Effect.succeed(effectiveConfig),
streamSettings: Stream.never,
haveSettingsChanged: () => false,
initialSnapshot: (settings) =>
makePendingOpenCodeProvider(settings).pipe(Effect.map(stampIdentity)),
makePendingOpenCodeProvider(settings, serverConfig.cwd).pipe(
Effect.map(stampIdentity),
Effect.provide(Layer.merge(Path.layer, NodeFileSystem.layer)),
),
checkProvider,
enrichSnapshot: ({ snapshot, publishSnapshot }) =>
enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities).pipe(
Expand Down
127 changes: 125 additions & 2 deletions apps/server/src/provider/Layers/OpenCodeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ import {
type ModelCapabilities,
type OpenCodeSettings,
type ServerProviderModel,
type ServerProviderSkill,
} from "@t3tools/contracts";
import * as Cause from "effect/Cause";
import * as Data from "effect/Data";
import * as DateTime from "effect/DateTime";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";

import { createModelCapabilities } from "@t3tools/shared/model";
import { compareSemverVersions } from "@t3tools/shared/semver";
Expand Down Expand Up @@ -217,6 +220,112 @@ function openCodeCapabilitiesForModel(input: {
});
}

const SKILLS_DIR_NAME = ".agents";
const SKILLS_SUBDIR_NAME = "skills";
const SKILL_FILE_NAME = "SKILL.md";

function parseSkillFrontmatter(
content: string,
): { name: string; description?: string; displayName?: string } | null {
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
if (!frontmatterMatch) {
return null;
}

const frontmatter = frontmatterMatch[1]!;
const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
if (!nameMatch) {
return null;
}

const name = nameMatch[1]!.trim();
if (name.length === 0) {
return null;
}

const descMatch = frontmatter.match(/^description:\s*(?:>\s*\n([\s\S]*?)|(.+))?$/m);
let description: string | undefined;
if (descMatch) {
const raw = descMatch[1] ?? descMatch[2];
if (raw) {
description = raw
.split("\n")
.map((line) => line.replace(/^\s*\|?\s*/, ""))
.join(" ")
.trim();
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Folded description truncates lines

Medium Severity

In parseSkillFrontmatter, folded description: > blocks are parsed with a regex that ends at the first line boundary, so only the first continuation line is captured. Additional folded lines are dropped before join/trim, so skill tooltips and descriptions can be wrong whenever SKILL.md uses multi-line folded YAML.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 532a15e. Configure here.

}

const displayNameMatch = frontmatter.match(/^displayName:\s*(.+)$/m);
const displayName = displayNameMatch ? displayNameMatch[1]!.trim() : undefined;

const result: { name: string; description?: string; displayName?: string } = { name };
if (description !== undefined) {
result.description = description;
}
if (displayName !== undefined) {
result.displayName = displayName;
}
return result;
}

function loadOpenCodeSkills(
cwd: string,
): Effect.Effect<ReadonlyArray<ServerProviderSkill>, never, FileSystem.FileSystem | Path.Path> {
return Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;

const skillsDir = path.join(cwd, SKILLS_DIR_NAME, SKILLS_SUBDIR_NAME);
const entriesExit = yield* Effect.exit(fs.readDirectory(skillsDir, { recursive: false }));

if (entriesExit._tag !== "Success") {
return [];
}

const skills: Array<ServerProviderSkill> = [];

for (const entryName of entriesExit.value) {
const fullPath = path.join(skillsDir, entryName);
const statExit = yield* Effect.exit(fs.stat(fullPath));

if (statExit._tag !== "Success" || statExit.value.type !== "Directory") {
continue;
}

const contentExit = yield* Effect.exit(
fs.readFileString(path.join(fullPath, SKILL_FILE_NAME)),
);

if (contentExit._tag !== "Success") {
continue;
}

const parsed = parseSkillFrontmatter(contentExit.value);
if (!parsed) {
continue;
}

const shortDescription =
parsed.description && parsed.description.length > 200
? parsed.description.slice(0, 200) + "..."
: parsed.description;

const skill: ServerProviderSkill = {
name: parsed.name,
path: fullPath,
enabled: true,
...(parsed.description !== undefined ? { description: parsed.description } : {}),
...(parsed.displayName !== undefined ? { displayName: parsed.displayName } : {}),
...(shortDescription !== undefined ? { shortDescription } : {}),
};
skills.push(skill);
}

return skills;
});
}

function flattenOpenCodeModels(input: OpenCodeInventory): ReadonlyArray<ServerProviderModel> {
const connected = new Set(input.providerList.connected);
const models: Array<ServerProviderModel> = [];
Expand Down Expand Up @@ -252,7 +361,8 @@ function flattenOpenCodeModels(input: OpenCodeInventory): ReadonlyArray<ServerPr

export const makePendingOpenCodeProvider = (
openCodeSettings: OpenCodeSettings,
): Effect.Effect<ServerProviderDraft> =>
cwd: string,
): Effect.Effect<ServerProviderDraft, never, FileSystem.FileSystem | Path.Path> =>
Effect.gen(function* () {
const checkedAt = yield* Effect.map(DateTime.now, DateTime.formatIso);
const models = providerModelsFromSettings(
Expand All @@ -261,13 +371,15 @@ export const makePendingOpenCodeProvider = (
openCodeSettings.customModels,
DEFAULT_OPENCODE_MODEL_CAPABILITIES,
);
const skills = yield* loadOpenCodeSkills(cwd);

if (!openCodeSettings.enabled) {
return buildServerProvider({
presentation: OPENCODE_PRESENTATION,
enabled: false,
checkedAt,
models,
skills,
probe: {
installed: false,
version: null,
Expand All @@ -286,6 +398,7 @@ export const makePendingOpenCodeProvider = (
enabled: true,
checkedAt,
models,
skills,
probe: {
installed: false,
version: null,
Expand All @@ -300,12 +413,18 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu
openCodeSettings: OpenCodeSettings,
cwd: string,
environment: NodeJS.ProcessEnv = process.env,
): Effect.fn.Return<ServerProviderDraft, never, OpenCodeRuntime> {
): Effect.fn.Return<
ServerProviderDraft,
never,
OpenCodeRuntime | FileSystem.FileSystem | Path.Path
> {
const openCodeRuntime = yield* OpenCodeRuntime;
const checkedAt = DateTime.formatIso(yield* DateTime.now);
const customModels = openCodeSettings.customModels;
const isExternalServer = openCodeSettings.serverUrl.trim().length > 0;

const skills = yield* loadOpenCodeSkills(cwd);

const fallback = (cause: unknown, version: string | null = null) => {
const failure = formatOpenCodeProbeError({
cause,
Expand All @@ -322,6 +441,7 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu
customModels,
DEFAULT_OPENCODE_MODEL_CAPABILITIES,
),
skills,
probe: {
installed: failure.installed,
version,
Expand All @@ -343,6 +463,7 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu
customModels,
DEFAULT_OPENCODE_MODEL_CAPABILITIES,
),
skills,
probe: {
installed: false,
version: null,
Expand Down Expand Up @@ -394,6 +515,7 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu
customModels,
DEFAULT_OPENCODE_MODEL_CAPABILITIES,
),
skills,
probe: {
installed: true,
version,
Expand Down Expand Up @@ -455,6 +577,7 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu
enabled: true,
checkedAt,
models,
skills,
probe: {
installed: true,
version,
Expand Down
Loading