diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts index 4c71ff577673..9dd882db131e 100644 --- a/packages/opencode/src/command/index.ts +++ b/packages/opencode/src/command/index.ts @@ -1,4 +1,5 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node" +import path from "path" import { InstanceState } from "@/effect/instance-state" import { EffectBridge } from "@/effect/bridge" import type { InstanceContext } from "@/project/instance-context" @@ -132,12 +133,19 @@ export const layer = Layer.effect( for (const item of yield* skill.all()) { if (commands[item.name]) continue + const dir = item.location === "" ? undefined : path.dirname(item.location) commands[item.name] = { name: item.name, description: item.description, source: "skill", get template() { - return item.content + if (!dir) return item.content + return [ + item.content, + "", + `Base directory for this skill: ${dir}`, + "Relative paths in this skill (e.g., scripts/, references/) are relative to this base directory.", + ].join("\n") }, hints: [], } diff --git a/packages/opencode/src/skill/index.ts b/packages/opencode/src/skill/index.ts index 08a88a215432..14a378ac1671 100644 --- a/packages/opencode/src/skill/index.ts +++ b/packages/opencode/src/skill/index.ts @@ -1,6 +1,5 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node" import path from "path" -import { pathToFileURL } from "url" import { Effect, Layer, Context, Schema } from "effect" import { NamedError } from "@opencode-ai/core/util/error" import type { Agent } from "@/agent/agent" @@ -17,6 +16,7 @@ import { RuntimeFlags } from "@/effect/runtime-flags" import { Glob } from "@opencode-ai/core/util/glob" import { Discovery } from "./discovery" import { isRecord } from "@/util/record" +import { escapeHtml } from "@/util/html" const CLAUDE_EXTERNAL_DIR = ".claude" const AGENTS_EXTERNAL_DIR = ".agents" @@ -339,7 +339,7 @@ export function fmt(list: Info[], opts: { verbose: boolean }) { " ", ` ${skill.name}`, ` ${skill.description}`, - ` ${pathToFileURL(skill.location).href}`, + ` ${escapeHtml(skill.location)}`, " ", ]), "", diff --git a/packages/opencode/test/skill/skill.test.ts b/packages/opencode/test/skill/skill.test.ts index fd79a68cee21..a2e5b6b77c6c 100644 --- a/packages/opencode/test/skill/skill.test.ts +++ b/packages/opencode/test/skill/skill.test.ts @@ -77,6 +77,33 @@ const withHome = (home: string, self: Effect.Effect) => ) describe("skill", () => { + it.effect("formats verbose locations as XML-safe filesystem paths", () => + Effect.sync(() => { + const output = Skill.fmt( + [ + { + name: "tagged-skill", + description: "A tagged skill.", + location: "/tmp/plugin.git#v1.3.0/SKILL.md", + content: "", + }, + { + name: "built-in-skill", + description: "A built-in skill.", + location: "", + content: "", + }, + ], + { verbose: true }, + ) + + expect(output).toContain("/tmp/plugin.git#v1.3.0/SKILL.md") + expect(output).toContain("<built-in>") + expect(output).not.toContain("file://") + expect(output).not.toContain("%23") + }), + ) + it.live("discovers skills from .opencode/skill/ directory", () => provideTmpdirInstance( (dir) =>