refactor(core): unify filesystem search service (#31566)

This commit is contained in:
Dax
2026-06-09 20:38:02 -04:00
committed by GitHub
parent ce4e658e3f
commit a0409e64d8
57 changed files with 965 additions and 2855 deletions
+5 -18
View File
@@ -2,7 +2,6 @@ import { EOL } from "os"
import { Effect } from "effect"
import { FileSystem } from "@opencode-ai/core/filesystem"
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
import { Search } from "@opencode-ai/core/filesystem/search"
import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema"
import { effectCmd } from "../../effect-cmd"
import { cmd } from "../cmd"
@@ -23,7 +22,11 @@ const FileSearchCommand = effectCmd({
description: "Search query",
}),
handler: Effect.fn("Cli.debug.file.search")(function* (args) {
const results = yield* filesystem(FileSystem.Service.use((svc) => svc.find({ query: args.query })))
const results = yield* Effect.orDie(
filesystem(
FileSystem.Service.use((svc) => svc.find({ query: args.query })),
),
)
process.stdout.write(results.map((item) => item.path).join(EOL) + EOL)
}),
})
@@ -58,21 +61,6 @@ const FileListCommand = effectCmd({
}),
})
const FileTreeCommand = effectCmd({
command: "tree [dir]",
describe: "show directory tree",
builder: (yargs) =>
yargs.positional("dir", {
type: "string",
description: "Directory to tree",
default: process.cwd(),
}),
handler: Effect.fn("Cli.debug.file.tree")(function* (args) {
const tree = yield* Effect.orDie(Search.Service.use((svc) => svc.tree({ cwd: args.dir, limit: 200 })))
console.log(JSON.stringify(tree, null, 2))
}),
})
export const FileCommand = cmd({
command: "file",
describe: "file system debugging utilities",
@@ -81,7 +69,6 @@ export const FileCommand = cmd({
.command(FileReadCommand)
.command(FileListCommand)
.command(FileSearchCommand)
.command(FileTreeCommand)
.demandCommand(),
async handler() {},
})
+20 -40
View File
@@ -1,6 +1,6 @@
import { EOL } from "os"
import { Effect, Stream } from "effect"
import { Search } from "@opencode-ai/core/filesystem/search"
import { Effect } from "effect"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { effectCmd } from "../../effect-cmd"
import { cmd } from "../cmd"
import { InstanceRef } from "@/effect/instance-ref"
@@ -8,25 +8,10 @@ import { InstanceRef } from "@/effect/instance-ref"
export const RipgrepCommand = cmd({
command: "rg",
describe: "ripgrep debugging utilities",
builder: (yargs) => yargs.command(TreeCommand).command(FilesCommand).command(SearchCommand).demandCommand(),
builder: (yargs) => yargs.command(FilesCommand).command(SearchCommand).demandCommand(),
async handler() {},
})
const TreeCommand = effectCmd({
command: "tree",
describe: "show file tree using ripgrep",
builder: (yargs) =>
yargs.option("limit", {
type: "number",
}),
handler: Effect.fn("Cli.debug.rg.tree")(function* (args) {
const ctx = yield* InstanceRef
if (!ctx) return
const tree = yield* Effect.orDie(Search.Service.use((svc) => svc.tree({ cwd: ctx.directory, limit: args.limit })))
process.stdout.write(tree + EOL)
}),
})
const FilesCommand = effectCmd({
command: "files",
describe: "list files using ripgrep",
@@ -47,19 +32,15 @@ const FilesCommand = effectCmd({
handler: Effect.fn("Cli.debug.rg.files")(function* (args) {
const ctx = yield* InstanceRef
if (!ctx) return
const search = yield* Search.Service
const files = yield* search
.files({
const ripgrep = yield* Ripgrep.Service
const files = yield* ripgrep
.glob({
cwd: ctx.directory,
glob: args.glob ? [args.glob] : undefined,
pattern: args.glob ?? "**/*",
limit: args.limit ?? 10_000,
})
.pipe(
Stream.take(args.limit ?? Infinity),
Stream.runCollect,
Effect.map((c) => [...c]),
Effect.orDie,
)
process.stdout.write(files.join(EOL) + EOL)
.pipe(Effect.orDie)
process.stdout.write(files.map((file) => file.path).join(EOL) + EOL)
}),
})
@@ -84,16 +65,15 @@ const SearchCommand = effectCmd({
handler: Effect.fn("Cli.debug.rg.search")(function* (args) {
const ctx = yield* InstanceRef
if (!ctx) return
const results = yield* Effect.orDie(
Search.Service.use((svc) =>
svc.search({
cwd: ctx.directory,
pattern: args.pattern,
glob: args.glob as string[] | undefined,
limit: args.limit,
}),
),
)
process.stdout.write(JSON.stringify(results.items, null, 2) + EOL)
const ripgrep = yield* Ripgrep.Service
const results = yield* ripgrep
.grep({
cwd: ctx.directory,
pattern: args.pattern,
include: args.glob?.[0],
limit: args.limit ?? 10_000,
})
.pipe(Effect.orDie)
process.stdout.write(JSON.stringify(results, null, 2) + EOL)
}),
})
+6 -5
View File
@@ -8,8 +8,7 @@ import { Auth } from "@/auth"
import { Account } from "@/account/account"
import { Config } from "@/config/config"
import { Git } from "@/git"
import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
import { Search } from "@opencode-ai/core/filesystem/search"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { Storage } from "@/storage/storage"
import { Snapshot } from "@/snapshot"
import { Plugin } from "@/plugin"
@@ -61,8 +60,6 @@ export const AppLayer = Layer.mergeAll(
Account.defaultLayer,
Config.defaultLayer,
Git.defaultLayer,
Ripgrep.defaultLayer,
Search.defaultLayer,
Storage.defaultLayer,
Snapshot.defaultLayer,
Plugin.defaultLayer,
@@ -102,7 +99,11 @@ export const AppLayer = Layer.mergeAll(
Installation.defaultLayer,
ShareNext.defaultLayer,
SessionShare.defaultLayer,
).pipe(Layer.provideMerge(InstanceLayer.layer), Layer.provideMerge(Observability.layer))
).pipe(
Layer.provideMerge(Ripgrep.defaultLayer),
Layer.provideMerge(InstanceLayer.layer),
Layer.provideMerge(Observability.layer),
)
const rt = ManagedRuntime.make(AppLayer, { memoMap })
type Runtime = Pick<typeof rt, "runSync" | "runPromise" | "runPromiseExit" | "runFork" | "runCallback" | "dispose">
@@ -6,9 +6,7 @@ import { Snapshot } from "../snapshot"
import * as Project from "./project"
import * as Vcs from "./vcs"
import { InstanceState } from "@/effect/instance-state"
import { registerDisposer } from "@/effect/instance-registry"
import { ShareNext } from "@/share/share-next"
import { Search } from "@opencode-ai/core/filesystem/search"
import { Effect, Layer } from "effect"
import { Config } from "@/config/config"
import { Service } from "./bootstrap-service"
@@ -27,25 +25,15 @@ export const layer = Layer.effect(
const lsp = yield* LSP.Service
const plugin = yield* Plugin.Service
const project = yield* Project.Service
const search = yield* Search.Service
const shareNext = yield* ShareNext.Service
const snapshot = yield* Snapshot.Service
const vcs = yield* Vcs.Service
// once we dispose the service - also release all the internal fff resources
const off = registerDisposer((directory) => Effect.runPromise(search.release(directory)))
yield* Effect.addFinalizer(() => Effect.sync(off))
const run = Effect.gen(function* () {
const ctx = yield* InstanceState.context
yield* Effect.logInfo("bootstrapping", { directory: ctx.directory })
// everything depends on config so eager load it for nice traces
yield* config.get()
// in 99% of use cases user that is opened opencode at certain directory will
// conduct a file search in this direcotry, it could be switched later but
// mostly always we will need a file picker for cwd
// so synchronously start FFF scan for a cwd so it is ready before first toolcall generated
yield* search.warm(ctx.directory).pipe(Effect.ignore)
// Plugin can mutate config so it has to be initialized before anything else.
yield* plugin.init()
// Each service self-manages its own slow work via Effect.forkScoped against
@@ -68,7 +56,6 @@ export const defaultLayer: Layer.Layer<Service> = layer.pipe(
LSP.defaultLayer,
Plugin.defaultLayer,
Project.defaultLayer,
Search.defaultLayer,
ShareNext.defaultLayer,
Snapshot.defaultLayer,
Vcs.defaultLayer,
@@ -81,7 +68,6 @@ export const node = LayerNode.make(layer, [
LSP.node,
Plugin.node,
Project.node,
Search.node,
ShareNext.node,
Snapshot.node,
Vcs.node,
@@ -1,5 +1,4 @@
import { FileSystem } from "@opencode-ai/core/filesystem"
import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
import { NonNegativeInt } from "@opencode-ai/core/schema"
import { LSP } from "@/lsp/lsp"
import { Schema } from "effect"
@@ -38,6 +37,20 @@ export const FindSymbolQuery = Schema.Struct({
query: Schema.String,
})
export const LegacyMatch = Schema.Struct({
path: Schema.Struct({ text: Schema.String }),
lines: Schema.Struct({ text: Schema.String }),
line_number: NonNegativeInt,
absolute_offset: NonNegativeInt,
submatches: Schema.Array(
Schema.Struct({
match: Schema.Struct({ text: Schema.String }),
start: NonNegativeInt,
end: NonNegativeInt,
}),
),
})
export const LegacyEntry = Schema.Struct({
name: Schema.String,
path: Schema.String,
@@ -94,7 +107,7 @@ export const FileApi = HttpApi.make("file")
.add(
HttpApiEndpoint.get("findText", FilePaths.findText, {
query: FindTextQuery,
success: described(Schema.Array(Ripgrep.SearchMatch), "Matches"),
success: described(Schema.Array(LegacyMatch), "Matches"),
}).annotateMerge(
OpenApi.annotations({
identifier: "find.text",
@@ -1,8 +1,7 @@
import * as InstanceState from "@/effect/instance-state"
import { FileSystem } from "@opencode-ai/core/filesystem"
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
import { Search } from "@opencode-ai/core/filesystem/search"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Location } from "@opencode-ai/core/location"
import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema"
@@ -15,7 +14,6 @@ import { InstanceHttpApi } from "../api"
export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handlers) =>
Effect.gen(function* () {
const ripgrep = yield* Ripgrep.Service
const search = yield* Search.Service
const locations = yield* LocationServiceMap
const filesystem = Effect.fnUntraced(function* <A, E, R>(effect: Effect.Effect<A, E, R>) {
@@ -26,8 +24,18 @@ export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handl
const findText = Effect.fn("FileHttpApi.findText")(function* (ctx: { query: { pattern: string } }) {
return (yield* ripgrep
.search({ cwd: (yield* InstanceState.context).directory, pattern: ctx.query.pattern, limit: 10 })
.pipe(Effect.orDie)).items
.grep({ cwd: (yield* InstanceState.context).directory, pattern: ctx.query.pattern, limit: 10 })
.pipe(Effect.orDie)).map((match) => ({
path: { text: match.entry.path },
lines: { text: match.text },
line_number: match.line,
absolute_offset: match.offset,
submatches: match.submatches.map((submatch) => ({
match: { text: submatch.text },
start: submatch.start,
end: submatch.end,
})),
}))
})
const findFile = Effect.fn("FileHttpApi.findFile")(function* (ctx: {
@@ -35,19 +43,18 @@ export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handl
}) {
const directory = (yield* InstanceState.context).directory
const limit = ctx.query.limit ?? 10
const kind = ctx.query.type ?? (ctx.query.dirs === "false" ? "file" : "all")
const type = ctx.query.type ?? (ctx.query.dirs === "false" ? "file" : undefined)
const started = performance.now()
const fff = yield* search.file({ cwd: directory, query: ctx.query.query, limit, kind }).pipe(Effect.orDie)
const found = yield* filesystem(FileSystem.Service.use((fs) => fs.find({ query: ctx.query.query, limit, type })))
yield* Effect.logInfo("find file", {
engine: "fff",
query: ctx.query.query,
kind,
type,
directory,
limit,
results: fff.length,
results: found.length,
duration: Math.round(performance.now() - started),
})
return fff.map((item) => item.path)
return found.map((item) => item.path)
})
const findSymbol = Effect.fn("FileHttpApi.findSymbol")(function* () {
@@ -73,10 +80,10 @@ export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handl
return (yield* fs.list({ path: RelativePath.make(ctx.query.path) })).map((item) => ({
name: path.basename(item.path),
path: item.path,
absolute: path.join(directory, item.path),
absolute: path.resolve(location.directory, item.path),
type: item.type,
ignored: ignored.ignores(
path.relative(location.project.directory, path.join(location.directory, item.path)) +
path.relative(location.project.directory, path.resolve(location.directory, item.path)) +
(item.type === "directory" ? "/" : ""),
),
}))
@@ -112,4 +119,4 @@ export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handl
.handle("content", content)
.handle("status", status)
}),
).pipe(Layer.provide(LocationServiceMap.layer), Layer.provide(Search.defaultLayer))
).pipe(Layer.provide(LocationServiceMap.layer))
@@ -17,7 +17,7 @@ import { BackgroundJob } from "@/background/job"
import { Config } from "@/config/config"
import { Command } from "@/command"
import * as Observability from "@opencode-ai/core/observability"
import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { Format } from "@/format"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { LSP } from "@/lsp/lsp"
@@ -234,7 +234,6 @@ export function createRoutes(
Provider.defaultLayer,
PtyTicket.defaultLayer,
Question.defaultLayer,
Ripgrep.defaultLayer,
RuntimeFlags.defaultLayer,
Session.defaultLayer,
SessionCompaction.defaultLayer,
@@ -259,6 +258,7 @@ export function createRoutes(
HttpServer.layerServices,
]),
Layer.provide(Layer.succeed(CorsConfig)(corsOptions)),
Layer.provideMerge(Ripgrep.defaultLayer),
Layer.provide(InstanceLayer.layer),
Layer.provideMerge(Observability.layer),
)
@@ -17,13 +17,13 @@ export const assertExternalDirectoryEffect = Effect.fn("Tool.assertExternalDirec
target?: string,
options?: Options,
) {
if (!target) return
if (!target) return false
if (options?.bypass) return
if (options?.bypass) return false
const ins = yield* InstanceState.context
const full = process.platform === "win32" ? FSUtil.normalizePath(target) : target
if (containsPath(full, ins)) return
if (containsPath(full, ins)) return false
const kind = options?.kind ?? "file"
const dir = kind === "directory" ? full : path.dirname(full)
@@ -41,6 +41,7 @@ export const assertExternalDirectoryEffect = Effect.fn("Tool.assertExternalDirec
parentDir: dir,
},
})
return true
})
export async function assertExternalDirectory(ctx: Tool.Context, target?: string, options?: Options) {
+10 -15
View File
@@ -2,7 +2,7 @@ import path from "path"
import { Effect, Schema } from "effect"
import { InstanceState } from "@/effect/instance-state"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Search } from "@opencode-ai/core/filesystem/search"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { assertExternalDirectoryEffect } from "./external-directory"
import DESCRIPTION from "./glob.txt"
import * as Tool from "./tool"
@@ -18,8 +18,7 @@ export const GlobTool = Tool.define(
"glob",
Effect.gen(function* () {
const fs = yield* FSUtil.Service
const searchSvc = yield* Search.Service
const ripgrep = yield* Ripgrep.Service
return {
description: DESCRIPTION,
parameters: Parameters,
@@ -48,18 +47,14 @@ export const GlobTool = Tool.define(
})
const limit = 100
const files = yield* searchSvc.glob({
cwd: search,
pattern: params.pattern,
limit,
signal: ctx.abort,
})
const files = yield* ripgrep.glob({ cwd: search, pattern: params.pattern, limit })
const truncated = files.length === limit
const output = []
if (files.files.length === 0) output.push("No files found")
if (files.files.length > 0) {
output.push(...files.files)
if (files.truncated) {
if (files.length === 0) output.push("No files found")
if (files.length > 0) {
output.push(...files.map((file) => path.resolve(search, file.path)))
if (truncated) {
output.push("")
output.push(
`(Results are truncated: showing first ${limit} results. Consider using a more specific path or pattern.)`,
@@ -70,8 +65,8 @@ export const GlobTool = Tool.define(
return {
title: path.relative(ins.worktree, search),
metadata: {
count: files.files.length,
truncated: files.truncated,
count: files.length,
truncated,
},
output: output.join("\n"),
}
+15 -40
View File
@@ -2,13 +2,11 @@ import path from "path"
import { Effect, Schema } from "effect"
import { InstanceState } from "@/effect/instance-state"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Search } from "@opencode-ai/core/filesystem/search"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { assertExternalDirectoryEffect } from "./external-directory"
import DESCRIPTION from "./grep.txt"
import * as Tool from "./tool"
const MAX_LINE_LENGTH = 2000
export const Parameters = Schema.Struct({
pattern: Schema.String.annotate({ description: "The regex pattern to search for in file contents" }),
path: Schema.optional(Schema.String).annotate({
@@ -23,8 +21,7 @@ export const GrepTool = Tool.define(
"grep",
Effect.gen(function* () {
const fs = yield* FSUtil.Service
const searchSvc = yield* Search.Service
const ripgrep = yield* Ripgrep.Service
return {
description: DESCRIPTION,
parameters: Parameters,
@@ -63,30 +60,27 @@ export const GrepTool = Tool.define(
const search = FSUtil.resolve(requested)
const info = yield* fs.stat(search).pipe(Effect.catch(() => Effect.succeed(undefined)))
const cwd = info?.type === "Directory" ? search : path.dirname(search)
const file = info?.type === "Directory" ? undefined : [path.relative(cwd, search)]
const result = yield* searchSvc.search({
const result = yield* ripgrep.grep({
cwd,
pattern: params.pattern,
glob: params.include ? [params.include] : undefined,
file,
signal: ctx.abort,
include: params.include,
limit: 100,
})
if (result.items.length === 0) return empty
if (result.length === 0) return empty
const rows = result.items.map((item) => ({
path: FSUtil.resolve(path.isAbsolute(item.path.text) ? item.path.text : path.join(cwd, item.path.text)),
line: item.line_number,
text: item.lines.text,
const rows = result.map((item) => ({
path: path.resolve(cwd, item.entry.path),
line: item.line,
text: item.text,
}))
const limit = 100
const truncated = rows.length > limit
const final = truncated ? rows.slice(0, limit) : rows
const truncated = rows.length === limit
const final = rows
if (final.length === 0) return empty
const total = rows.length
const hasMore = truncated || result.hasNextPage
const hasMore = truncated || result.length === limit
const output = [`Found ${total} matches${hasMore ? " (more matches available)" : ""}`]
let current = ""
@@ -96,31 +90,12 @@ export const GrepTool = Tool.define(
current = match.path
output.push(`${match.path}:`)
}
const text =
match.text.length > MAX_LINE_LENGTH ? match.text.substring(0, MAX_LINE_LENGTH) + "..." : match.text
output.push(` Line ${match.line}: ${text}`)
output.push(` Line ${match.line}: ${match.text}`)
}
if (truncated) {
output.push("")
output.push(
`(Results truncated: showing ${limit} of ${total} matches (${total - limit} hidden). Consider using a more specific path or pattern.)`,
)
}
if (result.hasNextPage) {
output.push("")
output.push(`(Results truncated. Consider using a more specific path or pattern.)`)
}
if (result.partial) {
output.push("")
output.push("(Some paths were inaccessible and skipped)")
}
if (result.regexFallbackError) {
output.push("")
output.push(`(Regex fallback: ${result.regexFallbackError})`)
output.push("(Results truncated. Consider using a more specific path or pattern.)")
}
return {
+1 -4
View File
@@ -8,7 +8,6 @@ import DESCRIPTION from "./read.txt"
import { InstanceState } from "@/effect/instance-state"
import { assertExternalDirectoryEffect } from "./external-directory"
import { Instruction } from "../session/instruction"
import { Search } from "@opencode-ai/core/filesystem/search"
import { isPdfAttachment, sniffAttachmentMime } from "@/util/media"
const DEFAULT_READ_LIMIT = 2000
@@ -65,14 +64,13 @@ type Metadata = {
export const ReadTool = Tool.define<
typeof Parameters,
Metadata,
FSUtil.Service | Instruction.Service | LSP.Service | Search.Service | Scope.Scope
FSUtil.Service | Instruction.Service | LSP.Service | Scope.Scope
>(
"read",
Effect.gen(function* () {
const fs = yield* FSUtil.Service
const instruction = yield* Instruction.Service
const lsp = yield* LSP.Service
const search = yield* Search.Service
const scope = yield* Scope.Scope
const miss = Effect.fn("ReadTool.miss")(function* (filepath: string) {
@@ -117,7 +115,6 @@ export const ReadTool = Tool.define<
})
const warm = Effect.fn("ReadTool.warm")(function* (filepath: string) {
yield* search.open({ file: filepath }).pipe(Effect.ignore)
// LSP warm-up is optional; do not let a background defect fail an otherwise successful read.
yield* lsp.touchFile(filepath).pipe(Effect.ignoreCause, Effect.forkIn(scope))
})
+3 -30
View File
@@ -1,6 +1,6 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { httpClient } from "@opencode-ai/core/effect/layer-node-platform"
import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { PlanExitTool } from "./plan"
import { Session } from "@/session/session"
import { QuestionTool } from "./question"
@@ -36,7 +36,6 @@ import { Effect, Layer, Context } from "effect"
import { FetchHttpClient, HttpClient } from "effect/unstable/http"
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Search } from "@opencode-ai/core/filesystem/search"
import { Format } from "../format"
import { InstanceState } from "@/effect/instance-state"
import { EffectBridge } from "@/effect/bridge"
@@ -81,30 +80,7 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/ToolRegistry") {}
export const layer: Layer.Layer<
Service,
never,
| Config.Service
| Plugin.Service
| Question.Service
| Todo.Service
| Agent.Service
| Skill.Service
| Session.Service
| BackgroundJob.Service
| Provider.Service
| LSP.Service
| Instruction.Service
| FSUtil.Service
| EventV2Bridge.Service
| HttpClient.HttpClient
| ChildProcessSpawner
| Search.Service
| Format.Service
| Truncate.Service
| RuntimeFlags.Service
| Database.Service
> = Layer.effect(
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const config = yield* Config.Service
@@ -358,7 +334,6 @@ export const defaultLayer = Layer.suspend(() =>
Layer.provide(FetchHttpClient.layer),
Layer.provide(Format.defaultLayer),
Layer.provide(CrossSpawnSpawner.defaultLayer),
Layer.provide(Search.defaultLayer),
Layer.provide(Truncate.defaultLayer),
)
.pipe(Layer.provide(Database.defaultLayer), Layer.provide(RuntimeFlags.defaultLayer)),
@@ -440,7 +415,7 @@ function isJsonSchemaObject(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value)
}
export const node = LayerNode.make(layer, [
export const node = LayerNode.make(layer.pipe(Layer.provide(Ripgrep.defaultLayer)), [
Config.node,
Plugin.node,
Question.node,
@@ -456,8 +431,6 @@ export const node = LayerNode.make(layer, [
EventV2Bridge.node,
httpClient,
CrossSpawnSpawner.node,
Ripgrep.node,
Search.node,
Format.node,
Truncate.node,
RuntimeFlags.node,
+11 -12
View File
@@ -1,8 +1,7 @@
import path from "path"
import { pathToFileURL } from "url"
import { Effect, Schema } from "effect"
import * as Stream from "effect/Stream"
import { Search } from "@opencode-ai/core/filesystem/search"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { Skill } from "../skill"
import * as Tool from "./tool"
import DESCRIPTION from "./skill.txt"
@@ -15,7 +14,7 @@ export const SkillTool = Tool.define(
"skill",
Effect.gen(function* () {
const skill = yield* Skill.Service
const searchSvc = yield* Search.Service
const ripgrep = yield* Ripgrep.Service
return {
description: DESCRIPTION,
@@ -35,14 +34,14 @@ export const SkillTool = Tool.define(
const dir = path.dirname(info.location)
const base = pathToFileURL(dir).href
const limit = 10
const files = yield* searchSvc.files({ cwd: dir, follow: false, hidden: true, signal: ctx.abort }).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>${file}</file>`).join("\n")),
)
const files = yield* ripgrep.find({
cwd: dir,
pattern: "!**/SKILL.md",
hidden: true,
follow: false,
signal: ctx.abort,
limit: 10,
})
return {
title: `Loaded skill: ${info.name}`,
@@ -57,7 +56,7 @@ export const SkillTool = Tool.define(
"Note: file list is sampled.",
"",
"<skill_files>",
files,
files.map((file) => `<file>${path.resolve(dir, file.path)}</file>`).join("\n"),
"</skill_files>",
"</skill_content>",
].join("\n"),