| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734 |
- import { Log } from "../util/log"
- import { Bus } from "../bus"
- import { describeRoute, generateSpecs, openAPISpecs } from "hono-openapi"
- import { Hono } from "hono"
- import { streamSSE } from "hono/streaming"
- import { Session } from "../session"
- import { resolver, validator as zValidator } from "hono-openapi/zod"
- import { z } from "zod"
- import { Provider } from "../provider/provider"
- import { App } from "../app/app"
- import { mapValues } from "remeda"
- import { NamedError } from "../util/error"
- import { ModelsDev } from "../provider/models"
- import { Ripgrep } from "../file/ripgrep"
- import { Config } from "../config/config"
- import { File } from "../file"
- import { LSP } from "../lsp"
- import { MessageV2 } from "../session/message-v2"
- import { Mode } from "../session/mode"
- const ERRORS = {
- 400: {
- description: "Bad request",
- content: {
- "application/json": {
- schema: resolver(
- z
- .object({
- data: z.record(z.string(), z.any()),
- })
- .openapi({
- ref: "Error",
- }),
- ),
- },
- },
- },
- } as const
- export namespace Server {
- const log = Log.create({ service: "server" })
- export type Routes = ReturnType<typeof app>
- function app() {
- const app = new Hono()
- const result = app
- .onError((err, c) => {
- if (err instanceof NamedError) {
- return c.json(err.toObject(), {
- status: 400,
- })
- }
- return c.json(new NamedError.Unknown({ message: err.toString() }).toObject(), {
- status: 400,
- })
- })
- .use(async (c, next) => {
- log.info("request", {
- method: c.req.method,
- path: c.req.path,
- })
- const start = Date.now()
- await next()
- log.info("response", {
- duration: Date.now() - start,
- })
- })
- .get(
- "/doc",
- openAPISpecs(app, {
- documentation: {
- info: {
- title: "opencode",
- version: "0.0.3",
- description: "opencode api",
- },
- openapi: "3.0.0",
- },
- }),
- )
- .get(
- "/event",
- describeRoute({
- description: "Get events",
- responses: {
- 200: {
- description: "Event stream",
- content: {
- "application/json": {
- schema: resolver(
- Bus.payloads().openapi({
- ref: "Event",
- }),
- ),
- },
- },
- },
- },
- }),
- async (c) => {
- log.info("event connected")
- return streamSSE(c, async (stream) => {
- stream.writeSSE({
- data: JSON.stringify({}),
- })
- const unsub = Bus.subscribeAll(async (event) => {
- await stream.writeSSE({
- data: JSON.stringify(event),
- })
- })
- await new Promise<void>((resolve) => {
- stream.onAbort(() => {
- unsub()
- resolve()
- log.info("event disconnected")
- })
- })
- })
- },
- )
- .get(
- "/app",
- describeRoute({
- description: "Get app info",
- responses: {
- 200: {
- description: "200",
- content: {
- "application/json": {
- schema: resolver(App.Info),
- },
- },
- },
- },
- }),
- async (c) => {
- return c.json(App.info())
- },
- )
- .post(
- "/app/init",
- describeRoute({
- description: "Initialize the app",
- responses: {
- 200: {
- description: "Initialize the app",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- },
- }),
- async (c) => {
- await App.initialize()
- return c.json(true)
- },
- )
- .get(
- "/config",
- describeRoute({
- description: "Get config info",
- responses: {
- 200: {
- description: "Get config info",
- content: {
- "application/json": {
- schema: resolver(Config.Info),
- },
- },
- },
- },
- }),
- async (c) => {
- return c.json(await Config.get())
- },
- )
- .get(
- "/session",
- describeRoute({
- description: "List all sessions",
- responses: {
- 200: {
- description: "List of sessions",
- content: {
- "application/json": {
- schema: resolver(Session.Info.array()),
- },
- },
- },
- },
- }),
- async (c) => {
- const sessions = await Array.fromAsync(Session.list())
- return c.json(sessions)
- },
- )
- .post(
- "/session",
- describeRoute({
- description: "Create a new session",
- responses: {
- ...ERRORS,
- 200: {
- description: "Successfully created session",
- content: {
- "application/json": {
- schema: resolver(Session.Info),
- },
- },
- },
- },
- }),
- async (c) => {
- const session = await Session.create()
- return c.json(session)
- },
- )
- .delete(
- "/session/:id",
- describeRoute({
- description: "Delete a session and all its data",
- responses: {
- 200: {
- description: "Successfully deleted session",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- },
- }),
- zValidator(
- "param",
- z.object({
- id: z.string(),
- }),
- ),
- async (c) => {
- await Session.remove(c.req.valid("param").id)
- return c.json(true)
- },
- )
- .post(
- "/session/:id/init",
- describeRoute({
- description: "Analyze the app and create an AGENTS.md file",
- responses: {
- 200: {
- description: "200",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- },
- }),
- zValidator(
- "param",
- z.object({
- id: z.string().openapi({ description: "Session ID" }),
- }),
- ),
- zValidator(
- "json",
- z.object({
- providerID: z.string(),
- modelID: z.string(),
- }),
- ),
- async (c) => {
- const sessionID = c.req.valid("param").id
- const body = c.req.valid("json")
- await Session.initialize({ ...body, sessionID })
- return c.json(true)
- },
- )
- .post(
- "/session/:id/abort",
- describeRoute({
- description: "Abort a session",
- responses: {
- 200: {
- description: "Aborted session",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- },
- }),
- zValidator(
- "param",
- z.object({
- id: z.string(),
- }),
- ),
- async (c) => {
- return c.json(Session.abort(c.req.valid("param").id))
- },
- )
- .post(
- "/session/:id/share",
- describeRoute({
- description: "Share a session",
- responses: {
- 200: {
- description: "Successfully shared session",
- content: {
- "application/json": {
- schema: resolver(Session.Info),
- },
- },
- },
- },
- }),
- zValidator(
- "param",
- z.object({
- id: z.string(),
- }),
- ),
- async (c) => {
- const id = c.req.valid("param").id
- await Session.share(id)
- const session = await Session.get(id)
- return c.json(session)
- },
- )
- .delete(
- "/session/:id/share",
- describeRoute({
- description: "Unshare the session",
- responses: {
- 200: {
- description: "Successfully unshared session",
- content: {
- "application/json": {
- schema: resolver(Session.Info),
- },
- },
- },
- },
- }),
- zValidator(
- "param",
- z.object({
- id: z.string(),
- }),
- ),
- async (c) => {
- const id = c.req.valid("param").id
- await Session.unshare(id)
- const session = await Session.get(id)
- return c.json(session)
- },
- )
- .post(
- "/session/:id/summarize",
- describeRoute({
- description: "Summarize the session",
- responses: {
- 200: {
- description: "Summarized session",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- },
- }),
- zValidator(
- "param",
- z.object({
- id: z.string().openapi({ description: "Session ID" }),
- }),
- ),
- zValidator(
- "json",
- z.object({
- providerID: z.string(),
- modelID: z.string(),
- }),
- ),
- async (c) => {
- const id = c.req.valid("param").id
- const body = c.req.valid("json")
- await Session.summarize({ ...body, sessionID: id })
- return c.json(true)
- },
- )
- .get(
- "/session/:id/message",
- describeRoute({
- description: "List messages for a session",
- responses: {
- 200: {
- description: "List of messages",
- content: {
- "application/json": {
- schema: resolver(MessageV2.Info.array()),
- },
- },
- },
- },
- }),
- zValidator(
- "param",
- z.object({
- id: z.string().openapi({ description: "Session ID" }),
- }),
- ),
- async (c) => {
- const messages = await Session.messages(c.req.valid("param").id)
- return c.json(messages)
- },
- )
- .post(
- "/session/:id/message",
- describeRoute({
- description: "Create and send a new message to a session",
- responses: {
- 200: {
- description: "Created message",
- content: {
- "application/json": {
- schema: resolver(MessageV2.Assistant),
- },
- },
- },
- },
- }),
- zValidator(
- "param",
- z.object({
- id: z.string().openapi({ description: "Session ID" }),
- }),
- ),
- zValidator(
- "json",
- z.object({
- providerID: z.string(),
- modelID: z.string(),
- mode: z.string(),
- parts: MessageV2.UserPart.array(),
- }),
- ),
- async (c) => {
- const sessionID = c.req.valid("param").id
- const body = c.req.valid("json")
- const msg = await Session.chat({ ...body, sessionID })
- return c.json(msg)
- },
- )
- .get(
- "/config/providers",
- describeRoute({
- description: "List all providers",
- responses: {
- 200: {
- description: "List of providers",
- content: {
- "application/json": {
- schema: resolver(
- z.object({
- providers: ModelsDev.Provider.array(),
- default: z.record(z.string(), z.string()),
- }),
- ),
- },
- },
- },
- },
- }),
- async (c) => {
- const providers = await Provider.list().then((x) => mapValues(x, (item) => item.info))
- return c.json({
- providers: Object.values(providers),
- default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
- })
- },
- )
- .get(
- "/find",
- describeRoute({
- description: "Find text in files",
- responses: {
- 200: {
- description: "Matches",
- content: {
- "application/json": {
- schema: resolver(Ripgrep.Match.shape.data.array()),
- },
- },
- },
- },
- }),
- zValidator(
- "query",
- z.object({
- pattern: z.string(),
- }),
- ),
- async (c) => {
- const app = App.info()
- const pattern = c.req.valid("query").pattern
- const result = await Ripgrep.search({
- cwd: app.path.cwd,
- pattern,
- limit: 10,
- })
- return c.json(result)
- },
- )
- .get(
- "/find/file",
- describeRoute({
- description: "Find files",
- responses: {
- 200: {
- description: "File paths",
- content: {
- "application/json": {
- schema: resolver(z.string().array()),
- },
- },
- },
- },
- }),
- zValidator(
- "query",
- z.object({
- query: z.string(),
- }),
- ),
- async (c) => {
- const query = c.req.valid("query").query
- const app = App.info()
- const result = await Ripgrep.files({
- cwd: app.path.cwd,
- query,
- limit: 10,
- })
- return c.json(result)
- },
- )
- .get(
- "/find/symbol",
- describeRoute({
- description: "Find workspace symbols",
- responses: {
- 200: {
- description: "Symbols",
- content: {
- "application/json": {
- schema: resolver(LSP.Symbol.array()),
- },
- },
- },
- },
- }),
- zValidator(
- "query",
- z.object({
- query: z.string(),
- }),
- ),
- async (c) => {
- const query = c.req.valid("query").query
- const result = await LSP.workspaceSymbol(query)
- return c.json(result)
- },
- )
- .get(
- "/file",
- describeRoute({
- description: "Read a file",
- responses: {
- 200: {
- description: "File content",
- content: {
- "application/json": {
- schema: resolver(
- z.object({
- type: z.enum(["raw", "patch"]),
- content: z.string(),
- }),
- ),
- },
- },
- },
- },
- }),
- zValidator(
- "query",
- z.object({
- path: z.string(),
- }),
- ),
- async (c) => {
- const path = c.req.valid("query").path
- const content = await File.read(path)
- log.info("read file", {
- path,
- content: content.content,
- })
- return c.json(content)
- },
- )
- .get(
- "/file/status",
- describeRoute({
- description: "Get file status",
- responses: {
- 200: {
- description: "File status",
- content: {
- "application/json": {
- schema: resolver(File.Info.array()),
- },
- },
- },
- },
- }),
- async (c) => {
- const content = await File.status()
- return c.json(content)
- },
- )
- .post(
- "/log",
- describeRoute({
- description: "Write a log entry to the server logs",
- responses: {
- 200: {
- description: "Log entry written successfully",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- },
- }),
- zValidator(
- "json",
- z.object({
- service: z.string().openapi({ description: "Service name for the log entry" }),
- level: z.enum(["debug", "info", "error", "warn"]).openapi({ description: "Log level" }),
- message: z.string().openapi({ description: "Log message" }),
- extra: z
- .record(z.string(), z.any())
- .optional()
- .openapi({ description: "Additional metadata for the log entry" }),
- }),
- ),
- async (c) => {
- const { service, level, message, extra } = c.req.valid("json")
- const logger = Log.create({ service })
- switch (level) {
- case "debug":
- logger.debug(message, extra)
- break
- case "info":
- logger.info(message, extra)
- break
- case "error":
- logger.error(message, extra)
- break
- case "warn":
- logger.warn(message, extra)
- break
- }
- return c.json(true)
- },
- )
- .get(
- "/mode",
- describeRoute({
- description: "List all modes",
- responses: {
- 200: {
- description: "List of modes",
- content: {
- "application/json": {
- schema: resolver(Mode.Info.array()),
- },
- },
- },
- },
- }),
- async (c) => {
- const modes = await Mode.list()
- return c.json(modes)
- },
- )
- return result
- }
- export async function openapi() {
- const a = app()
- const result = await generateSpecs(a, {
- documentation: {
- info: {
- title: "opencode",
- version: "1.0.0",
- description: "opencode api",
- },
- openapi: "3.0.0",
- },
- })
- return result
- }
- export function listen(opts: { port: number; hostname: string }) {
- const server = Bun.serve({
- port: opts.port,
- hostname: opts.hostname,
- idleTimeout: 0,
- fetch: app().fetch,
- })
- return server
- }
- }
|