feat(api): publish v2 reference

This commit is contained in:
Dax Raad 2026-07-06 01:29:08 -04:00
parent e6e951b252
commit eed23d8ee9
40 changed files with 26473 additions and 147 deletions

View file

@ -450,20 +450,24 @@ export interface CredentialApi<E = never> {
readonly remove: CredentialRemoveOperation<E>
}
type Endpoint12_0Request = Parameters<RawClient["server.project"]["project.current"]>[0]
export type Endpoint12_0Input = { readonly location?: Endpoint12_0Request["query"]["location"] }
export type Endpoint12_0Output = EffectValue<ReturnType<RawClient["server.project"]["project.current"]>>
export type ProjectCurrentOperation<E = never> = (input?: Endpoint12_0Input) => Effect.Effect<Endpoint12_0Output, E>
export type Endpoint12_0Output = EffectValue<ReturnType<RawClient["server.project"]["project.list"]>>
export type ProjectListOperation<E = never> = () => Effect.Effect<Endpoint12_0Output, E>
type Endpoint12_1Request = Parameters<RawClient["server.project"]["project.directories"]>[0]
export type Endpoint12_1Input = {
readonly projectID: Endpoint12_1Request["params"]["projectID"]
readonly location?: Endpoint12_1Request["query"]["location"]
type Endpoint12_1Request = Parameters<RawClient["server.project"]["project.current"]>[0]
export type Endpoint12_1Input = { readonly location?: Endpoint12_1Request["query"]["location"] }
export type Endpoint12_1Output = EffectValue<ReturnType<RawClient["server.project"]["project.current"]>>
export type ProjectCurrentOperation<E = never> = (input?: Endpoint12_1Input) => Effect.Effect<Endpoint12_1Output, E>
type Endpoint12_2Request = Parameters<RawClient["server.project"]["project.directories"]>[0]
export type Endpoint12_2Input = {
readonly projectID: Endpoint12_2Request["params"]["projectID"]
readonly location?: Endpoint12_2Request["query"]["location"]
}
export type Endpoint12_1Output = EffectValue<ReturnType<RawClient["server.project"]["project.directories"]>>
export type ProjectDirectoriesOperation<E = never> = (input: Endpoint12_1Input) => Effect.Effect<Endpoint12_1Output, E>
export type Endpoint12_2Output = EffectValue<ReturnType<RawClient["server.project"]["project.directories"]>>
export type ProjectDirectoriesOperation<E = never> = (input: Endpoint12_2Input) => Effect.Effect<Endpoint12_2Output, E>
export interface ProjectApi<E = never> {
readonly list: ProjectListOperation<E>
readonly current: ProjectCurrentOperation<E>
readonly directories: ProjectDirectoriesOperation<E>
}

View file

@ -544,25 +544,29 @@ const Endpoint11_1 = (raw: RawClient["server.credential"]) => (input: Endpoint11
const adaptGroup11 = (raw: RawClient["server.credential"]) => ({ update: Endpoint11_0(raw), remove: Endpoint11_1(raw) })
type Endpoint12_0Request = Parameters<RawClient["server.project"]["project.current"]>[0]
type Endpoint12_0Input = { readonly location?: Endpoint12_0Request["query"]["location"] }
const Endpoint12_0 = (raw: RawClient["server.project"]) => (input?: Endpoint12_0Input) =>
const Endpoint12_0 = (raw: RawClient["server.project"]) => () =>
raw["project.list"]({}).pipe(Effect.mapError(mapClientError))
type Endpoint12_1Request = Parameters<RawClient["server.project"]["project.current"]>[0]
type Endpoint12_1Input = { readonly location?: Endpoint12_1Request["query"]["location"] }
const Endpoint12_1 = (raw: RawClient["server.project"]) => (input?: Endpoint12_1Input) =>
raw["project.current"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint12_1Request = Parameters<RawClient["server.project"]["project.directories"]>[0]
type Endpoint12_1Input = {
readonly projectID: Endpoint12_1Request["params"]["projectID"]
readonly location?: Endpoint12_1Request["query"]["location"]
type Endpoint12_2Request = Parameters<RawClient["server.project"]["project.directories"]>[0]
type Endpoint12_2Input = {
readonly projectID: Endpoint12_2Request["params"]["projectID"]
readonly location?: Endpoint12_2Request["query"]["location"]
}
const Endpoint12_1 = (raw: RawClient["server.project"]) => (input: Endpoint12_1Input) =>
const Endpoint12_2 = (raw: RawClient["server.project"]) => (input: Endpoint12_2Input) =>
raw["project.directories"]({
params: { projectID: input["projectID"] },
query: { location: input["location"] },
}).pipe(Effect.mapError(mapClientError))
const adaptGroup12 = (raw: RawClient["server.project"]) => ({
current: Endpoint12_0(raw),
directories: Endpoint12_1(raw),
list: Endpoint12_0(raw),
current: Endpoint12_1(raw),
directories: Endpoint12_2(raw),
})
type Endpoint13_0Request = Parameters<RawClient["server.form"]["form.request.list"]>[0]

View file

@ -89,6 +89,7 @@ import type {
CredentialUpdateOutput,
CredentialRemoveInput,
CredentialRemoveOutput,
ProjectListOutput,
ProjectCurrentInput,
ProjectCurrentOutput,
ProjectDirectoriesInput,
@ -899,6 +900,11 @@ export function make(options: ClientOptions) {
),
},
project: {
list: (requestOptions?: RequestOptions) =>
request<ProjectListOutput>(
{ method: "GET", path: `/api/project`, successStatus: 200, declaredStatuses: [401, 400], empty: false },
requestOptions,
),
current: (input?: ProjectCurrentInput, requestOptions?: RequestOptions) =>
request<ProjectCurrentOutput>(
{

View file

@ -2458,6 +2458,17 @@ export type CredentialRemoveInput = {
export type CredentialRemoveOutput = void
export type ProjectListOutput = ReadonlyArray<{
readonly id: string
readonly worktree: string
readonly vcs?: "git" | "hg"
readonly name?: string
readonly icon?: { readonly url?: string; readonly override?: string; readonly color?: string }
readonly commands?: { readonly start?: string }
readonly time: { readonly created: number; readonly updated: number; readonly initialized?: number }
readonly sandboxes: ReadonlyArray<string>
}>
export type ProjectCurrentInput = {
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined

View file

@ -3,8 +3,10 @@ export * as Project from "./project"
import { Context, Effect, Layer, Schema } from "effect"
import { ChildProcess } from "effect/unstable/process"
import { asc, desc } from "drizzle-orm"
import path from "path"
import { AbsolutePath } from "./schema"
import { Database } from "./database/database"
import { FSUtil } from "./fs-util"
import { Git } from "./git"
import { AppProcess } from "./process"
@ -12,6 +14,7 @@ import { makeGlobalNode } from "./effect/app-node"
import { Hash } from "./util/hash"
import { ProjectDirectories } from "./project/directories"
import { ProjectSchema } from "./project/schema"
import { ProjectTable } from "./project/sql"
export const ID = ProjectSchema.ID
export type ID = ProjectSchema.ID
@ -25,9 +28,8 @@ export type Current = ProjectSchema.Current
export const Directory = ProjectSchema.Directory
export type Directory = ProjectSchema.Directory
export class Info extends Schema.Class<Info>("Project.Info")({
id: ID,
}) {}
export const Info = ProjectSchema.Info
export interface Info extends Schema.Schema.Type<typeof Info> {}
export const DirectoriesInput = ProjectSchema.DirectoriesInput
export type DirectoriesInput = typeof DirectoriesInput.Type
@ -54,6 +56,7 @@ export const root = Effect.fn("Project.root")(function* (
})
export interface Interface {
readonly list: () => Effect.Effect<ReadonlyArray<Info>>
readonly directories: (input: DirectoriesInput) => Effect.Effect<Directories>
readonly resolve: (input: AbsolutePath) => Effect.Effect<Resolved>
/**
@ -70,14 +73,50 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/ProjectV2") {}
function fromRow(row: typeof ProjectTable.$inferSelect): Info {
const icon =
row.icon_url || row.icon_url_override || row.icon_color
? {
url: row.icon_url ?? undefined,
override: row.icon_url_override ?? undefined,
color: row.icon_color ?? undefined,
}
: undefined
return {
id: row.id,
worktree: row.worktree,
vcs: row.vcs ?? undefined,
name: row.name ?? undefined,
icon,
commands: row.commands ?? undefined,
time: {
created: row.time_created,
updated: row.time_updated,
initialized: row.time_initialized ?? undefined,
},
sandboxes: row.sandboxes,
}
}
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const fs = yield* FSUtil.Service
const git = yield* Git.Service
const proc = yield* AppProcess.Service
const db = (yield* Database.Service).db
const projectDirectories = yield* ProjectDirectories.Service
const list = Effect.fn("Project.list")(function* () {
const rows = yield* db
.select()
.from(ProjectTable)
.orderBy(desc(ProjectTable.time_updated), asc(ProjectTable.id))
.all()
.pipe(Effect.orDie)
return rows.map(fromRow)
})
const directories = Effect.fn("Project.directories")(function* (input: DirectoriesInput) {
return yield* projectDirectories.list(input.projectID)
})
@ -190,12 +229,12 @@ const layer = Layer.effect(
yield* fs.writeFileString(path.join(input.store, "opencode"), input.id).pipe(Effect.ignore)
})
return Service.of({ directories, resolve, commit })
return Service.of({ list, directories, resolve, commit })
}),
)
export const node = makeGlobalNode({
service: Service,
layer: layer,
deps: [FSUtil.node, Git.node, AppProcess.node, ProjectDirectories.node],
deps: [Database.node, FSUtil.node, Git.node, AppProcess.node, ProjectDirectories.node],
})

View file

@ -13,6 +13,9 @@ export type Current = typeof Current.Type
export const Directory = Project.Directory
export type Directory = typeof Directory.Type
export const Info = Project.Info
export interface Info extends Schema.Schema.Type<typeof Info> {}
export const DirectoriesInput = Project.DirectoriesInput
export type DirectoriesInput = typeof DirectoriesInput.Type

View file

@ -6,7 +6,7 @@ import { ProjectSchema } from "./schema"
export const ProjectTable = sqliteTable("project", {
id: text().$type<ProjectSchema.ID>().primaryKey(),
worktree: absoluteColumn().notNull(),
vcs: text(),
vcs: text().$type<"git" | "hg">(),
name: text(),
icon_url: text(),
icon_url_override: text(),

View file

@ -77,6 +77,7 @@ describe("node build", () => {
Effect.sync(() => {
acquisitions++
return Project.Service.of({
list: () => Effect.succeed([]),
directories: () => Effect.succeed([]),
resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory }),
commit: () => Effect.void,

View file

@ -12,6 +12,7 @@ const ref = { directory: AbsolutePath.make("/repo/packages/app"), workspaceID }
const projectLayer = Layer.succeed(
Project.Service,
Project.Service.of({
list: () => Effect.succeed([]),
directories: () => Effect.succeed([]),
resolve: () =>
Effect.succeed({

View file

@ -2,15 +2,71 @@ import { describe, expect } from "bun:test"
import { $ } from "bun"
import fs from "fs/promises"
import path from "path"
import { Effect, Schema } from "effect"
import { Effect, Layer, Schema } from "effect"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { Database } from "@opencode-ai/core/database/database"
import { ProjectV2 } from "@opencode-ai/core/project"
import { ProjectTable } from "@opencode-ai/core/project/sql"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { Hash } from "@opencode-ai/core/util/hash"
import { tmpdir } from "./fixture/tmpdir"
import { testEffect } from "./lib/effect"
const it = testEffect(AppNodeBuilder.build(ProjectV2.node))
const it = testEffect(
Layer.merge(AppNodeBuilder.build(ProjectV2.node), AppNodeBuilder.build(Database.node)),
)
describe("ProjectV2.list", () => {
it.effect("returns complete projects ordered by recent update", () =>
Effect.gen(function* () {
const db = (yield* Database.Service).db
const project = yield* ProjectV2.Service
yield* db
.insert(ProjectTable)
.values([
{
id: ProjectV2.ID.make("older"),
worktree: abs("/older"),
vcs: "git",
name: "Older",
icon_color: "#000000",
commands: { start: "bun dev" },
sandboxes: [abs("/older/sandbox")],
time_created: 1,
time_updated: 1,
},
{
id: ProjectV2.ID.make("newer"),
worktree: abs("/newer"),
sandboxes: [],
time_created: 2,
time_updated: 2,
time_initialized: 3,
},
])
.run()
expect(yield* project.list()).toEqual([
{
id: ProjectV2.ID.make("newer"),
worktree: abs("/newer"),
time: { created: 2, updated: 2, initialized: 3 },
sandboxes: [],
},
{
id: ProjectV2.ID.make("older"),
worktree: abs("/older"),
vcs: "git",
name: "Older",
icon: { color: "#000000" },
commands: { start: "bun dev" },
time: { created: 1, updated: 1 },
sandboxes: [abs("/older/sandbox")],
},
])
}),
)
})
function remoteID(remote: string) {
return ProjectV2.ID.make(Hash.fast(`git-remote:${remote}`))

View file

@ -33,6 +33,7 @@ const model = Model.make({
const projects = Layer.succeed(
ProjectV2.Service,
ProjectV2.Service.of({
list: () => Effect.succeed([]),
resolve: (directory) => Effect.succeed({ id: ProjectV2.ID.global, directory }),
directories: () => Effect.succeed([]),
commit: () => Effect.void,

View file

@ -31,6 +31,7 @@ import { tmpdir } from "./fixture/tmpdir"
const projects = Layer.succeed(
ProjectV2.Service,
ProjectV2.Service.of({
list: () => Effect.succeed([]),
resolve: (directory) => Effect.succeed({ id: ProjectV2.ID.global, directory }),
directories: () => Effect.succeed([]),
commit: () => Effect.void,

View file

@ -55,6 +55,7 @@ const readToolNode = makeLocationNode({
const projects = Layer.succeed(
ProjectV2.Service,
ProjectV2.Service.of({
list: () => Effect.succeed([]),
resolve: (directory) => Effect.succeed({ id: ProjectV2.ID.global, directory }),
directories: () => Effect.succeed([]),
commit: () => Effect.void,

View file

@ -18,6 +18,7 @@ import { testEffect } from "./lib/effect"
const projects = Layer.succeed(
ProjectV2.Service,
ProjectV2.Service.of({
list: () => Effect.succeed([]),
resolve: (directory) => Effect.succeed({ id: ProjectV2.ID.global, directory }),
directories: () => Effect.succeed([]),
commit: () => Effect.void,

View file

@ -0,0 +1,6 @@
---
title: "API Reference"
description: "OpenCode HTTP API."
---
The endpoint reference is generated from the current OpenCode V2 [OpenAPI specification](/openapi.json).

4
packages/docs/config.mdx Normal file
View file

@ -0,0 +1,4 @@
---
title: "Config"
description: "Configure OpenCode."
---

View file

@ -17,11 +17,24 @@
"tabs": [
{
"tab": "Docs",
"pages": ["index"]
"pages": ["index", "config", "plugins", "troubleshooting"]
},
{
"tab": "SDK",
"pages": ["sdk/index"]
},
{
"tab": "API",
"groups": [
{
"group": "Overview",
"pages": ["api/index"]
},
{
"group": "Endpoints",
"openapi": "openapi.json"
}
]
}
],
"global": {}

View file

@ -98,19 +98,19 @@ Standalone binaries are not available in beta.
With OpenCode you can use any LLM provider by configuring its API key.
If you are new to LLM providers, we recommend [OpenCode Zen](https://opencode.ai/docs/zen). It's a curated list of models
that have been tested and verified by the OpenCode team.
Run `/connect` in the TUI and select your provider.
1. Run `/connect` in the TUI, select **opencode**, and open [opencode.ai/auth](https://opencode.ai/auth).
```text
/connect
```
```text
/connect
```
If you'd like easy access to all the best coding models you can try out
[OpenCode Console](https://console.opencode.ai).
2. Sign in, add your billing details, and copy your API key.
3. Paste your API key into the prompt.
You can also try [OpenCode Go](https://opencode.ai/go) a $10/month subscription
plan that grants you access to the best open source models.
Alternatively, select another provider. See the current [provider directory](https://opencode.ai/docs/providers#directory).
See the current [provider directory](https://opencode.ai/docs/providers#directory).
---
@ -130,54 +130,16 @@ How is authentication handled in @packages/functions/src/api/index.ts
### Add features
For larger features, start by asking OpenCode to create a plan.
1. **Create a plan**
Switch to **Plan mode** with `Tab`. Plan mode prevents OpenCode from making changes while it proposes an implementation.
```text
<TAB>
```
Describe the feature with enough context to understand the desired behavior.
```text
When a user deletes a note, flag it as deleted in the database.
Create a screen that shows recently deleted notes.
From this screen, the user can restore a note or permanently delete it.
```
<Tip>Give OpenCode plenty of context and examples.</Tip>
2. **Iterate on the plan**
Give feedback or add more detail after OpenCode proposes a plan.
```text
Use the attached image as a visual reference for the new screen.
```
You can drag and drop images into the terminal to add them to your prompt.
3. **Build the feature**
When the plan looks right, press `Tab` to return to **Build mode** and ask OpenCode to implement it.
```text
Sounds good. Go ahead and make the changes.
```
### Make changes
For straightforward work, ask OpenCode to make the change directly and include relevant files and examples.
Ask OpenCode to add a feature by describing the desired behavior and providing relevant context.
```text
Add authentication to the /settings route. Follow the approach used in
@packages/functions/src/notes.ts and implement it in
@packages/functions/src/settings.ts.
When a user deletes a note, flag it as deleted in the database.
Create a screen that shows recently deleted notes.
From this screen, the user can restore a note or permanently delete it.
```
<Tip>Give OpenCode plenty of context and examples.</Tip>
### Undo changes
Use `/undo` when a change isn't what you wanted.

View file

@ -1 +0,0 @@
../sdk/openapi.json

26010
packages/docs/openapi.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,4 @@
---
title: "Plugins"
description: "Extend OpenCode with plugins."
---

View file

@ -0,0 +1,177 @@
---
title: "Troubleshooting"
description: "Diagnose OpenCode startup, server, and session issues."
---
<Tip>
You can ask OpenCode to debug itself. Describe the problem and ask it to use this troubleshooting page; it can read the
steps below, inspect its service and logs, and help identify the issue.
</Tip>
OpenCode runs as two processes: the TUI is a client, while a background server owns sessions, plugins, permissions, and
other application state. Start by determining whether an issue is in the client, the shared server, or a specific project.
## Check the background service
Show the current server status:
```bash
opencode2 service status
```
Verify that its API is healthy:
```bash
opencode2 api get /api/health
```
If the service is stuck or unhealthy, restart it:
```bash
opencode2 service restart
```
From inside the TUI, run `/reload` to restart the managed service and reconnect:
```text
/reload
```
You can also stop and start it explicitly:
```bash
opencode2 service stop
opencode2 service start
```
<Note>
OpenCode normally discovers or starts the shared background service automatically. The service commands are only needed
when diagnosing its lifecycle.
</Note>
## Run an isolated session
Use standalone mode to run the TUI with a private server that exits with it:
```bash
opencode2 --standalone
```
If an issue disappears in standalone mode, it is likely related to the shared background service rather than the TUI or
project itself.
## Inspect the API
The `api` command uses the same discovery and authentication flow as the TUI. It accepts either an HTTP method and path or
an OpenAPI operation ID.
See the [API reference](/api) for all endpoints and operation IDs.
Pass a JSON request body with `--data` or `-d`, and add headers with `--header` or `-H`.
<Warning>
Running `opencode2 api` may start the background service when no compatible healthy service is available.
</Warning>
## Read logs
Installed builds write logs to:
```text
~/.local/share/opencode/log/opencode.log
```
Follow the log while reproducing the problem:
```bash
tail -f ~/.local/share/opencode/log/opencode.log
```
Each line includes a process `run` ID and a `role` field. Use `role=cli` for TUI and command startup, and `role=server` for
session, provider, plugin, permission, and tool activity.
```bash
grep 'role=cli' ~/.local/share/opencode/log/opencode.log
grep 'role=server' ~/.local/share/opencode/log/opencode.log
grep 'run=8fc3b1d5' ~/.local/share/opencode/log/opencode.log
```
Increase verbosity for one reproduction:
```bash
OPENCODE_LOG_LEVEL=DEBUG opencode2
```
## Service files
The shared server registers itself at:
```text
~/.local/state/opencode/service.json
```
Its private service configuration is stored separately at:
```text
~/.config/opencode/service.json
```
The database normally lives at:
```text
~/.local/share/opencode/opencode-next.db
```
`OPENCODE_DB` can override the database location.
<Warning>
Do not delete or edit service files or the database while troubleshooting. Use the service commands to manage the daemon,
and make a backup before inspecting persistent data with external tools.
</Warning>
## Explicit servers
When connecting with `--server`, set `OPENCODE_PASSWORD` if the server requires authentication:
```bash
OPENCODE_PASSWORD=secret opencode2 --server http://127.0.0.1:4096
```
The CLI checks the server before opening the TUI and reports whether it is unreachable, requires a password, or rejected
the supplied password.
## Report an issue
Include the following when reporting a reproducible problem:
- Output from `opencode2 --version`
- Output from `opencode2 service status`
- The smallest sequence of steps that reproduces the issue
- Whether the issue also occurs with `opencode2 --standalone`
- Relevant log lines, including their `run` and `role` fields
Remove API keys, authorization headers, prompts, file contents, and other sensitive data before sharing logs.
## Local development
When working from the OpenCode repository, run V2 commands from the repository root:
```bash
bun dev
```
The local development channel keeps its logs, SQLite database, and service registration separate from installed builds:
```text
~/.local/share/opencode/log/opencode-local.log
~/.local/share/opencode/opencode-local.db
~/.local/state/opencode/service-local.json
```
Use the same diagnostics through the package development command:
```bash
bun dev service status
bun dev service restart
bun dev api get /api/health
```

View file

@ -4,17 +4,19 @@ import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { LocationQuery, locationQueryOpenApi } from "./location.js"
export const AgentGroup = HttpApiGroup.make("server.agent").add(
HttpApiEndpoint.get("agent.list", "/api/agent", {
query: LocationQuery,
success: Location.response(Schema.Array(Agent.Info)),
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.agent.list",
summary: "List agents",
description: "Retrieve currently registered agents.",
}),
),
)
export const AgentGroup = HttpApiGroup.make("server.agent")
.add(
HttpApiEndpoint.get("agent.list", "/api/agent", {
query: LocationQuery,
success: Location.response(Schema.Array(Agent.Info)),
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.agent.list",
summary: "List agents",
description: "Retrieve currently registered agents.",
}),
),
)
.annotateMerge(OpenApi.annotations({ title: "agent" }))

View file

@ -21,7 +21,7 @@ export const CommandGroup = HttpApiGroup.make("server.command")
)
.annotateMerge(
OpenApi.annotations({
title: "commands",
title: "command",
description: "Experimental command routes.",
}),
)

View file

@ -20,6 +20,7 @@ export const CredentialGroup = HttpApiGroup.make("server.credential")
}),
),
)
.annotateMerge(OpenApi.annotations({ title: "credential" }))
.add(
HttpApiEndpoint.delete("credential.remove", "/api/credential/:credentialID", {
params: { credentialID: Credential.ID },

View file

@ -2,14 +2,16 @@ import { Location } from "@opencode-ai/schema/location"
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
export const DebugGroup = HttpApiGroup.make("server.debug").add(
HttpApiEndpoint.get("debug.location", "/api/debug/location", {
success: Schema.Array(Location.Ref),
}).annotateMerge(
OpenApi.annotations({
identifier: "v2.debug.location",
summary: "List loaded locations",
description: "List locations currently loaded by the server.",
}),
),
)
export const DebugGroup = HttpApiGroup.make("server.debug")
.add(
HttpApiEndpoint.get("debug.location", "/api/debug/location", {
success: Schema.Array(Location.Ref),
}).annotateMerge(
OpenApi.annotations({
identifier: "v2.debug.location",
summary: "List loaded locations",
description: "List locations currently loaded by the server.",
}),
),
)
.annotateMerge(OpenApi.annotations({ title: "debug" }))

View file

@ -55,7 +55,7 @@ const make = <const Definitions extends ReadonlyArray<Definition>>(definitions:
}),
),
)
.annotateMerge(OpenApi.annotations({ title: "events", description: "Experimental event stream routes." })),
.annotateMerge(OpenApi.annotations({ title: "event", description: "Experimental event stream routes." })),
}
}

View file

@ -144,4 +144,4 @@ export const makeFormGroup = <
}),
),
)
.annotateMerge(OpenApi.annotations({ title: "forms", description: "Session form routes." }))
.annotateMerge(OpenApi.annotations({ title: "form", description: "Session form routes." }))

View file

@ -1,14 +1,16 @@
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
export const HealthGroup = HttpApiGroup.make("server.health").add(
HttpApiEndpoint.get("health.get", "/api/health", {
success: Schema.Struct({ healthy: Schema.Literal(true) }),
}).annotateMerge(
OpenApi.annotations({
identifier: "v2.health.get",
summary: "Check server health",
description: "Check whether the API server is ready to accept requests.",
}),
),
)
export const HealthGroup = HttpApiGroup.make("server.health")
.add(
HttpApiEndpoint.get("health.get", "/api/health", {
success: Schema.Struct({ healthy: Schema.Literal(true) }),
}).annotateMerge(
OpenApi.annotations({
identifier: "v2.health.get",
summary: "Check server health",
description: "Check whether the API server is ready to accept requests.",
}),
),
)
.annotateMerge(OpenApi.annotations({ title: "health" }))

View file

@ -126,5 +126,5 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration")
),
)
.annotateMerge(
OpenApi.annotations({ title: "integrations", description: "Integration discovery and authentication routes." }),
OpenApi.annotations({ title: "integration", description: "Integration discovery and authentication routes." }),
)

View file

@ -26,17 +26,19 @@ export const locationQueryOpenApi = OpenApi.annotations({
},
})
export const LocationGroup = HttpApiGroup.make("server.location").add(
HttpApiEndpoint.get("location.get", "/api/location", {
query: LocationQuery,
success: Location.Info,
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.location.get",
summary: "Get location",
description: "Resolve the requested location or the server default location.",
}),
),
)
export const LocationGroup = HttpApiGroup.make("server.location")
.add(
HttpApiEndpoint.get("location.get", "/api/location", {
query: LocationQuery,
success: Location.Info,
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.location.get",
summary: "Get location",
description: "Resolve the requested location or the server default location.",
}),
),
)
.annotateMerge(OpenApi.annotations({ title: "location" }))

View file

@ -51,7 +51,7 @@ export const MessageGroup = HttpApiGroup.make("server.message")
)
.annotateMerge(
OpenApi.annotations({
title: "messages",
title: "session",
description: "Experimental message routes.",
}),
)

View file

@ -38,7 +38,7 @@ export const ModelGroup = HttpApiGroup.make("server.model")
)
.annotateMerge(
OpenApi.annotations({
title: "models",
title: "model",
description: "Experimental model routes.",
}),
)

View file

@ -134,4 +134,4 @@ export const makePermissionGroup = <
}),
),
)
.annotateMerge(OpenApi.annotations({ title: "permissions", description: "Experimental permission routes." }))
.annotateMerge(OpenApi.annotations({ title: "permission", description: "Experimental permission routes." }))

View file

@ -21,7 +21,7 @@ export const PluginGroup = HttpApiGroup.make("server.plugin")
)
.annotateMerge(
OpenApi.annotations({
title: "plugins",
title: "plugin",
description: "Experimental plugin routes.",
}),
)

View file

@ -1,10 +1,22 @@
import { Project } from "@opencode-ai/schema/project"
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { LocationQuery, locationQueryOpenApi } from "./location.js"
const root = "/api/project"
export const ProjectGroup = HttpApiGroup.make("server.project")
.add(
HttpApiEndpoint.get("project.list", root, {
success: Schema.Array(Project.Info),
}).annotateMerge(
OpenApi.annotations({
identifier: "v2.project.list",
summary: "List projects",
description: "List known projects.",
}),
),
)
.add(
HttpApiEndpoint.get("project.current", `${root}/current`, {
query: LocationQuery,
@ -36,7 +48,7 @@ export const ProjectGroup = HttpApiGroup.make("server.project")
)
.annotateMerge(
OpenApi.annotations({
title: "projects",
title: "project",
description: "Location-scoped project routes.",
}),
)

View file

@ -39,7 +39,7 @@ export const ProviderGroup = HttpApiGroup.make("server.provider")
)
.annotateMerge(
OpenApi.annotations({
title: "providers",
title: "provider",
description: "Experimental provider routes.",
}),
)

View file

@ -30,7 +30,7 @@ export const makeQuestionGroup = <
}),
),
)
.annotateMerge(OpenApi.annotations({ title: "questions", description: "Experimental question routes." }))
.annotateMerge(OpenApi.annotations({ title: "question", description: "Experimental question routes." }))
// Effect applies group middleware only to endpoints already added; session endpoints use session placement below.
.middleware(locationMiddleware)
.add(
@ -80,5 +80,5 @@ export const makeQuestionGroup = <
),
)
.annotateMerge(
OpenApi.annotations({ title: "session questions", description: "Experimental session question routes." }),
OpenApi.annotations({ title: "question", description: "Experimental session question routes." }),
)

View file

@ -567,7 +567,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
)
.annotateMerge(
OpenApi.annotations({
title: "sessions",
title: "session",
description: "Experimental session routes.",
}),
)

View file

@ -21,7 +21,7 @@ export const SkillGroup = HttpApiGroup.make("server.skill")
)
.annotateMerge(
OpenApi.annotations({
title: "skills",
title: "skill",
description: "Experimental skill routes.",
}),
)

View file

@ -6,6 +6,7 @@ import { Api } from "../api"
export const ProjectHandler = HttpApiBuilder.group(Api, "server.project", (handlers) =>
handlers
.handle("project.list", () => Project.Service.use((project) => project.list()))
.handle("project.current", () =>
Location.Service.use((location) =>
Effect.succeed({ id: location.project.id, directory: location.project.directory }),