diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts index 96e171733df0..1ae77650c65f 100644 --- a/packages/opencode/src/command/index.ts +++ b/packages/opencode/src/command/index.ts @@ -1,11 +1,15 @@ +import path from "path" +import { pathToFileURL } from "url" import { BusEvent } from "@/bus/bus-event" import { InstanceState } from "@/effect/instance-state" import { EffectBridge } from "@/effect/bridge" import type { InstanceContext } from "@/project/instance-context" import { SessionID, MessageID } from "@/session/schema" import { Effect, Layer, Context, Schema } from "effect" +import * as Stream from "effect/Stream" import { Config } from "@/config/config" import { MCP } from "../mcp" +import { Ripgrep } from "../file/ripgrep" import { Skill } from "../skill" import PROMPT_INITIALIZE from "./template/initialize.txt" import PROMPT_REVIEW from "./template/review.txt" @@ -69,6 +73,8 @@ export const layer = Layer.effect( const mcp = yield* MCP.Service const skill = yield* Skill.Service + const rg = yield* Ripgrep.Service + const init = Effect.fn("Command.state")(function* (ctx: InstanceContext) { const cfg = yield* config.get() const bridge = yield* EffectBridge.make() @@ -140,12 +146,40 @@ export const layer = Layer.effect( for (const item of yield* skill.all()) { if (commands[item.name]) continue + const dir = path.dirname(item.location) + const base = pathToFileURL(dir).href commands[item.name] = { name: item.name, description: item.description, source: "skill", get template() { - return item.content + return bridge.promise( + Effect.gen(function* () { + const limit = 10 + const files = yield* rg.files({ cwd: dir, follow: false, hidden: true }).pipe( + Stream.filter((file) => !file.includes("SKILL.md")), + Stream.map((file) => path.resolve(dir, file)), + Stream.take(limit), + Stream.runCollect, + Effect.map((chunk) => [...chunk].map((file) => `${file}`).join("\n")), + ) + return [ + ``, + `# Skill: ${item.name}`, + "", + item.content.trim(), + "", + `Base directory for this skill: ${base}`, + "Relative paths in this skill (e.g., scripts/, reference/) are relative to this base directory.", + "Note: file list is sampled.", + "", + "", + files, + "", + "", + ].join("\n") + }), + ) }, hints: [], } diff --git a/packages/opencode/test/command/index.test.ts b/packages/opencode/test/command/index.test.ts new file mode 100644 index 000000000000..a1507c4091b3 --- /dev/null +++ b/packages/opencode/test/command/index.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, test } from "bun:test" +import { hints } from "@/command/index" + +describe("command", () => { + test("hints extracts numbered placeholders", () => { + expect(hints("do $1 and $2")).toEqual(["$1", "$2"]) + }) + + test("hints extracts $ARGUMENTS placeholder", () => { + expect(hints("do $ARGUMENTS")).toEqual(["$ARGUMENTS"]) + }) + + test("hints extracts mixed placeholders", () => { + expect(hints("$1 $2 $ARGUMENTS")).toEqual(["$1", "$2", "$ARGUMENTS"]) + }) + + test("hints deduplicates repeated placeholders", () => { + expect(hints("$1 and $1 again")).toEqual(["$1"]) + }) + + test("hints sorts numbered placeholders", () => { + expect(hints("$3 $1 $2")).toEqual(["$1", "$2", "$3"]) + }) + + test("hints returns empty array for no placeholders", () => { + expect(hints("no placeholders here")).toEqual([]) + }) + + test("hints handles empty string", () => { + expect(hints("")).toEqual([]) + }) +})