Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d802b0a277 | ||
|
|
e1c1193f3e | ||
|
|
29250a0efb | ||
|
|
c6e6bdf59f | ||
|
|
d80e1199ca | ||
|
|
10ea59066f | ||
|
|
79d6b10d7c | ||
|
|
6e78f36a0f | ||
|
|
16866e1180 | ||
|
|
6d130e5deb | ||
|
|
e30d8173c1 | ||
|
|
7a79f3a5ea |
@@ -29,7 +29,7 @@
|
|||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.14.44",
|
"version": "1.14.46",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.14.44",
|
"version": "1.14.46",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
@@ -120,7 +120,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.14.44",
|
"version": "1.14.46",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-sts": "3.782.0",
|
"@aws-sdk/client-sts": "3.782.0",
|
||||||
"@jsx-email/render": "1.1.1",
|
"@jsx-email/render": "1.1.1",
|
||||||
@@ -147,7 +147,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.14.44",
|
"version": "1.14.46",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "3.0.64",
|
"@ai-sdk/anthropic": "3.0.64",
|
||||||
"@ai-sdk/openai": "3.0.48",
|
"@ai-sdk/openai": "3.0.48",
|
||||||
@@ -171,7 +171,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.14.44",
|
"version": "1.14.46",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
@@ -195,7 +195,7 @@
|
|||||||
},
|
},
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
"name": "@opencode-ai/core",
|
"name": "@opencode-ai/core",
|
||||||
"version": "1.14.44",
|
"version": "1.14.46",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -229,7 +229,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.14.44",
|
"version": "1.14.46",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -283,7 +283,7 @@
|
|||||||
},
|
},
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.14.44",
|
"version": "1.14.46",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
@@ -313,7 +313,7 @@
|
|||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.14.44",
|
"version": "1.14.46",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
@@ -329,7 +329,7 @@
|
|||||||
},
|
},
|
||||||
"packages/http-recorder": {
|
"packages/http-recorder": {
|
||||||
"name": "@opencode-ai/http-recorder",
|
"name": "@opencode-ai/http-recorder",
|
||||||
"version": "1.14.44",
|
"version": "1.14.46",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -342,7 +342,7 @@
|
|||||||
},
|
},
|
||||||
"packages/llm": {
|
"packages/llm": {
|
||||||
"name": "@opencode-ai/llm",
|
"name": "@opencode-ai/llm",
|
||||||
"version": "1.14.44",
|
"version": "1.14.46",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@smithy/eventstream-codec": "4.2.14",
|
"@smithy/eventstream-codec": "4.2.14",
|
||||||
"@smithy/util-utf8": "4.2.2",
|
"@smithy/util-utf8": "4.2.2",
|
||||||
@@ -360,7 +360,7 @@
|
|||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "1.14.44",
|
"version": "1.14.46",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -495,7 +495,7 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.14.44",
|
"version": "1.14.46",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -533,7 +533,7 @@
|
|||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.14.44",
|
"version": "1.14.46",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-spawn": "catalog:",
|
"cross-spawn": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -548,7 +548,7 @@
|
|||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.14.44",
|
"version": "1.14.46",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
@@ -583,7 +583,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.14.44",
|
"version": "1.14.46",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
@@ -632,7 +632,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.14.44",
|
"version": "1.14.46",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.14.44",
|
"version": "1.14.46",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.14.44",
|
"version": "1.14.46",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.14.44",
|
"version": "1.14.46",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.14.44",
|
"version": "1.14.46",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.14.44",
|
"version": "1.14.46",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.14.44",
|
"version": "1.14.46",
|
||||||
"name": "@opencode-ai/core",
|
"name": "@opencode-ai/core",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Config } from "effect"
|
import { Config } from "effect"
|
||||||
|
import { InstallationChannel } from "../installation/version"
|
||||||
|
|
||||||
function truthy(key: string) {
|
function truthy(key: string) {
|
||||||
const value = process.env[key]?.toLowerCase()
|
const value = process.env[key]?.toLowerCase()
|
||||||
@@ -10,6 +11,13 @@ function falsy(key: string) {
|
|||||||
return value === "false" || value === "0"
|
return value === "false" || value === "0"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Channels where new experiments default to ON (unstable / internal users).
|
||||||
|
// Stable channels (`prod`, `latest`) stay opt-in.
|
||||||
|
const UNSTABLE_CHANNELS = new Set(["dev", "beta", "local"])
|
||||||
|
function unstableDefault(key: string) {
|
||||||
|
return truthy(key) || (!falsy(key) && UNSTABLE_CHANNELS.has(InstallationChannel))
|
||||||
|
}
|
||||||
|
|
||||||
function number(key: string) {
|
function number(key: string) {
|
||||||
const value = process.env[key]
|
const value = process.env[key]
|
||||||
if (!value) return undefined
|
if (!value) return undefined
|
||||||
@@ -48,6 +56,9 @@ export const Flag = {
|
|||||||
OPENCODE_DISABLE_CLAUDE_CODE_PROMPT: OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_PROMPT"),
|
OPENCODE_DISABLE_CLAUDE_CODE_PROMPT: OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_PROMPT"),
|
||||||
OPENCODE_DISABLE_CLAUDE_CODE_SKILLS,
|
OPENCODE_DISABLE_CLAUDE_CODE_SKILLS,
|
||||||
OPENCODE_DISABLE_EXTERNAL_SKILLS: truthy("OPENCODE_DISABLE_EXTERNAL_SKILLS"),
|
OPENCODE_DISABLE_EXTERNAL_SKILLS: truthy("OPENCODE_DISABLE_EXTERNAL_SKILLS"),
|
||||||
|
// Default-on for dev/beta/local; opt-in for stable. Set
|
||||||
|
// OPENCODE_EXPERIMENTAL_CUSTOMIZE_SKILL=false to force off, =true to force on.
|
||||||
|
OPENCODE_EXPERIMENTAL_CUSTOMIZE_SKILL: unstableDefault("OPENCODE_EXPERIMENTAL_CUSTOMIZE_SKILL"),
|
||||||
OPENCODE_FAKE_VCS: process.env["OPENCODE_FAKE_VCS"],
|
OPENCODE_FAKE_VCS: process.env["OPENCODE_FAKE_VCS"],
|
||||||
OPENCODE_SERVER_PASSWORD: process.env["OPENCODE_SERVER_PASSWORD"],
|
OPENCODE_SERVER_PASSWORD: process.env["OPENCODE_SERVER_PASSWORD"],
|
||||||
OPENCODE_SERVER_USERNAME: process.env["OPENCODE_SERVER_USERNAME"],
|
OPENCODE_SERVER_USERNAME: process.env["OPENCODE_SERVER_USERNAME"],
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.14.44",
|
"version": "1.14.46",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"homepage": "https://opencode.ai",
|
"homepage": "https://opencode.ai",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.14.44",
|
"version": "1.14.46",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
id = "opencode"
|
id = "opencode"
|
||||||
name = "OpenCode"
|
name = "OpenCode"
|
||||||
description = "The open source coding agent."
|
description = "The open source coding agent."
|
||||||
version = "1.14.44"
|
version = "1.14.46"
|
||||||
schema_version = 1
|
schema_version = 1
|
||||||
authors = ["Anomaly"]
|
authors = ["Anomaly"]
|
||||||
repository = "https://github.com/anomalyco/opencode"
|
repository = "https://github.com/anomalyco/opencode"
|
||||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
|||||||
icon = "./icons/opencode.svg"
|
icon = "./icons/opencode.svg"
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-aarch64]
|
[agent_servers.opencode.targets.darwin-aarch64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.44/opencode-darwin-arm64.zip"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.46/opencode-darwin-arm64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-x86_64]
|
[agent_servers.opencode.targets.darwin-x86_64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.44/opencode-darwin-x64.zip"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.46/opencode-darwin-x64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-aarch64]
|
[agent_servers.opencode.targets.linux-aarch64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.44/opencode-linux-arm64.tar.gz"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.46/opencode-linux-arm64.tar.gz"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-x86_64]
|
[agent_servers.opencode.targets.linux-x86_64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.44/opencode-linux-x64.tar.gz"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.46/opencode-linux-x64.tar.gz"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.windows-x86_64]
|
[agent_servers.opencode.targets.windows-x86_64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.44/opencode-windows-x64.zip"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.46/opencode-windows-x64.zip"
|
||||||
cmd = "./opencode.exe"
|
cmd = "./opencode.exe"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.14.44",
|
"version": "1.14.46",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.14.44",
|
"version": "1.14.46",
|
||||||
"name": "@opencode-ai/http-recorder",
|
"name": "@opencode-ai/http-recorder",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.14.44",
|
"version": "1.14.46",
|
||||||
"name": "@opencode-ai/llm",
|
"name": "@opencode-ai/llm",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.14.44",
|
"version": "1.14.46",
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
# OpenAPI Translation Cleanup Plan
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Trim `packages/opencode/src/server/routes/instance/httpapi/public.ts` until OpenAPI generation is mostly a direct projection of the `HttpApi` route declarations, without breaking the generated SDK surface.
|
||||||
|
|
||||||
|
The main failure mode to eliminate is spec-only behavior: anything that appears in `/doc` or the SDK but is not accepted by runtime `HttpApi` validation.
|
||||||
|
|
||||||
|
## Current Culprit
|
||||||
|
|
||||||
|
`public.ts` exports `PublicApi` with a large `OpenApi.annotations({ transform })` hook. That hook rewrites the generated spec for legacy SDK compatibility.
|
||||||
|
|
||||||
|
The highest-risk rewrite is `InstanceQueryParameters`, which injected `directory` and `workspace` into every instance route in OpenAPI even when the runtime query schema did not accept them. This caused the SDK and `/doc` to advertise calls that could fail with `400` at runtime.
|
||||||
|
|
||||||
|
## Non-Negotiables
|
||||||
|
|
||||||
|
- Do not break the generated JavaScript SDK without an explicit versioned migration plan.
|
||||||
|
- Runtime route schemas are the source of truth for accepted params, payloads, and responses.
|
||||||
|
- `/doc`, generated SDK types, and runtime validation must agree for every endpoint.
|
||||||
|
- Prefer endpoint or schema annotations over post-generation spec surgery.
|
||||||
|
- Remove one category of rewrite at a time, with focused compatibility checks.
|
||||||
|
|
||||||
|
## PR Checklist
|
||||||
|
|
||||||
|
Status legend: `[x]` done locally, `[~]` in progress locally, `[ ]` not started.
|
||||||
|
|
||||||
|
Current combined PR scope:
|
||||||
|
|
||||||
|
- `[x]` PR 1 drift tests: added OpenAPI/runtime query assertions and a negative fixture in `test/server/httpapi-query-schema-drift.test.ts`.
|
||||||
|
- `[x]` PR 2 injection removal: removed broad `directory` / `workspace` post-generation injection from `public.ts` and replaced it with explicit runtime query schemas on affected routes.
|
||||||
|
- `[ ]` PR 3+ cleanup: leave query override, path pattern, error shape, auth, and component-shape rewrites for later PRs.
|
||||||
|
|
||||||
|
### PR 1: Add OpenAPI/Runtime Query Drift Tests
|
||||||
|
|
||||||
|
- `[x]` Add or extend `packages/opencode/test/server/httpapi-query-schema-drift.test.ts`.
|
||||||
|
- `[x]` Import `OpenApi.fromApi` and `PublicApi`.
|
||||||
|
- `[x]` Generate the public spec in-process with `OpenApi.fromApi(PublicApi)`.
|
||||||
|
- `[x]` Add a route inventory for the existing runtime reproducers: `session`, `file`, `experimental`, and `instance` routes.
|
||||||
|
- `[x]` For each inventory entry, assert every OpenAPI query parameter is declared by the runtime query schema.
|
||||||
|
- `[x]` Add a negative regression fixture that fails on spec-only `directory` / `workspace` params.
|
||||||
|
- `[x]` Keep this part test-only.
|
||||||
|
|
||||||
|
Verification:
|
||||||
|
|
||||||
|
- `[x]` `bun test --timeout 5000 test/server/httpapi-query-schema-drift.test.ts` from `packages/opencode`.
|
||||||
|
- `[x]` `bun typecheck` from `packages/opencode`.
|
||||||
|
|
||||||
|
### PR 2: Delete Spec-Only Workspace Query Injection
|
||||||
|
|
||||||
|
- `[x]` Edit `packages/opencode/src/server/routes/instance/httpapi/public.ts`.
|
||||||
|
- `[x]` Delete `InstanceQueryParameters`.
|
||||||
|
- `[x]` Delete the `isInstanceRoute` constant.
|
||||||
|
- `[x]` Delete the branch that prepends `directory` and `workspace` to every instance operation.
|
||||||
|
- `[x]` Keep `normalizeParameter(param, route)` for parameters that are actually produced by `HttpApi`.
|
||||||
|
- `[x]` Add `WorkspaceRoutingQuery` / `WorkspaceRoutingQueryFields` to runtime query schemas for affected routes.
|
||||||
|
- `[x]` Regenerate SDK and inspect diff. Result: no `directory` / `workspace` request-param removals; generated SDK diff is declaration ordering only.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- Added `WorkspaceRoutingQuery` in `middleware/workspace-routing.ts` as the canonical runtime schema for middleware-consumed query params.
|
||||||
|
- Replaced v2 union-query schemas with plain struct query schemas so `OpenApi.fromApi` emits their query params directly. This intentionally exposes the beta `/api/session` pagination/filter params in the SDK; cursor mutual-exclusion rules now live in the handlers, while `directory` / `workspace` remain allowed with cursors for routing.
|
||||||
|
|
||||||
|
Expected code shape:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
for (const param of operation.parameters ?? []) normalizeParameter(param, `${method.toUpperCase()} ${path}`)
|
||||||
|
```
|
||||||
|
|
||||||
|
Verification:
|
||||||
|
|
||||||
|
- `[x]` `bun test --timeout 5000 test/server/httpapi-query-schema-drift.test.ts` from `packages/opencode`.
|
||||||
|
- `[x]` `bun dev generate > /tmp/opencode-openapi.json` from `packages/opencode`.
|
||||||
|
- `[x]` `./packages/sdk/js/script/build.ts` from repo root.
|
||||||
|
- `[x]` Inspect SDK diff for removed `directory` / `workspace` params. Result: none after explicit runtime schemas; v2 list/message now also expose their existing beta pagination/filter query params in the SDK.
|
||||||
|
- `[x]` `bun typecheck` from `packages/opencode`.
|
||||||
|
|
||||||
|
### PR 3: Replace Broad Query Type Override Sets With Route-Level Helpers
|
||||||
|
|
||||||
|
- Edit `packages/opencode/src/server/routes/instance/httpapi/public.ts`.
|
||||||
|
- Remove broad name-based assumptions from `QueryNumberParameters` and `QueryBooleanParameters` one field at a time.
|
||||||
|
- Add shared query schema helpers near route group code if needed, for example in `groups/metadata.ts` or a new `groups/query.ts`.
|
||||||
|
- Prefer route declarations like `Schema.NumberFromString.check(...)` and boolean string decoders like the existing `QueryBoolean` in `groups/session.ts`.
|
||||||
|
- Keep only route-specific `QueryParameterSchemas` entries when SDK compatibility requires a public encoded type that Effect OpenAPI cannot emit yet.
|
||||||
|
|
||||||
|
Concrete first targets:
|
||||||
|
|
||||||
|
- `[x]` Consolidate `roots` / `archived` onto an explicit shared route schema helper. Keep `QueryBooleanParameters` until route-level schema metadata can preserve the SDK's `boolean | "true" | "false"` call shape without a global transform.
|
||||||
|
- `[x]` Replace broad `QueryNumberParameters` reliance for `start` / `cursor` / `limit` with route-specific SDK compatibility schemas. Keep improving route-level constraints where behavior is intentionally stricter.
|
||||||
|
- Keep `GET /find/file limit`, `GET /session/{sessionID}/diff messageID`, and `GET /session/{sessionID}/message limit` overrides until their route schemas generate identical SDK types directly.
|
||||||
|
|
||||||
|
Verification:
|
||||||
|
|
||||||
|
- Focused HTTP tests for changed query fields.
|
||||||
|
- `bun dev generate > /tmp/opencode-openapi.json` from `packages/opencode`.
|
||||||
|
- `./packages/sdk/js/script/build.ts` from repo root.
|
||||||
|
- Inspect generated SDK request param types before deleting each override.
|
||||||
|
- `bun typecheck` from `packages/opencode`.
|
||||||
|
|
||||||
|
### PR 4: Move Path Parameter Patterns Into ID Schemas
|
||||||
|
|
||||||
|
- Audit `PathParameterSchemas` and `pathParameterSchema()` in `public.ts`.
|
||||||
|
- Check source schemas in files like `packages/opencode/src/session/schema.ts`, `packages/opencode/src/permission/schema.ts`, and pty schema definitions.
|
||||||
|
- Add or fix `ZodOverride` / OpenAPI-compatible annotations on branded ID schemas so generated path params include the same patterns without `public.ts` overrides.
|
||||||
|
- Delete one path override only after generated OpenAPI is unchanged for that param.
|
||||||
|
|
||||||
|
Concrete first targets:
|
||||||
|
|
||||||
|
- `sessionID`
|
||||||
|
- `messageID`
|
||||||
|
- `partID`
|
||||||
|
- `permissionID`
|
||||||
|
- `ptyID`
|
||||||
|
|
||||||
|
Leave ambiguous route-local `id` overrides for workspace routes until they are renamed or explicitly typed in endpoint params.
|
||||||
|
|
||||||
|
Verification:
|
||||||
|
|
||||||
|
- `bun dev generate > /tmp/opencode-openapi.json` from `packages/opencode`.
|
||||||
|
- `./packages/sdk/js/script/build.ts` from repo root.
|
||||||
|
- Inspect generated path param types and patterns.
|
||||||
|
- `bun typecheck` from `packages/opencode`.
|
||||||
|
|
||||||
|
### PR 5: Replace Built-In Error Rewrites With Declared API Errors
|
||||||
|
|
||||||
|
- Edit route group files under `packages/opencode/src/server/routes/instance/httpapi/groups/`.
|
||||||
|
- Replace SDK-visible `HttpApiError.BadRequest` / `HttpApiError.NotFound` with explicit error schemas from `packages/opencode/src/server/routes/instance/httpapi/errors.ts` or add new ones there.
|
||||||
|
- Update handlers to fail with the declared API errors at the boundary.
|
||||||
|
- Remove matching cases from `normalizeLegacyErrorResponses()` only after generated OpenAPI remains SDK-compatible.
|
||||||
|
- Do this group by group, starting with one small route group.
|
||||||
|
|
||||||
|
Concrete first targets:
|
||||||
|
|
||||||
|
- `groups/config.ts` `PATCH /config` bad request.
|
||||||
|
- `groups/session.ts` endpoints that already translate domain not-found errors.
|
||||||
|
- `groups/file.ts` if any handler currently relies on built-in error shape.
|
||||||
|
|
||||||
|
Verification:
|
||||||
|
|
||||||
|
- Focused HTTP tests asserting response body shape for changed error paths.
|
||||||
|
- `bun dev generate > /tmp/opencode-openapi.json` from `packages/opencode`.
|
||||||
|
- `./packages/sdk/js/script/build.ts` from repo root.
|
||||||
|
- Inspect SDK error union diff.
|
||||||
|
- `bun typecheck` from `packages/opencode`.
|
||||||
|
|
||||||
|
### PR 6: Remove Auth/Security Spec Rewrites If SDK Can Tolerate It
|
||||||
|
|
||||||
|
- Audit `delete operation.security`, `delete operation.responses?.["401"]`, and `delete spec.components?.securitySchemes` in `public.ts`.
|
||||||
|
- Decide whether SDK should expose auth in generated operation metadata.
|
||||||
|
- If preserving no-auth SDK surface is required, leave this rewrite and document it as intentional compatibility code.
|
||||||
|
- If removing it, update SDK generation expectations and docs in the same PR.
|
||||||
|
|
||||||
|
Verification:
|
||||||
|
|
||||||
|
- `./packages/sdk/js/script/build.ts` from repo root.
|
||||||
|
- Inspect generated client call signatures and error unions.
|
||||||
|
- Do not merge if auth churn changes normal SDK call ergonomics unintentionally.
|
||||||
|
|
||||||
|
### PR 7: Tackle Component Shape Rewrites One At A Time
|
||||||
|
|
||||||
|
- Audit these in `public.ts`: `normalizeComponentNames`, `collapseDuplicateComponents`, `applyLegacySchemaOverrides`, `normalizeComponentDescriptions`, `stripOptionalNull`, `fixSelfReferencingComponents`.
|
||||||
|
- For each rewrite, make a tiny PR that removes or narrows only that rewrite.
|
||||||
|
- If generated SDK type names churn broadly, stop and either keep the rewrite or fix `effect-smol` generation first.
|
||||||
|
|
||||||
|
Concrete first targets:
|
||||||
|
|
||||||
|
- Delete cosmetic `normalizeComponentDescriptions` if SDK output does not change materially.
|
||||||
|
- Narrow `applyLegacySchemaOverrides` entries that correspond to schemas already fixed at the source.
|
||||||
|
- Keep `stripOptionalNull` until there is an explicit SDK migration plan, because it likely affects many optional fields.
|
||||||
|
|
||||||
|
Verification:
|
||||||
|
|
||||||
|
- `bun dev generate > /tmp/opencode-openapi.json` from `packages/opencode`.
|
||||||
|
- `./packages/sdk/js/script/build.ts` from repo root.
|
||||||
|
- Inspect generated SDK type-name and optionality diffs.
|
||||||
|
|
||||||
|
## Upstream Middleware Query Support
|
||||||
|
|
||||||
|
Long-term, `WorkspaceRoutingMiddleware` should declare the query fields it reads once, and `HttpApi` should use that declaration for both runtime validation and OpenAPI generation.
|
||||||
|
|
||||||
|
Target in `effect-smol`:
|
||||||
|
|
||||||
|
- Extend `HttpApiMiddleware.Service` config with optional query schema support, or add a dedicated middleware query annotation.
|
||||||
|
- Make runtime request decoding include middleware query schemas.
|
||||||
|
- Make `OpenApi.fromApi` emit middleware query params for endpoints using that middleware.
|
||||||
|
|
||||||
|
Once available, remove `WorkspaceRoutingQueryFields` spreads from route groups and declare `directory` / `workspace` only on `WorkspaceRoutingMiddleware`.
|
||||||
|
|
||||||
|
## Suggested PR Order
|
||||||
|
|
||||||
|
1. Add drift detection tests only.
|
||||||
|
2. Remove `InstanceQueryParameters` spec injection; rely on `WorkspaceRoutingQueryFields` already present in runtime schemas.
|
||||||
|
3. Convert query type overrides into route/schema-level helpers where possible.
|
||||||
|
4. Convert path parameter overrides into schema annotations or upstream fixes.
|
||||||
|
5. Replace built-in error response rewrites with explicit declared API errors by route group.
|
||||||
|
6. Tackle component naming/nullability rewrites only after SDK compatibility snapshots are stable.
|
||||||
|
|
||||||
|
## Verification Checklist Per PR
|
||||||
|
|
||||||
|
- Focused HTTP tests for changed routes.
|
||||||
|
- OpenAPI drift tests.
|
||||||
|
- `bun dev generate > /tmp/opencode-openapi.json` from `packages/opencode`.
|
||||||
|
- `./packages/sdk/js/script/build.ts` from repo root.
|
||||||
|
- Inspect generated SDK diff for public API churn.
|
||||||
|
- `bun typecheck` from `packages/opencode`.
|
||||||
Vendored
+4
@@ -0,0 +1,4 @@
|
|||||||
|
declare module "*.md" {
|
||||||
|
const content: string
|
||||||
|
export default content
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
|||||||
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js"
|
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js"
|
||||||
import {
|
import {
|
||||||
CallToolResultSchema,
|
CallToolResultSchema,
|
||||||
|
ToolSchema,
|
||||||
type Tool as MCPToolDef,
|
type Tool as MCPToolDef,
|
||||||
ToolListChangedNotificationSchema,
|
ToolListChangedNotificationSchema,
|
||||||
} from "@modelcontextprotocol/sdk/types.js"
|
} from "@modelcontextprotocol/sdk/types.js"
|
||||||
@@ -36,6 +37,15 @@ import { withStatics } from "@opencode-ai/core/schema"
|
|||||||
const log = Log.create({ service: "mcp" })
|
const log = Log.create({ service: "mcp" })
|
||||||
const DEFAULT_TIMEOUT = 30_000
|
const DEFAULT_TIMEOUT = 30_000
|
||||||
|
|
||||||
|
const TolerantToolSchema = ToolSchema.extend({
|
||||||
|
outputSchema: z.unknown().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const TolerantListToolsResultSchema = z.looseObject({
|
||||||
|
tools: z.array(TolerantToolSchema),
|
||||||
|
nextCursor: z.string().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
export const Resource = Schema.Struct({
|
export const Resource = Schema.Struct({
|
||||||
name: Schema.String,
|
name: Schema.String,
|
||||||
uri: Schema.String,
|
uri: Schema.String,
|
||||||
@@ -119,6 +129,38 @@ function remoteURL(key: string, value: string) {
|
|||||||
log.warn("invalid remote mcp url", { key })
|
log.warn("invalid remote mcp url", { key })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isOutputSchemaValidationError(error: Error) {
|
||||||
|
return /can't resolve reference|resolves to more than one schema|outputSchema|schema.*reference|reference.*schema/i.test(
|
||||||
|
error.message,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function listTools(key: string, client: MCPClient, timeout: number) {
|
||||||
|
return Effect.tryPromise({
|
||||||
|
try: () => client.listTools(undefined, { timeout }),
|
||||||
|
catch: (err) => (err instanceof Error ? err : new Error(String(err))),
|
||||||
|
}).pipe(
|
||||||
|
Effect.map((result) => result.tools),
|
||||||
|
Effect.catch((error) => {
|
||||||
|
if (!isOutputSchemaValidationError(error)) return Effect.fail(error)
|
||||||
|
|
||||||
|
log.warn("failed to validate MCP tool output schemas, retrying without output schema validation", { key, error })
|
||||||
|
return Effect.tryPromise({
|
||||||
|
try: () => client.request({ method: "tools/list" }, TolerantListToolsResultSchema, { timeout }),
|
||||||
|
catch: (err) => (err instanceof Error ? err : new Error(String(err))),
|
||||||
|
}).pipe(
|
||||||
|
Effect.map((result) =>
|
||||||
|
result.tools.map((tool) => ({
|
||||||
|
name: tool.name,
|
||||||
|
description: tool.description,
|
||||||
|
inputSchema: tool.inputSchema,
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Convert MCP tool definition to AI SDK Tool type
|
// Convert MCP tool definition to AI SDK Tool type
|
||||||
function convertMcpTool(mcpTool: MCPToolDef, client: MCPClient, timeout?: number): Tool {
|
function convertMcpTool(mcpTool: MCPToolDef, client: MCPClient, timeout?: number): Tool {
|
||||||
const inputSchema = mcpTool.inputSchema
|
const inputSchema = mcpTool.inputSchema
|
||||||
@@ -151,11 +193,7 @@ function convertMcpTool(mcpTool: MCPToolDef, client: MCPClient, timeout?: number
|
|||||||
}
|
}
|
||||||
|
|
||||||
function defs(key: string, client: MCPClient, timeout?: number) {
|
function defs(key: string, client: MCPClient, timeout?: number) {
|
||||||
return Effect.tryPromise({
|
return listTools(key, client, timeout ?? DEFAULT_TIMEOUT).pipe(
|
||||||
try: () => withTimeout(client.listTools(), timeout ?? DEFAULT_TIMEOUT),
|
|
||||||
catch: (err) => (err instanceof Error ? err : new Error(String(err))),
|
|
||||||
}).pipe(
|
|
||||||
Effect.map((result) => result.tools),
|
|
||||||
Effect.catch((err) => {
|
Effect.catch((err) => {
|
||||||
log.error("failed to get tools from client", { key, error: err })
|
log.error("failed to get tools from client", { key, error: err })
|
||||||
return Effect.succeed(undefined)
|
return Effect.succeed(undefined)
|
||||||
|
|||||||
@@ -234,8 +234,8 @@ export const FileDiff = Schema.Struct({
|
|||||||
// populates patch, but loosening matches the sibling schema so a
|
// populates patch, but loosening matches the sibling schema so a
|
||||||
// future code path that omits it can't crash /instance/vcs/diff.
|
// future code path that omits it can't crash /instance/vcs/diff.
|
||||||
patch: Schema.optional(Schema.String),
|
patch: Schema.optional(Schema.String),
|
||||||
additions: NonNegativeInt,
|
additions: Schema.Finite,
|
||||||
deletions: NonNegativeInt,
|
deletions: Schema.Finite,
|
||||||
status: Schema.optional(Schema.Literals(["added", "deleted", "modified"])),
|
status: Schema.optional(Schema.Literals(["added", "deleted", "modified"])),
|
||||||
})
|
})
|
||||||
.annotate({ identifier: "VcsFileDiff" })
|
.annotate({ identifier: "VcsFileDiff" })
|
||||||
@@ -244,8 +244,8 @@ export type FileDiff = Schema.Schema.Type<typeof FileDiff>
|
|||||||
|
|
||||||
export const FileStatus = Schema.Struct({
|
export const FileStatus = Schema.Struct({
|
||||||
file: Schema.String,
|
file: Schema.String,
|
||||||
additions: NonNegativeInt,
|
additions: Schema.Finite,
|
||||||
deletions: NonNegativeInt,
|
deletions: Schema.Finite,
|
||||||
status: Schema.Literals(["added", "deleted", "modified"]),
|
status: Schema.Literals(["added", "deleted", "modified"]),
|
||||||
})
|
})
|
||||||
.annotate({ identifier: "VcsFileStatus" })
|
.annotate({ identifier: "VcsFileStatus" })
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import * as Stream from "effect/Stream"
|
|||||||
import { HttpServerResponse } from "effect/unstable/http"
|
import { HttpServerResponse } from "effect/unstable/http"
|
||||||
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||||
import * as Sse from "effect/unstable/encoding/Sse"
|
import * as Sse from "effect/unstable/encoding/Sse"
|
||||||
|
import { WorkspaceRoutingQuery } from "./middleware/workspace-routing"
|
||||||
|
|
||||||
const log = Log.create({ service: "server" })
|
const log = Log.create({ service: "server" })
|
||||||
|
|
||||||
@@ -16,6 +17,7 @@ export const EventApi = HttpApi.make("event").add(
|
|||||||
HttpApiGroup.make("event")
|
HttpApiGroup.make("event")
|
||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.get("subscribe", EventPaths.event, {
|
HttpApiEndpoint.get("subscribe", EventPaths.event, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: Schema.String.pipe(HttpApiSchema.asText({ contentType: "text/event-stream" })),
|
success: Schema.String.pipe(HttpApiSchema.asText({ contentType: "text/event-stream" })),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Provider } from "@/provider/provider"
|
|||||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||||
import { Authorization } from "../middleware/authorization"
|
import { Authorization } from "../middleware/authorization"
|
||||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||||
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
|
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
|
||||||
import { described } from "./metadata"
|
import { described } from "./metadata"
|
||||||
|
|
||||||
const root = "/config"
|
const root = "/config"
|
||||||
@@ -13,6 +13,7 @@ export const ConfigApi = HttpApi.make("config")
|
|||||||
HttpApiGroup.make("config")
|
HttpApiGroup.make("config")
|
||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.get("get", root, {
|
HttpApiEndpoint.get("get", root, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Config.Info, "Get config info"),
|
success: described(Config.Info, "Get config info"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -22,6 +23,7 @@ export const ConfigApi = HttpApi.make("config")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.patch("update", root, {
|
HttpApiEndpoint.patch("update", root, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: Config.Info,
|
payload: Config.Info,
|
||||||
success: described(Config.Info, "Successfully updated config"),
|
success: described(Config.Info, "Successfully updated config"),
|
||||||
error: HttpApiError.BadRequest,
|
error: HttpApiError.BadRequest,
|
||||||
@@ -33,6 +35,7 @@ export const ConfigApi = HttpApi.make("config")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.get("providers", `${root}/providers`, {
|
HttpApiEndpoint.get("providers", `${root}/providers`, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Provider.ConfigProvidersResult, "List of providers"),
|
success: described(Provider.ConfigProvidersResult, "List of providers"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
|
|||||||
@@ -4,12 +4,17 @@ import { ProviderID, ModelID } from "@/provider/schema"
|
|||||||
import { Session } from "@/session/session"
|
import { Session } from "@/session/session"
|
||||||
import { Worktree } from "@/worktree"
|
import { Worktree } from "@/worktree"
|
||||||
import { NonNegativeInt } from "@opencode-ai/core/schema"
|
import { NonNegativeInt } from "@opencode-ai/core/schema"
|
||||||
import { Schema, SchemaGetter } from "effect"
|
import { Schema } from "effect"
|
||||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||||
import { Authorization } from "../middleware/authorization"
|
import { Authorization } from "../middleware/authorization"
|
||||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||||
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQueryFields } from "../middleware/workspace-routing"
|
import {
|
||||||
|
WorkspaceRoutingMiddleware,
|
||||||
|
WorkspaceRoutingQuery,
|
||||||
|
WorkspaceRoutingQueryFields,
|
||||||
|
} from "../middleware/workspace-routing"
|
||||||
import { described } from "./metadata"
|
import { described } from "./metadata"
|
||||||
|
import { QueryBoolean } from "./query"
|
||||||
|
|
||||||
const ConsoleStateResponse = Schema.Struct({
|
const ConsoleStateResponse = Schema.Struct({
|
||||||
consoleManagedProviders: Schema.mutable(Schema.Array(Schema.String)),
|
consoleManagedProviders: Schema.mutable(Schema.Array(Schema.String)),
|
||||||
@@ -48,12 +53,6 @@ export const ToolListQuery = Schema.Struct({
|
|||||||
model: ModelID,
|
model: ModelID,
|
||||||
})
|
})
|
||||||
|
|
||||||
const QueryBoolean = Schema.Literals(["true", "false"]).pipe(
|
|
||||||
Schema.decodeTo(Schema.Boolean, {
|
|
||||||
decode: SchemaGetter.transform((value) => value === "true"),
|
|
||||||
encode: SchemaGetter.transform((value) => (value ? "true" : "false")),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
const WorktreeList = Schema.Array(Schema.String)
|
const WorktreeList = Schema.Array(Schema.String)
|
||||||
export const SessionListQuery = Schema.Struct({
|
export const SessionListQuery = Schema.Struct({
|
||||||
...WorkspaceRoutingQueryFields,
|
...WorkspaceRoutingQueryFields,
|
||||||
@@ -82,6 +81,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
|
|||||||
HttpApiGroup.make("experimental")
|
HttpApiGroup.make("experimental")
|
||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.get("console", ExperimentalPaths.console, {
|
HttpApiEndpoint.get("console", ExperimentalPaths.console, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(ConsoleStateResponse, "Active Console provider metadata"),
|
success: described(ConsoleStateResponse, "Active Console provider metadata"),
|
||||||
error: HttpApiError.InternalServerError,
|
error: HttpApiError.InternalServerError,
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
@@ -92,6 +92,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.get("consoleOrgs", ExperimentalPaths.consoleOrgs, {
|
HttpApiEndpoint.get("consoleOrgs", ExperimentalPaths.consoleOrgs, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(ConsoleOrgList, "Switchable Console orgs"),
|
success: described(ConsoleOrgList, "Switchable Console orgs"),
|
||||||
error: HttpApiError.InternalServerError,
|
error: HttpApiError.InternalServerError,
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
@@ -102,6 +103,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.post("consoleSwitch", ExperimentalPaths.consoleSwitch, {
|
HttpApiEndpoint.post("consoleSwitch", ExperimentalPaths.consoleSwitch, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: ConsoleSwitchPayload,
|
payload: ConsoleSwitchPayload,
|
||||||
success: described(Schema.Boolean, "Switch success"),
|
success: described(Schema.Boolean, "Switch success"),
|
||||||
error: HttpApiError.BadRequest,
|
error: HttpApiError.BadRequest,
|
||||||
@@ -125,6 +127,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.get("toolIDs", ExperimentalPaths.toolIDs, {
|
HttpApiEndpoint.get("toolIDs", ExperimentalPaths.toolIDs, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(ToolIDs, "Tool IDs"),
|
success: described(ToolIDs, "Tool IDs"),
|
||||||
error: HttpApiError.BadRequest,
|
error: HttpApiError.BadRequest,
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
@@ -136,6 +139,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.get("worktree", ExperimentalPaths.worktree, {
|
HttpApiEndpoint.get("worktree", ExperimentalPaths.worktree, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(WorktreeList, "List of worktree directories"),
|
success: described(WorktreeList, "List of worktree directories"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -145,6 +149,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.post("worktreeCreate", ExperimentalPaths.worktree, {
|
HttpApiEndpoint.post("worktreeCreate", ExperimentalPaths.worktree, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: Schema.optional(Worktree.CreateInput),
|
payload: Schema.optional(Worktree.CreateInput),
|
||||||
success: described(Worktree.Info, "Worktree created"),
|
success: described(Worktree.Info, "Worktree created"),
|
||||||
error: HttpApiError.BadRequest,
|
error: HttpApiError.BadRequest,
|
||||||
@@ -156,6 +161,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.delete("worktreeRemove", ExperimentalPaths.worktree, {
|
HttpApiEndpoint.delete("worktreeRemove", ExperimentalPaths.worktree, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: Worktree.RemoveInput,
|
payload: Worktree.RemoveInput,
|
||||||
success: described(Schema.Boolean, "Worktree removed"),
|
success: described(Schema.Boolean, "Worktree removed"),
|
||||||
error: HttpApiError.BadRequest,
|
error: HttpApiError.BadRequest,
|
||||||
@@ -167,6 +173,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.post("worktreeReset", ExperimentalPaths.worktreeReset, {
|
HttpApiEndpoint.post("worktreeReset", ExperimentalPaths.worktreeReset, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: Worktree.ResetInput,
|
payload: Worktree.ResetInput,
|
||||||
success: described(Schema.Boolean, "Worktree reset"),
|
success: described(Schema.Boolean, "Worktree reset"),
|
||||||
error: HttpApiError.BadRequest,
|
error: HttpApiError.BadRequest,
|
||||||
@@ -189,6 +196,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.get("resource", ExperimentalPaths.resource, {
|
HttpApiEndpoint.get("resource", ExperimentalPaths.resource, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Record(Schema.String, MCP.Resource), "MCP resources"),
|
success: described(Schema.Record(Schema.String, MCP.Resource), "MCP resources"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ import { Schema } from "effect"
|
|||||||
import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||||
import { Authorization } from "../middleware/authorization"
|
import { Authorization } from "../middleware/authorization"
|
||||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||||
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQueryFields } from "../middleware/workspace-routing"
|
import {
|
||||||
|
WorkspaceRoutingMiddleware,
|
||||||
|
WorkspaceRoutingQuery,
|
||||||
|
WorkspaceRoutingQueryFields,
|
||||||
|
} from "../middleware/workspace-routing"
|
||||||
import { described } from "./metadata"
|
import { described } from "./metadata"
|
||||||
|
|
||||||
export const FileQuery = Schema.Struct({
|
export const FileQuery = Schema.Struct({
|
||||||
@@ -97,6 +101,7 @@ export const FileApi = HttpApi.make("file")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.get("status", FilePaths.status, {
|
HttpApiEndpoint.get("status", FilePaths.status, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Array(File.Info), "File status"),
|
success: described(Schema.Array(File.Info), "File status"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ import { Schema } from "effect"
|
|||||||
import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||||
import { Authorization } from "../middleware/authorization"
|
import { Authorization } from "../middleware/authorization"
|
||||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||||
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQueryFields } from "../middleware/workspace-routing"
|
import {
|
||||||
|
WorkspaceRoutingMiddleware,
|
||||||
|
WorkspaceRoutingQuery,
|
||||||
|
WorkspaceRoutingQueryFields,
|
||||||
|
} from "../middleware/workspace-routing"
|
||||||
import { described } from "./metadata"
|
import { described } from "./metadata"
|
||||||
|
|
||||||
const PathInfo = Schema.Struct({
|
const PathInfo = Schema.Struct({
|
||||||
@@ -55,6 +59,7 @@ export const InstanceApi = HttpApi.make("instance")
|
|||||||
HttpApiGroup.make("instance")
|
HttpApiGroup.make("instance")
|
||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.post("dispose", InstancePaths.dispose, {
|
HttpApiEndpoint.post("dispose", InstancePaths.dispose, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Boolean, "Instance disposed"),
|
success: described(Schema.Boolean, "Instance disposed"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -64,6 +69,7 @@ export const InstanceApi = HttpApi.make("instance")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.get("path", InstancePaths.path, {
|
HttpApiEndpoint.get("path", InstancePaths.path, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: PathInfo,
|
success: PathInfo,
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -74,6 +80,7 @@ export const InstanceApi = HttpApi.make("instance")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.get("vcs", InstancePaths.vcs, {
|
HttpApiEndpoint.get("vcs", InstancePaths.vcs, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Vcs.Info, "VCS info"),
|
success: described(Vcs.Info, "VCS info"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -84,6 +91,7 @@ export const InstanceApi = HttpApi.make("instance")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.get("vcsStatus", InstancePaths.vcsStatus, {
|
HttpApiEndpoint.get("vcsStatus", InstancePaths.vcsStatus, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Array(Vcs.FileStatus), "VCS status"),
|
success: described(Schema.Array(Vcs.FileStatus), "VCS status"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -103,6 +111,7 @@ export const InstanceApi = HttpApi.make("instance")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.get("vcsDiffRaw", InstancePaths.vcsDiffRaw, {
|
HttpApiEndpoint.get("vcsDiffRaw", InstancePaths.vcsDiffRaw, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(
|
success: described(
|
||||||
Schema.String.pipe(HttpApiSchema.asText({ contentType: "text/x-diff; charset=utf-8" })),
|
Schema.String.pipe(HttpApiSchema.asText({ contentType: "text/x-diff; charset=utf-8" })),
|
||||||
"Raw VCS diff",
|
"Raw VCS diff",
|
||||||
@@ -115,6 +124,7 @@ export const InstanceApi = HttpApi.make("instance")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.post("vcsApply", InstancePaths.vcsApply, {
|
HttpApiEndpoint.post("vcsApply", InstancePaths.vcsApply, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: Vcs.ApplyInput,
|
payload: Vcs.ApplyInput,
|
||||||
success: described(Vcs.ApplyResult, "VCS patch applied"),
|
success: described(Vcs.ApplyResult, "VCS patch applied"),
|
||||||
error: ApiVcsApplyError,
|
error: ApiVcsApplyError,
|
||||||
@@ -126,6 +136,7 @@ export const InstanceApi = HttpApi.make("instance")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.get("command", InstancePaths.command, {
|
HttpApiEndpoint.get("command", InstancePaths.command, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Array(Command.Info), "List of commands"),
|
success: described(Schema.Array(Command.Info), "List of commands"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -135,6 +146,7 @@ export const InstanceApi = HttpApi.make("instance")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.get("agent", InstancePaths.agent, {
|
HttpApiEndpoint.get("agent", InstancePaths.agent, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Array(Agent.Info), "List of agents"),
|
success: described(Schema.Array(Agent.Info), "List of agents"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -144,6 +156,7 @@ export const InstanceApi = HttpApi.make("instance")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.get("skill", InstancePaths.skill, {
|
HttpApiEndpoint.get("skill", InstancePaths.skill, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Array(Skill.Info), "List of skills"),
|
success: described(Schema.Array(Skill.Info), "List of skills"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -153,6 +166,7 @@ export const InstanceApi = HttpApi.make("instance")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.get("lsp", InstancePaths.lsp, {
|
HttpApiEndpoint.get("lsp", InstancePaths.lsp, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Array(LSP.Status), "LSP server status"),
|
success: described(Schema.Array(LSP.Status), "LSP server status"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -162,6 +176,7 @@ export const InstanceApi = HttpApi.make("instance")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.get("formatter", InstancePaths.formatter, {
|
HttpApiEndpoint.get("formatter", InstancePaths.formatter, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Array(Format.Status), "Formatter status"),
|
success: described(Schema.Array(Format.Status), "Formatter status"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Schema } from "effect"
|
|||||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||||
import { Authorization } from "../middleware/authorization"
|
import { Authorization } from "../middleware/authorization"
|
||||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||||
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
|
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
|
||||||
import { described } from "./metadata"
|
import { described } from "./metadata"
|
||||||
|
|
||||||
export const AddPayload = Schema.Struct({
|
export const AddPayload = Schema.Struct({
|
||||||
@@ -42,6 +42,7 @@ export const McpApi = HttpApi.make("mcp")
|
|||||||
HttpApiGroup.make("mcp")
|
HttpApiGroup.make("mcp")
|
||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.get("status", McpPaths.status, {
|
HttpApiEndpoint.get("status", McpPaths.status, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Record(Schema.String, MCP.Status), "MCP server status"),
|
success: described(Schema.Record(Schema.String, MCP.Status), "MCP server status"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -51,6 +52,7 @@ export const McpApi = HttpApi.make("mcp")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.post("add", McpPaths.status, {
|
HttpApiEndpoint.post("add", McpPaths.status, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: AddPayload,
|
payload: AddPayload,
|
||||||
success: described(StatusMap, "MCP server added successfully"),
|
success: described(StatusMap, "MCP server added successfully"),
|
||||||
error: HttpApiError.BadRequest,
|
error: HttpApiError.BadRequest,
|
||||||
@@ -63,6 +65,7 @@ export const McpApi = HttpApi.make("mcp")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.post("authStart", McpPaths.auth, {
|
HttpApiEndpoint.post("authStart", McpPaths.auth, {
|
||||||
params: { name: Schema.String },
|
params: { name: Schema.String },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(AuthStartResponse, "OAuth flow started"),
|
success: described(AuthStartResponse, "OAuth flow started"),
|
||||||
error: [UnsupportedOAuthError, HttpApiError.NotFound],
|
error: [UnsupportedOAuthError, HttpApiError.NotFound],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
@@ -74,6 +77,7 @@ export const McpApi = HttpApi.make("mcp")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.post("authCallback", McpPaths.authCallback, {
|
HttpApiEndpoint.post("authCallback", McpPaths.authCallback, {
|
||||||
params: { name: Schema.String },
|
params: { name: Schema.String },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: AuthCallbackPayload,
|
payload: AuthCallbackPayload,
|
||||||
success: described(MCP.Status, "OAuth authentication completed"),
|
success: described(MCP.Status, "OAuth authentication completed"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||||
@@ -87,6 +91,7 @@ export const McpApi = HttpApi.make("mcp")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.post("authAuthenticate", McpPaths.authAuthenticate, {
|
HttpApiEndpoint.post("authAuthenticate", McpPaths.authAuthenticate, {
|
||||||
params: { name: Schema.String },
|
params: { name: Schema.String },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(MCP.Status, "OAuth authentication completed"),
|
success: described(MCP.Status, "OAuth authentication completed"),
|
||||||
error: [UnsupportedOAuthError, HttpApiError.NotFound],
|
error: [UnsupportedOAuthError, HttpApiError.NotFound],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
@@ -98,6 +103,7 @@ export const McpApi = HttpApi.make("mcp")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.delete("authRemove", McpPaths.auth, {
|
HttpApiEndpoint.delete("authRemove", McpPaths.auth, {
|
||||||
params: { name: Schema.String },
|
params: { name: Schema.String },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(AuthRemoveResponse, "OAuth credentials removed"),
|
success: described(AuthRemoveResponse, "OAuth credentials removed"),
|
||||||
error: HttpApiError.NotFound,
|
error: HttpApiError.NotFound,
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
@@ -109,6 +115,7 @@ export const McpApi = HttpApi.make("mcp")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.post("connect", McpPaths.connect, {
|
HttpApiEndpoint.post("connect", McpPaths.connect, {
|
||||||
params: { name: Schema.String },
|
params: { name: Schema.String },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Boolean, "MCP server connected successfully"),
|
success: described(Schema.Boolean, "MCP server connected successfully"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -118,6 +125,7 @@ export const McpApi = HttpApi.make("mcp")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.post("disconnect", McpPaths.disconnect, {
|
HttpApiEndpoint.post("disconnect", McpPaths.disconnect, {
|
||||||
params: { name: Schema.String },
|
params: { name: Schema.String },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Boolean, "MCP server disconnected successfully"),
|
success: described(Schema.Boolean, "MCP server disconnected successfully"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Schema } from "effect"
|
|||||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||||
import { Authorization } from "../middleware/authorization"
|
import { Authorization } from "../middleware/authorization"
|
||||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||||
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
|
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
|
||||||
import { described } from "./metadata"
|
import { described } from "./metadata"
|
||||||
|
|
||||||
const root = "/permission"
|
const root = "/permission"
|
||||||
@@ -18,6 +18,7 @@ export const PermissionApi = HttpApi.make("permission")
|
|||||||
HttpApiGroup.make("permission")
|
HttpApiGroup.make("permission")
|
||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.get("list", root, {
|
HttpApiEndpoint.get("list", root, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Array(Permission.Request), "List of pending permissions"),
|
success: described(Schema.Array(Permission.Request), "List of pending permissions"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -28,6 +29,7 @@ export const PermissionApi = HttpApi.make("permission")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.post("reply", `${root}/:requestID/reply`, {
|
HttpApiEndpoint.post("reply", `${root}/:requestID/reply`, {
|
||||||
params: { requestID: PermissionID },
|
params: { requestID: PermissionID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: ReplyPayload,
|
payload: ReplyPayload,
|
||||||
success: described(Schema.Boolean, "Permission processed successfully"),
|
success: described(Schema.Boolean, "Permission processed successfully"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Schema } from "effect"
|
|||||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||||
import { Authorization } from "../middleware/authorization"
|
import { Authorization } from "../middleware/authorization"
|
||||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||||
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
|
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
|
||||||
import { described } from "./metadata"
|
import { described } from "./metadata"
|
||||||
|
|
||||||
const root = "/project"
|
const root = "/project"
|
||||||
@@ -19,6 +19,7 @@ export const ProjectApi = HttpApi.make("project")
|
|||||||
HttpApiGroup.make("project")
|
HttpApiGroup.make("project")
|
||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.get("list", root, {
|
HttpApiEndpoint.get("list", root, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Array(Project.Info), "List of projects"),
|
success: described(Schema.Array(Project.Info), "List of projects"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -28,6 +29,7 @@ export const ProjectApi = HttpApi.make("project")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.get("current", `${root}/current`, {
|
HttpApiEndpoint.get("current", `${root}/current`, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Project.Info, "Current project information"),
|
success: described(Project.Info, "Current project information"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -37,6 +39,7 @@ export const ProjectApi = HttpApi.make("project")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.post("initGit", `${root}/git/init`, {
|
HttpApiEndpoint.post("initGit", `${root}/git/init`, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Project.Info, "Project information after git initialization"),
|
success: described(Project.Info, "Project information after git initialization"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -47,6 +50,7 @@ export const ProjectApi = HttpApi.make("project")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.patch("update", `${root}/:projectID`, {
|
HttpApiEndpoint.patch("update", `${root}/:projectID`, {
|
||||||
params: { projectID: ProjectID },
|
params: { projectID: ProjectID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: UpdatePayload,
|
payload: UpdatePayload,
|
||||||
success: described(Project.Info, "Updated project information"),
|
success: described(Project.Info, "Updated project information"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Schema } from "effect"
|
|||||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||||
import { Authorization } from "../middleware/authorization"
|
import { Authorization } from "../middleware/authorization"
|
||||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||||
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
|
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
|
||||||
import { described } from "./metadata"
|
import { described } from "./metadata"
|
||||||
|
|
||||||
const root = "/provider"
|
const root = "/provider"
|
||||||
@@ -15,6 +15,7 @@ export const ProviderApi = HttpApi.make("provider")
|
|||||||
HttpApiGroup.make("provider")
|
HttpApiGroup.make("provider")
|
||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.get("list", root, {
|
HttpApiEndpoint.get("list", root, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Provider.ListResult, "List of providers"),
|
success: described(Provider.ListResult, "List of providers"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -24,6 +25,7 @@ export const ProviderApi = HttpApi.make("provider")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.get("auth", `${root}/auth`, {
|
HttpApiEndpoint.get("auth", `${root}/auth`, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(ProviderAuth.Methods, "Provider auth methods"),
|
success: described(ProviderAuth.Methods, "Provider auth methods"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -34,6 +36,7 @@ export const ProviderApi = HttpApi.make("provider")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.post("authorize", `${root}/:providerID/oauth/authorize`, {
|
HttpApiEndpoint.post("authorize", `${root}/:providerID/oauth/authorize`, {
|
||||||
params: { providerID: ProviderID },
|
params: { providerID: ProviderID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: ProviderAuth.AuthorizeInput,
|
payload: ProviderAuth.AuthorizeInput,
|
||||||
success: described(Schema.UndefinedOr(ProviderAuth.Authorization), "Authorization URL and method"),
|
success: described(Schema.UndefinedOr(ProviderAuth.Authorization), "Authorization URL and method"),
|
||||||
error: HttpApiError.BadRequest,
|
error: HttpApiError.BadRequest,
|
||||||
@@ -46,6 +49,7 @@ export const ProviderApi = HttpApi.make("provider")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.post("callback", `${root}/:providerID/oauth/callback`, {
|
HttpApiEndpoint.post("callback", `${root}/:providerID/oauth/callback`, {
|
||||||
params: { providerID: ProviderID },
|
params: { providerID: ProviderID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: ProviderAuth.CallbackInput,
|
payload: ProviderAuth.CallbackInput,
|
||||||
success: described(Schema.Boolean, "OAuth callback processed successfully"),
|
success: described(Schema.Boolean, "OAuth callback processed successfully"),
|
||||||
error: HttpApiError.BadRequest,
|
error: HttpApiError.BadRequest,
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ import { Schema } from "effect"
|
|||||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||||
import { Authorization } from "../middleware/authorization"
|
import { Authorization } from "../middleware/authorization"
|
||||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||||
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQueryFields } from "../middleware/workspace-routing"
|
import {
|
||||||
|
WorkspaceRoutingMiddleware,
|
||||||
|
WorkspaceRoutingQuery,
|
||||||
|
WorkspaceRoutingQueryFields,
|
||||||
|
} from "../middleware/workspace-routing"
|
||||||
import { ApiNotFoundError } from "../errors"
|
import { ApiNotFoundError } from "../errors"
|
||||||
import { described } from "./metadata"
|
import { described } from "./metadata"
|
||||||
|
|
||||||
@@ -37,6 +41,7 @@ export const PtyApi = HttpApi.make("pty")
|
|||||||
HttpApiGroup.make("pty")
|
HttpApiGroup.make("pty")
|
||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.get("shells", PtyPaths.shells, {
|
HttpApiEndpoint.get("shells", PtyPaths.shells, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Array(ShellItem), "List of shells"),
|
success: described(Schema.Array(ShellItem), "List of shells"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -46,6 +51,7 @@ export const PtyApi = HttpApi.make("pty")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.get("list", PtyPaths.list, {
|
HttpApiEndpoint.get("list", PtyPaths.list, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Array(Pty.Info), "List of sessions"),
|
success: described(Schema.Array(Pty.Info), "List of sessions"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -55,6 +61,7 @@ export const PtyApi = HttpApi.make("pty")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.post("create", PtyPaths.create, {
|
HttpApiEndpoint.post("create", PtyPaths.create, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: Pty.CreateInput,
|
payload: Pty.CreateInput,
|
||||||
success: described(Pty.Info, "Created session"),
|
success: described(Pty.Info, "Created session"),
|
||||||
error: HttpApiError.BadRequest,
|
error: HttpApiError.BadRequest,
|
||||||
@@ -67,6 +74,7 @@ export const PtyApi = HttpApi.make("pty")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.get("get", PtyPaths.get, {
|
HttpApiEndpoint.get("get", PtyPaths.get, {
|
||||||
params: { ptyID: PtyID },
|
params: { ptyID: PtyID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Pty.Info, "Session info"),
|
success: described(Pty.Info, "Session info"),
|
||||||
error: ApiNotFoundError,
|
error: ApiNotFoundError,
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
@@ -78,6 +86,7 @@ export const PtyApi = HttpApi.make("pty")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.put("update", PtyPaths.update, {
|
HttpApiEndpoint.put("update", PtyPaths.update, {
|
||||||
params: { ptyID: PtyID },
|
params: { ptyID: PtyID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: Pty.UpdateInput,
|
payload: Pty.UpdateInput,
|
||||||
success: described(Pty.Info, "Updated session"),
|
success: described(Pty.Info, "Updated session"),
|
||||||
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
||||||
@@ -90,6 +99,7 @@ export const PtyApi = HttpApi.make("pty")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.delete("remove", PtyPaths.remove, {
|
HttpApiEndpoint.delete("remove", PtyPaths.remove, {
|
||||||
params: { ptyID: PtyID },
|
params: { ptyID: PtyID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Boolean, "Session removed"),
|
success: described(Schema.Boolean, "Session removed"),
|
||||||
error: ApiNotFoundError,
|
error: ApiNotFoundError,
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
@@ -101,6 +111,7 @@ export const PtyApi = HttpApi.make("pty")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.post("connectToken", PtyPaths.connectToken, {
|
HttpApiEndpoint.post("connectToken", PtyPaths.connectToken, {
|
||||||
params: { ptyID: PtyID },
|
params: { ptyID: PtyID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(PtyTicket.ConnectToken, "WebSocket connect token"),
|
success: described(PtyTicket.ConnectToken, "WebSocket connect token"),
|
||||||
error: [HttpApiError.Forbidden, ApiNotFoundError],
|
error: [HttpApiError.Forbidden, ApiNotFoundError],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
@@ -129,6 +140,7 @@ export const PtyConnectApi = HttpApi.make("pty-connect").add(
|
|||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.get("connect", PtyPaths.connect, {
|
HttpApiEndpoint.get("connect", PtyPaths.connect, {
|
||||||
params: Params,
|
params: Params,
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Boolean, "Connected session"),
|
success: described(Schema.Boolean, "Connected session"),
|
||||||
error: [HttpApiError.Forbidden, HttpApiError.NotFound],
|
error: [HttpApiError.Forbidden, HttpApiError.NotFound],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { Schema, SchemaGetter } from "effect"
|
||||||
|
|
||||||
|
export const QueryBoolean = Schema.Literals(["true", "false"]).pipe(
|
||||||
|
Schema.decodeTo(Schema.Boolean, {
|
||||||
|
decode: SchemaGetter.transform((value) => value === "true"),
|
||||||
|
encode: SchemaGetter.transform((value) => (value ? "true" : "false")),
|
||||||
|
}),
|
||||||
|
)
|
||||||
@@ -4,7 +4,7 @@ import { Schema } from "effect"
|
|||||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||||
import { Authorization } from "../middleware/authorization"
|
import { Authorization } from "../middleware/authorization"
|
||||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||||
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
|
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
|
||||||
import { described } from "./metadata"
|
import { described } from "./metadata"
|
||||||
|
|
||||||
const root = "/question"
|
const root = "/question"
|
||||||
@@ -19,6 +19,7 @@ export const QuestionApi = HttpApi.make("question")
|
|||||||
HttpApiGroup.make("question")
|
HttpApiGroup.make("question")
|
||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.get("list", root, {
|
HttpApiEndpoint.get("list", root, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Array(Question.Request), "List of pending questions"),
|
success: described(Schema.Array(Question.Request), "List of pending questions"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -29,6 +30,7 @@ export const QuestionApi = HttpApi.make("question")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.post("reply", `${root}/:requestID/reply`, {
|
HttpApiEndpoint.post("reply", `${root}/:requestID/reply`, {
|
||||||
params: { requestID: QuestionID },
|
params: { requestID: QuestionID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: ReplyPayload,
|
payload: ReplyPayload,
|
||||||
success: described(Schema.Boolean, "Question answered successfully"),
|
success: described(Schema.Boolean, "Question answered successfully"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||||
@@ -41,6 +43,7 @@ export const QuestionApi = HttpApi.make("question")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.post("reject", `${root}/:requestID/reject`, {
|
HttpApiEndpoint.post("reject", `${root}/:requestID/reject`, {
|
||||||
params: { requestID: QuestionID },
|
params: { requestID: QuestionID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Boolean, "Question rejected successfully"),
|
success: described(Schema.Boolean, "Question rejected successfully"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
|
|||||||
@@ -10,21 +10,20 @@ import { SessionSummary } from "@/session/summary"
|
|||||||
import { Todo } from "@/session/todo"
|
import { Todo } from "@/session/todo"
|
||||||
import { MessageID, PartID, SessionID } from "@/session/schema"
|
import { MessageID, PartID, SessionID } from "@/session/schema"
|
||||||
import { Snapshot } from "@/snapshot"
|
import { Snapshot } from "@/snapshot"
|
||||||
import { Schema, SchemaGetter, Struct } from "effect"
|
import { Schema, Struct } from "effect"
|
||||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||||
import { Authorization } from "../middleware/authorization"
|
import { Authorization } from "../middleware/authorization"
|
||||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||||
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQueryFields } from "../middleware/workspace-routing"
|
import {
|
||||||
|
WorkspaceRoutingMiddleware,
|
||||||
|
WorkspaceRoutingQuery,
|
||||||
|
WorkspaceRoutingQueryFields,
|
||||||
|
} from "../middleware/workspace-routing"
|
||||||
import { ApiNotFoundError } from "../errors"
|
import { ApiNotFoundError } from "../errors"
|
||||||
import { described } from "./metadata"
|
import { described } from "./metadata"
|
||||||
|
import { QueryBoolean } from "./query"
|
||||||
|
|
||||||
const root = "/session"
|
const root = "/session"
|
||||||
const QueryBoolean = Schema.Literals(["true", "false"]).pipe(
|
|
||||||
Schema.decodeTo(Schema.Boolean, {
|
|
||||||
decode: SchemaGetter.transform((value) => value === "true"),
|
|
||||||
encode: SchemaGetter.transform((value) => (value ? "true" : "false")),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
export const ListQuery = Schema.Struct({
|
export const ListQuery = Schema.Struct({
|
||||||
...WorkspaceRoutingQueryFields,
|
...WorkspaceRoutingQueryFields,
|
||||||
scope: Schema.optional(Schema.Literals(["project"])),
|
scope: Schema.optional(Schema.Literals(["project"])),
|
||||||
@@ -116,6 +115,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.get("status", SessionPaths.status, {
|
HttpApiEndpoint.get("status", SessionPaths.status, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(StatusMap, "Get session status"),
|
success: described(StatusMap, "Get session status"),
|
||||||
error: HttpApiError.BadRequest,
|
error: HttpApiError.BadRequest,
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
@@ -127,6 +127,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.get("get", SessionPaths.get, {
|
HttpApiEndpoint.get("get", SessionPaths.get, {
|
||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Session.Info, "Get session"),
|
success: described(Session.Info, "Get session"),
|
||||||
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
@@ -138,6 +139,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.get("children", SessionPaths.children, {
|
HttpApiEndpoint.get("children", SessionPaths.children, {
|
||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Array(Session.Info), "List of children"),
|
success: described(Schema.Array(Session.Info), "List of children"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
@@ -149,6 +151,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.get("todo", SessionPaths.todo, {
|
HttpApiEndpoint.get("todo", SessionPaths.todo, {
|
||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Array(Todo.Info), "Todo list"),
|
success: described(Schema.Array(Todo.Info), "Todo list"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
@@ -183,6 +186,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.get("message", SessionPaths.message, {
|
HttpApiEndpoint.get("message", SessionPaths.message, {
|
||||||
params: { sessionID: SessionID, messageID: MessageID },
|
params: { sessionID: SessionID, messageID: MessageID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(MessageV2.WithParts, "Message"),
|
success: described(MessageV2.WithParts, "Message"),
|
||||||
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
@@ -193,6 +197,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.post("create", SessionPaths.create, {
|
HttpApiEndpoint.post("create", SessionPaths.create, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: [HttpApiSchema.NoContent, Session.CreateInput],
|
payload: [HttpApiSchema.NoContent, Session.CreateInput],
|
||||||
success: described(Session.Info, "Successfully created session"),
|
success: described(Session.Info, "Successfully created session"),
|
||||||
error: HttpApiError.BadRequest,
|
error: HttpApiError.BadRequest,
|
||||||
@@ -205,6 +210,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.delete("remove", SessionPaths.remove, {
|
HttpApiEndpoint.delete("remove", SessionPaths.remove, {
|
||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Boolean, "Successfully deleted session"),
|
success: described(Schema.Boolean, "Successfully deleted session"),
|
||||||
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
@@ -216,6 +222,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.patch("update", SessionPaths.update, {
|
HttpApiEndpoint.patch("update", SessionPaths.update, {
|
||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: UpdatePayload,
|
payload: UpdatePayload,
|
||||||
success: described(Session.Info, "Successfully updated session"),
|
success: described(Session.Info, "Successfully updated session"),
|
||||||
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
||||||
@@ -228,6 +235,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.post("fork", SessionPaths.fork, {
|
HttpApiEndpoint.post("fork", SessionPaths.fork, {
|
||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: ForkPayload,
|
payload: ForkPayload,
|
||||||
success: described(Session.Info, "200"),
|
success: described(Session.Info, "200"),
|
||||||
error: ApiNotFoundError,
|
error: ApiNotFoundError,
|
||||||
@@ -240,6 +248,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.post("abort", SessionPaths.abort, {
|
HttpApiEndpoint.post("abort", SessionPaths.abort, {
|
||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Boolean, "Aborted session"),
|
success: described(Schema.Boolean, "Aborted session"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
@@ -251,6 +260,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.post("init", SessionPaths.init, {
|
HttpApiEndpoint.post("init", SessionPaths.init, {
|
||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: InitPayload,
|
payload: InitPayload,
|
||||||
success: described(Schema.Boolean, "200"),
|
success: described(Schema.Boolean, "200"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||||
@@ -264,6 +274,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.post("share", SessionPaths.share, {
|
HttpApiEndpoint.post("share", SessionPaths.share, {
|
||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Session.Info, "Successfully shared session"),
|
success: described(Session.Info, "Successfully shared session"),
|
||||||
error: [HttpApiError.InternalServerError, ApiNotFoundError],
|
error: [HttpApiError.InternalServerError, ApiNotFoundError],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
@@ -275,6 +286,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.delete("unshare", SessionPaths.share, {
|
HttpApiEndpoint.delete("unshare", SessionPaths.share, {
|
||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Session.Info, "Successfully unshared session"),
|
success: described(Session.Info, "Successfully unshared session"),
|
||||||
error: [HttpApiError.InternalServerError, ApiNotFoundError],
|
error: [HttpApiError.InternalServerError, ApiNotFoundError],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
@@ -286,6 +298,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.post("summarize", SessionPaths.summarize, {
|
HttpApiEndpoint.post("summarize", SessionPaths.summarize, {
|
||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: SummarizePayload,
|
payload: SummarizePayload,
|
||||||
success: described(Schema.Boolean, "Summarized session"),
|
success: described(Schema.Boolean, "Summarized session"),
|
||||||
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
||||||
@@ -298,6 +311,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.post("prompt", SessionPaths.prompt, {
|
HttpApiEndpoint.post("prompt", SessionPaths.prompt, {
|
||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: PromptPayload,
|
payload: PromptPayload,
|
||||||
success: described(MessageV2.WithParts, "Created message"),
|
success: described(MessageV2.WithParts, "Created message"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||||
@@ -310,6 +324,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.post("promptAsync", SessionPaths.promptAsync, {
|
HttpApiEndpoint.post("promptAsync", SessionPaths.promptAsync, {
|
||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: PromptPayload,
|
payload: PromptPayload,
|
||||||
success: described(HttpApiSchema.NoContent, "Prompt accepted"),
|
success: described(HttpApiSchema.NoContent, "Prompt accepted"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||||
@@ -323,6 +338,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.post("command", SessionPaths.command, {
|
HttpApiEndpoint.post("command", SessionPaths.command, {
|
||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: CommandPayload,
|
payload: CommandPayload,
|
||||||
success: described(MessageV2.WithParts, "Created message"),
|
success: described(MessageV2.WithParts, "Created message"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||||
@@ -335,6 +351,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.post("shell", SessionPaths.shell, {
|
HttpApiEndpoint.post("shell", SessionPaths.shell, {
|
||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: ShellPayload,
|
payload: ShellPayload,
|
||||||
success: described(MessageV2.WithParts, "Created message"),
|
success: described(MessageV2.WithParts, "Created message"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||||
@@ -347,6 +364,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.post("revert", SessionPaths.revert, {
|
HttpApiEndpoint.post("revert", SessionPaths.revert, {
|
||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: RevertPayload,
|
payload: RevertPayload,
|
||||||
success: described(Session.Info, "Updated session"),
|
success: described(Session.Info, "Updated session"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||||
@@ -360,6 +378,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.post("unrevert", SessionPaths.unrevert, {
|
HttpApiEndpoint.post("unrevert", SessionPaths.unrevert, {
|
||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Session.Info, "Updated session"),
|
success: described(Session.Info, "Updated session"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
@@ -371,6 +390,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.post("permissionRespond", SessionPaths.permissions, {
|
HttpApiEndpoint.post("permissionRespond", SessionPaths.permissions, {
|
||||||
params: { sessionID: SessionID, permissionID: PermissionID },
|
params: { sessionID: SessionID, permissionID: PermissionID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: PermissionResponsePayload,
|
payload: PermissionResponsePayload,
|
||||||
success: described(Schema.Boolean, "Permission processed successfully"),
|
success: described(Schema.Boolean, "Permission processed successfully"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||||
@@ -384,6 +404,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.delete("deleteMessage", SessionPaths.deleteMessage, {
|
HttpApiEndpoint.delete("deleteMessage", SessionPaths.deleteMessage, {
|
||||||
params: { sessionID: SessionID, messageID: MessageID },
|
params: { sessionID: SessionID, messageID: MessageID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Boolean, "Successfully deleted message"),
|
success: described(Schema.Boolean, "Successfully deleted message"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
@@ -396,6 +417,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.delete("deletePart", SessionPaths.deletePart, {
|
HttpApiEndpoint.delete("deletePart", SessionPaths.deletePart, {
|
||||||
params: { sessionID: SessionID, messageID: MessageID, partID: PartID },
|
params: { sessionID: SessionID, messageID: MessageID, partID: PartID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Boolean, "Successfully deleted part"),
|
success: described(Schema.Boolean, "Successfully deleted part"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
@@ -406,6 +428,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.patch("updatePart", SessionPaths.updatePart, {
|
HttpApiEndpoint.patch("updatePart", SessionPaths.updatePart, {
|
||||||
params: { sessionID: SessionID, messageID: MessageID, partID: PartID },
|
params: { sessionID: SessionID, messageID: MessageID, partID: PartID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: MessageV2.Part,
|
payload: MessageV2.Part,
|
||||||
success: described(MessageV2.Part, "Successfully updated part"),
|
success: described(MessageV2.Part, "Successfully updated part"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Schema } from "effect"
|
|||||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||||
import { Authorization } from "../middleware/authorization"
|
import { Authorization } from "../middleware/authorization"
|
||||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||||
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
|
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
|
||||||
import { described } from "./metadata"
|
import { described } from "./metadata"
|
||||||
|
|
||||||
const root = "/sync"
|
const root = "/sync"
|
||||||
@@ -46,6 +46,7 @@ export const SyncApi = HttpApi.make("sync")
|
|||||||
HttpApiGroup.make("sync")
|
HttpApiGroup.make("sync")
|
||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.post("start", SyncPaths.start, {
|
HttpApiEndpoint.post("start", SyncPaths.start, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Boolean, "Workspace sync started"),
|
success: described(Schema.Boolean, "Workspace sync started"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -55,6 +56,7 @@ export const SyncApi = HttpApi.make("sync")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.post("replay", SyncPaths.replay, {
|
HttpApiEndpoint.post("replay", SyncPaths.replay, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: ReplayPayload,
|
payload: ReplayPayload,
|
||||||
success: described(ReplayResponse, "Replayed sync events"),
|
success: described(ReplayResponse, "Replayed sync events"),
|
||||||
error: HttpApiError.BadRequest,
|
error: HttpApiError.BadRequest,
|
||||||
@@ -66,6 +68,7 @@ export const SyncApi = HttpApi.make("sync")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.post("steal", SyncPaths.steal, {
|
HttpApiEndpoint.post("steal", SyncPaths.steal, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: SessionPayload,
|
payload: SessionPayload,
|
||||||
success: described(SessionPayload, "Session stolen into workspace"),
|
success: described(SessionPayload, "Session stolen into workspace"),
|
||||||
error: HttpApiError.BadRequest,
|
error: HttpApiError.BadRequest,
|
||||||
@@ -77,6 +80,7 @@ export const SyncApi = HttpApi.make("sync")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.post("history", SyncPaths.history, {
|
HttpApiEndpoint.post("history", SyncPaths.history, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: HistoryPayload,
|
payload: HistoryPayload,
|
||||||
success: described(Schema.Array(HistoryEvent), "Sync events"),
|
success: described(Schema.Array(HistoryEvent), "Sync events"),
|
||||||
error: HttpApiError.BadRequest,
|
error: HttpApiError.BadRequest,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Schema } from "effect"
|
|||||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||||
import { Authorization } from "../middleware/authorization"
|
import { Authorization } from "../middleware/authorization"
|
||||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||||
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
|
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
|
||||||
import { ApiNotFoundError } from "../errors"
|
import { ApiNotFoundError } from "../errors"
|
||||||
import { described } from "./metadata"
|
import { described } from "./metadata"
|
||||||
|
|
||||||
@@ -54,6 +54,7 @@ export const TuiApi = HttpApi.make("tui")
|
|||||||
HttpApiGroup.make("tui")
|
HttpApiGroup.make("tui")
|
||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.post("appendPrompt", TuiPaths.appendPrompt, {
|
HttpApiEndpoint.post("appendPrompt", TuiPaths.appendPrompt, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: TuiEvent.PromptAppend.properties,
|
payload: TuiEvent.PromptAppend.properties,
|
||||||
success: described(Schema.Boolean, "Prompt processed successfully"),
|
success: described(Schema.Boolean, "Prompt processed successfully"),
|
||||||
error: HttpApiError.BadRequest,
|
error: HttpApiError.BadRequest,
|
||||||
@@ -65,6 +66,7 @@ export const TuiApi = HttpApi.make("tui")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.post("openHelp", TuiPaths.openHelp, {
|
HttpApiEndpoint.post("openHelp", TuiPaths.openHelp, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Boolean, "Help dialog opened successfully"),
|
success: described(Schema.Boolean, "Help dialog opened successfully"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -74,6 +76,7 @@ export const TuiApi = HttpApi.make("tui")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.post("openSessions", TuiPaths.openSessions, {
|
HttpApiEndpoint.post("openSessions", TuiPaths.openSessions, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Boolean, "Session dialog opened successfully"),
|
success: described(Schema.Boolean, "Session dialog opened successfully"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -83,6 +86,7 @@ export const TuiApi = HttpApi.make("tui")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.post("openThemes", TuiPaths.openThemes, {
|
HttpApiEndpoint.post("openThemes", TuiPaths.openThemes, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Boolean, "Theme dialog opened successfully"),
|
success: described(Schema.Boolean, "Theme dialog opened successfully"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -92,6 +96,7 @@ export const TuiApi = HttpApi.make("tui")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.post("openModels", TuiPaths.openModels, {
|
HttpApiEndpoint.post("openModels", TuiPaths.openModels, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Boolean, "Model dialog opened successfully"),
|
success: described(Schema.Boolean, "Model dialog opened successfully"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -101,6 +106,7 @@ export const TuiApi = HttpApi.make("tui")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.post("submitPrompt", TuiPaths.submitPrompt, {
|
HttpApiEndpoint.post("submitPrompt", TuiPaths.submitPrompt, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Boolean, "Prompt submitted successfully"),
|
success: described(Schema.Boolean, "Prompt submitted successfully"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -110,6 +116,7 @@ export const TuiApi = HttpApi.make("tui")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.post("clearPrompt", TuiPaths.clearPrompt, {
|
HttpApiEndpoint.post("clearPrompt", TuiPaths.clearPrompt, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Boolean, "Prompt cleared successfully"),
|
success: described(Schema.Boolean, "Prompt cleared successfully"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -119,6 +126,7 @@ export const TuiApi = HttpApi.make("tui")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.post("executeCommand", TuiPaths.executeCommand, {
|
HttpApiEndpoint.post("executeCommand", TuiPaths.executeCommand, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: CommandPayload,
|
payload: CommandPayload,
|
||||||
success: described(Schema.Boolean, "Command executed successfully"),
|
success: described(Schema.Boolean, "Command executed successfully"),
|
||||||
error: HttpApiError.BadRequest,
|
error: HttpApiError.BadRequest,
|
||||||
@@ -130,6 +138,7 @@ export const TuiApi = HttpApi.make("tui")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.post("showToast", TuiPaths.showToast, {
|
HttpApiEndpoint.post("showToast", TuiPaths.showToast, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: TuiEvent.ToastShow.properties,
|
payload: TuiEvent.ToastShow.properties,
|
||||||
success: described(Schema.Boolean, "Toast notification shown successfully"),
|
success: described(Schema.Boolean, "Toast notification shown successfully"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
@@ -140,6 +149,7 @@ export const TuiApi = HttpApi.make("tui")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.post("publish", TuiPaths.publish, {
|
HttpApiEndpoint.post("publish", TuiPaths.publish, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: TuiPublishPayload,
|
payload: TuiPublishPayload,
|
||||||
success: described(Schema.Boolean, "Event published successfully"),
|
success: described(Schema.Boolean, "Event published successfully"),
|
||||||
error: HttpApiError.BadRequest,
|
error: HttpApiError.BadRequest,
|
||||||
@@ -151,6 +161,7 @@ export const TuiApi = HttpApi.make("tui")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.post("selectSession", TuiPaths.selectSession, {
|
HttpApiEndpoint.post("selectSession", TuiPaths.selectSession, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: TuiEvent.SessionSelect.properties,
|
payload: TuiEvent.SessionSelect.properties,
|
||||||
success: described(Schema.Boolean, "Session selected successfully"),
|
success: described(Schema.Boolean, "Session selected successfully"),
|
||||||
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
||||||
@@ -162,6 +173,7 @@ export const TuiApi = HttpApi.make("tui")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.get("controlNext", TuiPaths.controlNext, {
|
HttpApiEndpoint.get("controlNext", TuiPaths.controlNext, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(TuiRequestPayload, "Next TUI request"),
|
success: described(TuiRequestPayload, "Next TUI request"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -171,6 +183,7 @@ export const TuiApi = HttpApi.make("tui")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.post("controlResponse", TuiPaths.controlResponse, {
|
HttpApiEndpoint.post("controlResponse", TuiPaths.controlResponse, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: Schema.Unknown,
|
payload: Schema.Unknown,
|
||||||
success: described(Schema.Boolean, "Response submitted successfully"),
|
success: described(Schema.Boolean, "Response submitted successfully"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
|
|||||||
@@ -3,46 +3,31 @@ import { SessionMessage } from "@/v2/session-message"
|
|||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import { HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||||
import { Authorization } from "../../middleware/authorization"
|
import { Authorization } from "../../middleware/authorization"
|
||||||
|
import { WorkspaceRoutingQueryFields } from "../../middleware/workspace-routing"
|
||||||
|
|
||||||
|
export const MessagesQuery = Schema.Struct({
|
||||||
|
...WorkspaceRoutingQueryFields,
|
||||||
|
limit: Schema.optional(
|
||||||
|
Schema.NumberFromString.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(200)),
|
||||||
|
).annotate({
|
||||||
|
description: "Maximum number of messages to return. When omitted, the endpoint returns its default page size.",
|
||||||
|
}),
|
||||||
|
order: Schema.optional(Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")])).annotate({
|
||||||
|
description: "Message order for the first page. Use desc for newest first or asc for oldest first.",
|
||||||
|
}),
|
||||||
|
cursor: Schema.optional(
|
||||||
|
Schema.String.annotate({
|
||||||
|
description:
|
||||||
|
"Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. Do not combine with order.",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}).annotate({ identifier: "V2SessionMessagesQuery" })
|
||||||
|
|
||||||
export const MessageGroup = HttpApiGroup.make("v2.message")
|
export const MessageGroup = HttpApiGroup.make("v2.message")
|
||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.get("messages", "/api/session/:sessionID/message", {
|
HttpApiEndpoint.get("messages", "/api/session/:sessionID/message", {
|
||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
query: Schema.Union([
|
query: MessagesQuery,
|
||||||
Schema.Struct({
|
|
||||||
limit: Schema.optional(
|
|
||||||
Schema.NumberFromString.check(
|
|
||||||
Schema.isInt(),
|
|
||||||
Schema.isGreaterThanOrEqualTo(1),
|
|
||||||
Schema.isLessThanOrEqualTo(200),
|
|
||||||
),
|
|
||||||
).annotate({
|
|
||||||
description:
|
|
||||||
"Maximum number of messages to return. When omitted, the endpoint returns its default page size.",
|
|
||||||
}),
|
|
||||||
order: Schema.optional(Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")])).annotate({
|
|
||||||
description: "Message order for the first page. Use desc for newest first or asc for oldest first.",
|
|
||||||
}),
|
|
||||||
cursor: Schema.optional(Schema.Never),
|
|
||||||
}),
|
|
||||||
Schema.Struct({
|
|
||||||
limit: Schema.optional(
|
|
||||||
Schema.NumberFromString.check(
|
|
||||||
Schema.isInt(),
|
|
||||||
Schema.isGreaterThanOrEqualTo(1),
|
|
||||||
Schema.isLessThanOrEqualTo(200),
|
|
||||||
),
|
|
||||||
).annotate({
|
|
||||||
description:
|
|
||||||
"Maximum number of messages to return. When omitted, the endpoint returns its default page size.",
|
|
||||||
}),
|
|
||||||
cursor: Schema.String.annotate({
|
|
||||||
description:
|
|
||||||
"Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. Do not combine with order.",
|
|
||||||
}),
|
|
||||||
order: Schema.optional(Schema.Never),
|
|
||||||
}),
|
|
||||||
]).annotate({ identifier: "V2SessionMessagesQuery" }),
|
|
||||||
success: Schema.Struct({
|
success: Schema.Struct({
|
||||||
items: Schema.Array(SessionMessage.Message),
|
items: Schema.Array(SessionMessage.Message),
|
||||||
cursor: Schema.Struct({
|
cursor: Schema.Struct({
|
||||||
|
|||||||
@@ -1,67 +1,39 @@
|
|||||||
import { WorkspaceID } from "@/control-plane/schema"
|
|
||||||
import { SessionID } from "@/session/schema"
|
import { SessionID } from "@/session/schema"
|
||||||
import { SessionMessage } from "@/v2/session-message"
|
import { SessionMessage } from "@/v2/session-message"
|
||||||
import { Prompt } from "@/v2/session-prompt"
|
import { Prompt } from "@/v2/session-prompt"
|
||||||
import { SessionV2 } from "@/v2/session"
|
import { SessionV2 } from "@/v2/session"
|
||||||
import { Schema, SchemaGetter } from "effect"
|
import { Schema } from "effect"
|
||||||
import { HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||||
import { Authorization } from "../../middleware/authorization"
|
import { Authorization } from "../../middleware/authorization"
|
||||||
|
import { WorkspaceRoutingQuery, WorkspaceRoutingQueryFields } from "../../middleware/workspace-routing"
|
||||||
|
import { QueryBoolean } from "../query"
|
||||||
|
|
||||||
|
export const SessionsQuery = Schema.Struct({
|
||||||
|
...WorkspaceRoutingQueryFields,
|
||||||
|
limit: Schema.optional(
|
||||||
|
Schema.NumberFromString.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(200)),
|
||||||
|
).annotate({
|
||||||
|
description: "Maximum number of sessions to return. Defaults to the newest 50 sessions.",
|
||||||
|
}),
|
||||||
|
order: Schema.optional(Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")])).annotate({
|
||||||
|
description: "Session order for the first page. Use desc for newest first or asc for oldest first.",
|
||||||
|
}),
|
||||||
|
path: Schema.optional(Schema.String),
|
||||||
|
roots: Schema.optional(QueryBoolean),
|
||||||
|
start: Schema.optional(Schema.NumberFromString),
|
||||||
|
search: Schema.optional(Schema.String),
|
||||||
|
cursor: Schema.optional(
|
||||||
|
Schema.String.annotate({
|
||||||
|
description:
|
||||||
|
"Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. Do not combine with order or filters.",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}).annotate({ identifier: "V2SessionsQuery" })
|
||||||
|
|
||||||
export const SessionGroup = HttpApiGroup.make("v2.session")
|
export const SessionGroup = HttpApiGroup.make("v2.session")
|
||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.get("sessions", "/api/session", {
|
HttpApiEndpoint.get("sessions", "/api/session", {
|
||||||
query: Schema.Union([
|
query: SessionsQuery,
|
||||||
Schema.Struct({
|
|
||||||
limit: Schema.optional(
|
|
||||||
Schema.NumberFromString.check(
|
|
||||||
Schema.isInt(),
|
|
||||||
Schema.isGreaterThanOrEqualTo(1),
|
|
||||||
Schema.isLessThanOrEqualTo(200),
|
|
||||||
),
|
|
||||||
).annotate({
|
|
||||||
description: "Maximum number of sessions to return. Defaults to the newest 50 sessions.",
|
|
||||||
}),
|
|
||||||
order: Schema.optional(Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")])).annotate({
|
|
||||||
description: "Session order for the first page. Use desc for newest first or asc for oldest first.",
|
|
||||||
}),
|
|
||||||
directory: Schema.String.pipe(Schema.optional),
|
|
||||||
path: Schema.String.pipe(Schema.optional),
|
|
||||||
workspace: WorkspaceID.pipe(Schema.optional),
|
|
||||||
roots: Schema.Literals(["true", "false"])
|
|
||||||
.pipe(
|
|
||||||
Schema.decodeTo(Schema.Boolean, {
|
|
||||||
decode: SchemaGetter.transform((value) => value === "true"),
|
|
||||||
encode: SchemaGetter.transform((value) => (value ? "true" : "false")),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.pipe(Schema.optional),
|
|
||||||
start: Schema.NumberFromString.pipe(Schema.optional),
|
|
||||||
search: Schema.String.pipe(Schema.optional),
|
|
||||||
cursor: Schema.optional(Schema.Never),
|
|
||||||
}),
|
|
||||||
Schema.Struct({
|
|
||||||
limit: Schema.optional(
|
|
||||||
Schema.NumberFromString.check(
|
|
||||||
Schema.isInt(),
|
|
||||||
Schema.isGreaterThanOrEqualTo(1),
|
|
||||||
Schema.isLessThanOrEqualTo(200),
|
|
||||||
),
|
|
||||||
).annotate({
|
|
||||||
description: "Maximum number of sessions to return. Defaults to the newest 50 sessions.",
|
|
||||||
}),
|
|
||||||
cursor: Schema.String.annotate({
|
|
||||||
description:
|
|
||||||
"Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. Do not combine with order.",
|
|
||||||
}),
|
|
||||||
order: Schema.optional(Schema.Never),
|
|
||||||
directory: Schema.optional(Schema.Never),
|
|
||||||
path: Schema.optional(Schema.Never),
|
|
||||||
workspace: Schema.optional(Schema.Never),
|
|
||||||
roots: Schema.optional(Schema.Never),
|
|
||||||
start: Schema.optional(Schema.Never),
|
|
||||||
search: Schema.optional(Schema.Never),
|
|
||||||
}),
|
|
||||||
]).annotate({ identifier: "V2SessionsQuery" }),
|
|
||||||
success: Schema.Struct({
|
success: Schema.Struct({
|
||||||
items: Schema.Array(SessionV2.Info),
|
items: Schema.Array(SessionV2.Info),
|
||||||
cursor: Schema.Struct({
|
cursor: Schema.Struct({
|
||||||
@@ -82,6 +54,7 @@ export const SessionGroup = HttpApiGroup.make("v2.session")
|
|||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.post("prompt", "/api/session/:sessionID/prompt", {
|
HttpApiEndpoint.post("prompt", "/api/session/:sessionID/prompt", {
|
||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: Schema.Struct({
|
payload: Schema.Struct({
|
||||||
prompt: Prompt,
|
prompt: Prompt,
|
||||||
delivery: SessionV2.Delivery.pipe(Schema.optional),
|
delivery: SessionV2.Delivery.pipe(Schema.optional),
|
||||||
@@ -98,6 +71,7 @@ export const SessionGroup = HttpApiGroup.make("v2.session")
|
|||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.post("compact", "/api/session/:sessionID/compact", {
|
HttpApiEndpoint.post("compact", "/api/session/:sessionID/compact", {
|
||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: HttpApiSchema.NoContent,
|
success: HttpApiSchema.NoContent,
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -110,6 +84,7 @@ export const SessionGroup = HttpApiGroup.make("v2.session")
|
|||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.post("wait", "/api/session/:sessionID/wait", {
|
HttpApiEndpoint.post("wait", "/api/session/:sessionID/wait", {
|
||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: HttpApiSchema.NoContent,
|
success: HttpApiSchema.NoContent,
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -122,6 +97,7 @@ export const SessionGroup = HttpApiGroup.make("v2.session")
|
|||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.get("context", "/api/session/:sessionID/context", {
|
HttpApiEndpoint.get("context", "/api/session/:sessionID/context", {
|
||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: Schema.Array(SessionMessage.Message),
|
success: Schema.Array(SessionMessage.Message),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, Op
|
|||||||
import { ApiVcsApplyError } from "./instance"
|
import { ApiVcsApplyError } from "./instance"
|
||||||
import { Authorization } from "../middleware/authorization"
|
import { Authorization } from "../middleware/authorization"
|
||||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||||
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
|
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
|
||||||
import { described } from "./metadata"
|
import { described } from "./metadata"
|
||||||
|
|
||||||
const root = "/experimental/workspace"
|
const root = "/experimental/workspace"
|
||||||
@@ -40,6 +40,7 @@ export const WorkspaceApi = HttpApi.make("workspace")
|
|||||||
HttpApiGroup.make("workspace")
|
HttpApiGroup.make("workspace")
|
||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.get("adapters", WorkspacePaths.adapters, {
|
HttpApiEndpoint.get("adapters", WorkspacePaths.adapters, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Array(WorkspaceAdapterEntry), "Workspace adapters"),
|
success: described(Schema.Array(WorkspaceAdapterEntry), "Workspace adapters"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -49,6 +50,7 @@ export const WorkspaceApi = HttpApi.make("workspace")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.get("list", WorkspacePaths.list, {
|
HttpApiEndpoint.get("list", WorkspacePaths.list, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Array(Workspace.Info), "Workspaces"),
|
success: described(Schema.Array(Workspace.Info), "Workspaces"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -58,6 +60,7 @@ export const WorkspaceApi = HttpApi.make("workspace")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.post("create", WorkspacePaths.list, {
|
HttpApiEndpoint.post("create", WorkspacePaths.list, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: CreatePayload,
|
payload: CreatePayload,
|
||||||
success: described(Workspace.Info, "Workspace created"),
|
success: described(Workspace.Info, "Workspace created"),
|
||||||
error: HttpApiError.BadRequest,
|
error: HttpApiError.BadRequest,
|
||||||
@@ -69,6 +72,7 @@ export const WorkspaceApi = HttpApi.make("workspace")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.post("syncList", WorkspacePaths.syncList, {
|
HttpApiEndpoint.post("syncList", WorkspacePaths.syncList, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(HttpApiSchema.NoContent, "Workspace list synced"),
|
success: described(HttpApiSchema.NoContent, "Workspace list synced"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -78,6 +82,7 @@ export const WorkspaceApi = HttpApi.make("workspace")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.get("status", WorkspacePaths.status, {
|
HttpApiEndpoint.get("status", WorkspacePaths.status, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Array(Workspace.ConnectionStatus), "Workspace status"),
|
success: described(Schema.Array(Workspace.ConnectionStatus), "Workspace status"),
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -88,6 +93,7 @@ export const WorkspaceApi = HttpApi.make("workspace")
|
|||||||
),
|
),
|
||||||
HttpApiEndpoint.delete("remove", WorkspacePaths.remove, {
|
HttpApiEndpoint.delete("remove", WorkspacePaths.remove, {
|
||||||
params: { id: Workspace.Info.fields.id },
|
params: { id: Workspace.Info.fields.id },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.UndefinedOr(Workspace.Info), "Workspace removed"),
|
success: described(Schema.UndefinedOr(Workspace.Info), "Workspace removed"),
|
||||||
error: HttpApiError.BadRequest,
|
error: HttpApiError.BadRequest,
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
@@ -98,6 +104,7 @@ export const WorkspaceApi = HttpApi.make("workspace")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
HttpApiEndpoint.post("warp", WorkspacePaths.warp, {
|
HttpApiEndpoint.post("warp", WorkspacePaths.warp, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
payload: WarpPayload,
|
payload: WarpPayload,
|
||||||
success: described(HttpApiSchema.NoContent, "Session warped"),
|
success: described(HttpApiSchema.NoContent, "Session warped"),
|
||||||
error: [ApiWorkspaceWarpError, ApiVcsApplyError],
|
error: [ApiWorkspaceWarpError, ApiVcsApplyError],
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export const messageHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.message
|
|||||||
return handlers.handle(
|
return handlers.handle(
|
||||||
"messages",
|
"messages",
|
||||||
Effect.fn(function* (ctx) {
|
Effect.fn(function* (ctx) {
|
||||||
|
if (ctx.query.cursor && ctx.query.order !== undefined) return yield* new HttpApiError.BadRequest({})
|
||||||
const decoded = yield* Effect.try({
|
const decoded = yield* Effect.try({
|
||||||
try: () => (ctx.query.cursor ? cursor.decode(ctx.query.cursor) : undefined),
|
try: () => (ctx.query.cursor ? cursor.decode(ctx.query.cursor) : undefined),
|
||||||
catch: () => new HttpApiError.BadRequest({}),
|
catch: () => new HttpApiError.BadRequest({}),
|
||||||
|
|||||||
@@ -22,6 +22,31 @@ type SessionCursor = typeof SessionCursor.Type
|
|||||||
|
|
||||||
const decodeCursor = Schema.decodeUnknownSync(SessionCursor)
|
const decodeCursor = Schema.decodeUnknownSync(SessionCursor)
|
||||||
|
|
||||||
|
function hasCursorFilter(query: {
|
||||||
|
readonly order?: unknown
|
||||||
|
readonly path?: unknown
|
||||||
|
readonly roots?: unknown
|
||||||
|
readonly start?: unknown
|
||||||
|
readonly search?: unknown
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
query.order !== undefined ||
|
||||||
|
query.path !== undefined ||
|
||||||
|
query.roots !== undefined ||
|
||||||
|
query.start !== undefined ||
|
||||||
|
query.search !== undefined
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasCursorRoutingMismatch(
|
||||||
|
query: { readonly directory?: string; readonly workspace?: string },
|
||||||
|
decoded: SessionCursor | undefined,
|
||||||
|
) {
|
||||||
|
if (!decoded) return false
|
||||||
|
if (query.directory !== undefined && query.directory !== decoded.directory) return true
|
||||||
|
return query.workspace !== undefined && query.workspace !== decoded.workspaceID
|
||||||
|
}
|
||||||
|
|
||||||
const sessionCursor = {
|
const sessionCursor = {
|
||||||
encode(
|
encode(
|
||||||
session: SessionV2.Info,
|
session: SessionV2.Info,
|
||||||
@@ -46,10 +71,12 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.session
|
|||||||
.handle(
|
.handle(
|
||||||
"sessions",
|
"sessions",
|
||||||
Effect.fn(function* (ctx) {
|
Effect.fn(function* (ctx) {
|
||||||
|
if (ctx.query.cursor && hasCursorFilter(ctx.query)) return yield* new HttpApiError.BadRequest({})
|
||||||
const decoded = yield* Effect.try({
|
const decoded = yield* Effect.try({
|
||||||
try: () => (ctx.query.cursor ? sessionCursor.decode(ctx.query.cursor) : undefined),
|
try: () => (ctx.query.cursor ? sessionCursor.decode(ctx.query.cursor) : undefined),
|
||||||
catch: () => new HttpApiError.BadRequest({}),
|
catch: () => new HttpApiError.BadRequest({}),
|
||||||
})
|
})
|
||||||
|
if (hasCursorRoutingMismatch(ctx.query, decoded)) return yield* new HttpApiError.BadRequest({})
|
||||||
const order = decoded?.order ?? ctx.query.order ?? "desc"
|
const order = decoded?.order ?? ctx.query.order ?? "desc"
|
||||||
const filters = decoded ?? {
|
const filters = decoded ?? {
|
||||||
directory: ctx.query.directory,
|
directory: ctx.query.directory,
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ export const WorkspaceRoutingQueryFields = {
|
|||||||
workspace: Schema.optional(Schema.String),
|
workspace: Schema.optional(Schema.String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const WorkspaceRoutingQuery = Schema.Struct(WorkspaceRoutingQueryFields)
|
||||||
|
|
||||||
type RemoteTarget = Extract<Target, { type: "remote" }>
|
type RemoteTarget = Extract<Target, { type: "remote" }>
|
||||||
|
|
||||||
type RequestPlan = Data.TaggedEnum<{
|
type RequestPlan = Data.TaggedEnum<{
|
||||||
|
|||||||
@@ -51,47 +51,37 @@ type OpenApiResponse = {
|
|||||||
content?: Record<string, { schema?: OpenApiSchema }>
|
content?: Record<string, { schema?: OpenApiSchema }>
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instance routes use middleware for directory/workspace resolution, but HttpApi
|
|
||||||
// doesn't surface middleware query params in the spec. Inject them explicitly.
|
|
||||||
const InstanceQueryParameters = [
|
|
||||||
{
|
|
||||||
name: "directory",
|
|
||||||
in: "query",
|
|
||||||
required: false,
|
|
||||||
schema: { type: "string" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "workspace",
|
|
||||||
in: "query",
|
|
||||||
required: false,
|
|
||||||
schema: { type: "string" },
|
|
||||||
},
|
|
||||||
] satisfies OpenApiParameter[]
|
|
||||||
|
|
||||||
// Query schemas describe decoded Effect values, but the generated SDK needs the
|
// Query schemas describe decoded Effect values, but the generated SDK needs the
|
||||||
// public call shape. These keep SDK callers passing numbers/booleans while the
|
// public call shape. These keep SDK callers passing numbers/booleans while the
|
||||||
// server still decodes string query params at runtime.
|
// server still decodes string query params at runtime.
|
||||||
const QueryNumberParameters = new Set(["start", "cursor", "limit", "method"])
|
|
||||||
const QueryBooleanParameters = new Set(["roots", "archived"])
|
const QueryBooleanParameters = new Set(["roots", "archived"])
|
||||||
const QueryParameterSchemas = {
|
const QueryParameterSchemas: Record<string, OpenApiSchema> = {
|
||||||
|
"GET /experimental/session start": { type: "number" },
|
||||||
"GET /find/file limit": { type: "integer", minimum: 1, maximum: 200 },
|
"GET /find/file limit": { type: "integer", minimum: 1, maximum: 200 },
|
||||||
|
"GET /experimental/session cursor": { type: "number" },
|
||||||
|
"GET /experimental/session limit": { type: "number" },
|
||||||
|
"GET /session start": { type: "number" },
|
||||||
|
"GET /session limit": { type: "number" },
|
||||||
"GET /session/{sessionID}/diff messageID": { type: "string", pattern: "^msg.*" },
|
"GET /session/{sessionID}/diff messageID": { type: "string", pattern: "^msg.*" },
|
||||||
"GET /session/{sessionID}/message limit": { type: "integer", minimum: 0, maximum: Number.MAX_SAFE_INTEGER },
|
"GET /session/{sessionID}/message limit": { type: "integer", minimum: 0, maximum: Number.MAX_SAFE_INTEGER },
|
||||||
} satisfies Record<string, OpenApiSchema>
|
"GET /api/session limit": { type: "number" },
|
||||||
|
"GET /api/session start": { type: "number" },
|
||||||
|
"GET /api/session/{sessionID}/message limit": { type: "number" },
|
||||||
|
}
|
||||||
|
|
||||||
const PathParameterSchemas = {
|
const PathParameterSchemas: Record<string, OpenApiSchema> = {
|
||||||
sessionID: { type: "string", pattern: "^ses.*" },
|
sessionID: { type: "string", pattern: "^ses.*" },
|
||||||
messageID: { type: "string", pattern: "^msg.*" },
|
messageID: { type: "string", pattern: "^msg.*" },
|
||||||
partID: { type: "string", pattern: "^prt.*" },
|
partID: { type: "string", pattern: "^prt.*" },
|
||||||
permissionID: { type: "string", pattern: "^per.*" },
|
permissionID: { type: "string", pattern: "^per.*" },
|
||||||
ptyID: { type: "string", pattern: "^pty.*" },
|
ptyID: { type: "string", pattern: "^pty.*" },
|
||||||
} satisfies Record<string, OpenApiSchema>
|
}
|
||||||
|
|
||||||
const LegacyComponentDescriptions = {
|
const LegacyComponentDescriptions: Record<string, string> = {
|
||||||
LogLevel: "Log level",
|
LogLevel: "Log level",
|
||||||
ServerConfig: "Server configuration for opencode serve and web commands",
|
ServerConfig: "Server configuration for opencode serve and web commands",
|
||||||
LayoutConfig: "@deprecated Always uses stretch layout.",
|
LayoutConfig: "@deprecated Always uses stretch layout.",
|
||||||
} satisfies Record<string, string>
|
}
|
||||||
|
|
||||||
function matchLegacyOpenApi(input: Record<string, unknown>) {
|
function matchLegacyOpenApi(input: Record<string, unknown>) {
|
||||||
const spec = input as OpenApiSpec
|
const spec = input as OpenApiSpec
|
||||||
@@ -122,7 +112,6 @@ function matchLegacyOpenApi(input: Record<string, unknown>) {
|
|||||||
delete spec.components?.securitySchemes
|
delete spec.components?.securitySchemes
|
||||||
|
|
||||||
for (const [path, item] of Object.entries(spec.paths ?? {})) {
|
for (const [path, item] of Object.entries(spec.paths ?? {})) {
|
||||||
const isInstanceRoute = !path.startsWith("/global/") && !path.startsWith("/auth/")
|
|
||||||
for (const method of ["get", "post", "put", "delete", "patch"] as const) {
|
for (const method of ["get", "post", "put", "delete", "patch"] as const) {
|
||||||
const operation = item[method]
|
const operation = item[method]
|
||||||
if (!operation) continue
|
if (!operation) continue
|
||||||
@@ -183,14 +172,8 @@ function matchLegacyOpenApi(input: Record<string, unknown>) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!isInstanceRoute) continue
|
const route = `${method.toUpperCase()} ${path}`
|
||||||
operation.parameters = [
|
for (const param of operation.parameters ?? []) normalizeParameter(param, route)
|
||||||
...InstanceQueryParameters,
|
|
||||||
...(operation.parameters ?? []).filter(
|
|
||||||
(param) => param.in !== "query" || (param.name !== "directory" && param.name !== "workspace"),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
for (const param of operation.parameters) normalizeParameter(param, `${method.toUpperCase()} ${path}`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return input
|
return input
|
||||||
@@ -292,7 +275,7 @@ function applyLegacySchemaOverrides(spec: OpenApiSpec) {
|
|||||||
|
|
||||||
function normalizeComponentDescriptions(spec: OpenApiSpec) {
|
function normalizeComponentDescriptions(spec: OpenApiSpec) {
|
||||||
for (const [name, schema] of Object.entries(spec.components?.schemas ?? {})) {
|
for (const [name, schema] of Object.entries(spec.components?.schemas ?? {})) {
|
||||||
const description = LegacyComponentDescriptions[name as keyof typeof LegacyComponentDescriptions]
|
const description = LegacyComponentDescriptions[name]
|
||||||
if (description) {
|
if (description) {
|
||||||
schema.description = description
|
schema.description = description
|
||||||
continue
|
continue
|
||||||
@@ -438,7 +421,7 @@ function fixSelfReferencingComponents(spec: OpenApiSpec) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Simplest fix: generate the raw spec (without transform) to get correct schemas
|
// Simplest fix: generate the raw spec (without transform) to get correct schemas
|
||||||
const raw = OpenApi.fromApi(OpenCodeHttpApi) as unknown as OpenApiSpec
|
const raw: OpenApiSpec = OpenApi.fromApi(OpenCodeHttpApi)
|
||||||
const rawSchemas = raw.components?.schemas
|
const rawSchemas = raw.components?.schemas
|
||||||
if (!rawSchemas) return
|
if (!rawSchemas) return
|
||||||
for (const name of selfRefs) {
|
for (const name of selfRefs) {
|
||||||
@@ -507,15 +490,11 @@ function normalizeParameter(param: OpenApiParameter, route: string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (param.in === "query") {
|
if (param.in === "query") {
|
||||||
const override = QueryParameterSchemas[`${route} ${param.name}` as keyof typeof QueryParameterSchemas]
|
const override = QueryParameterSchemas[`${route} ${param.name}`]
|
||||||
if (override) {
|
if (override) {
|
||||||
param.schema = override
|
param.schema = override
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (QueryNumberParameters.has(param.name)) {
|
|
||||||
param.schema = { type: "number" }
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (QueryBooleanParameters.has(param.name)) {
|
if (QueryBooleanParameters.has(param.name)) {
|
||||||
param.schema = {
|
param.schema = {
|
||||||
anyOf: [{ type: "boolean" }, { type: "string", enum: ["true", "false"] }],
|
anyOf: [{ type: "boolean" }, { type: "string", enum: ["true", "false"] }],
|
||||||
@@ -527,7 +506,7 @@ function normalizeParameter(param: OpenApiParameter, route: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function pathParameterSchema(route: string, name: string) {
|
function pathParameterSchema(route: string, name: string) {
|
||||||
if (name in PathParameterSchemas) return PathParameterSchemas[name as keyof typeof PathParameterSchemas]
|
if (name in PathParameterSchemas) return PathParameterSchemas[name]
|
||||||
if (name === "id" && route.startsWith("DELETE /experimental/workspace/")) return { type: "string", pattern: "^wrk.*" }
|
if (name === "id" && route.startsWith("DELETE /experimental/workspace/")) return { type: "string", pattern: "^wrk.*" }
|
||||||
if (name === "id" && route.startsWith("POST /experimental/workspace/")) return { type: "string", pattern: "^wrk.*" }
|
if (name === "id" && route.startsWith("POST /experimental/workspace/")) return { type: "string", pattern: "^wrk.*" }
|
||||||
if (name === "requestID" && route.startsWith("POST /permission/")) return { type: "string", pattern: "^per.*" }
|
if (name === "requestID" && route.startsWith("POST /permission/")) return { type: "string", pattern: "^per.*" }
|
||||||
|
|||||||
@@ -143,8 +143,8 @@ export type ReasoningPart = Types.DeepMutable<Schema.Schema.Type<typeof Reasonin
|
|||||||
const filePartSourceBase = {
|
const filePartSourceBase = {
|
||||||
text: Schema.Struct({
|
text: Schema.Struct({
|
||||||
value: Schema.String,
|
value: Schema.String,
|
||||||
start: NonNegativeInt,
|
start: Schema.Finite,
|
||||||
end: NonNegativeInt,
|
end: Schema.Finite,
|
||||||
}).annotate({ identifier: "FilePartSourceText" }),
|
}).annotate({ identifier: "FilePartSourceText" }),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,13 +270,13 @@ export const StepFinishPart = Schema.Struct({
|
|||||||
snapshot: Schema.optional(Schema.String),
|
snapshot: Schema.optional(Schema.String),
|
||||||
cost: Schema.Finite,
|
cost: Schema.Finite,
|
||||||
tokens: Schema.Struct({
|
tokens: Schema.Struct({
|
||||||
total: Schema.optional(NonNegativeInt),
|
total: Schema.optional(Schema.Finite),
|
||||||
input: NonNegativeInt,
|
input: Schema.Finite,
|
||||||
output: NonNegativeInt,
|
output: Schema.Finite,
|
||||||
reasoning: NonNegativeInt,
|
reasoning: Schema.Finite,
|
||||||
cache: Schema.Struct({
|
cache: Schema.Struct({
|
||||||
read: NonNegativeInt,
|
read: Schema.Finite,
|
||||||
write: NonNegativeInt,
|
write: Schema.Finite,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
@@ -554,13 +554,13 @@ export const Assistant = Schema.Struct({
|
|||||||
summary: Schema.optional(Schema.Boolean),
|
summary: Schema.optional(Schema.Boolean),
|
||||||
cost: Schema.Finite,
|
cost: Schema.Finite,
|
||||||
tokens: Schema.Struct({
|
tokens: Schema.Struct({
|
||||||
total: Schema.optional(NonNegativeInt),
|
total: Schema.optional(Schema.Finite),
|
||||||
input: NonNegativeInt,
|
input: Schema.Finite,
|
||||||
output: NonNegativeInt,
|
output: Schema.Finite,
|
||||||
reasoning: NonNegativeInt,
|
reasoning: Schema.Finite,
|
||||||
cache: Schema.Struct({
|
cache: Schema.Struct({
|
||||||
read: NonNegativeInt,
|
read: Schema.Finite,
|
||||||
write: NonNegativeInt,
|
write: Schema.Finite,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
structured: Schema.optional(Schema.Any),
|
structured: Schema.optional(Schema.Any),
|
||||||
|
|||||||
@@ -172,12 +172,12 @@ export const Info = Schema.Struct({
|
|||||||
cost: Schema.Finite,
|
cost: Schema.Finite,
|
||||||
summary: Schema.optional(Schema.Boolean),
|
summary: Schema.optional(Schema.Boolean),
|
||||||
tokens: Schema.Struct({
|
tokens: Schema.Struct({
|
||||||
input: NonNegativeInt,
|
input: Schema.Finite,
|
||||||
output: NonNegativeInt,
|
output: Schema.Finite,
|
||||||
reasoning: NonNegativeInt,
|
reasoning: Schema.Finite,
|
||||||
cache: Schema.Struct({
|
cache: Schema.Struct({
|
||||||
read: NonNegativeInt,
|
read: Schema.Finite,
|
||||||
write: NonNegativeInt,
|
write: Schema.Finite,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -142,9 +142,9 @@ function sessionPath(worktree: string, cwd: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Summary = Schema.Struct({
|
const Summary = Schema.Struct({
|
||||||
additions: NonNegativeInt,
|
additions: Schema.Finite,
|
||||||
deletions: NonNegativeInt,
|
deletions: Schema.Finite,
|
||||||
files: NonNegativeInt,
|
files: Schema.Finite,
|
||||||
diffs: optionalOmitUndefined(Schema.Array(Snapshot.FileDiff)),
|
diffs: optionalOmitUndefined(Schema.Array(Snapshot.FileDiff)),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -353,7 +353,7 @@ export function plan(input: { slug: string; time: { created: number } }, instanc
|
|||||||
export const getUsage = (input: { model: Provider.Model; usage: LanguageModelUsage; metadata?: ProviderMetadata }) => {
|
export const getUsage = (input: { model: Provider.Model; usage: LanguageModelUsage; metadata?: ProviderMetadata }) => {
|
||||||
const safe = (value: number) => {
|
const safe = (value: number) => {
|
||||||
if (!Number.isFinite(value)) return 0
|
if (!Number.isFinite(value)) return 0
|
||||||
return value
|
return Math.max(0, value)
|
||||||
}
|
}
|
||||||
const inputTokens = safe(input.usage.inputTokens ?? 0)
|
const inputTokens = safe(input.usage.inputTokens ?? 0)
|
||||||
const outputTokens = safe(input.usage.outputTokens ?? 0)
|
const outputTokens = safe(input.usage.outputTokens ?? 0)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { ConfigMarkdown } from "@/config/markdown"
|
|||||||
import { Glob } from "@opencode-ai/core/util/glob"
|
import { Glob } from "@opencode-ai/core/util/glob"
|
||||||
import * as Log from "@opencode-ai/core/util/log"
|
import * as Log from "@opencode-ai/core/util/log"
|
||||||
import { Discovery } from "./discovery"
|
import { Discovery } from "./discovery"
|
||||||
|
import CUSTOMIZE_OPENCODE_SKILL_BODY from "./prompt/customize-opencode.md" with { type: "text" }
|
||||||
|
|
||||||
const log = Log.create({ service: "skill" })
|
const log = Log.create({ service: "skill" })
|
||||||
const CLAUDE_EXTERNAL_DIR = ".claude"
|
const CLAUDE_EXTERNAL_DIR = ".claude"
|
||||||
@@ -25,6 +26,15 @@ const EXTERNAL_SKILL_PATTERN = "skills/**/SKILL.md"
|
|||||||
const OPENCODE_SKILL_PATTERN = "{skill,skills}/**/SKILL.md"
|
const OPENCODE_SKILL_PATTERN = "{skill,skills}/**/SKILL.md"
|
||||||
const SKILL_PATTERN = "**/SKILL.md"
|
const SKILL_PATTERN = "**/SKILL.md"
|
||||||
|
|
||||||
|
// Built-in skill that ships with opencode. The model's intuition for what an
|
||||||
|
// opencode.json should look like is often wrong, and opencode hard-fails on
|
||||||
|
// invalid config, so users hit cryptic startup errors. Loading this skill
|
||||||
|
// when the model is asked to touch opencode's own config files gives it the
|
||||||
|
// actual schemas instead of guesses.
|
||||||
|
const CUSTOMIZE_OPENCODE_SKILL_NAME = "customize-opencode"
|
||||||
|
const CUSTOMIZE_OPENCODE_SKILL_DESCRIPTION =
|
||||||
|
"Use ONLY when the user is editing or creating opencode's own configuration: opencode.json, opencode.jsonc, files under .opencode/, or files under ~/.config/opencode/. Also use when creating or fixing opencode agents, subagents, skills, plugins, MCP servers, or permission rules. Do not use for the user's own application code, or for any project that is not configuring opencode itself."
|
||||||
|
|
||||||
export const Info = Schema.Struct({
|
export const Info = Schema.Struct({
|
||||||
name: Schema.String,
|
name: Schema.String,
|
||||||
description: Schema.optional(Schema.String),
|
description: Schema.optional(Schema.String),
|
||||||
@@ -230,6 +240,16 @@ export const layer = Layer.effect(
|
|||||||
const state = yield* InstanceState.make(
|
const state = yield* InstanceState.make(
|
||||||
Effect.fn("Skill.state")(function* () {
|
Effect.fn("Skill.state")(function* () {
|
||||||
const s: State = { skills: {}, dirs: new Set() }
|
const s: State = { skills: {}, dirs: new Set() }
|
||||||
|
// Register the built-in skill BEFORE disk discovery so a user-disk
|
||||||
|
// skill with the same name can override it.
|
||||||
|
if (Flag.OPENCODE_EXPERIMENTAL_CUSTOMIZE_SKILL) {
|
||||||
|
s.skills[CUSTOMIZE_OPENCODE_SKILL_NAME] = {
|
||||||
|
name: CUSTOMIZE_OPENCODE_SKILL_NAME,
|
||||||
|
description: CUSTOMIZE_OPENCODE_SKILL_DESCRIPTION,
|
||||||
|
location: "<built-in>",
|
||||||
|
content: CUSTOMIZE_OPENCODE_SKILL_BODY,
|
||||||
|
}
|
||||||
|
}
|
||||||
yield* loadSkills(s, yield* InstanceState.get(discovered), bus)
|
yield* loadSkills(s, yield* InstanceState.get(discovered), bus)
|
||||||
return s
|
return s
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -0,0 +1,354 @@
|
|||||||
|
<!--
|
||||||
|
Built-in skill. Name and description are registered in code at
|
||||||
|
packages/opencode/src/skill/index.ts (see SKILL_NAME and SKILL_DESCRIPTION).
|
||||||
|
The body below becomes the skill's content.
|
||||||
|
-->
|
||||||
|
|
||||||
|
# Customizing opencode
|
||||||
|
|
||||||
|
opencode validates its own config strictly and refuses to start when a field
|
||||||
|
is wrong. The shapes below are the accepted shapes. When in doubt, fetch
|
||||||
|
`https://opencode.ai/config.json` (the JSON Schema) and validate against it.
|
||||||
|
|
||||||
|
Every `opencode.json` should declare `"$schema": "https://opencode.ai/config.json"`
|
||||||
|
so the user's editor catches mistakes as they type.
|
||||||
|
|
||||||
|
## Where files live
|
||||||
|
|
||||||
|
| Scope | Path |
|
||||||
|
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| Project config | `./opencode.json`, `./opencode.jsonc`, or `.opencode/opencode.json` (opencode walks up from the cwd to the worktree root) |
|
||||||
|
| Global config | `~/.config/opencode/opencode.json` (NOT `~/.opencode/`) |
|
||||||
|
| Project agents | `.opencode/agent/<name>.md` or `.opencode/agents/<name>.md` |
|
||||||
|
| Global agents | `~/.config/opencode/agent(s)/<name>.md` |
|
||||||
|
| Project skills | `.opencode/skill(s)/<name>/SKILL.md` |
|
||||||
|
| Global skills | `~/.config/opencode/skill(s)/<name>/SKILL.md` |
|
||||||
|
| External skills (auto-loaded) | `~/.claude/skills/<name>/SKILL.md`, `~/.agents/skills/<name>/SKILL.md` |
|
||||||
|
|
||||||
|
Configs from each scope are deep-merged. Project overrides global. Unknown
|
||||||
|
top-level keys in `opencode.json` are rejected with `ConfigInvalidError`.
|
||||||
|
|
||||||
|
## opencode.json
|
||||||
|
|
||||||
|
Every field is optional.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"username": "string",
|
||||||
|
"model": "provider/model-id",
|
||||||
|
"small_model": "provider/model-id",
|
||||||
|
"default_agent": "agent-name",
|
||||||
|
"shell": "/bin/zsh",
|
||||||
|
"logLevel": "DEBUG" | "INFO" | "WARN" | "ERROR",
|
||||||
|
"share": "manual" | "auto" | "disabled",
|
||||||
|
"autoupdate": true | false | "notify",
|
||||||
|
"snapshot": true,
|
||||||
|
"instructions": ["AGENTS.md", "docs/style.md"],
|
||||||
|
|
||||||
|
"skills": {
|
||||||
|
"paths": [".opencode/skills", "/abs/path/to/skills"],
|
||||||
|
"urls": ["https://example.com/.well-known/skills/"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"agent": {
|
||||||
|
"my-agent": {
|
||||||
|
"model": "anthropic/claude-sonnet-4-6",
|
||||||
|
"mode": "subagent",
|
||||||
|
"description": "...",
|
||||||
|
"permission": { "edit": "deny" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"command": {
|
||||||
|
"deploy": { "description": "...", "prompt": "..." }
|
||||||
|
},
|
||||||
|
|
||||||
|
"provider": {
|
||||||
|
"anthropic": { "options": { "apiKey": "..." } }
|
||||||
|
},
|
||||||
|
"disabled_providers": ["openai"],
|
||||||
|
"enabled_providers": ["anthropic"],
|
||||||
|
|
||||||
|
"mcp": {
|
||||||
|
"playwright": {
|
||||||
|
"type": "local",
|
||||||
|
"command": ["npx", "-y", "@playwright/mcp"],
|
||||||
|
"enabled": true,
|
||||||
|
"env": {}
|
||||||
|
},
|
||||||
|
"remote-thing": {
|
||||||
|
"type": "remote",
|
||||||
|
"url": "https://...",
|
||||||
|
"headers": { "Authorization": "Bearer ..." }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"plugin": [
|
||||||
|
"opencode-gemini-auth",
|
||||||
|
"opencode-foo@1.2.3",
|
||||||
|
"./local-plugin.ts",
|
||||||
|
["opencode-bar", { "option": "value" }]
|
||||||
|
],
|
||||||
|
|
||||||
|
"permission": {
|
||||||
|
"edit": "deny",
|
||||||
|
"bash": { "git *": "allow", "*": "ask" }
|
||||||
|
},
|
||||||
|
|
||||||
|
"formatter": false,
|
||||||
|
"lsp": false,
|
||||||
|
|
||||||
|
"experimental": {
|
||||||
|
"primary_tools": ["edit"],
|
||||||
|
"mcp_timeout": 30000
|
||||||
|
},
|
||||||
|
|
||||||
|
"tool_output": { "max_lines": 200, "max_bytes": 8192 },
|
||||||
|
|
||||||
|
"compaction": { "auto": true, "tail_turns": 15 }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Shape notes worth being explicit about:
|
||||||
|
|
||||||
|
- `model` always carries a provider prefix: `"anthropic/claude-sonnet-4-6"`.
|
||||||
|
- `skills` is an object with `paths` and/or `urls`, not an array.
|
||||||
|
- `agent` is an object keyed by agent name, not an array.
|
||||||
|
- `plugin` is an array of strings or `[name, options]` tuples, not an object.
|
||||||
|
- `mcp[name].command` is an array of strings, never a single string. `type` is required.
|
||||||
|
- `permission` is either a string action or an object keyed by tool name.
|
||||||
|
|
||||||
|
## Skills
|
||||||
|
|
||||||
|
opencode's skill loader scans for `**/SKILL.md` inside skill directories. The
|
||||||
|
file is named `SKILL.md` exactly, and lives in its own folder named after the
|
||||||
|
skill:
|
||||||
|
|
||||||
|
```
|
||||||
|
.opencode/skills/my-skill/SKILL.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Frontmatter:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
name: my-skill
|
||||||
|
description: One sentence covering what this skill does AND when to trigger it. Front-load the literal keywords or filenames the user is likely to say.
|
||||||
|
---
|
||||||
|
|
||||||
|
# My Skill
|
||||||
|
|
||||||
|
(skill body in markdown: instructions, examples, references)
|
||||||
|
```
|
||||||
|
|
||||||
|
- `name` is required, lowercase hyphen-separated, up to 64 chars, and matches the folder name.
|
||||||
|
- `description` is effectively required: skills without one are filtered out and never surfaced to the model. Cover both _what_ the skill does and _when_ to use it. Write in third person ("Use when...", not "I help with..."). Front-load concrete trigger keywords and filenames; gate with "Use ONLY when..." if the skill should stay quiet on adjacent topics.
|
||||||
|
- Optional: `license`, `compatibility`, `metadata` (string-string map).
|
||||||
|
|
||||||
|
Register skills from non-default locations via `skills.paths` (scanned
|
||||||
|
recursively for `**/SKILL.md`) and `skills.urls` (each URL serves a list of
|
||||||
|
skills).
|
||||||
|
|
||||||
|
## Agents
|
||||||
|
|
||||||
|
Two ways to define an agent. Use the file form for anything non-trivial.
|
||||||
|
|
||||||
|
### Inline (in `opencode.json`)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"agent": {
|
||||||
|
"my-reviewer": {
|
||||||
|
"description": "Reviews PRs for style violations.",
|
||||||
|
"mode": "subagent",
|
||||||
|
"model": "anthropic/claude-sonnet-4-6",
|
||||||
|
"permission": { "edit": "deny", "bash": "ask" },
|
||||||
|
"prompt": "You are a strict PR reviewer..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### File
|
||||||
|
|
||||||
|
```
|
||||||
|
.opencode/agent/my-reviewer.md OR .opencode/agents/my-reviewer.md
|
||||||
|
```
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
description: Reviews PRs for style violations.
|
||||||
|
mode: subagent
|
||||||
|
model: anthropic/claude-sonnet-4-6
|
||||||
|
permission:
|
||||||
|
edit: deny
|
||||||
|
bash: ask
|
||||||
|
---
|
||||||
|
|
||||||
|
You are a strict PR reviewer. Focus on...
|
||||||
|
```
|
||||||
|
|
||||||
|
The file body becomes the agent's `prompt`. Do not also put `prompt:` in the
|
||||||
|
frontmatter.
|
||||||
|
|
||||||
|
`mode` is one of `"primary"`, `"subagent"`, `"all"`.
|
||||||
|
|
||||||
|
Allowed top-level frontmatter fields: `name, model, variant, description, mode,
|
||||||
|
hidden, color, steps, options, permission, disable, temperature, top_p`. Any
|
||||||
|
unknown field is silently routed into `options`.
|
||||||
|
|
||||||
|
To disable a built-in agent: `agent: { build: { disable: true } }`, or in a
|
||||||
|
file, `disable: true` in frontmatter.
|
||||||
|
|
||||||
|
`default_agent` must point to a non-hidden, primary-mode agent.
|
||||||
|
|
||||||
|
### Built-in agents
|
||||||
|
|
||||||
|
opencode ships with `build`, `plan`, `general`, `explore`, plus optionally
|
||||||
|
`scout` (gated on `OPENCODE_EXPERIMENTAL_SCOUT`). Hidden internal agents:
|
||||||
|
`compaction`, `title`, `summary`. To override a built-in's fields, define the
|
||||||
|
same key in `agent: { <name>: { ... } }`.
|
||||||
|
|
||||||
|
## Plugins
|
||||||
|
|
||||||
|
`plugin:` is an array. Each entry is one of:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"plugin": [
|
||||||
|
"opencode-gemini-auth", // npm spec, latest
|
||||||
|
"opencode-foo@1.2.3", // npm spec, pinned
|
||||||
|
"./local-plugin.ts", // file path, relative to the declaring config
|
||||||
|
"file:///abs/path/plugin.js", // file URL
|
||||||
|
["opencode-bar", { "key": "val" }] // tuple form with options
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Auto-discovered plugins (no config entry needed): any `*.ts` or `*.js` file in
|
||||||
|
`.opencode/plugin/` or `.opencode/plugins/`.
|
||||||
|
|
||||||
|
A plugin module exports `default` (or any named export) of type
|
||||||
|
`Plugin = (input: PluginInput, options?) => Promise<Hooks>`. The export is a
|
||||||
|
function, not a plain object literal, and the function returns an object
|
||||||
|
(return `{}` if there is nothing to register).
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { Plugin } from "@opencode-ai/plugin"
|
||||||
|
|
||||||
|
export default (async ({ client, project, directory, $ }) => {
|
||||||
|
return {
|
||||||
|
config: (cfg) => {
|
||||||
|
// cfg is the live merged config; mutate fields here.
|
||||||
|
},
|
||||||
|
"tool.execute.before": async (input, output) => {
|
||||||
|
// mutate output.args before the tool runs
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}) satisfies Plugin
|
||||||
|
```
|
||||||
|
|
||||||
|
Hook surface (mutate `output` in place; return `void`):
|
||||||
|
|
||||||
|
- `event(input)`: every bus event
|
||||||
|
- `config(cfg)`: once on init with the merged config
|
||||||
|
- `chat.message`, `chat.params`, `chat.headers`
|
||||||
|
- `tool.execute.before`, `tool.execute.after`
|
||||||
|
- `tool.definition`
|
||||||
|
- `command.execute.before`
|
||||||
|
- `shell.env`
|
||||||
|
- `permission.ask`
|
||||||
|
- `experimental.chat.messages.transform`, `experimental.chat.system.transform`,
|
||||||
|
`experimental.session.compacting`, `experimental.compaction.autocontinue`,
|
||||||
|
`experimental.text.complete`
|
||||||
|
|
||||||
|
Special object-shaped (not callbacks): `tool: { my_tool: { ... } }`,
|
||||||
|
`auth: { ... }`, `provider: { ... }`.
|
||||||
|
|
||||||
|
## MCP servers
|
||||||
|
|
||||||
|
`mcp:` is an object keyed by server name. Each server is discriminated by
|
||||||
|
`type`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcp": {
|
||||||
|
"playwright": {
|
||||||
|
"type": "local",
|
||||||
|
"command": ["npx", "-y", "@playwright/mcp"],
|
||||||
|
"enabled": true,
|
||||||
|
"env": { "BROWSER": "chromium" }
|
||||||
|
},
|
||||||
|
"github": {
|
||||||
|
"type": "remote",
|
||||||
|
"url": "https://...",
|
||||||
|
"enabled": true,
|
||||||
|
"headers": { "Authorization": "Bearer ${GITHUB_TOKEN}" }
|
||||||
|
},
|
||||||
|
"old-server": { "enabled": false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`command` is an array of strings. `type` is required. Use `enabled: false` to
|
||||||
|
disable a server inherited from a parent config.
|
||||||
|
|
||||||
|
## Permissions
|
||||||
|
|
||||||
|
```json
|
||||||
|
"permission": {
|
||||||
|
"edit": "deny",
|
||||||
|
"bash": { "git *": "allow", "rm *": "deny", "*": "ask" },
|
||||||
|
"external_directory": { "~/secrets/**": "deny", "*": "allow" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Actions: `"allow"`, `"ask"`, `"deny"`.
|
||||||
|
|
||||||
|
Per-tool value forms: `"allow"` shorthand (treated as `{"*": "allow"}`), or an
|
||||||
|
object `{ pattern: action }`. Within an object, **insertion order matters**.
|
||||||
|
opencode evaluates the LAST matching rule, so put broad rules first and narrow
|
||||||
|
rules last.
|
||||||
|
|
||||||
|
`permission: "allow"` (a string at the top level) is shorthand for "allow
|
||||||
|
everything" and is rarely what the user wants.
|
||||||
|
|
||||||
|
Known permission keys: `read, edit, glob, grep, list, bash, task,
|
||||||
|
external_directory, todowrite, question, webfetch, websearch, codesearch,
|
||||||
|
repo_clone, repo_overview, lsp, doom_loop, skill`. Some of these (`todowrite,
|
||||||
|
question, webfetch, websearch, codesearch, doom_loop`) only accept a flat
|
||||||
|
action, not a per-pattern object.
|
||||||
|
|
||||||
|
`external_directory` patterns are filesystem paths (use `~/`, absolute paths,
|
||||||
|
or globs like `~/projects/**`).
|
||||||
|
|
||||||
|
Per-agent `permission:` overrides top-level `permission:`. Plan Mode lives on
|
||||||
|
the `plan` agent's permission ruleset (`edit: deny *`).
|
||||||
|
|
||||||
|
## Escape hatches
|
||||||
|
|
||||||
|
When a user's config is broken and opencode won't start, these env vars help:
|
||||||
|
|
||||||
|
- `OPENCODE_DISABLE_PROJECT_CONFIG=1`: skip the project's local `opencode.json`
|
||||||
|
and start from globals only. Run from the project directory, opencode loads,
|
||||||
|
the user edits the broken file, then they restart without the flag.
|
||||||
|
- `OPENCODE_CONFIG=/path/to/file.json`: load an additional explicit config.
|
||||||
|
- `OPENCODE_CONFIG_CONTENT='{"$schema":"https://opencode.ai/config.json"}'`:
|
||||||
|
inject inline JSON as a final local-scope merge.
|
||||||
|
- `OPENCODE_DISABLE_DEFAULT_PLUGINS=1`: skip default plugins.
|
||||||
|
- `OPENCODE_PURE=1`: skip external plugins entirely.
|
||||||
|
- `OPENCODE_DISABLE_EXTERNAL_SKILLS=1`,
|
||||||
|
`OPENCODE_DISABLE_CLAUDE_CODE_SKILLS=1`: skip the external skill scans under
|
||||||
|
`~/.claude/` and `~/.agents/`.
|
||||||
|
|
||||||
|
## When proposing edits
|
||||||
|
|
||||||
|
- Validate against the schema before writing. If you are unsure of a field's
|
||||||
|
exact shape, fetch `https://opencode.ai/config.json` rather than guessing.
|
||||||
|
- Preserve `$schema` and any existing fields the user did not ask to change.
|
||||||
|
- For agent, skill, and plugin definitions, prefer creating new files in the
|
||||||
|
correct location over inlining everything in `opencode.json`.
|
||||||
|
- If the user's existing config is malformed, point them at the env-var escape
|
||||||
|
hatch above so they can edit from inside opencode without breaking their
|
||||||
|
session.
|
||||||
|
- opencode hard-fails on invalid config by design. There is no graceful
|
||||||
|
degradation, so get the shape right the first time.
|
||||||
@@ -25,8 +25,8 @@ export const FileDiff = Schema.Struct({
|
|||||||
// session response and broke session loading on Desktop.
|
// session response and broke session loading on Desktop.
|
||||||
file: Schema.optional(Schema.String),
|
file: Schema.optional(Schema.String),
|
||||||
patch: Schema.optional(Schema.String),
|
patch: Schema.optional(Schema.String),
|
||||||
additions: NonNegativeInt,
|
additions: Schema.Finite,
|
||||||
deletions: NonNegativeInt,
|
deletions: Schema.Finite,
|
||||||
status: Schema.optional(Schema.Literals(["added", "deleted", "modified"])),
|
status: Schema.optional(Schema.Literals(["added", "deleted", "modified"])),
|
||||||
})
|
})
|
||||||
.annotate({ identifier: "SnapshotFileDiff" })
|
.annotate({ identifier: "SnapshotFileDiff" })
|
||||||
|
|||||||
@@ -118,12 +118,12 @@ export namespace Step {
|
|||||||
finish: Schema.String,
|
finish: Schema.String,
|
||||||
cost: Schema.Finite,
|
cost: Schema.Finite,
|
||||||
tokens: Schema.Struct({
|
tokens: Schema.Struct({
|
||||||
input: NonNegativeInt,
|
input: Schema.Finite,
|
||||||
output: NonNegativeInt,
|
output: Schema.Finite,
|
||||||
reasoning: NonNegativeInt,
|
reasoning: Schema.Finite,
|
||||||
cache: Schema.Struct({
|
cache: Schema.Struct({
|
||||||
read: NonNegativeInt,
|
read: Schema.Finite,
|
||||||
write: NonNegativeInt,
|
write: Schema.Finite,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
snapshot: Schema.String.pipe(Schema.optional),
|
snapshot: Schema.String.pipe(Schema.optional),
|
||||||
@@ -305,7 +305,7 @@ export namespace Tool {
|
|||||||
|
|
||||||
export const RetryError = Schema.Struct({
|
export const RetryError = Schema.Struct({
|
||||||
message: Schema.String,
|
message: Schema.String,
|
||||||
statusCode: NonNegativeInt.pipe(Schema.optional),
|
statusCode: Schema.Finite.pipe(Schema.optional),
|
||||||
isRetryable: Schema.Boolean,
|
isRetryable: Schema.Boolean,
|
||||||
responseHeaders: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
|
responseHeaders: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
|
||||||
responseBody: Schema.String.pipe(Schema.optional),
|
responseBody: Schema.String.pipe(Schema.optional),
|
||||||
@@ -320,7 +320,7 @@ export const Retried = EventV2.define({
|
|||||||
aggregate: "sessionID",
|
aggregate: "sessionID",
|
||||||
schema: {
|
schema: {
|
||||||
...Base,
|
...Base,
|
||||||
attempt: NonNegativeInt,
|
attempt: Schema.Finite,
|
||||||
error: RetryError,
|
error: RetryError,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ import type { MCP as MCPNS } from "../../src/mcp/index"
|
|||||||
|
|
||||||
// Per-client state for controlling mock behavior
|
// Per-client state for controlling mock behavior
|
||||||
interface MockClientState {
|
interface MockClientState {
|
||||||
tools: Array<{ name: string; description?: string; inputSchema: object }>
|
tools: Array<{ name: string; description?: string; inputSchema: object; outputSchema?: object }>
|
||||||
listToolsCalls: number
|
listToolsCalls: number
|
||||||
|
requestCalls: number
|
||||||
listToolsShouldFail: boolean
|
listToolsShouldFail: boolean
|
||||||
listToolsError: string
|
listToolsError: string
|
||||||
listPromptsShouldFail: boolean
|
listPromptsShouldFail: boolean
|
||||||
@@ -36,6 +37,7 @@ function getOrCreateClientState(name?: string): MockClientState {
|
|||||||
state = {
|
state = {
|
||||||
tools: [{ name: "test_tool", description: "A test tool", inputSchema: { type: "object", properties: {} } }],
|
tools: [{ name: "test_tool", description: "A test tool", inputSchema: { type: "object", properties: {} } }],
|
||||||
listToolsCalls: 0,
|
listToolsCalls: 0,
|
||||||
|
requestCalls: 0,
|
||||||
listToolsShouldFail: false,
|
listToolsShouldFail: false,
|
||||||
listToolsError: "listTools failed",
|
listToolsError: "listTools failed",
|
||||||
listPromptsShouldFail: false,
|
listPromptsShouldFail: false,
|
||||||
@@ -139,6 +141,12 @@ void mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({
|
|||||||
return { tools: this._state?.tools ?? [] }
|
return { tools: this._state?.tools ?? [] }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async request(request: { method: string }, schema: { parse: (value: unknown) => unknown }) {
|
||||||
|
if (this._state) this._state.requestCalls++
|
||||||
|
if (request.method === "tools/list") return schema.parse({ tools: this._state?.tools ?? [] })
|
||||||
|
throw new Error(`unsupported request: ${request.method}`)
|
||||||
|
}
|
||||||
|
|
||||||
async listPrompts() {
|
async listPrompts() {
|
||||||
if (this._state?.listPromptsShouldFail) {
|
if (this._state?.listPromptsShouldFail) {
|
||||||
throw new Error("listPrompts failed")
|
throw new Error("listPrompts failed")
|
||||||
@@ -205,6 +213,11 @@ function withInstance(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function statusName(status: Record<string, MCPNS.Status> | MCPNS.Status, server: string) {
|
||||||
|
if ("status" in status) return status.status
|
||||||
|
return status[server]?.status
|
||||||
|
}
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// Test: tools() are cached after connect
|
// Test: tools() are cached after connect
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
@@ -433,6 +446,59 @@ test(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
test(
|
||||||
|
"falls back when MCP output schema refs fail SDK tool discovery",
|
||||||
|
withInstance({}, (mcp) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
lastCreatedClientName = "stitch-like-server"
|
||||||
|
const serverState = getOrCreateClientState("stitch-like-server")
|
||||||
|
serverState.listToolsShouldFail = true
|
||||||
|
serverState.listToolsError = "can't resolve reference #/$defs/ScreenInstance from id #"
|
||||||
|
serverState.tools = [
|
||||||
|
{
|
||||||
|
name: "render_screen",
|
||||||
|
description: "renders a screen",
|
||||||
|
inputSchema: { type: "object", properties: { prompt: { type: "string" } }, required: ["prompt"] },
|
||||||
|
outputSchema: { type: "object", properties: { screen: { $ref: "#/$defs/ScreenInstance" } } },
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const addResult = yield* mcp.add("stitch-like-server", {
|
||||||
|
type: "local",
|
||||||
|
command: ["echo", "test"],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(statusName(addResult.status, "stitch-like-server")).toBe("connected")
|
||||||
|
|
||||||
|
const tools = yield* mcp.tools()
|
||||||
|
expect(Object.keys(tools).some((key) => key.includes("render_screen"))).toBe(true)
|
||||||
|
expect(serverState.listToolsCalls).toBe(1)
|
||||||
|
expect(serverState.requestCalls).toBe(1)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
test(
|
||||||
|
"does not fall back for non-schema MCP tool discovery errors",
|
||||||
|
withInstance({}, (mcp) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
lastCreatedClientName = "broken-server"
|
||||||
|
const serverState = getOrCreateClientState("broken-server")
|
||||||
|
serverState.listToolsShouldFail = true
|
||||||
|
serverState.listToolsError = "transport closed"
|
||||||
|
|
||||||
|
const addResult = yield* mcp.add("broken-server", {
|
||||||
|
type: "local",
|
||||||
|
command: ["echo", "test"],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(statusName(addResult.status, "broken-server")).toBe("failed")
|
||||||
|
expect(serverState.listToolsCalls).toBe(1)
|
||||||
|
expect(serverState.requestCalls).toBe(0)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// Test: disabled server via config
|
// Test: disabled server via config
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ process.env["XDG_CONFIG_HOME"] = path.join(dir, "config")
|
|||||||
process.env["XDG_STATE_HOME"] = path.join(dir, "state")
|
process.env["XDG_STATE_HOME"] = path.join(dir, "state")
|
||||||
process.env["OPENCODE_MODELS_PATH"] = path.join(import.meta.dir, "tool", "fixtures", "models-api.json")
|
process.env["OPENCODE_MODELS_PATH"] = path.join(import.meta.dir, "tool", "fixtures", "models-api.json")
|
||||||
process.env["OPENCODE_EXPERIMENTAL_EVENT_SYSTEM"] = "true"
|
process.env["OPENCODE_EXPERIMENTAL_EVENT_SYSTEM"] = "true"
|
||||||
|
// Tests assert exact skill counts from disk discovery; the built-in
|
||||||
|
// customize-opencode skill is opt-in for stable channels and on by default
|
||||||
|
// for unstable channels (including "local" where CI runs). Disable it here
|
||||||
|
// so disk-discovery tests aren't off-by-one.
|
||||||
|
process.env["OPENCODE_EXPERIMENTAL_CUSTOMIZE_SKILL"] = "false"
|
||||||
|
|
||||||
// Set test home directory to isolate tests from user's actual home directory
|
// Set test home directory to isolate tests from user's actual home directory
|
||||||
// This prevents tests from picking up real user configs/skills from ~/.claude/skills
|
// This prevents tests from picking up real user configs/skills from ~/.claude/skills
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ import { type Scenario } from "./types"
|
|||||||
|
|
||||||
void (await import("@opencode-ai/core/util/log")).init({ print: false })
|
void (await import("@opencode-ai/core/util/log")).init({ print: false })
|
||||||
|
|
||||||
|
function cursor(input: Record<string, unknown>) {
|
||||||
|
return Buffer.from(JSON.stringify(input)).toString("base64url")
|
||||||
|
}
|
||||||
|
|
||||||
const scenarios: Scenario[] = [
|
const scenarios: Scenario[] = [
|
||||||
http.protected
|
http.protected
|
||||||
.get("/global/health", "global.health")
|
.get("/global/health", "global.health")
|
||||||
@@ -520,7 +524,10 @@ const scenarios: Scenario[] = [
|
|||||||
yield* ctx.worktreeRemove(ctx.state.directory)
|
yield* ctx.worktreeRemove(ctx.state.directory)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
http.protected.get("/experimental/session", "experimental.session.list").json(200, array),
|
http.protected
|
||||||
|
.get("/experimental/session", "experimental.session.list")
|
||||||
|
.at((ctx) => ({ path: "/experimental/session?roots=false&archived=false", headers: ctx.headers() }))
|
||||||
|
.json(200, array),
|
||||||
http.protected.get("/experimental/resource", "experimental.resource.list").json(),
|
http.protected.get("/experimental/resource", "experimental.resource.list").json(),
|
||||||
http.protected
|
http.protected
|
||||||
.post("/sync/history", "sync.history.list")
|
.post("/sync/history", "sync.history.list")
|
||||||
@@ -598,6 +605,64 @@ const scenarios: Scenario[] = [
|
|||||||
},
|
},
|
||||||
"none",
|
"none",
|
||||||
),
|
),
|
||||||
|
http.protected
|
||||||
|
.get("/api/session", "v2.session.list.filters")
|
||||||
|
.at((ctx) => ({
|
||||||
|
path: `/api/session?${new URLSearchParams({
|
||||||
|
limit: "2",
|
||||||
|
order: "asc",
|
||||||
|
path: ".",
|
||||||
|
roots: "false",
|
||||||
|
start: "0",
|
||||||
|
search: "missing",
|
||||||
|
directory: ctx.directory ?? "",
|
||||||
|
})}`,
|
||||||
|
headers: ctx.headers(),
|
||||||
|
}))
|
||||||
|
.json(
|
||||||
|
200,
|
||||||
|
(body) => {
|
||||||
|
object(body)
|
||||||
|
array(body.items)
|
||||||
|
object(body.cursor)
|
||||||
|
},
|
||||||
|
"none",
|
||||||
|
),
|
||||||
|
http.protected
|
||||||
|
.get("/api/session", "v2.session.list.cursor")
|
||||||
|
.at((ctx) => ({
|
||||||
|
path: `/api/session?${new URLSearchParams({
|
||||||
|
limit: "2",
|
||||||
|
directory: ctx.directory ?? "",
|
||||||
|
cursor: cursor({
|
||||||
|
id: "ses_httpapi_missing",
|
||||||
|
time: 0,
|
||||||
|
order: "desc",
|
||||||
|
direction: "next",
|
||||||
|
directory: ctx.directory,
|
||||||
|
}),
|
||||||
|
})}`,
|
||||||
|
headers: ctx.headers(),
|
||||||
|
}))
|
||||||
|
.json(
|
||||||
|
200,
|
||||||
|
(body) => {
|
||||||
|
object(body)
|
||||||
|
array(body.items)
|
||||||
|
object(body.cursor)
|
||||||
|
},
|
||||||
|
"none",
|
||||||
|
),
|
||||||
|
http.protected
|
||||||
|
.get("/api/session", "v2.session.list.cursor.invalid")
|
||||||
|
.at((ctx) => ({
|
||||||
|
path: `/api/session?${new URLSearchParams({
|
||||||
|
cursor: cursor({ id: "ses_httpapi_missing", time: 0, order: "desc", direction: "next" }),
|
||||||
|
search: "not-allowed-with-cursor",
|
||||||
|
})}`,
|
||||||
|
headers: ctx.headers(),
|
||||||
|
}))
|
||||||
|
.status(400, undefined, "none"),
|
||||||
http.protected
|
http.protected
|
||||||
.get("/api/session/{sessionID}/context", "v2.session.context")
|
.get("/api/session/{sessionID}/context", "v2.session.context")
|
||||||
.at((ctx) => ({
|
.at((ctx) => ({
|
||||||
@@ -620,6 +685,53 @@ const scenarios: Scenario[] = [
|
|||||||
},
|
},
|
||||||
"none",
|
"none",
|
||||||
),
|
),
|
||||||
|
http.protected
|
||||||
|
.get("/api/session/{sessionID}/message", "v2.session.messages.params")
|
||||||
|
.at((ctx) => ({
|
||||||
|
path: `${route("/api/session/{sessionID}/message", { sessionID: "ses_httpapi_missing" })}?${new URLSearchParams({
|
||||||
|
limit: "2",
|
||||||
|
order: "asc",
|
||||||
|
})}`,
|
||||||
|
headers: ctx.headers(),
|
||||||
|
}))
|
||||||
|
.json(
|
||||||
|
200,
|
||||||
|
(body) => {
|
||||||
|
object(body)
|
||||||
|
array(body.items)
|
||||||
|
object(body.cursor)
|
||||||
|
},
|
||||||
|
"none",
|
||||||
|
),
|
||||||
|
http.protected
|
||||||
|
.get("/api/session/{sessionID}/message", "v2.session.messages.cursor")
|
||||||
|
.at((ctx) => ({
|
||||||
|
path: `${route("/api/session/{sessionID}/message", { sessionID: "ses_httpapi_missing" })}?${new URLSearchParams({
|
||||||
|
limit: "2",
|
||||||
|
directory: ctx.directory ?? "",
|
||||||
|
cursor: cursor({ id: "msg_httpapi_missing", time: 0, order: "desc", direction: "next" }),
|
||||||
|
})}`,
|
||||||
|
headers: ctx.headers(),
|
||||||
|
}))
|
||||||
|
.json(
|
||||||
|
200,
|
||||||
|
(body) => {
|
||||||
|
object(body)
|
||||||
|
array(body.items)
|
||||||
|
object(body.cursor)
|
||||||
|
},
|
||||||
|
"none",
|
||||||
|
),
|
||||||
|
http.protected
|
||||||
|
.get("/api/session/{sessionID}/message", "v2.session.messages.cursor.invalid")
|
||||||
|
.at((ctx) => ({
|
||||||
|
path: `${route("/api/session/{sessionID}/message", { sessionID: "ses_httpapi_missing" })}?${new URLSearchParams({
|
||||||
|
cursor: cursor({ id: "msg_httpapi_missing", time: 0, order: "desc", direction: "next" }),
|
||||||
|
order: "asc",
|
||||||
|
})}`,
|
||||||
|
headers: ctx.headers(),
|
||||||
|
}))
|
||||||
|
.status(400, undefined, "none"),
|
||||||
http.protected
|
http.protected
|
||||||
.post("/api/session/{sessionID}/prompt", "v2.session.prompt.invalid")
|
.post("/api/session/{sessionID}/prompt", "v2.session.prompt.invalid")
|
||||||
.at((ctx) => ({
|
.at((ctx) => ({
|
||||||
|
|||||||
@@ -1,14 +1,73 @@
|
|||||||
import { afterEach, describe, expect } from "bun:test"
|
import { afterEach, describe, expect } from "bun:test"
|
||||||
import { Effect } from "effect"
|
import { Effect, Schema } from "effect"
|
||||||
|
import { OpenApi } from "effect/unstable/httpapi"
|
||||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||||
import { Server } from "../../src/server/server"
|
import { Server } from "../../src/server/server"
|
||||||
import { SessionID } from "../../src/session/schema"
|
import { SessionID } from "../../src/session/schema"
|
||||||
|
import { PublicApi } from "../../src/server/routes/instance/httpapi/public"
|
||||||
|
import {
|
||||||
|
FilePaths,
|
||||||
|
FileQuery,
|
||||||
|
FindFileQuery,
|
||||||
|
FindTextQuery,
|
||||||
|
} from "../../src/server/routes/instance/httpapi/groups/file"
|
||||||
|
import {
|
||||||
|
ExperimentalPaths,
|
||||||
|
SessionListQuery as ExperimentalSessionListQuery,
|
||||||
|
ToolListQuery,
|
||||||
|
} from "../../src/server/routes/instance/httpapi/groups/experimental"
|
||||||
|
import { InstancePaths, VcsDiffQuery } from "../../src/server/routes/instance/httpapi/groups/instance"
|
||||||
|
import {
|
||||||
|
ListQuery as SessionListQuery,
|
||||||
|
MessagesQuery,
|
||||||
|
SessionPaths,
|
||||||
|
} from "../../src/server/routes/instance/httpapi/groups/session"
|
||||||
|
import { MessagesQuery as V2MessagesQuery } from "../../src/server/routes/instance/httpapi/groups/v2/message"
|
||||||
|
import { SessionsQuery as V2SessionsQuery } from "../../src/server/routes/instance/httpapi/groups/v2/session"
|
||||||
|
import { QueryBoolean } from "../../src/server/routes/instance/httpapi/groups/query"
|
||||||
import { resetDatabase } from "../fixture/db"
|
import { resetDatabase } from "../fixture/db"
|
||||||
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||||
import { it } from "../lib/effect"
|
import { it } from "../lib/effect"
|
||||||
|
|
||||||
const originalWorkspaces = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
|
const originalWorkspaces = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
|
||||||
|
|
||||||
|
type Method = "get" | "post" | "put" | "delete" | "patch"
|
||||||
|
type QuerySchema = { readonly fields: Record<string, unknown> }
|
||||||
|
type OpenApiSchema = { readonly maximum?: number; readonly minimum?: number; readonly type?: string }
|
||||||
|
type OpenApiParameter = { readonly name: string; readonly in: string; readonly schema?: OpenApiSchema }
|
||||||
|
type OpenApiOperation = { readonly parameters?: readonly OpenApiParameter[] }
|
||||||
|
|
||||||
|
const openApiDriftRoutes = [
|
||||||
|
{ method: "get", path: SessionPaths.list, query: SessionListQuery },
|
||||||
|
{ method: "get", path: SessionPaths.messages, query: MessagesQuery },
|
||||||
|
{ method: "get", path: FilePaths.findFile, query: FindFileQuery },
|
||||||
|
{ method: "get", path: FilePaths.findText, query: FindTextQuery },
|
||||||
|
{ method: "get", path: FilePaths.list, query: FileQuery },
|
||||||
|
{ method: "get", path: ExperimentalPaths.session, query: ExperimentalSessionListQuery },
|
||||||
|
{ method: "get", path: ExperimentalPaths.tool, query: ToolListQuery },
|
||||||
|
{ method: "get", path: InstancePaths.vcsDiff, query: VcsDiffQuery },
|
||||||
|
{ method: "get", path: "/api/session", query: V2SessionsQuery },
|
||||||
|
{ method: "get", path: "/api/session/:sessionID/message", query: V2MessagesQuery },
|
||||||
|
] satisfies Array<{ method: Method; path: string; query: QuerySchema }>
|
||||||
|
|
||||||
|
const numericSdkQueryParams = [
|
||||||
|
{ method: "get", path: ExperimentalPaths.session, name: "start", schema: { type: "number" } },
|
||||||
|
{ method: "get", path: ExperimentalPaths.session, name: "cursor", schema: { type: "number" } },
|
||||||
|
{ method: "get", path: ExperimentalPaths.session, name: "limit", schema: { type: "number" } },
|
||||||
|
{ method: "get", path: FilePaths.findFile, name: "limit", schema: { type: "integer", minimum: 1, maximum: 200 } },
|
||||||
|
{ method: "get", path: SessionPaths.list, name: "start", schema: { type: "number" } },
|
||||||
|
{ method: "get", path: SessionPaths.list, name: "limit", schema: { type: "number" } },
|
||||||
|
{
|
||||||
|
method: "get",
|
||||||
|
path: SessionPaths.messages,
|
||||||
|
name: "limit",
|
||||||
|
schema: { type: "integer", minimum: 0, maximum: Number.MAX_SAFE_INTEGER },
|
||||||
|
},
|
||||||
|
{ method: "get", path: "/api/session", name: "limit", schema: { type: "number" } },
|
||||||
|
{ method: "get", path: "/api/session", name: "start", schema: { type: "number" } },
|
||||||
|
{ method: "get", path: "/api/session/:sessionID/message", name: "limit", schema: { type: "number" } },
|
||||||
|
] satisfies Array<{ method: Method; path: string; name: string; schema: OpenApiSchema }>
|
||||||
|
|
||||||
function app() {
|
function app() {
|
||||||
return Server.Default().app
|
return Server.Default().app
|
||||||
}
|
}
|
||||||
@@ -27,6 +86,33 @@ function withTmp<A, E, R>(
|
|||||||
).pipe(Effect.flatMap(fn))
|
).pipe(Effect.flatMap(fn))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openApiPath(path: string) {
|
||||||
|
return path.replace(/:([A-Za-z0-9_]+)/g, "{$1}")
|
||||||
|
}
|
||||||
|
|
||||||
|
function queryParameters(operation: OpenApiOperation | undefined) {
|
||||||
|
return (operation?.parameters ?? []).filter((param) => param.in === "query").map((param) => param.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
function queryParameter(operation: OpenApiOperation | undefined, name: string) {
|
||||||
|
return (operation?.parameters ?? []).find((param) => param.in === "query" && param.name === name)
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertAdvertisedQueryParamsAreRuntimeFields(input: {
|
||||||
|
readonly method: Method
|
||||||
|
readonly operation: OpenApiOperation | undefined
|
||||||
|
readonly path: string
|
||||||
|
readonly query: QuerySchema
|
||||||
|
}) {
|
||||||
|
const runtimeFields = new Set(Object.keys(input.query.fields))
|
||||||
|
const advertisedOnly = queryParameters(input.operation).filter((name) => !runtimeFields.has(name))
|
||||||
|
|
||||||
|
expect(
|
||||||
|
advertisedOnly,
|
||||||
|
`${input.method.toUpperCase()} ${input.path} advertises query params not accepted by runtime schema`,
|
||||||
|
).toEqual([])
|
||||||
|
}
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = originalWorkspaces
|
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = originalWorkspaces
|
||||||
await disposeAllInstances()
|
await disposeAllInstances()
|
||||||
@@ -44,6 +130,68 @@ describe("httpapi query schema drift", () => {
|
|||||||
expect(status, `route ${url} 400'd, query schema is missing routing fields`).not.toBe(400)
|
expect(status, `route ${url} 400'd, query schema is missing routing fields`).not.toBe(400)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it.effect(
|
||||||
|
"boolean query schema accepts only true and false strings",
|
||||||
|
Effect.sync(() => {
|
||||||
|
const decode = Schema.decodeUnknownSync(QueryBoolean)
|
||||||
|
const encode = Schema.encodeUnknownSync(QueryBoolean)
|
||||||
|
|
||||||
|
expect(decode("true")).toBe(true)
|
||||||
|
expect(decode("false")).toBe(false)
|
||||||
|
expect(encode(true)).toBe("true")
|
||||||
|
expect(encode(false)).toBe("false")
|
||||||
|
|
||||||
|
for (const input of ["1", "yes", "True", "", true, false]) {
|
||||||
|
expect(() => decode(input)).toThrow()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect(
|
||||||
|
"OpenAPI workspace query params are declared by runtime query schemas",
|
||||||
|
Effect.sync(() => {
|
||||||
|
const spec = OpenApi.fromApi(PublicApi)
|
||||||
|
for (const route of openApiDriftRoutes) {
|
||||||
|
assertAdvertisedQueryParamsAreRuntimeFields({
|
||||||
|
...route,
|
||||||
|
operation: spec.paths[openApiPath(route.path)]?.[route.method],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect(
|
||||||
|
"OpenAPI numeric query params preserve generated SDK call shapes",
|
||||||
|
Effect.sync(() => {
|
||||||
|
const spec = OpenApi.fromApi(PublicApi)
|
||||||
|
for (const expected of numericSdkQueryParams) {
|
||||||
|
expect(
|
||||||
|
queryParameter(spec.paths[openApiPath(expected.path)]?.[expected.method], expected.name)?.schema,
|
||||||
|
`${expected.method.toUpperCase()} ${expected.path} ${expected.name}`,
|
||||||
|
).toEqual(expected.schema)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect(
|
||||||
|
"drift assertion catches spec-only workspace query params",
|
||||||
|
Effect.sync(() => {
|
||||||
|
expect(() =>
|
||||||
|
assertAdvertisedQueryParamsAreRuntimeFields({
|
||||||
|
method: "get",
|
||||||
|
operation: {
|
||||||
|
parameters: [
|
||||||
|
{ name: "directory", in: "query" },
|
||||||
|
{ name: "workspace", in: "query" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
path: "/fixture",
|
||||||
|
query: { fields: {} },
|
||||||
|
}),
|
||||||
|
).toThrow("advertises query params not accepted by runtime schema")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.live(
|
it.live(
|
||||||
"session list accepts directory and workspace",
|
"session list accepts directory and workspace",
|
||||||
withTmp({ config: { formatter: false, lsp: false } }, (tmp) =>
|
withTmp({ config: { formatter: false, lsp: false } }, (tmp) =>
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
// Regression: a stored step-finish part with a negative token count made the
|
||||||
|
// messages endpoint 400. Some providers reported `outputTokens` excluding
|
||||||
|
// reasoning while also reporting `reasoningTokens` separately, so the
|
||||||
|
// `outputTokens - reasoningTokens` math in Session.getUsage underflowed to
|
||||||
|
// negative. The pre-fix `safe()` clamp only guarded against non-finite. The
|
||||||
|
// strict `NonNegativeInt` schema then made every load of the message list
|
||||||
|
// fail to encode, killing Desktop boot for every user with such a row.
|
||||||
|
import { afterEach, describe, expect } from "bun:test"
|
||||||
|
import { Effect } from "effect"
|
||||||
|
import { eq } from "drizzle-orm"
|
||||||
|
import { ModelID, ProviderID } from "../../src/provider/schema"
|
||||||
|
import { WithInstance } from "../../src/project/with-instance"
|
||||||
|
import { Server } from "../../src/server/server"
|
||||||
|
import { SessionPaths } from "../../src/server/routes/instance/httpapi/groups/session"
|
||||||
|
import { Session } from "@/session/session"
|
||||||
|
import { MessageID, PartID } from "../../src/session/schema"
|
||||||
|
import * as Database from "@/storage/db"
|
||||||
|
import { PartTable } from "@/session/session.sql"
|
||||||
|
import { resetDatabase } from "../fixture/db"
|
||||||
|
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||||
|
import { it } from "../lib/effect"
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await disposeAllInstances()
|
||||||
|
await resetDatabase()
|
||||||
|
})
|
||||||
|
|
||||||
|
function seedNegativeTokenSession(directory: string) {
|
||||||
|
return Effect.promise(async () =>
|
||||||
|
WithInstance.provide({
|
||||||
|
directory,
|
||||||
|
fn: () =>
|
||||||
|
Effect.runPromise(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const session = yield* Session.Service
|
||||||
|
const info = yield* session.create({})
|
||||||
|
const message = yield* session.updateMessage({
|
||||||
|
id: MessageID.ascending(),
|
||||||
|
role: "user",
|
||||||
|
sessionID: info.id,
|
||||||
|
agent: "build",
|
||||||
|
model: { providerID: ProviderID.make("test"), modelID: ModelID.make("test") },
|
||||||
|
time: { created: Date.now() },
|
||||||
|
})
|
||||||
|
const partID = PartID.ascending()
|
||||||
|
yield* session.updatePart({
|
||||||
|
id: partID,
|
||||||
|
sessionID: info.id,
|
||||||
|
messageID: message.id,
|
||||||
|
type: "step-finish",
|
||||||
|
reason: "stop",
|
||||||
|
cost: 0,
|
||||||
|
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||||
|
})
|
||||||
|
|
||||||
|
// Bypass the schema with a direct SQL update to install the
|
||||||
|
// negative `output` value we want to test loading.
|
||||||
|
Database.use((db) =>
|
||||||
|
db
|
||||||
|
.update(PartTable)
|
||||||
|
.set({
|
||||||
|
data: {
|
||||||
|
type: "step-finish",
|
||||||
|
reason: "stop",
|
||||||
|
cost: 0,
|
||||||
|
tokens: { input: 0, output: -42, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||||
|
} as never,
|
||||||
|
})
|
||||||
|
.where(eq(PartTable.id, partID))
|
||||||
|
.run(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return info.id
|
||||||
|
}).pipe(Effect.provide(Session.defaultLayer)),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("messages endpoint tolerates legacy negative token counts", () => {
|
||||||
|
it.live(
|
||||||
|
"returns 200 even when a step-finish part has tokens.output < 0",
|
||||||
|
Effect.acquireRelease(
|
||||||
|
Effect.promise(() => tmpdir({ config: { formatter: false, lsp: false } })),
|
||||||
|
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||||
|
).pipe(
|
||||||
|
Effect.flatMap((tmp) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const sessionID = yield* seedNegativeTokenSession(tmp.path)
|
||||||
|
const url = `${SessionPaths.messages.replace(":sessionID", sessionID)}?limit=80&directory=${encodeURIComponent(tmp.path)}`
|
||||||
|
const res = yield* Effect.promise(async () => Server.Default().app.request(url))
|
||||||
|
expect(res.status, "messages endpoint 400'd on legacy negative tokens").not.toBe(400)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.14.44",
|
"version": "1.14.46",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.14.44",
|
"version": "1.14.46",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -4163,6 +4163,13 @@ export class Session3 extends HeyApiClient {
|
|||||||
parameters?: {
|
parameters?: {
|
||||||
directory?: string
|
directory?: string
|
||||||
workspace?: string
|
workspace?: string
|
||||||
|
limit?: number
|
||||||
|
order?: "asc" | "desc"
|
||||||
|
path?: string
|
||||||
|
roots?: boolean | "true" | "false"
|
||||||
|
start?: number
|
||||||
|
search?: string
|
||||||
|
cursor?: string
|
||||||
},
|
},
|
||||||
options?: Options<never, ThrowOnError>,
|
options?: Options<never, ThrowOnError>,
|
||||||
) {
|
) {
|
||||||
@@ -4173,6 +4180,13 @@ export class Session3 extends HeyApiClient {
|
|||||||
args: [
|
args: [
|
||||||
{ in: "query", key: "directory" },
|
{ in: "query", key: "directory" },
|
||||||
{ in: "query", key: "workspace" },
|
{ in: "query", key: "workspace" },
|
||||||
|
{ in: "query", key: "limit" },
|
||||||
|
{ in: "query", key: "order" },
|
||||||
|
{ in: "query", key: "path" },
|
||||||
|
{ in: "query", key: "roots" },
|
||||||
|
{ in: "query", key: "start" },
|
||||||
|
{ in: "query", key: "search" },
|
||||||
|
{ in: "query", key: "cursor" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -4331,6 +4345,9 @@ export class Session3 extends HeyApiClient {
|
|||||||
sessionID: string
|
sessionID: string
|
||||||
directory?: string
|
directory?: string
|
||||||
workspace?: string
|
workspace?: string
|
||||||
|
limit?: number
|
||||||
|
order?: "asc" | "desc"
|
||||||
|
cursor?: string
|
||||||
},
|
},
|
||||||
options?: Options<never, ThrowOnError>,
|
options?: Options<never, ThrowOnError>,
|
||||||
) {
|
) {
|
||||||
@@ -4342,6 +4359,9 @@ export class Session3 extends HeyApiClient {
|
|||||||
{ in: "path", key: "sessionID" },
|
{ in: "path", key: "sessionID" },
|
||||||
{ in: "query", key: "directory" },
|
{ in: "query", key: "directory" },
|
||||||
{ in: "query", key: "workspace" },
|
{ in: "query", key: "workspace" },
|
||||||
|
{ in: "query", key: "limit" },
|
||||||
|
{ in: "query", key: "order" },
|
||||||
|
{ in: "query", key: "cursor" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -6235,6 +6235,16 @@ export type V2SessionListData = {
|
|||||||
query?: {
|
query?: {
|
||||||
directory?: string
|
directory?: string
|
||||||
workspace?: string
|
workspace?: string
|
||||||
|
limit?: number
|
||||||
|
order?: "asc" | "desc"
|
||||||
|
path?: string
|
||||||
|
roots?: boolean | "true" | "false"
|
||||||
|
start?: number
|
||||||
|
search?: string
|
||||||
|
/**
|
||||||
|
* Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. Do not combine with order or filters.
|
||||||
|
*/
|
||||||
|
cursor?: string
|
||||||
}
|
}
|
||||||
url: "/api/session"
|
url: "/api/session"
|
||||||
}
|
}
|
||||||
@@ -6352,6 +6362,12 @@ export type V2SessionMessagesData = {
|
|||||||
query?: {
|
query?: {
|
||||||
directory?: string
|
directory?: string
|
||||||
workspace?: string
|
workspace?: string
|
||||||
|
limit?: number
|
||||||
|
order?: "asc" | "desc"
|
||||||
|
/**
|
||||||
|
* Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. Do not combine with order.
|
||||||
|
*/
|
||||||
|
cursor?: string
|
||||||
}
|
}
|
||||||
url: "/api/session/{sessionID}/message"
|
url: "/api/session/{sessionID}/message"
|
||||||
}
|
}
|
||||||
|
|||||||
+1178
-1128
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.14.44",
|
"version": "1.14.46",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.14.44",
|
"version": "1.14.46",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"version": "1.14.44",
|
"version": "1.14.46",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"displayName": "opencode",
|
"displayName": "opencode",
|
||||||
"description": "opencode for VS Code",
|
"description": "opencode for VS Code",
|
||||||
"version": "1.14.44",
|
"version": "1.14.46",
|
||||||
"publisher": "sst-dev",
|
"publisher": "sst-dev",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
Reference in New Issue
Block a user