For AI agents: a documentation index is available at https://www.plungeai.com/docs/llms.txt. Append .md to any page URL, or send Accept: text/markdown, to get markdown. Setup instructions for agents are at https://www.plungeai.com/docs/agents.md. One ozk_ key opens every plane, models included.

Documentation Index: fetch the complete documentation index at /llms.txt. Use this file to discover all available pages before exploring further.

oceancode serve runs the engine as a long-lived HTTP server, by default on http://127.0.0.1:4096 (--port and --host change it; the default pager shape takes --listen <host:port>). Every route below is relative to that base URL. HTTP basic auth is off until OC_SERVER_PASSWORD is set; then it covers every route, SSE included, with the username from OC_SERVER_USERNAME (default oceancode). A non-loopback bind refuses to start without the password. Events stream over SSE: /event serves the v1 frames, /api/event and /global/event the v2 payload-wrapped envelope. Flags and variables: Headless and server.

curl -u "oceancode:$OC_SERVER_PASSWORD" http://127.0.0.1:4096/api/health

v2 — /api

MethodPathOperationWhat it does
GET/api/agentv2.agent.listbuilt-in + config agents.
GET/api/commandv2.command.list—
PATCH/api/credential/{credentialID}v2.credential.update(relabel). The contract declares 204.
DELETE/api/credential/{credentialID}v2.credential.removeThe contract declares 204.
GET/api/event—The v2 GlobalEvent stream shared by GET /api/event (v2.event.subscribe) and GET /global/event (global.event).
GET/api/fs/findv2.fs.findranked recursive search.
GET/api/fs/listv2.fs.listthe direct children of a directory.
GET/api/fs/read/{*path}v2.fs.readThe wildcard path is the file to read; the body is the raw bytes under the detected mime.
GET/api/healthv2.health.get—
GET/api/integrationv2.integration.list—
GET/api/integration/attempt/{attemptID}v2.integration.attempt.statusAn unknown attempt is a 404 NotFoundError.
DELETE/api/integration/attempt/{attemptID}v2.integration.attempt.cancelThe contract declares 204.
POST/api/integration/attempt/{attemptID}/completev2.integration.attempt.completeThe contract declares 204.
GET/api/integration/{integrationID}v2.integration.get—
POST/api/integration/{integrationID}/connect/keyv2.integration.connect.keyThe contract declares 204.
POST/api/integration/{integrationID}/connect/oauthv2.integration.connect.oauth—
GET/api/locationv2.location.getThe LocationInfo schema requires a resolved {directory, project} (project carries id + directory), so it is built via the shared helper rather than a bare {directory}.
GET/api/modelv2.model.list—
GET/api/permission/request—the pending requests wrapped in {location, data}.
GET/api/permission/saved—the always-allow rules as PermissionSavedInfos wrapped in {data}.
DELETE/api/permission/saved/{id}—drops a saved rule addressed by the composite "<permission>:<pattern>" id.
GET/api/providerv2.provider.list—
GET/api/provider/{providerID}v2.provider.get—
GET/api/ptyv2.pty.list—
POST/api/ptyv2.pty.create—
GET/api/pty/{ptyID}v2.pty.get—
PUT/api/pty/{ptyID}v2.pty.update—
DELETE/api/pty/{ptyID}v2.pty.removeThe v2 contract is 204.
GET/api/pty/{ptyID}/connectv2.pty.connect(WebSocket upgrade).
POST/api/pty/{ptyID}/connect-tokenv2.pty.connectToken—
GET/api/question/request—the {location, data} envelope over the request list.
GET/api/referencev2.reference.list—
GET/api/sessionv2.session.list—
POST/api/sessionv2.session.create—
GET/api/session/activev2.session.activeThe sessions with an in-flight coordinator drain, keyed by id.
GET/api/session/{sessionID}v2.session.get—
POST/api/session/{sessionID}/agentv2.session.switchAgentPublishes the durable agent-switched event the projector already handles.
POST/api/session/{sessionID}/compactv2.session.compactTriggers a compaction turn via Runner::compact (the same summarize-then-replace path the runner's own overflow recovery uses). 204 on success; errors map through the shared session-error writer.
GET/api/session/{sessionID}/contextv2.session.contextThe active post-compaction context window, wrapped in {data:[…]}.
POST/api/session/{sessionID}/interruptv2.session.interruptthe v2 alias of the v1 session.abort semantics.
GET/api/session/{sessionID}/messagev2.session.messages—
GET/api/session/{sessionID}/message/{messageID}v2.session.message—
POST/api/session/{sessionID}/modelv2.session.switchModel—
GET/api/session/{sessionID}/permission—the pending requests scoped to one session.
POST/api/session/{sessionID}/permission—evaluates the requested action/resources against the session's ruleset synchronously (no blocking wait) and returns {id, effect}, mirroring the v2 contract's non-interactive evaluation shape.
GET/api/session/{sessionID}/permission/{requestID}—a single pending request.
POST/api/session/{sessionID}/permission/{requestID}/reply——
GET/api/session/{sessionID}/permission/mode—the session's mode + lockout.
POST/api/session/{sessionID}/permission/mode—sets the mode from { "mode": "acceptEdits" }; 400 on an unknown mode, 409 when bypass is locked.
POST/api/session/{sessionID}/promptv2.session.promptAdmits durable user input (the session.next.prompted event) then drives the coordinator to run the turn. The drain runs Runner::run, which streams the provider turn and projects the assistant message.
GET/api/session/{sessionID}/question——
POST/api/session/{sessionID}/question/{requestID}/reject—TS validates the path params BEFORE the handler resolves the session, so a malformed requestID is a 400 even for a session that does not exist.
POST/api/session/{sessionID}/question/{requestID}/reply——
POST/api/session/{sessionID}/revert/clear—the v2 alias of Store::unrevert.
POST/api/session/{sessionID}/revert/commit—The ported store has no durable "committed" state distinct from "staged" (unlike upstream's stage/commit split, which also touches the working tree), so commit is a no-op success once a bookmark is confirmed to exist — staging already persisted the revert.
POST/api/session/{sessionID}/revert/stage—Faithful to the v1 revert semantics (Store::revert stages a bookmark at messageID); files (a diff/file-list request) is accepted but the ported RevertState carries no diff/files projection, so those fields are omitted rather than fabricated.
POST/api/session/{sessionID}/waitv2.session.waitTS's V2Session.wait is a stub: after confirming the session exists it unconditionally fails with Session.OperationUnavailableError, mapped to a 503 ServiceUnavailableError. Match the status and message exactly so a client cannot tell the servers apart — the existence check still produces 404 for an unknown id first.
POST/api/session/{sessionID}/subagent/{childID}/message—Steers (delivery:"steer") or queues ("queue", the default) text into the child's live turn, or resumes it when it has settled. Returns the delivery status. A missing child is a 404.
POST/api/session/{sessionID}/subagent/{childID}/resume—Re-enters a completed child with text on a fresh step budget and returns its new result. A missing child is a 404.
GET/api/skillv2.skill.list—

v1 — auth / catalog / config

MethodPathOperationWhat it does
GET/agentapp.agentsthe v1 shape.
PUT/auth/{providerID}auth.setpersist a credential to auth.json.
DELETE/auth/{providerID}auth.removedrop the stored credential.
GET/commandcommand.listthe v1 shape.
GET/configconfig.getthe merged config.
PATCH/configconfig.updateDeep-merges the body over the project config document and persists it. The merged value is written back into Services so the cached read path (config.get, formatter.status, agent/command discovery) reflects the change. TS ConfigHttpApi.update returns ctx.payload — the SUBMITTED patch, not the merged config — so the request body is echoed.
GET/config/providersconfig.providersTS config.providers returns providerSvc.list() — the RESOLVED, connected providers (those with a usable credential via auth.json, a stored DB credential, or a declared env key) — plus a per-provider default-model map. It is NOT the bare oceancode.json provider override block: the TUI's connect gate iterates this list, and an override-only answer left the gate permanently false.
GET/eventevent.subscribethe v1 SSE stream ({id,type,properties} frames). Mirrors EventSubscribe.

experimental

MethodPathOperationWhat it does
GET/experimental/capabilitiesexperimental.capabilities.getMirrors experimental.ts capabilities(): {backgroundSubagents: …}: on unless OC_EXPERIMENTAL_BACKGROUND_SUBAGENTS is 0/false — the same rule the task tool's background mode reads.
GET/experimental/consoleexperimental.console.getMirrors experimental.ts getConsole(): {consoleManagedProviders, activeOrgName?, switchableOrgCount}. With no cloud accounts registered account.orgsByAccount() returns [] (account.ts:329 over an empty repo.list()), so switchableOrgCount is 0; consoleManagedProviders is [] (config.ts:589); activeOrgName is omitted. No 500 — the empty result is a success.
GET/experimental/console/orgsexperimental.console.listOrgsMirrors experimental.ts listConsoleOrgs(): {orgs: [...]}. With no accounts, orgsByAccount() is [] so the flattened list is empty (experimental.ts:56).
POST/experimental/console/switchexperimental.console.switchOrgMirrors experimental.ts switchConsole(): account.use(accountID, orgID) wrapped in a catch that maps any failure to a 400 BadRequest (experimental.ts:84). With no account registered use() fails, so this is the faithful 400.
POST/experimental/control-plane/move-sessionexperimental.controlPlane.moveSessionMirrors control-plane.ts moveSession(): 204 on success; failures map to MoveSessionError. MoveSession first loads the session (move-session.ts:77) — a missing one yields "Session not found: {id}" (control-plane.ts:33). The move itself is a local operation and is a no-op success when the destination is the session's current directory (move-session.ts:79).
POST/experimental/project/{projectID}/copyv2.projectCopy.create—
DELETE/experimental/project/{projectID}/copyv2.projectCopy.removeThe contract declares 204.
POST/experimental/project/{projectID}/copy/generate-nameexperimental.projectCopy.generateNameThe LLM naming path is not ported; a deterministic slug is returned, matching the {name} shape.
POST/experimental/project/{projectID}/copy/refreshv2.projectCopy.refreshThe contract declares 204.
GET/experimental/resourceexperimental.resource.listMirrors experimental.ts resource(): mcp.resources() -> Record<id, Resource>. The MCP manager bridges tools but does not enumerate resources, so a fresh instance returns the empty map (experimental.ts:169).
GET/experimental/sessionexperimental.session.listMirrors experimental.ts session(): sessions.listGlobal(...) -> GlobalSession[]. The global cross-project aggregate (slug/summary/cost/tokens/project) is not ported; a fresh self-hosted instance's listGlobal yields [] (experimental.ts:134).
POST/experimental/session/{sessionID}/backgroundexperimental.session.backgroundMirrors experimental.ts sessionBackground(): returns false immediately when the experimentalBackgroundSubagents runtime flag is off (experimental.ts:156). That flag is off by default.
GET/experimental/tooltool.listevery registered tool's id/description/parameters.
GET/experimental/tool/idstool.ids—
GET/experimental/workspaceexperimental.workspace.list—
POST/experimental/workspaceexperimental.workspace.create—
GET/experimental/workspace/adapterexperimental.workspace.adapter.listMirrors workspace.ts adapters() -> listAdapters(projectID) (workspace.ts:16). The only built-in is the worktree adapter (control-plane/adapters/index.ts BUILTIN), reported with its name/description (adapters/worktree.ts:29-30).
GET/experimental/workspace/statusexperimental.workspace.statusMirrors workspace.ts status(): [...connections.values()] filtered to the project's workspaces (workspace.ts:55, 833). With no active sync connections the map is empty.
POST/experimental/workspace/sync-listexperimental.workspace.syncListMirrors workspace.ts syncList(): discover workspaces via the registered adapters and register any new ones, no body, 204 (workspace.ts:51, 743). The built-in worktree adapter discovers nothing without configured remotes, so this is a no-op 204.
POST/experimental/workspace/warpexperimental.workspace.warpMirrors workspace.ts warp(): workspace.sessionWarp(...) where a missing workspace maps to a 404 NotFoundError (workspace.ts:81). With no synced workspaces the target is always absent.
DELETE/experimental/workspace/{id}experimental.workspace.remove—
GET/experimental/worktreeworktree.listthe copy worktree directories.
POST/experimental/worktreeworktree.createa linked worktree on a new branch under the repo of the request directory.
DELETE/experimental/worktreeworktree.removeINTENTIONAL, SAFER DIVERGENCE FROM TS, carried verbatim from Go: git worktree remove refuses to remove the repository's MAIN worktree and that refusal surfaces as a 400. The TS handler is permissive and reports 200 for the same request because its remove path does not guard the main worktree. The guard is kept — matching TS would mean performing (or claiming) a destructive removal of the primary checkout. The non-main path is identical (200) on all three servers.
POST/experimental/worktree/resetworktree.resethard-reset a linked worktree (the body's directory) back to the default branch of the repo identified by the request directory.
MethodPathOperationWhat it does
GET/filefile.listdirect children as v1 FileNodes.
GET/file/contentfile.readThe TS v1 /file/content returns only {type, content} (no mimeType) and trims the decoded text; both are mirrored so the envelope matches byte-for-byte.
GET/file/statusfile.statusthe working-tree git status mapped onto the v1 File shape.
GET/findfind.textripgrep-style file-content search rooted at the request directory. pattern is the regexp; the results are the ripgrep match records the TS handler forwards verbatim.
GET/find/filefind.filesranked file-name search returning the matching paths.
GET/find/symbolfind.symbolsThe LSP client exposes hover/definition/diagnostics but not workspace/symbol, and no live client is retained, so symbol search has no backend; the endpoint returns an empty-but-valid array.
GET/formatterformatter.statusthe per-formatter enabled/extensions status computed from config.

global / instance

MethodPathOperationWhat it does
GET/global/configglobal.config.get—
PATCH/global/configglobal.config.updateTS GlobalHttpApi.configUpdate persists and returns only the submitted patch projection (an empty patch yields {} after provider/default stripping), not the full merged config. Echo the request body.
POST/global/disposeglobal.disposeMirrors global.ts dispose(): returns true after disposing. The server is a single long-lived process, so there is nothing to tear down per request (the OS process owns the lifecycle).
GET/global/event—The v2 GlobalEvent stream shared by GET /api/event (v2.event.subscribe) and GET /global/event (global.event).
GET/global/healthglobal.healthMirrors global.ts health(): {healthy: true, version}, plus the four fields this port adds so every running instance says what it is: model_format is always "markdown" — the only payload format the product renders since D9; the field stays because clients read it, but it is no longer configurable — ledger says whether the graph's Session Ledger hook is installed (ocean_app::Services::ledger) — leaf T48b / T52b — build is the binary's own build stamp (crate::server_build, leaf T65), and dropped_events is the bus's drop-oldest counter (ocean_events::Bus::dropped_events, long-runs plan Task 13 / L-12): live events evicted from a saturated subscriber queue since boot — a saturation signal, not an error count, and never reset.
POST/global/upgradeglobal.upgradeMirrors global.ts upgrade(): when the installation method is "unknown" it reports {success:false, error:"Unknown installation method"}. This build is not produced by a known installer. The contract models this as the failure variant of the 200 anyOf (the 400 is reserved for the BadRequest envelope), so it is returned with status 200.
POST/instance/disposeinstance.disposeMirrors instance.ts dispose(): returns true. Single-process, so no per-request teardown.
POST/logapp.logwrite the structured log line then return true.

lsp / mcp

MethodPathOperationWhat it does
GET/lsplsp.statusThe registry resolves candidate servers but does not hold live clients (spawn is on demand), so there is no connected server to report; the endpoint returns an empty-but-valid array.
GET/mcpmcp.statusa map of server name -> status.
POST/mcpmcp.addregister a server at runtime and connect it, returning the full status snapshot.
POST/mcp/{name}/authmcp.auth.startbegin the remote OAuth flow, returning the authorization URL the caller opens.
DELETE/mcp/{name}/authmcp.auth.removeThe 200 body is {"success": true} (additionalProperties:false), not a bare boolean.
POST/mcp/{name}/auth/authenticatemcp.auth.authenticate—
POST/mcp/{name}/auth/callbackmcp.auth.callbackcompletes the remote OAuth flow with the code returned to the loopback callback and answers with the server's resulting status.
POST/mcp/{name}/connectmcp.connect—
POST/mcp/{name}/disconnectmcp.disconnect—
GET/mcp/{name}/prompts—the server's cached prompt list (P1-T38).
GET/mcp/{name}/resources—the server's advertised resources (P1-T38), via McpManager::list_resources → Client::list_resources.
GET/mcp/{name}/tools—the server's cached tool definitions (WP6d).
POST/mcp/{name}/trustmcp.trust(GAP-06): persist the per-server trust decision to <home>/.oceancode/trust.json (keyed by the project dir) and apply it to the running manager, returning the refreshed GET /mcp map.
POST/mcp/{name}/enablemcp.enable(GAP-06 parity): persist the enable/disable decision to <home>/.oceancode/trust.json's disabled_mcp (so the next boot honours it) and apply it live to the running manager, returning the refreshed GET /mcp map. An unknown server id is a 404.

hooks (GAP-06)

MethodPathOperationWhat it does
GET/hooks—the boot-time hook source report plus this project's repo trust bit (GAP-06). Read-only: the rows are whatever the boot loader (or the last reload) recorded.
POST/hooks—add a command hook to one of Claude Code's three writable settings files (GAP-06 parity), then reload the dispatcher and return the refreshed GET /hooks shape. An unknown event / empty command is a 400 (InvalidInput); a settings file that is not a JSON object is a 400 (InvalidData, never clobbered). The body is an ocean_app::hooks_boot::HookEdit.
POST/hooks/reload—re-run the boot hook loader (re-attaching the dispatcher) and return the refreshed report (GAP-06).
POST/hooks/remove—remove a hook from one of the three writable settings files (GAP-06 parity), then reload and return the refreshed GET /hooks shape. A path that is not one of the three writable scopes is a 400 (InvalidInput); a handler that is not present is a 404 (NotFound). The body is an ocean_app::hooks_boot::HookRemove.
POST/trust/repo—flip this project's repo-tier trust (GAP-06): persist the decision, release/hold the repo's .mcp.json servers and reload the hook dispatcher, then return the refreshed GET /hooks shape. A trust-store write error is a 400.

the Claude Code settings mirror (2026-09-10)

MethodPathOperationWhat it does
GET/claude/settings—the Claude Code settings mirror (model, effortLevel/modelSettings, ultracode, alwaysThinkingEnabled of the three tiered settings.json files) with the effective effort and the model it pins (ocean_app::claude_settings, 2026-09-10). Read-only: the rows are whatever boot or the last watcher reload recorded.

path / permission

MethodPathOperationWhat it does
GET/pathpath.getThe home/state/config values mirror the TS server's Global.Path.*, derived via the xdg-basedir port in ocean-config so they are byte-identical to the TS server on the same host/env (e.g. ~/.config/oceancode, not the macOS os.UserConfigDir() location).
GET/permission—the v1 alias, a bare array.
POST/permission/{requestID}/reply—the v1 alias, dispatched on requestID alone.

plugin

MethodPathOperationWhat it does
GET/plugin—the installed plugins with their enable/trust bits.
POST/plugin/install—install from source, returning {name, version, enabled, trusted}.
GET/plugin/marketplace—the added marketplaces with their plugin entries.
POST/plugin/marketplace/add—add a marketplace by spec.
DELETE/plugin/marketplace/{name}—remove a marketplace, returning the refreshed marketplace list (GAP-06 parity). A marketplace that is not present is a 404.
POST/plugin/{name}/enable—turn a plugin's loading on or off.
POST/plugin/{name}/trust—grant or revoke a plugin's execution trust. A missing/invalid body is a 400 (falsify_trust_route_requires_body), so trust is never granted by an empty request.
GET/plugin/{name}—the full detail of one installed plugin (GAP-06 parity). A name that is not installed is a 404.
DELETE/plugin/{name}—uninstall a plugin, returning the refreshed installed list (GAP-06 parity). A plugin that is not installed is a 404.

project

MethodPathOperationWhat it does
GET/projectproject.list—
GET/project/currentproject.currentresolve the request directory.
POST/project/git/initproject.initGitinitialize a repository in the request directory (idempotent).
PATCH/project/{projectID}project.update—
GET/project/{projectID}/directoriesproject.directories—

provider

MethodPathOperationWhat it does
GET/providerprovider.list{all, default, connected}.
GET/provider/authprovider.authMirrors provider.ts auth(): svc.methods() -> Record<providerID, Method[]>. No v1 auth-method plugins are ported, so this is the empty map a fresh instance returns.
POST/provider/{providerID}/oauth/authorizeprovider.oauth.authorizeMirrors provider.ts authorize(): with no oauth method registered for the provider the TS service hits an undefined method lookup, which the handler maps to a generic BadRequest ProviderAuthError. The Claude Pro/Max subscription login is served elsewhere (the v2 integration endpoints and the oceancode login CLI), so a v1 client is never offered a method here.
POST/provider/{providerID}/oauth/callbackprovider.oauth.callbackMirrors provider.ts callback(): with no pending oauth result the TS service returns OauthMissing.

pty

MethodPathOperationWhat it does
GET/ptypty.list—
POST/ptypty.create—
GET/pty/shellspty.shellsthe available login shells.
GET/pty/{ptyID}pty.get—
PUT/pty/{ptyID}pty.update—
DELETE/pty/{ptyID}pty.removeThe v1 contract answers 200 true (unlike the v2 delete, which is 204).
GET/pty/{ptyID}/connectpty.connect(WebSocket upgrade).
POST/pty/{ptyID}/connect-tokenpty.connectToken—

question

MethodPathOperationWhat it does
GET/question—every pending question across sessions.
POST/question/{requestID}/reject—dismisses a pending question, failing the waiting Ask with a rejection.
POST/question/{requestID}/reply—resolves a pending question with the user's answers, unblocking the waiting Ask.

session (v1)

MethodPathOperationWhat it does
GET/sessionsession.listThe v1 response is the bare array.
POST/sessionsession.createMaps the supported v1 body fields onto SessionStore::create; parentID, metadata and permission have no ported column and are dropped.
GET/session/status—the run status of every session, keyed by id.
GET/session/{sessionID}session.get—
DELETE/session/{sessionID}session.deleteRemoves the session, its children, message projection and event log.
PATCH/session/{sessionID}session.updatethe v1 title patch.
POST/session/{sessionID}/abortsession.abortThe TS handler is idempotent: it interrupts any running turn and returns true even for an unknown session id (no 404 pre-check). Mirrored. The interrupt goes through the runner, not the bare coordinator, so it also reaches the session's sub-agents — foreground through the token cascade, background through the runner's job index (long-runs plan Task 12, L-9).
GET/session/{sessionID}/children—the sub-sessions whose parentID is this session. The store has no parent filter, so this lists and filters in-process (the TS handler likewise scans).
POST/session/{sessionID}/command—resolves a registered command, substitutes its arguments, and runs it as a prompt turn. An unknown command maps to CommandNotFoundError (404).
GET/session/{sessionID}/diffsession.diffthe structured diff for the session's working directory (git mode).
POST/session/{sessionID}/fork—copies the session into a new one (optionally truncated at messageID).
POST/session/{sessionID}/init—runs the project-analysis (/init) flow as a prompt turn.
GET/session/{sessionID}/checkpoints—the recorded checkpoints (id, reason, message bookmark, seq), oldest first. An unknown session lists nothing.
POST/session/{sessionID}/undo—restore the last pre-write state. With no earlier checkpoint it is a no-op with a nothing to undo body, never an error.
POST/session/{sessionID}/redo—re-apply the next checkpoint's files. Available only after an undo; otherwise a no-op with a nothing to redo body.
GET/session/{sessionID}/checkpoints/{a}/diff/{b}—a unified diff between two checkpoints (text/plain); the header names both short ids. An unknown session or checkpoint is a 404.
POST/session/{sessionID}/checkpoints/revert-hunk—restore one diff hunk of file to its pre-edit (checkpoint) bytes, leaving every other hunk. An unknown hunk id is a 404 (never a whole-file overwrite).
POST/session/{sessionID}/checkpoints/apply-hunk—re-apply one previously reverted hunk of file.
GET/session/{sessionID}/messagesession.messagesthe v1 alias.
POST/session/{sessionID}/messagesession.promptthe v1 prompt alias.
GET/session/{sessionID}/message/{messageID}session.message—
DELETE/session/{sessionID}/message/{messageID}session.deleteMessage—
DELETE/session/{sessionID}/message/{messageID}/part/{partID}part.delete—
PATCH/session/{sessionID}/message/{messageID}/part/{partID}part.updateThe body is a v1 Part (the full tagged-union shape). The flat ported model stores assistant content (text/reasoning/tool) inline rather than as addressable Part rows, so for those types the update is applied to the inline content; for every type the updated Part is returned with the path-derived ids, matching the v1 contract.
POST/session/{sessionID}/permissions/{permissionID}—the v1 session-scoped response. The {response} enum maps directly onto permission::Reply.
POST/session/{sessionID}/prompt_async—admits durably and returns immediately; the turn runs in the background.
POST/session/{sessionID}/revert—records a revert bookmark at the given message/part.
POST/session/{sessionID}/share—mints (or returns) the share row.
DELETE/session/{sessionID}/share—drops it.
POST/session/{sessionID}/shell—runs a shell command in the session's working directory, recording it as a durable shell message.
GET/session/{sessionID}/subagent—the session's effective sub-agent model (2026-09-16), so a remote TUI shows the identity row's sub-agents segment. The engine owns the whole precedence (ocean_app::Services::effective_subagent_model); this hands its answer back as {"model": ModelRef | null}. An unknown session is 404, like the sibling per-session GETs.
POST/session/{sessionID}/summarize—runs a summarization turn and reports whether a summary was produced.
GET/session/{sessionID}/todosession.todoThe ETag header is the table's entity tag; send it back as If-Match on PUT for a conditional edit.
PUT/session/{sessionID}/todo—the user's edit. Rows are matched by id or content; changed and new rows become user-authored; rows left out are removed. With If-Match set to a stale tag nothing is written and the answer is 412 carrying the current rows and tag.
POST/session/{sessionID}/unrevert—clears the revert bookmark.
GET/skillapp.skillsthe v1 bare array.

sync

MethodPathOperationWhat it does
POST/sync/historysync.history.listMirrors sync.ts history(): select every durable event from the event table, excluding the (aggregateID, seq) cursors in the payload, ordered by seq (sync.ts:72). The payload maps aggregateID -> lastSeq the caller already has.
POST/sync/replaysync.replayMirrors sync.ts replay(): replay the serialized events into the event store, return {sessionID: events[0].aggregateID} (sync.ts:34). TS's ReplayPayload declares directory: Schema.String and events: Schema.NonEmptyArray(ReplayEvent), so the HttpApi rejects a missing directory or an empty array with a 400 BEFORE the handler runs.
POST/sync/startsync.startMirrors sync.ts start(): fork startWorkspaceSyncing with Effect.ignore, return true (sync.ts:27). The fork is best-effort and its result is ignored, so the response is unconditionally true.
POST/sync/stealsync.stealMirrors sync.ts steal(): requires the instance workspaceID; without one it returns 400 BadRequest (sync.ts:62). A self-hosted instance has no workspace binding. The contract's 400 is the effect HttpApiError BadRequest ({_tag:"BadRequest"}), not the {name,data} envelope.

tui

MethodPathOperationWhat it does
POST/tui/append-prompttui.appendPrompt—
POST/tui/clear-prompttui.clearPrompt—
GET/tui/control/nexttui.control.nextlong-poll for the next queued TUI request (FIFO). Parks until a request is enqueued or the client disconnects.
POST/tui/control/responsetui.control.responseenqueue the TUI's reply for whoever is waiting (fire-and-forget FIFO).
POST/tui/execute-commandtui.executeCommand—
POST/tui/open-helptui.openHelp—
POST/tui/open-modelstui.openModels—
POST/tui/open-sessionstui.openSessions—
POST/tui/open-themestui.openThemes—
POST/tui/publishtui.publishdispatch a TUI event onto the request queue for a connected client to pull via control/next.
POST/tui/select-sessiontui.selectSession—
POST/tui/show-toasttui.showToast—
POST/tui/submit-prompttui.submitPrompt—

vcs

MethodPathOperationWhat it does
GET/vcsvcs.getcurrent/default branch info.
POST/vcs/applyvcs.applyapplies a unified patch to the working tree. The contract's 200 body is {applied: boolean}.
GET/vcs/diffvcs.diffthe structured per-file diff.
GET/vcs/diff/rawvcs.diff.rawThe contract's 200 body is a text/x-diff string (not JSON), so the diff is written verbatim with that content type.
GET/vcs/statusvcs.statusthe working-tree file status list.

Generated from crates/ocean-server/src/router.rs — 218 operations.

Planned: TI-34

The docs assistant is not available yet. You can hand these docs to your own assistant instead.