feat(opencode): add filesystem read and list routes

This commit is contained in:
Dax Raad 2026-06-02 01:54:10 -04:00
commit 5937e606df
10 changed files with 414 additions and 15 deletions

View file

@ -4,6 +4,7 @@ import { ModelGroup } from "./v2/model"
import { ProviderGroup } from "./v2/provider"
import { SessionGroup } from "./v2/session"
import { PermissionGroup, PermissionSavedGroup, SessionPermissionGroup } from "./v2/permission"
import { FileSystemGroup } from "./v2/fs"
export const V2Api = HttpApi.make("v2")
.add(SessionGroup)
@ -13,6 +14,7 @@ export const V2Api = HttpApi.make("v2")
.add(PermissionGroup)
.add(SessionPermissionGroup)
.add(PermissionSavedGroup)
.add(FileSystemGroup)
.annotateMerge(
OpenApi.annotations({
title: "opencode experimental HttpApi",

View file

@ -0,0 +1,54 @@
import { LocationFileSystem } from "@opencode-ai/core/location-filesystem"
import { RelativePath } from "@opencode-ai/core/schema"
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { V2Authorization } from "../../middleware/authorization"
import { LocationQuery, locationQueryOpenApi, V2LocationMiddleware } from "./location"
const ReadQuery = Schema.Struct({
...LocationQuery.fields,
path: RelativePath,
})
const ListQuery = Schema.Struct({
...LocationQuery.fields,
path: RelativePath.pipe(Schema.optional),
})
export const FileSystemGroup = HttpApiGroup.make("v2.fs")
.add(
HttpApiEndpoint.get("read", "/api/fs/read", {
query: ReadQuery,
success: LocationFileSystem.Content,
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.fs.read",
summary: "Read file",
description: "Read one file relative to the requested location.",
}),
),
)
.add(
HttpApiEndpoint.get("list", "/api/fs/list", {
query: ListQuery,
success: Schema.Array(LocationFileSystem.Entry),
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.fs.list",
summary: "List directory",
description: "List direct children of one directory relative to the requested location.",
}),
),
)
.annotateMerge(
OpenApi.annotations({
title: "v2 filesystem",
description: "Experimental v2 location-scoped filesystem routes.",
}),
)
.middleware(V2LocationMiddleware)
.middleware(V2Authorization)

View file

@ -1,6 +1,7 @@
import { Catalog } from "@opencode-ai/core/catalog"
import { Location } from "@opencode-ai/core/location"
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
import { LocationFileSystem } from "@opencode-ai/core/location-filesystem"
import { PermissionV2 } from "@opencode-ai/core/permission"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { PluginBoot } from "@opencode-ai/core/plugin/boot"
@ -35,7 +36,7 @@ export const locationQueryOpenApi = OpenApi.annotations({
export class V2LocationMiddleware extends HttpApiMiddleware.Service<
V2LocationMiddleware,
{
provides: Catalog.Service | PluginBoot.Service | PermissionV2.Service
provides: Catalog.Service | PluginBoot.Service | PermissionV2.Service | LocationFileSystem.Service
}
>()("@opencode/ExperimentalHttpApiV2Location") {}

View file

@ -8,6 +8,7 @@ import { modelHandlers } from "./v2/model"
import { providerHandlers } from "./v2/provider"
import { sessionHandlers } from "./v2/session"
import { permissionHandlers, savedPermissionHandlers, sessionPermissionHandlers } from "./v2/permission"
import { fileSystemHandlers } from "./v2/fs"
export const v2Handlers = Layer.mergeAll(
sessionHandlers,
@ -17,6 +18,7 @@ export const v2Handlers = Layer.mergeAll(
permissionHandlers,
sessionPermissionHandlers,
savedPermissionHandlers,
fileSystemHandlers,
).pipe(
Layer.provide(v2LocationLayer),
Layer.provide(LocationServiceMap.layer),

View file

@ -0,0 +1,12 @@
import { LocationFileSystem } from "@opencode-ai/core/location-filesystem"
import { Effect } from "effect"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { InstanceHttpApi } from "../../api"
export const fileSystemHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.fs", (handlers) =>
Effect.gen(function* () {
return handlers
.handle("read", (ctx) => LocationFileSystem.Service.use((fs) => fs.read(ctx.query)))
.handle("list", (ctx) => LocationFileSystem.Service.use((fs) => fs.list(ctx.query)))
}),
)

View file

@ -575,6 +575,12 @@ const scenarios: Scenario[] = [
),
http.protected.get("/api/model", "v2.model.list").json(200, array),
http.protected.get("/api/provider", "v2.provider.list").json(200, array),
http.protected
.get("/api/fs/read", "v2.fs.read")
.seeded((ctx) => ctx.file("hello.txt", "hello\n"))
.at((ctx) => ({ path: "/api/fs/read?path=hello.txt", headers: ctx.headers() }))
.json(200, object),
http.protected.get("/api/fs/list", "v2.fs.list").json(200, array),
http.protected
.get("/api/provider/{providerID}", "v2.provider.get")
.at((ctx) => ({ path: route("/api/provider/{providerID}", { providerID: "missing" }), headers: ctx.headers() }))
@ -645,13 +651,10 @@ const scenarios: Scenario[] = [
.at((ctx) => ({
path: `/api/session?${new URLSearchParams({
limit: "2",
directory: ctx.directory ?? "",
cursor: cursor({
id: "ses_httpapi_missing",
time: 0,
order: "desc",
direction: "next",
directory: ctx.directory,
anchor: { id: "ses_httpapi_missing", time: 0, direction: "next" },
}),
})}`,
headers: ctx.headers(),
@ -669,8 +672,7 @@ const scenarios: Scenario[] = [
.get("/api/session", "v2.session.list.cursor.invalid")
.at((ctx) => ({
path: `/api/session?${new URLSearchParams({
cursor: cursor({ id: "ses_httpapi_missing", time: 0, order: "desc", direction: "next" }),
search: "not-allowed-with-cursor",
cursor: "invalid",
})}`,
headers: ctx.headers(),
}))