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([])
+ })
+})