Compare commits

..

2 Commits

Author SHA1 Message Date
Simon Klee db2e1f1dfb Merge branch 'dev' into simon-thread-network-defaults 2026-04-24 13:20:42 +02:00
Simon Klee c4c0be106b cli/tui: honor configured network defaults in thread mode
Thread mode resolves network options without loading config-backed
defaults, so configured hostname, port and mDNS settings are ignored
unless they are passed on the command line.

Use the config-aware resolver so thread mode follows the same network
resolution path as the rest of the CLI.

675a46e23e regressed a fix from 390b0a79
2026-04-24 12:23:28 +02:00
33 changed files with 169 additions and 420 deletions
+16 -16
View File
@@ -29,7 +29,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "1.14.24",
"version": "1.14.22",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -83,7 +83,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.14.24",
"version": "1.14.22",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -117,7 +117,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.14.24",
"version": "1.14.22",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -144,7 +144,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.14.24",
"version": "1.14.22",
"dependencies": {
"@ai-sdk/anthropic": "3.0.64",
"@ai-sdk/openai": "3.0.48",
@@ -168,7 +168,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.14.24",
"version": "1.14.22",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -192,7 +192,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.14.24",
"version": "1.14.22",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -225,7 +225,7 @@
},
"packages/desktop-electron": {
"name": "@opencode-ai/desktop-electron",
"version": "1.14.24",
"version": "1.14.22",
"dependencies": {
"drizzle-orm": "catalog:",
"effect": "catalog:",
@@ -269,7 +269,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.14.24",
"version": "1.14.22",
"dependencies": {
"@opencode-ai/shared": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -298,7 +298,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.14.24",
"version": "1.14.22",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -314,7 +314,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.14.24",
"version": "1.14.22",
"bin": {
"opencode": "./bin/opencode",
},
@@ -459,7 +459,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.14.24",
"version": "1.14.22",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"effect": "catalog:",
@@ -494,7 +494,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.14.24",
"version": "1.14.22",
"dependencies": {
"cross-spawn": "catalog:",
},
@@ -509,7 +509,7 @@
},
"packages/shared": {
"name": "@opencode-ai/shared",
"version": "1.14.24",
"version": "1.14.22",
"bin": {
"opencode": "./bin/opencode",
},
@@ -533,7 +533,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.14.24",
"version": "1.14.22",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -568,7 +568,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.14.24",
"version": "1.14.22",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -617,7 +617,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.14.24",
"version": "1.14.22",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.14.24",
"version": "1.14.22",
"description": "",
"type": "module",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.14.24",
"version": "1.14.22",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.14.24",
"version": "1.14.22",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.14.24",
"version": "1.14.22",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.14.24",
"version": "1.14.22",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop-electron",
"private": true,
"version": "1.14.24",
"version": "1.14.22",
"type": "module",
"license": "MIT",
"homepage": "https://opencode.ai",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "1.14.24",
"version": "1.14.22",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.14.24",
"version": "1.14.22",
"private": true,
"type": "module",
"license": "MIT",
+6 -6
View File
@@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The open source coding agent."
version = "1.14.24"
version = "1.14.22"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/anomalyco/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.24/opencode-darwin-arm64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.22/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.24/opencode-darwin-x64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.22/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.24/opencode-linux-arm64.tar.gz"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.22/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.24/opencode-linux-x64.tar.gz"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.22/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.24/opencode-windows-x64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.22/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.14.24",
"version": "1.14.22",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.14.24",
"version": "1.14.22",
"name": "opencode",
"type": "module",
"license": "MIT",
+5 -8
View File
@@ -412,12 +412,10 @@ Current instance route inventory:
- `workspace` - `bridged`
best small reads: `GET /experimental/workspace/adaptor`, `GET /experimental/workspace`, `GET /experimental/workspace/status`
defer create/remove mutations first
- `file` - `bridged` (partial)
bridged endpoints: `GET /file`, `GET /file/content`, `GET /file/status`
defer search endpoints first
- `mcp` - `bridged` (partial)
bridged endpoints: `GET /mcp`
defer interactive OAuth/auth flows first
- `file` - `later`
good JSON-only candidate set, but larger than the current first-wave slices
- `mcp` - `later`
has JSON-only endpoints, but interactive OAuth/auth flows make it a worse early fit
- `session` - `defer`
large, stateful, mixes CRUD with prompt/shell/command/share/revert flows and a streaming route
- `event` - `defer`
@@ -451,8 +449,7 @@ Recommended near-term sequence:
- [x] port `project` read endpoints (`GET /project`, `GET /project/current`)
- [x] port `GET /config` full read endpoint
- [x] port `workspace` read endpoints
- [x] port `file` JSON read endpoints
- [x] port `mcp` status read endpoint
- [ ] port `file` JSON read endpoints
- [ ] decide when to remove the flag and make Effect routes the default
## Rule of thumb
+2 -2
View File
@@ -8,7 +8,7 @@ import { UI } from "@/cli/ui"
import { Log } from "@/util"
import { errorMessage } from "@/util/error"
import { withTimeout } from "@/util/timeout"
import { withNetworkOptions, resolveNetworkOptionsNoConfig } from "@/cli/network"
import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network"
import { Filesystem } from "@/util"
import type { GlobalEvent } from "@opencode-ai/sdk/v2"
import type { EventSource } from "./context/sdk"
@@ -182,7 +182,7 @@ export const TuiThreadCommand = cmd({
const prompt = await input(args.prompt)
const config = await TuiConfig.get()
const network = resolveNetworkOptionsNoConfig(args)
const network = await resolveNetworkOptions(args)
const external =
process.argv.includes("--port") ||
process.argv.includes("--hostname") ||
+54 -48
View File
@@ -9,63 +9,69 @@ import { formatPatch, structuredPatch } from "diff"
import fuzzysort from "fuzzysort"
import ignore from "ignore"
import path from "path"
import z from "zod"
import { Global } from "../global"
import { Instance } from "../project/instance"
import { Log } from "../util"
import { Protected } from "./protected"
import { Ripgrep } from "./ripgrep"
import { zod } from "@/util/effect-zod"
import { type DeepMutable, withStatics } from "@/util/schema"
export const Info = Schema.Struct({
path: Schema.String,
added: Schema.Int,
removed: Schema.Int,
status: Schema.Literals(["added", "deleted", "modified"]),
})
.annotate({ identifier: "File" })
.pipe(withStatics((s) => ({ zod: zod(s) })))
export type Info = DeepMutable<Schema.Schema.Type<typeof Info>>
export const Info = z
.object({
path: z.string(),
added: z.number().int(),
removed: z.number().int(),
status: z.enum(["added", "deleted", "modified"]),
})
.meta({
ref: "File",
})
export const Node = Schema.Struct({
name: Schema.String,
path: Schema.String,
absolute: Schema.String,
type: Schema.Literals(["file", "directory"]),
ignored: Schema.Boolean,
})
.annotate({ identifier: "FileNode" })
.pipe(withStatics((s) => ({ zod: zod(s) })))
export type Node = DeepMutable<Schema.Schema.Type<typeof Node>>
export type Info = z.infer<typeof Info>
const Hunk = Schema.Struct({
oldStart: Schema.Number,
oldLines: Schema.Number,
newStart: Schema.Number,
newLines: Schema.Number,
lines: Schema.Array(Schema.String),
})
export const Node = z
.object({
name: z.string(),
path: z.string(),
absolute: z.string(),
type: z.enum(["file", "directory"]),
ignored: z.boolean(),
})
.meta({
ref: "FileNode",
})
export type Node = z.infer<typeof Node>
const Patch = Schema.Struct({
oldFileName: Schema.String,
newFileName: Schema.String,
oldHeader: Schema.optional(Schema.String),
newHeader: Schema.optional(Schema.String),
hunks: Schema.Array(Hunk),
index: Schema.optional(Schema.String),
})
export const Content = Schema.Struct({
type: Schema.Literals(["text", "binary"]),
content: Schema.String,
diff: Schema.optional(Schema.String),
patch: Schema.optional(Patch),
encoding: Schema.optional(Schema.Literal("base64")),
mimeType: Schema.optional(Schema.String),
})
.annotate({ identifier: "FileContent" })
.pipe(withStatics((s) => ({ zod: zod(s) })))
export type Content = DeepMutable<Schema.Schema.Type<typeof Content>>
export const Content = z
.object({
type: z.enum(["text", "binary"]),
content: z.string(),
diff: z.string().optional(),
patch: z
.object({
oldFileName: z.string(),
newFileName: z.string(),
oldHeader: z.string().optional(),
newHeader: z.string().optional(),
hunks: z.array(
z.object({
oldStart: z.number(),
oldLines: z.number(),
newStart: z.number(),
newLines: z.number(),
lines: z.array(z.string()),
}),
),
index: z.string().optional(),
})
.optional(),
encoding: z.literal("base64").optional(),
mimeType: z.string().optional(),
})
.meta({
ref: "FileContent",
})
export type Content = z.infer<typeof Content>
export const Event = {
Edited: BusEvent.define(
+44 -29
View File
@@ -30,8 +30,6 @@ import { EffectBridge } from "@/effect"
import { InstanceState } from "@/effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
import { zod as effectZod } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
const log = Log.create({ service: "mcp" })
const DEFAULT_TIMEOUT = 30_000
@@ -71,33 +69,50 @@ export const Failed = NamedError.create(
type MCPClient = Client
const StatusConnected = Schema.Struct({ status: Schema.Literal("connected") }).annotate({
identifier: "MCPStatusConnected",
})
const StatusDisabled = Schema.Struct({ status: Schema.Literal("disabled") }).annotate({
identifier: "MCPStatusDisabled",
})
const StatusFailed = Schema.Struct({ status: Schema.Literal("failed"), error: Schema.String }).annotate({
identifier: "MCPStatusFailed",
})
const StatusNeedsAuth = Schema.Struct({ status: Schema.Literal("needs_auth") }).annotate({
identifier: "MCPStatusNeedsAuth",
})
const StatusNeedsClientRegistration = Schema.Struct({
status: Schema.Literal("needs_client_registration"),
error: Schema.String,
}).annotate({ identifier: "MCPStatusNeedsClientRegistration" })
export const Status = Schema.Union([
StatusConnected,
StatusDisabled,
StatusFailed,
StatusNeedsAuth,
StatusNeedsClientRegistration,
])
.annotate({ identifier: "MCPStatus", discriminator: "status" })
.pipe(withStatics((s) => ({ zod: effectZod(s) })))
export type Status = Schema.Schema.Type<typeof Status>
export const Status = z
.discriminatedUnion("status", [
z
.object({
status: z.literal("connected"),
})
.meta({
ref: "MCPStatusConnected",
}),
z
.object({
status: z.literal("disabled"),
})
.meta({
ref: "MCPStatusDisabled",
}),
z
.object({
status: z.literal("failed"),
error: z.string(),
})
.meta({
ref: "MCPStatusFailed",
}),
z
.object({
status: z.literal("needs_auth"),
})
.meta({
ref: "MCPStatusNeedsAuth",
}),
z
.object({
status: z.literal("needs_client_registration"),
error: z.string(),
})
.meta({
ref: "MCPStatusNeedsClientRegistration",
}),
])
.meta({
ref: "MCPStatus",
})
export type Status = z.infer<typeof Status>
// Store transports for OAuth servers to allow finishing auth
type TransportWithAuth = StreamableHTTPClientTransport | SSEClientTransport
+1 -1
View File
@@ -1177,7 +1177,7 @@ const layer: Layer.Layer<
model.modalities?.output?.includes("video") ?? existingModel?.capabilities.output.video ?? false,
pdf: model.modalities?.output?.includes("pdf") ?? existingModel?.capabilities.output.pdf ?? false,
},
interleaved: model.interleaved ?? existingModel?.capabilities.interleaved ?? false,
interleaved: model.interleaved ?? false,
},
cost: {
input: model?.cost?.input ?? existingModel?.cost?.input ?? 0,
+17 -37
View File
@@ -175,24 +175,6 @@ function normalizeMessages(
return result
}
// Deepseek requires all assistant messages to have reasoning on them
if (model.api.id.includes("deepseek")) {
msgs = msgs.map((msg) => {
if (msg.role !== "assistant") return msg
if (Array.isArray(msg.content)) {
if (msg.content.some((part) => part.type === "reasoning")) return msg
return { ...msg, content: [...msg.content, { type: "reasoning", text: "" }] }
}
return {
...msg,
content: [
...(msg.content ? [{ type: "text" as const, text: msg.content }] : []),
{ type: "reasoning" as const, text: "" },
],
}
})
}
if (typeof model.capabilities.interleaved === "object" && model.capabilities.interleaved.field) {
const field = model.capabilities.interleaved.field
return msgs.map((msg) => {
@@ -203,19 +185,24 @@ function normalizeMessages(
// Filter out reasoning parts from content
const filteredContent = msg.content.filter((part: any) => part.type !== "reasoning")
// Include reasoning_content | reasoning_details directly on the message for all assistant messages.
// Always set the field even when empty — some providers (e.g. DeepSeek) may return empty
// reasoning_content which still needs to be sent back in subsequent requests.
// Include reasoning_content | reasoning_details directly on the message for all assistant messages
if (reasoningText) {
return {
...msg,
content: filteredContent,
providerOptions: {
...msg.providerOptions,
openaiCompatible: {
...msg.providerOptions?.openaiCompatible,
[field]: reasoningText,
},
},
}
}
return {
...msg,
content: filteredContent,
providerOptions: {
...msg.providerOptions,
openaiCompatible: {
...msg.providerOptions?.openaiCompatible,
[field]: reasoningText,
},
},
}
}
@@ -418,10 +405,7 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
const id = model.id.toLowerCase()
const adaptiveEfforts = anthropicAdaptiveEfforts(model.api.id)
if (
id.includes("deepseek-chat") ||
id.includes("deepseek-reasoner") ||
id.includes("deepseek-r1") ||
id.includes("deepseek-v3") ||
id.includes("deepseek") ||
id.includes("minimax") ||
id.includes("glm") ||
id.includes("kimi") ||
@@ -547,11 +531,7 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
case "venice-ai-sdk-provider":
// https://docs.venice.ai/overview/guides/reasoning-models#reasoning-effort
case "@ai-sdk/openai-compatible":
const efforts = [...WIDELY_SUPPORTED_EFFORTS]
if (model.api.id.includes("deepseek-v4")) {
efforts.push("max")
}
return Object.fromEntries(efforts.map((effort) => [effort, { reasoningEffort: effort }]))
return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
case "@ai-sdk/azure":
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/azure
@@ -117,7 +117,7 @@ export const FileRoutes = lazy(() =>
description: "Files and directories",
content: {
"application/json": {
schema: resolver(File.Node.zod.array()),
schema: resolver(File.Node.array()),
},
},
},
@@ -146,7 +146,7 @@ export const FileRoutes = lazy(() =>
description: "File content",
content: {
"application/json": {
schema: resolver(File.Content.zod),
schema: resolver(File.Content),
},
},
},
@@ -175,7 +175,7 @@ export const FileRoutes = lazy(() =>
description: "File status",
content: {
"application/json": {
schema: resolver(File.Info.zod.array()),
schema: resolver(File.Info.array()),
},
},
},
@@ -1,84 +0,0 @@
import { File } from "@/file"
import { Effect, Layer, Schema } from "effect"
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
const FileQuery = Schema.Struct({
path: Schema.String,
})
export const FilePaths = {
list: "/file",
content: "/file/content",
status: "/file/status",
} as const
export const FileApi = HttpApi.make("file")
.add(
HttpApiGroup.make("file")
.add(
HttpApiEndpoint.get("list", FilePaths.list, {
query: FileQuery,
success: Schema.Array(File.Node),
}).annotateMerge(
OpenApi.annotations({
identifier: "file.list",
summary: "List files",
description: "List files and directories in a specified path.",
}),
),
HttpApiEndpoint.get("content", FilePaths.content, {
query: FileQuery,
success: File.Content,
}).annotateMerge(
OpenApi.annotations({
identifier: "file.read",
summary: "Read file",
description: "Read the content of a specified file.",
}),
),
HttpApiEndpoint.get("status", FilePaths.status, {
success: Schema.Array(File.Info),
}).annotateMerge(
OpenApi.annotations({
identifier: "file.status",
summary: "Get file status",
description: "Get the git status of all files in the project.",
}),
),
)
.annotateMerge(
OpenApi.annotations({
title: "file",
description: "Experimental HttpApi file routes.",
}),
),
)
.annotateMerge(
OpenApi.annotations({
title: "opencode experimental HttpApi",
version: "0.0.1",
description: "Experimental HttpApi surface for selected instance routes.",
}),
)
export const fileHandlers = Layer.unwrap(
Effect.gen(function* () {
const svc = yield* File.Service
const list = Effect.fn("FileHttpApi.list")(function* (ctx: { query: { path: string } }) {
return yield* svc.list(ctx.query.path)
})
const content = Effect.fn("FileHttpApi.content")(function* (ctx: { query: { path: string } }) {
return yield* svc.read(ctx.query.path)
})
const status = Effect.fn("FileHttpApi.status")(function* () {
return yield* svc.status()
})
return HttpApiBuilder.group(FileApi, "file", (handlers) =>
handlers.handle("list", list).handle("content", content).handle("status", status),
)
}),
).pipe(Layer.provide(File.defaultLayer))
@@ -1,48 +0,0 @@
import { MCP } from "@/mcp"
import { Effect, Layer, Schema } from "effect"
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
export const McpPaths = {
status: "/mcp",
} as const
export const McpApi = HttpApi.make("mcp")
.add(
HttpApiGroup.make("mcp")
.add(
HttpApiEndpoint.get("status", McpPaths.status, {
success: Schema.Record(Schema.String, MCP.Status),
}).annotateMerge(
OpenApi.annotations({
identifier: "mcp.status",
summary: "Get MCP status",
description: "Get the status of all Model Context Protocol (MCP) servers.",
}),
),
)
.annotateMerge(
OpenApi.annotations({
title: "mcp",
description: "Experimental HttpApi MCP routes.",
}),
),
)
.annotateMerge(
OpenApi.annotations({
title: "opencode experimental HttpApi",
version: "0.0.1",
description: "Experimental HttpApi surface for selected instance routes.",
}),
)
export const mcpHandlers = Layer.unwrap(
Effect.gen(function* () {
const mcp = yield* MCP.Service
const status = Effect.fn("McpHttpApi.status")(function* () {
return yield* mcp.status()
})
return HttpApiBuilder.group(McpApi, "mcp", (handlers) => handlers.handle("status", status))
}),
).pipe(Layer.provide(MCP.defaultLayer))
@@ -10,8 +10,6 @@ import { Instance } from "@/project/instance"
import { lazy } from "@/util/lazy"
import { Filesystem } from "@/util"
import { ConfigApi, configHandlers } from "./config"
import { FileApi, fileHandlers } from "./file"
import { McpApi, mcpHandlers } from "./mcp"
import { PermissionApi, permissionHandlers } from "./permission"
import { ProjectApi, projectHandlers } from "./project"
import { ProviderApi, providerHandlers } from "./provider"
@@ -116,13 +114,9 @@ const ProjectSecured = ProjectApi.middleware(Authorization)
const ProviderSecured = ProviderApi.middleware(Authorization)
const ConfigSecured = ConfigApi.middleware(Authorization)
const WorkspaceSecured = WorkspaceApi.middleware(Authorization)
const FileSecured = FileApi.middleware(Authorization)
const McpSecured = McpApi.middleware(Authorization)
export const routes = Layer.mergeAll(
HttpApiBuilder.layer(ConfigSecured).pipe(Layer.provide(configHandlers)),
HttpApiBuilder.layer(FileSecured).pipe(Layer.provide(fileHandlers)),
HttpApiBuilder.layer(McpSecured).pipe(Layer.provide(mcpHandlers)),
HttpApiBuilder.layer(ProjectSecured).pipe(Layer.provide(projectHandlers)),
HttpApiBuilder.layer(QuestionSecured).pipe(Layer.provide(questionHandlers)),
HttpApiBuilder.layer(PermissionSecured).pipe(Layer.provide(permissionHandlers)),
@@ -16,8 +16,6 @@ import { QuestionRoutes } from "./question"
import { PermissionRoutes } from "./permission"
import { Flag } from "@/flag/flag"
import { ExperimentalHttpApiServer } from "./httpapi/server"
import { FilePaths } from "./httpapi/file"
import { McpPaths } from "./httpapi/mcp"
import { ProjectRoutes } from "./project"
import { SessionRoutes } from "./session"
import { PtyRoutes } from "./pty"
@@ -50,10 +48,6 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => {
app.post("/provider/:providerID/oauth/callback", (c) => handler(c.req.raw, context))
app.get("/project", (c) => handler(c.req.raw, context))
app.get("/project/current", (c) => handler(c.req.raw, context))
app.get(FilePaths.list, (c) => handler(c.req.raw, context))
app.get(FilePaths.content, (c) => handler(c.req.raw, context))
app.get(FilePaths.status, (c) => handler(c.req.raw, context))
app.get(McpPaths.status, (c) => handler(c.req.raw, context))
}
return app
@@ -21,7 +21,7 @@ export const McpRoutes = lazy(() =>
description: "MCP server status",
content: {
"application/json": {
schema: resolver(z.record(z.string(), MCP.Status.zod)),
schema: resolver(z.record(z.string(), MCP.Status)),
},
},
},
@@ -44,7 +44,7 @@ export const McpRoutes = lazy(() =>
description: "MCP server added successfully",
content: {
"application/json": {
schema: resolver(z.record(z.string(), MCP.Status.zod)),
schema: resolver(z.record(z.string(), MCP.Status)),
},
},
},
@@ -121,7 +121,7 @@ export const McpRoutes = lazy(() =>
description: "OAuth authentication completed",
content: {
"application/json": {
schema: resolver(MCP.Status.zod),
schema: resolver(MCP.Status),
},
},
},
@@ -153,7 +153,7 @@ export const McpRoutes = lazy(() =>
description: "OAuth authentication completed",
content: {
"application/json": {
schema: resolver(MCP.Status.zod),
schema: resolver(MCP.Status),
},
},
},
@@ -1,57 +0,0 @@
import { afterEach, describe, expect, test } from "bun:test"
import { Context } from "effect"
import path from "path"
import { ExperimentalHttpApiServer } from "../../src/server/routes/instance/httpapi/server"
import { FilePaths } from "../../src/server/routes/instance/httpapi/file"
import { Instance } from "../../src/project/instance"
import { Log } from "../../src/util"
import { resetDatabase } from "../fixture/db"
import { tmpdir } from "../fixture/fixture"
void Log.init({ print: false })
const context = Context.empty() as Context.Context<unknown>
function request(route: string, directory: string, query?: Record<string, string>) {
const url = new URL(`http://localhost${route}`)
for (const [key, value] of Object.entries(query ?? {})) {
url.searchParams.set(key, value)
}
return ExperimentalHttpApiServer.webHandler().handler(
new Request(url, {
headers: {
"x-opencode-directory": directory,
},
}),
context,
)
}
afterEach(async () => {
await Instance.disposeAll()
await resetDatabase()
})
describe("file HttpApi", () => {
test("serves read endpoints", async () => {
await using tmp = await tmpdir({ git: true })
await Bun.write(path.join(tmp.path, "hello.txt"), "hello")
const [list, content, status] = await Promise.all([
request(FilePaths.list, tmp.path, { path: "." }),
request(FilePaths.content, tmp.path, { path: "hello.txt" }),
request(FilePaths.status, tmp.path),
])
expect(list.status).toBe(200)
expect(await list.json()).toContainEqual(
expect.objectContaining({ name: "hello.txt", path: "hello.txt", type: "file" }),
)
expect(content.status).toBe(200)
expect(await content.json()).toMatchObject({ type: "text", content: "hello" })
expect(status.status).toBe(200)
expect(await status.json()).toContainEqual({ path: "hello.txt", added: 1, removed: 0, status: "added" })
})
})
@@ -1,48 +0,0 @@
import { afterEach, describe, expect, test } from "bun:test"
import { Context } from "effect"
import { ExperimentalHttpApiServer } from "../../src/server/routes/instance/httpapi/server"
import { McpPaths } from "../../src/server/routes/instance/httpapi/mcp"
import { Instance } from "../../src/project/instance"
import { Log } from "../../src/util"
import { resetDatabase } from "../fixture/db"
import { tmpdir } from "../fixture/fixture"
void Log.init({ print: false })
const context = Context.empty() as Context.Context<unknown>
function request(route: string, directory: string) {
return ExperimentalHttpApiServer.webHandler().handler(
new Request(`http://localhost${route}`, {
headers: {
"x-opencode-directory": directory,
},
}),
context,
)
}
afterEach(async () => {
await Instance.disposeAll()
await resetDatabase()
})
describe("mcp HttpApi", () => {
test("serves status endpoint", async () => {
await using tmp = await tmpdir({
config: {
mcp: {
demo: {
type: "local",
command: ["echo", "demo"],
enabled: false,
},
},
},
})
const response = await request(McpPaths.status, tmp.path)
expect(response.status).toBe(200)
expect(await response.json()).toEqual({ demo: { status: "disabled" } })
})
})
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "1.14.24",
"version": "1.14.22",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "1.14.24",
"version": "1.14.22",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.14.24",
"version": "1.14.22",
"name": "@opencode-ai/shared",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "1.14.24",
"version": "1.14.22",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.14.24",
"version": "1.14.22",
"type": "module",
"license": "MIT",
"exports": {
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "@opencode-ai/web",
"type": "module",
"license": "MIT",
"version": "1.14.24",
"version": "1.14.22",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "1.14.24",
"version": "1.14.22",
"publisher": "sst-dev",
"repository": {
"type": "git",