chore: fork — remove vendor CI, repoint release checks to Neuron Gitea

This commit is contained in:
2026-08-21 13:36:36 -05:00
parent 96215713ea
commit 49b5856226
140 changed files with 734 additions and 16034 deletions
+101
View File
@@ -0,0 +1,101 @@
import path from "path"
import { Effect, Schema } from "effect"
import { InstanceState } from "@/effect/instance-state"
import { assertExternalDirectoryEffect } from "./external-directory"
import DESCRIPTION from "./git.txt"
import * as Tool from "./tool"
const MAX_OUTPUT_BYTES = 50 * 1024
const OPERATIONS = ["status", "diff", "log", "blame", "branch"] as const
export const Parameters = Schema.Struct({
operation: Schema.Literals(OPERATIONS).annotate({
description: "The read-only git operation to run",
}),
path: Schema.optional(Schema.String).annotate({
description:
"File or directory the operation applies to. Required for blame. Defaults to the working directory for status/diff/log/branch.",
}),
ref: Schema.optional(Schema.String).annotate({
description: 'Revision argument for diff/log (e.g. "HEAD~1", "main").',
}),
})
export const GitTool = Tool.define(
"git",
Effect.gen(function* () {
return {
description: DESCRIPTION,
parameters: Parameters,
execute: (params: { operation: string; path?: string; ref?: string }, ctx: Tool.Context) =>
Effect.gen(function* () {
const ins = yield* InstanceState.context
let target = params.path ?? ins.directory
target = path.isAbsolute(target) ? target : path.resolve(ins.directory, target)
yield* assertExternalDirectoryEffect(ctx, target, {
bypass: false,
kind: "directory",
})
yield* ctx.ask({
permission: "read",
patterns: [path.relative(ins.worktree, target)],
always: ["*"],
metadata: params,
})
if (params.operation === "blame" && !params.path) {
throw new Error("blame requires a file path")
}
const args = ["git"]
const dirInfo = yield* Effect.promise(() =>
import("fs").then((fs) => fs.statSync(target).isDirectory()),
).pipe(Effect.catch(() => Effect.succeed(true)))
if (dirInfo) args.push("-C", target)
else args.push("-C", path.dirname(target))
args.push(params.operation)
if (params.ref && (params.operation === "diff" || params.operation === "log")) args.push(params.ref)
if (!dirInfo || params.operation === "blame") args.push(target)
const result = yield* Effect.tryPromise({
try: async () => {
const proc = Bun.spawn(args, {
cwd: ins.directory,
stdout: "pipe",
stderr: "pipe",
})
const [stdout, stderr, code] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
])
return { stdout, stderr, code }
},
catch: (cause) => new Error(`git ${params.operation} failed: ${cause}`),
})
if (result.code !== 0) {
throw new Error(`git ${params.operation} failed: ${result.stderr.trim() || `exit code ${result.code}`}`)
}
let output = result.stdout
const truncated = Buffer.byteLength(output) > MAX_OUTPUT_BYTES
if (truncated) {
output = Buffer.from(output).subarray(0, MAX_OUTPUT_BYTES).toString("utf8")
output += `\n\n(Output truncated at ${MAX_OUTPUT_BYTES / 1024} KB. Narrow the query, e.g. a specific path or ref.)`
}
if (output.length === 0) output = `(no output from git ${params.operation})`
return {
title: `git ${params.operation}`,
metadata: {
operation: params.operation,
truncated,
},
output,
}
}).pipe(Effect.orDie),
}
}),
)
+4
View File
@@ -0,0 +1,4 @@
- Run read-only git operations: status, diff, log, blame, branch
- Output is capped; narrow with a path or ref instead of dumping the whole repo history
- Use this instead of shell git commands for inspection; use shell for anything that mutates state (commit, push, checkout, stash)
- blame requires a file path
+52 -10
View File
@@ -1,5 +1,5 @@
import path from "path"
import { Effect, Schema } from "effect"
import { Effect, Option, Schema } from "effect"
import { InstanceState } from "@/effect/instance-state"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
@@ -7,6 +7,8 @@ import { assertExternalDirectoryEffect } from "./external-directory"
import DESCRIPTION from "./glob.txt"
import * as Tool from "./tool"
const LIMIT = 200
export const Parameters = Schema.Struct({
pattern: Schema.String.annotate({ description: "The glob pattern to match files against" }),
path: Schema.optional(Schema.String).annotate({
@@ -38,7 +40,8 @@ export const GlobTool = Tool.define(
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") {
if (!info) throw new Error(`path not found: ${search}`)
if (info.type === "File") {
throw new Error(`glob path must be a directory: ${search}`)
}
yield* assertExternalDirectoryEffect(ctx, search, {
@@ -46,18 +49,57 @@ export const GlobTool = Tool.define(
kind: "directory",
})
const limit = 100
const files = yield* ripgrep.glob({ cwd: search, pattern: params.pattern, limit })
const truncated = files.length === limit
// request one extra so we can tell whether results were cut off
const files = yield* ripgrep.glob({ cwd: search, pattern: params.pattern, limit: LIMIT + 1 })
const fileTruncated = files.length > LIMIT
const visibleFiles = fileTruncated ? files.slice(0, LIMIT) : [...files]
// rg --files only ever lists files; find matching directories separately
// so patterns like "signal" or "src/*" can still surface them
const scanned = yield* fs
.glob(params.pattern, { cwd: search, include: "all", dot: true })
.pipe(Effect.catch(() => Effect.succeed([] as string[])))
const known = new Set<string>(visibleFiles.map((f) => f.path))
const dirs: string[] = []
for (const rel of scanned) {
const normalized = rel.replaceAll("\\", "/").replace(/^(?:\.[\\/])+/, "")
if (known.has(normalized)) continue
const target = path.resolve(search, normalized)
const st = yield* fs.stat(target).pipe(Effect.catch(() => Effect.succeed(undefined)))
if (st?.type !== "Directory") continue
dirs.push(target + "/")
}
const entries = [
...visibleFiles.map((f) => path.resolve(search, f.path)),
...dirs,
]
// sort newest first so recently changed matches come before stale ones
const stamped = yield* Effect.forEach(
entries,
(p) =>
fs.stat(p.replace(/\/$/, "")).pipe(
Effect.map((st) => ({
path: p,
mtime: Option.getOrElse(st.mtime, () => new Date(0)).getTime(),
})),
Effect.catch(() => Effect.succeed({ path: p, mtime: 0 })),
),
{ concurrency: "unbounded" },
)
stamped.sort((a, b) => b.mtime - a.mtime)
const truncated = fileTruncated || stamped.length > LIMIT
const final = stamped.slice(0, LIMIT)
const output = []
if (files.length === 0) output.push("No files found")
if (files.length > 0) {
output.push(...files.map((file) => path.resolve(search, file.path)))
if (final.length === 0) output.push("No files found")
if (final.length > 0) {
output.push(...final.map((entry) => entry.path))
if (truncated) {
output.push("")
output.push(
`(Results are truncated: showing first ${limit} results. Consider using a more specific path or pattern.)`,
`(Results are truncated: showing first ${LIMIT} results. Consider using a more specific path or pattern.)`,
)
}
}
@@ -65,7 +107,7 @@ export const GlobTool = Tool.define(
return {
title: path.relative(ins.worktree, search),
metadata: {
count: files.length,
count: final.length,
truncated,
},
output: output.join("\n"),
+2 -2
View File
@@ -1,6 +1,6 @@
- Fast file pattern matching tool that works with any codebase size
- Supports glob patterns like "**/*.js" or "src/**/*.ts"
- Returns matching file paths
- Use this tool when you need to find files by name patterns
- Matches files and directories (directories end with "/"); results are sorted newest-first
- Use this tool when you need to find files by name patterns; use ls to list a single directory
- When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead
- You have the capability to call multiple tools in a single response. It is always better to speculatively perform multiple searches as a batch that are potentially useful.
+11 -14
View File
@@ -7,6 +7,8 @@ import { assertExternalDirectoryEffect } from "./external-directory"
import DESCRIPTION from "./grep.txt"
import * as Tool from "./tool"
const LIMIT = 100
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({
@@ -64,11 +66,13 @@ export const GrepTool = Tool.define(
cwd,
pattern: params.pattern,
include: params.include,
limit: 100,
// request one extra so we can tell whether results were cut off
limit: LIMIT + 1,
})
if (result.length === 0) return empty
const rows = result.map((item) => ({
const hasMore = result.length > LIMIT
const rows = result.slice(0, LIMIT).map((item) => ({
path: path.resolve(
requestedInfo?.type === "Directory" ? requested : path.dirname(requested),
item.entry.path,
@@ -77,17 +81,10 @@ export const GrepTool = Tool.define(
text: item.text,
}))
const limit = 100
const truncated = rows.length === limit
const final = rows
if (final.length === 0) return empty
const total = rows.length
const hasMore = truncated || result.length === limit
const output = [`Found ${total} matches${hasMore ? " (more matches available)" : ""}`]
const output = [`Found ${rows.length} matches${hasMore ? " (more matches available)" : ""}`]
let current = ""
for (const match of final) {
for (const match of rows) {
if (current !== match.path) {
if (current !== "") output.push("")
current = match.path
@@ -96,7 +93,7 @@ export const GrepTool = Tool.define(
output.push(` Line ${match.line}: ${match.text}`)
}
if (truncated) {
if (hasMore) {
output.push("")
output.push("(Results truncated. Consider using a more specific path or pattern.)")
}
@@ -104,8 +101,8 @@ export const GrepTool = Tool.define(
return {
title: params.pattern,
metadata: {
matches: total,
truncated,
matches: rows.length,
truncated: hasMore,
},
output: output.join("\n"),
}
+69
View File
@@ -0,0 +1,69 @@
import path from "path"
import { Effect, Schema } from "effect"
import { InstanceState } from "@/effect/instance-state"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { assertExternalDirectoryEffect } from "./external-directory"
import DESCRIPTION from "./ls.txt"
import * as Tool from "./tool"
export const Parameters = Schema.Struct({
path: Schema.optional(Schema.String).annotate({
description: `The directory to list. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter "undefined" or "null" - simply omit it for the default behavior.`,
}),
})
export const LsTool = Tool.define(
"ls",
Effect.gen(function* () {
const fs = yield* FSUtil.Service
return {
description: DESCRIPTION,
parameters: Parameters,
execute: (params: { path?: string }, ctx: Tool.Context) =>
Effect.gen(function* () {
const ins = yield* InstanceState.context
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) throw new Error(`Directory not found: ${search}`)
if (info.type === "File") throw new Error(`ls path must be a directory: ${search}`)
yield* assertExternalDirectoryEffect(ctx, search, {
bypass: false,
kind: "directory",
})
yield* ctx.ask({
permission: "read",
patterns: [path.relative(ins.worktree, search)],
always: ["*"],
metadata: {
path: params.path,
},
})
const entries = yield* fs.readDirectoryEntries(search)
const lines: string[] = []
for (const item of entries) {
if (item.type === "directory") {
lines.push(item.name + "/")
continue
}
if (item.type !== "symlink") {
lines.push(item.name)
continue
}
const target = yield* fs.stat(path.join(search, item.name)).pipe(Effect.catch(() => Effect.void))
lines.push(target?.type === "Directory" ? item.name + "/" : item.name)
}
lines.sort((a, b) => a.localeCompare(b))
return {
title: path.relative(ins.worktree, search),
metadata: {
count: lines.length,
},
output: lines.length > 0 ? lines.join("\n") : "(empty directory)",
}
}).pipe(Effect.orDie),
}
}),
)
+4
View File
@@ -0,0 +1,4 @@
- Lists the contents of a directory: files and subdirectories (subdirectories end with "/")
- Use this to see what exists in a directory or to check whether a directory is present
- The glob tool only matches files, never directories; use ls for anything directory-related
- Omit the path to list the current working directory
+1 -1
View File
@@ -19,6 +19,6 @@ All operations require:
workspaceSymbol also accepts:
- query: A query string to filter symbols by. Empty string requests all symbols.
For workspaceSymbol, filePath is not sent in the LSP workspace/symbol request. It is used by opencode to select and start the matching LSP server.
For workspaceSymbol, filePath is not sent in the LSP workspace/symbol request. It is used by Neuron to select and start the matching LSP server.
Note: LSP servers must be configured for the file type. If no server is available, an error will be returned.
+76
View File
@@ -0,0 +1,76 @@
import path from "path"
import { Effect, Schema } from "effect"
import { InstanceState } from "@/effect/instance-state"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { assertExternalDirectoryEffect } from "./external-directory"
import DESCRIPTION from "./move.txt"
import * as Tool from "./tool"
export const Parameters = Schema.Struct({
from: Schema.String.annotate({ description: "The file or directory to move or rename" }),
to: Schema.String.annotate({
description: "The destination path. Refuses to overwrite if the destination already exists.",
}),
})
export const MoveTool = Tool.define(
"move",
Effect.gen(function* () {
const fs = yield* FSUtil.Service
return {
description: DESCRIPTION,
parameters: Parameters,
execute: (params: { from: string; to: string }, ctx: Tool.Context) =>
Effect.gen(function* () {
const ins = yield* InstanceState.context
let from = params.from
from = path.isAbsolute(from) ? from : path.resolve(ins.directory, from)
let to = params.to
to = path.isAbsolute(to) ? to : path.resolve(ins.directory, to)
const fromInfo = yield* fs.stat(from).pipe(Effect.catch(() => Effect.succeed(undefined)))
if (!fromInfo) throw new Error(`Source not found: ${from}`)
const toInfo = yield* fs.stat(to).pipe(Effect.catch(() => Effect.succeed(undefined)))
if (toInfo && toInfo.type === "Directory" && fromInfo.type === "File") {
// moving a file into an existing directory keeps the basename
to = path.join(to, path.basename(from))
}
const finalInfo = yield* fs.stat(to).pipe(Effect.catch(() => Effect.succeed(undefined)))
if (finalInfo) throw new Error(`Destination already exists: ${to}`)
yield* assertExternalDirectoryEffect(ctx, from, {
bypass: false,
kind: fromInfo.type === "Directory" ? "directory" : "file",
})
yield* assertExternalDirectoryEffect(ctx, to, {
bypass: false,
kind: "file",
})
yield* ctx.ask({
permission: "edit",
patterns: [
path.relative(ins.worktree, from),
path.relative(ins.worktree, to),
],
always: ["*"],
metadata: { from, to },
})
yield* Effect.tryPromise({
try: async () => {
const nfs = await import("fs/promises")
await nfs.mkdir(path.dirname(to), { recursive: true })
await nfs.rename(from, to)
},
catch: (cause) => new Error(`Failed to move ${from} to ${to}: ${cause}`),
})
return {
title: path.relative(ins.worktree, to),
metadata: { from, to },
output: `Moved ${path.relative(ins.worktree, from)} to ${path.relative(ins.worktree, to)}`,
}
}).pipe(Effect.orDie),
}
}),
)
+3
View File
@@ -0,0 +1,3 @@
- Moves or renames a file or directory; creates destination parent directories as needed
- Refuses to overwrite an existing destination
- Prefer this over shell `mv` — it goes through the same permission checks and external-directory guards as other file tools
+22 -2
View File
@@ -8,6 +8,11 @@ import { ShellTool } from "./shell"
import { EditTool } from "./edit"
import { GlobTool } from "./glob"
import { GrepTool } from "./grep"
import { LsTool } from "./ls"
import { TreeTool } from "./tree"
import { GitTool } from "./git"
import { MoveTool } from "./move"
import { RemoveTool } from "./remove"
import { ReadTool } from "./read"
import { TaskTool } from "./task"
import { Database } from "@opencode-ai/core/database/database"
@@ -109,6 +114,11 @@ const layer = Layer.effect(
const websearch = yield* WebSearchTool
const shell = yield* ShellTool
const globtool = yield* GlobTool
const lstool = yield* LsTool
const treetool = yield* TreeTool
const gittool = yield* GitTool
const movetool = yield* MoveTool
const removetool = yield* RemoveTool
const writetool = yield* WriteTool
const edit = yield* EditTool
const greptool = yield* GrepTool
@@ -211,6 +221,11 @@ const layer = Layer.effect(
shell: Tool.init(shell),
read: Tool.init(read),
glob: Tool.init(globtool),
ls: Tool.init(lstool),
tree: Tool.init(treetool),
git: Tool.init(gittool),
move: Tool.init(movetool),
remove: Tool.init(removetool),
grep: Tool.init(greptool),
edit: Tool.init(edit),
write: Tool.init(writetool),
@@ -234,6 +249,11 @@ const layer = Layer.effect(
tool.shell,
tool.read,
tool.glob,
tool.ls,
tool.tree,
tool.git,
tool.move,
tool.remove,
tool.grep,
tool.edit,
tool.write,
@@ -244,8 +264,8 @@ const layer = Layer.effect(
tool.skill,
tool.patch,
...(tool.execute ? [tool.execute] : []),
...(flags.experimentalLspTool ? [tool.lsp] : []),
...(flags.experimentalPlanMode && flags.client === "cli" ? [tool.plan] : []),
tool.lsp,
...(flags.client === "cli" ? [tool.plan] : []),
],
task: tool.task,
read: tool.read,
+56
View File
@@ -0,0 +1,56 @@
import path from "path"
import { Effect, Schema } from "effect"
import { InstanceState } from "@/effect/instance-state"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { assertExternalDirectoryEffect } from "./external-directory"
import DESCRIPTION from "./remove.txt"
import * as Tool from "./tool"
export const Parameters = Schema.Struct({
path: Schema.String.annotate({ description: "The file to delete. Directories must be empty." }),
})
export const RemoveTool = Tool.define(
"remove",
Effect.gen(function* () {
const fs = yield* FSUtil.Service
return {
description: DESCRIPTION,
parameters: Parameters,
execute: (params: { path: string }, ctx: Tool.Context) =>
Effect.gen(function* () {
const ins = yield* InstanceState.context
let target = params.path
target = path.isAbsolute(target) ? target : path.resolve(ins.directory, target)
const info = yield* fs.stat(target).pipe(Effect.catch(() => Effect.succeed(undefined)))
if (!info) throw new Error(`Not found: ${target}`)
yield* assertExternalDirectoryEffect(ctx, target, {
bypass: false,
kind: info.type === "Directory" ? "directory" : "file",
})
yield* ctx.ask({
permission: "edit",
patterns: [path.relative(ins.worktree, target)],
always: ["*"],
metadata: { path: params.path },
})
yield* Effect.tryPromise({
try: async () => {
const nfs = await import("fs/promises")
if (info.type === "Directory") await nfs.rmdir(target)
else await nfs.unlink(target)
},
catch: (cause) => new Error(`Failed to remove ${target}: ${cause}`),
})
return {
title: path.relative(ins.worktree, target),
metadata: { removed: target },
output: `Removed ${path.relative(ins.worktree, target)}`,
}
}).pipe(Effect.orDie),
}
}),
)
+3
View File
@@ -0,0 +1,3 @@
- Deletes a file. Refuses directories unless they are empty.
- Prefer this over shell `rm` — it goes through the same permission checks and external-directory guards as other file tools
- This is permanent; there is no undo
-2
View File
@@ -12,8 +12,6 @@ interface Metadata {
[key: string]: any
}
// TODO: remove this hack
export type DynamicDescription = (agent: Agent.Info) => Effect.Effect<string>
/**
* Raised when the LLM calls a tool with arguments that fail the parameter
+101
View File
@@ -0,0 +1,101 @@
import path from "path"
import { Effect, Schema } from "effect"
import { InstanceState } from "@/effect/instance-state"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { assertExternalDirectoryEffect } from "./external-directory"
import DESCRIPTION from "./tree.txt"
import * as Tool from "./tool"
const DEFAULT_DEPTH = 3
const MAX_DEPTH = 8
const MAX_ENTRIES = 500
const IGNORED = new Set([".git", "node_modules"])
export const Parameters = Schema.Struct({
path: Schema.optional(Schema.String).annotate({
description: `The directory to start from. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory.`,
}),
depth: Schema.optional(Schema.Number).annotate({
description: `Maximum depth to descend (default ${DEFAULT_DEPTH}, max ${MAX_DEPTH}).`,
}),
})
export const TreeTool = Tool.define(
"tree",
Effect.gen(function* () {
const fs = yield* FSUtil.Service
return {
description: DESCRIPTION,
parameters: Parameters,
execute: (params: { path?: string; depth?: number }, ctx: Tool.Context) =>
Effect.gen(function* () {
const ins = yield* InstanceState.context
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) throw new Error(`Directory not found: ${search}`)
if (info.type === "File") throw new Error(`tree path must be a directory: ${search}`)
yield* assertExternalDirectoryEffect(ctx, search, {
bypass: false,
kind: "directory",
})
yield* ctx.ask({
permission: "read",
patterns: [path.relative(ins.worktree, search)],
always: ["*"],
metadata: {
path: params.path,
depth: params.depth,
},
})
const maxDepth = Math.min(Math.max(1, params.depth ?? DEFAULT_DEPTH), MAX_DEPTH)
const state = { count: 0, truncated: false }
const walk = (dir: string, prefix: string, depth: number): Effect.Effect<string[]> =>
Effect.gen(function* () {
if (depth > maxDepth || state.count >= MAX_ENTRIES) return []
const entries = yield* fs.readDirectoryEntries(dir).pipe(Effect.catch(() => Effect.succeed([])))
const visible = entries
.filter((entry) => !IGNORED.has(entry.name))
.sort((a, b) => a.name.localeCompare(b.name))
const lines: string[] = []
for (const entry of visible) {
if (state.count >= MAX_ENTRIES) {
state.truncated = true
break
}
let isDir = entry.type === "directory"
if (entry.type === "symlink") {
isDir = yield* fs.isDir(path.join(dir, entry.name))
}
lines.push(prefix + entry.name + (isDir ? "/" : ""))
state.count++
if (isDir) {
const nested = yield* walk(path.join(dir, entry.name), prefix + entry.name + "/", depth + 1)
lines.push(...nested)
}
}
return lines
})
const lines = yield* walk(search, "", 1)
const output = [`${path.relative(ins.worktree, search) || "."}`, ...lines]
if (state.truncated) {
output.push("")
output.push(`(Truncated at ${MAX_ENTRIES} entries. Use a more specific path or lower depth.)`)
}
return {
title: path.relative(ins.worktree, search),
metadata: {
count: state.count,
truncated: state.truncated,
},
output: output.join("\n"),
}
}).pipe(Effect.orDie),
}
}),
)
+4
View File
@@ -0,0 +1,4 @@
- Recursive directory overview rendered as a tree, one entry per line (directories end with "/")
- Use this to understand project structure at a glance; use ls for a single directory level
- Skips .git and node_modules; caps output depth and entry count
- Omit the path to start at the current working directory