mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-10 03:18:31 +00:00
feat(api): publish v2 reference
This commit is contained in:
parent
e6e951b252
commit
eed23d8ee9
40 changed files with 26473 additions and 147 deletions
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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>(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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}`))
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
6
packages/docs/api/index.mdx
Normal file
6
packages/docs/api/index.mdx
Normal 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
4
packages/docs/config.mdx
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: "Config"
|
||||
description: "Configure OpenCode."
|
||||
---
|
||||
|
|
@ -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": {}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
../sdk/openapi.json
|
||||
26010
packages/docs/openapi.json
Normal file
26010
packages/docs/openapi.json
Normal file
File diff suppressed because it is too large
Load diff
4
packages/docs/plugins.mdx
Normal file
4
packages/docs/plugins.mdx
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: "Plugins"
|
||||
description: "Extend OpenCode with plugins."
|
||||
---
|
||||
177
packages/docs/troubleshooting.mdx
Normal file
177
packages/docs/troubleshooting.mdx
Normal 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
|
||||
```
|
||||
|
|
@ -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" }))
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export const CommandGroup = HttpApiGroup.make("server.command")
|
|||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "commands",
|
||||
title: "command",
|
||||
description: "Experimental command routes.",
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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" }))
|
||||
|
|
|
|||
|
|
@ -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." })),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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." }))
|
||||
|
|
|
|||
|
|
@ -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" }))
|
||||
|
|
|
|||
|
|
@ -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." }),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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" }))
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export const MessageGroup = HttpApiGroup.make("server.message")
|
|||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "messages",
|
||||
title: "session",
|
||||
description: "Experimental message routes.",
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export const ModelGroup = HttpApiGroup.make("server.model")
|
|||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "models",
|
||||
title: "model",
|
||||
description: "Experimental model routes.",
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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." }))
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export const PluginGroup = HttpApiGroup.make("server.plugin")
|
|||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "plugins",
|
||||
title: "plugin",
|
||||
description: "Experimental plugin routes.",
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export const ProviderGroup = HttpApiGroup.make("server.provider")
|
|||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "providers",
|
||||
title: "provider",
|
||||
description: "Experimental provider routes.",
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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." }),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -567,7 +567,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
|||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "sessions",
|
||||
title: "session",
|
||||
description: "Experimental session routes.",
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export const SkillGroup = HttpApiGroup.make("server.skill")
|
|||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "skills",
|
||||
title: "skill",
|
||||
description: "Experimental skill routes.",
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 }),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue