refactor(ripgrep): use embedded wasm backend (#21703)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import path from "path"
|
||||
import { Effect } from "effect"
|
||||
import { EffectLogger } from "@/effect/logger"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import type { Tool } from "./tool"
|
||||
import { Instance } from "../project/instance"
|
||||
import { AppFileSystem } from "../filesystem"
|
||||
@@ -21,8 +22,9 @@ export const assertExternalDirectoryEffect = Effect.fn("Tool.assertExternalDirec
|
||||
|
||||
if (options?.bypass) return
|
||||
|
||||
const ins = yield* InstanceState.context
|
||||
const full = process.platform === "win32" ? AppFileSystem.normalizePath(target) : target
|
||||
if (Instance.containsPath(full)) return
|
||||
if (Instance.containsPath(full, ins)) return
|
||||
|
||||
const kind = options?.kind ?? "file"
|
||||
const dir = kind === "directory" ? full : path.dirname(full)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import z from "zod"
|
||||
import path from "path"
|
||||
import z from "zod"
|
||||
import { Effect, Option } from "effect"
|
||||
import * as Stream from "effect/Stream"
|
||||
import { Tool } from "./tool"
|
||||
import DESCRIPTION from "./glob.txt"
|
||||
import { Ripgrep } from "../file/ripgrep"
|
||||
import { Instance } from "../project/instance"
|
||||
import { assertExternalDirectoryEffect } from "./external-directory"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { AppFileSystem } from "../filesystem"
|
||||
import { Ripgrep } from "../file/ripgrep"
|
||||
import { assertExternalDirectoryEffect } from "./external-directory"
|
||||
import DESCRIPTION from "./glob.txt"
|
||||
import { Tool } from "./tool"
|
||||
|
||||
export const GlobTool = Tool.define(
|
||||
"glob",
|
||||
@@ -28,6 +28,7 @@ export const GlobTool = Tool.define(
|
||||
}),
|
||||
execute: (params: { pattern: string; path?: string }, ctx: Tool.Context) =>
|
||||
Effect.gen(function* () {
|
||||
const ins = yield* InstanceState.context
|
||||
yield* ctx.ask({
|
||||
permission: "glob",
|
||||
patterns: [params.pattern],
|
||||
@@ -38,8 +39,8 @@ export const GlobTool = Tool.define(
|
||||
},
|
||||
})
|
||||
|
||||
let search = params.path ?? Instance.directory
|
||||
search = path.isAbsolute(search) ? search : path.resolve(Instance.directory, search)
|
||||
let search = params.path ?? ins.directory
|
||||
search = path.isAbsolute(search) ? search : path.resolve(ins.directory, search)
|
||||
const info = yield* fs.stat(search).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
||||
if (info?.type === "File") {
|
||||
throw new Error(`glob path must be a directory: ${search}`)
|
||||
@@ -48,14 +49,14 @@ export const GlobTool = Tool.define(
|
||||
|
||||
const limit = 100
|
||||
let truncated = false
|
||||
const files = yield* rg.files({ cwd: search, glob: [params.pattern] }).pipe(
|
||||
const files = yield* rg.files({ cwd: search, glob: [params.pattern], signal: ctx.abort }).pipe(
|
||||
Stream.mapEffect((file) =>
|
||||
Effect.gen(function* () {
|
||||
const full = path.resolve(search, file)
|
||||
const info = yield* fs.stat(full).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
||||
const mtime =
|
||||
info?.mtime.pipe(
|
||||
Option.map((d) => d.getTime()),
|
||||
Option.map((date) => date.getTime()),
|
||||
Option.getOrElse(() => 0),
|
||||
) ?? 0
|
||||
return { path: full, mtime }
|
||||
@@ -75,7 +76,7 @@ export const GlobTool = Tool.define(
|
||||
const output = []
|
||||
if (files.length === 0) output.push("No files found")
|
||||
if (files.length > 0) {
|
||||
output.push(...files.map((f) => f.path))
|
||||
output.push(...files.map((file) => file.path))
|
||||
if (truncated) {
|
||||
output.push("")
|
||||
output.push(
|
||||
@@ -85,7 +86,7 @@ export const GlobTool = Tool.define(
|
||||
}
|
||||
|
||||
return {
|
||||
title: path.relative(Instance.worktree, search),
|
||||
title: path.relative(ins.worktree, search),
|
||||
metadata: {
|
||||
count: files.length,
|
||||
truncated,
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import path from "path"
|
||||
import z from "zod"
|
||||
import { Effect, Option } from "effect"
|
||||
import { Tool } from "./tool"
|
||||
import { Ripgrep } from "../file/ripgrep"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { AppFileSystem } from "../filesystem"
|
||||
|
||||
import DESCRIPTION from "./grep.txt"
|
||||
import { Instance } from "../project/instance"
|
||||
import path from "path"
|
||||
import { Ripgrep } from "../file/ripgrep"
|
||||
import { assertExternalDirectoryEffect } from "./external-directory"
|
||||
import DESCRIPTION from "./grep.txt"
|
||||
import { Tool } from "./tool"
|
||||
|
||||
const MAX_LINE_LENGTH = 2000
|
||||
|
||||
@@ -46,15 +45,16 @@ export const GrepTool = Tool.define(
|
||||
},
|
||||
})
|
||||
|
||||
const searchPath = AppFileSystem.resolve(
|
||||
path.isAbsolute(params.path ?? Instance.directory)
|
||||
? (params.path ?? Instance.directory)
|
||||
: path.join(Instance.directory, params.path ?? "."),
|
||||
const ins = yield* InstanceState.context
|
||||
const search = AppFileSystem.resolve(
|
||||
path.isAbsolute(params.path ?? ins.directory)
|
||||
? (params.path ?? ins.directory)
|
||||
: path.join(ins.directory, params.path ?? "."),
|
||||
)
|
||||
const info = yield* fs.stat(searchPath).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
||||
const cwd = info?.type === "Directory" ? searchPath : path.dirname(searchPath)
|
||||
const file = info?.type === "Directory" ? undefined : [searchPath]
|
||||
yield* assertExternalDirectoryEffect(ctx, searchPath, {
|
||||
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)]
|
||||
yield* assertExternalDirectoryEffect(ctx, search, {
|
||||
kind: info?.type === "Directory" ? "directory" : "file",
|
||||
})
|
||||
|
||||
@@ -63,8 +63,8 @@ export const GrepTool = Tool.define(
|
||||
pattern: params.pattern,
|
||||
glob: params.include ? [params.include] : undefined,
|
||||
file,
|
||||
signal: ctx.abort,
|
||||
})
|
||||
|
||||
if (result.items.length === 0) return empty
|
||||
|
||||
const rows = result.items.map((item) => ({
|
||||
@@ -101,46 +101,43 @@ export const GrepTool = Tool.define(
|
||||
|
||||
const limit = 100
|
||||
const truncated = matches.length > limit
|
||||
const finalMatches = truncated ? matches.slice(0, limit) : matches
|
||||
const final = truncated ? matches.slice(0, limit) : matches
|
||||
if (final.length === 0) return empty
|
||||
|
||||
if (finalMatches.length === 0) return empty
|
||||
const total = matches.length
|
||||
const output = [`Found ${total} matches${truncated ? ` (showing first ${limit})` : ""}`]
|
||||
|
||||
const totalMatches = matches.length
|
||||
const outputLines = [`Found ${totalMatches} matches${truncated ? ` (showing first ${limit})` : ""}`]
|
||||
|
||||
let currentFile = ""
|
||||
for (const match of finalMatches) {
|
||||
if (currentFile !== match.path) {
|
||||
if (currentFile !== "") {
|
||||
outputLines.push("")
|
||||
}
|
||||
currentFile = match.path
|
||||
outputLines.push(`${match.path}:`)
|
||||
let current = ""
|
||||
for (const match of final) {
|
||||
if (current !== match.path) {
|
||||
if (current !== "") output.push("")
|
||||
current = match.path
|
||||
output.push(`${match.path}:`)
|
||||
}
|
||||
const truncatedLineText =
|
||||
const text =
|
||||
match.text.length > MAX_LINE_LENGTH ? match.text.substring(0, MAX_LINE_LENGTH) + "..." : match.text
|
||||
outputLines.push(` Line ${match.line}: ${truncatedLineText}`)
|
||||
output.push(` Line ${match.line}: ${text}`)
|
||||
}
|
||||
|
||||
if (truncated) {
|
||||
outputLines.push("")
|
||||
outputLines.push(
|
||||
`(Results truncated: showing ${limit} of ${totalMatches} matches (${totalMatches - limit} hidden). Consider using a more specific path or pattern.)`,
|
||||
output.push("")
|
||||
output.push(
|
||||
`(Results truncated: showing ${limit} of ${total} matches (${total - limit} hidden). Consider using a more specific path or pattern.)`,
|
||||
)
|
||||
}
|
||||
|
||||
if (result.partial) {
|
||||
outputLines.push("")
|
||||
outputLines.push("(Some paths were inaccessible and skipped)")
|
||||
output.push("")
|
||||
output.push("(Some paths were inaccessible and skipped)")
|
||||
}
|
||||
|
||||
return {
|
||||
title: params.pattern,
|
||||
metadata: {
|
||||
matches: totalMatches,
|
||||
matches: total,
|
||||
truncated,
|
||||
},
|
||||
output: outputLines.join("\n"),
|
||||
output: output.join("\n"),
|
||||
}
|
||||
}).pipe(Effect.orDie),
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import * as path from "path"
|
||||
import z from "zod"
|
||||
import { Effect } from "effect"
|
||||
import * as Stream from "effect/Stream"
|
||||
import { Tool } from "./tool"
|
||||
import * as path from "path"
|
||||
import DESCRIPTION from "./ls.txt"
|
||||
import { Instance } from "../project/instance"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { Ripgrep } from "../file/ripgrep"
|
||||
import { assertExternalDirectoryEffect } from "./external-directory"
|
||||
import DESCRIPTION from "./ls.txt"
|
||||
import { Tool } from "./tool"
|
||||
|
||||
export const IGNORE_PATTERNS = [
|
||||
"node_modules/",
|
||||
@@ -53,80 +53,68 @@ export const ListTool = Tool.define(
|
||||
}),
|
||||
execute: (params: { path?: string; ignore?: string[] }, ctx: Tool.Context) =>
|
||||
Effect.gen(function* () {
|
||||
const searchPath = path.resolve(Instance.directory, params.path || ".")
|
||||
yield* assertExternalDirectoryEffect(ctx, searchPath, { kind: "directory" })
|
||||
const ins = yield* InstanceState.context
|
||||
const search = path.resolve(ins.directory, params.path || ".")
|
||||
yield* assertExternalDirectoryEffect(ctx, search, { kind: "directory" })
|
||||
|
||||
yield* ctx.ask({
|
||||
permission: "list",
|
||||
patterns: [searchPath],
|
||||
patterns: [search],
|
||||
always: ["*"],
|
||||
metadata: {
|
||||
path: searchPath,
|
||||
path: search,
|
||||
},
|
||||
})
|
||||
|
||||
const ignoreGlobs = IGNORE_PATTERNS.map((p) => `!${p}*`).concat(params.ignore?.map((p) => `!${p}`) || [])
|
||||
const files = yield* rg.files({ cwd: searchPath, glob: ignoreGlobs }).pipe(
|
||||
Stream.take(LIMIT),
|
||||
const glob = IGNORE_PATTERNS.map((item) => `!${item}*`).concat(params.ignore?.map((item) => `!${item}`) || [])
|
||||
const files = yield* rg.files({ cwd: search, glob, signal: ctx.abort }).pipe(
|
||||
Stream.take(LIMIT + 1),
|
||||
Stream.runCollect,
|
||||
Effect.map((chunk) => [...chunk]),
|
||||
)
|
||||
|
||||
// Build directory structure
|
||||
const dirs = new Set<string>()
|
||||
const filesByDir = new Map<string, string[]>()
|
||||
const truncated = files.length > LIMIT
|
||||
if (truncated) files.length = LIMIT
|
||||
|
||||
const dirs = new Set<string>()
|
||||
const map = new Map<string, string[]>()
|
||||
for (const file of files) {
|
||||
const dir = path.dirname(file)
|
||||
const parts = dir === "." ? [] : dir.split("/")
|
||||
|
||||
// Add all parent directories
|
||||
for (let i = 0; i <= parts.length; i++) {
|
||||
const dirPath = i === 0 ? "." : parts.slice(0, i).join("/")
|
||||
dirs.add(dirPath)
|
||||
dirs.add(i === 0 ? "." : parts.slice(0, i).join("/"))
|
||||
}
|
||||
|
||||
// Add file to its directory
|
||||
if (!filesByDir.has(dir)) filesByDir.set(dir, [])
|
||||
filesByDir.get(dir)!.push(path.basename(file))
|
||||
if (!map.has(dir)) map.set(dir, [])
|
||||
map.get(dir)!.push(path.basename(file))
|
||||
}
|
||||
|
||||
function renderDir(dirPath: string, depth: number): string {
|
||||
function render(dir: string, depth: number): string {
|
||||
const indent = " ".repeat(depth)
|
||||
let output = ""
|
||||
if (depth > 0) output += `${indent}${path.basename(dir)}/\n`
|
||||
|
||||
if (depth > 0) {
|
||||
output += `${indent}${path.basename(dirPath)}/\n`
|
||||
}
|
||||
|
||||
const childIndent = " ".repeat(depth + 1)
|
||||
const children = Array.from(dirs)
|
||||
.filter((d) => path.dirname(d) === dirPath && d !== dirPath)
|
||||
const child = " ".repeat(depth + 1)
|
||||
const dirs2 = Array.from(dirs)
|
||||
.filter((item) => path.dirname(item) === dir && item !== dir)
|
||||
.sort()
|
||||
|
||||
// Render subdirectories first
|
||||
for (const child of children) {
|
||||
output += renderDir(child, depth + 1)
|
||||
for (const item of dirs2) {
|
||||
output += render(item, depth + 1)
|
||||
}
|
||||
|
||||
// Render files
|
||||
const files = filesByDir.get(dirPath) || []
|
||||
const files = map.get(dir) || []
|
||||
for (const file of files.sort()) {
|
||||
output += `${childIndent}${file}\n`
|
||||
output += `${child}${file}\n`
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
const output = `${searchPath}/\n` + renderDir(".", 0)
|
||||
|
||||
return {
|
||||
title: path.relative(Instance.worktree, searchPath),
|
||||
title: path.relative(ins.worktree, search),
|
||||
metadata: {
|
||||
count: files.length,
|
||||
truncated: files.length >= LIMIT,
|
||||
truncated,
|
||||
},
|
||||
output,
|
||||
output: `${search}/\n` + render(".", 0),
|
||||
}
|
||||
}).pipe(Effect.orDie),
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ import path from "path"
|
||||
import { pathToFileURL } from "url"
|
||||
import z from "zod"
|
||||
import { Effect } from "effect"
|
||||
import { EffectLogger } from "@/effect/logger"
|
||||
import * as Stream from "effect/Stream"
|
||||
import { Tool } from "./tool"
|
||||
import { Skill } from "../skill"
|
||||
import { EffectLogger } from "@/effect/logger"
|
||||
import { Ripgrep } from "../file/ripgrep"
|
||||
import { Skill } from "../skill"
|
||||
import { Tool } from "./tool"
|
||||
|
||||
const Parameters = z.object({
|
||||
name: z.string().describe("The name of the skill from available_skills"),
|
||||
@@ -17,6 +17,7 @@ export const SkillTool = Tool.define(
|
||||
Effect.gen(function* () {
|
||||
const skill = yield* Skill.Service
|
||||
const rg = yield* Ripgrep.Service
|
||||
|
||||
return () =>
|
||||
Effect.gen(function* () {
|
||||
const list = yield* skill.available().pipe(Effect.provide(EffectLogger.layer))
|
||||
@@ -45,10 +46,9 @@ export const SkillTool = Tool.define(
|
||||
execute: (params: z.infer<typeof Parameters>, ctx: Tool.Context) =>
|
||||
Effect.gen(function* () {
|
||||
const info = yield* skill.get(params.name)
|
||||
|
||||
if (!info) {
|
||||
const all = yield* skill.all()
|
||||
const available = all.map((s) => s.name).join(", ")
|
||||
const available = all.map((item) => item.name).join(", ")
|
||||
throw new Error(`Skill "${params.name}" not found. Available skills: ${available || "none"}`)
|
||||
}
|
||||
|
||||
@@ -61,9 +61,8 @@ export const SkillTool = Tool.define(
|
||||
|
||||
const dir = path.dirname(info.location)
|
||||
const base = pathToFileURL(dir).href
|
||||
|
||||
const limit = 10
|
||||
const files = yield* rg.files({ cwd: dir, follow: false, hidden: true }).pipe(
|
||||
const files = yield* rg.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),
|
||||
|
||||
Reference in New Issue
Block a user