Compare commits

...

1 Commits

Author SHA1 Message Date
Kit Langton a981d6e862 refactor(core): centralize boundary normalization 2026-08-21 13:26:26 -04:00
3 changed files with 19 additions and 34 deletions
+8 -7
View File
@@ -34,6 +34,7 @@ import { MCPStdio } from "./stdio.js"
const DEFAULT_STARTUP_TIMEOUT = 30_000
const DEFAULT_CATALOG_TIMEOUT = 30_000
const DEFAULT_EXECUTION_TIMEOUT = 12 * 60 * 60 * 1_000 // 12 hours
const toError = (error: unknown) => (error instanceof Error ? error : new Error(String(error)))
// Some servers advertise tool outputSchemas the SDK's strict validator can't resolve; this drops
// only that field so a single bad schema doesn't blank out the whole tool list.
@@ -261,7 +262,7 @@ export const connect = Effect.fnUntraced(function* (
},
(result) => result.tools,
),
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
catch: toError,
}).pipe(
Effect.tapError((error) => Effect.logWarning("failed to list MCP tools", { server, error: error.message })),
)
@@ -286,7 +287,7 @@ export const connect = Effect.fnUntraced(function* (
},
(result) => result.prompts,
),
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
catch: toError,
}).pipe(
Effect.tapError((error) =>
Effect.logWarning("failed to list MCP prompts", { server, error: error.message }),
@@ -312,7 +313,7 @@ export const connect = Effect.fnUntraced(function* (
client.listResources(cursor === undefined ? undefined : { cursor }, { timeout: catalogTimeout }),
(result) => result.resources,
),
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
catch: toError,
}).pipe(
Effect.tapError((error) =>
Effect.logWarning("failed to list MCP resources", { server, error: error.message }),
@@ -337,7 +338,7 @@ export const connect = Effect.fnUntraced(function* (
}),
(result) => result.resourceTemplates,
),
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
catch: toError,
}).pipe(
Effect.tapError((error) =>
Effect.logWarning("failed to list MCP resource templates", { server, error: error.message }),
@@ -355,7 +356,7 @@ export const connect = Effect.fnUntraced(function* (
if (!client.getServerCapabilities()?.resources) return undefined
const result = yield* Effect.tryPromise({
try: (signal) => client.readResource({ uri: input.uri }, { signal, timeout: executionTimeout }),
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
catch: toError,
}).pipe(
Effect.tapError((error) =>
Effect.logWarning("failed to read MCP resource", { server, uri: input.uri, error: error.message }),
@@ -378,7 +379,7 @@ export const connect = Effect.fnUntraced(function* (
GetPromptResultSchema,
{ signal, timeout: executionTimeout },
),
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
catch: toError,
}).pipe(
Effect.map((result) => ({
messages: result.messages.map((message) => ({ role: message.role, content: message.content })),
@@ -393,7 +394,7 @@ export const connect = Effect.fnUntraced(function* (
// Keep progress tokens available while enforcing a hard wall-clock execution timeout.
{ signal, timeout: executionTimeout, onprogress: () => {} },
),
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
catch: toError,
}).pipe(
Effect.map((result) => ({
isError: result.isError === true,
+2 -12
View File
@@ -3,6 +3,7 @@ export * as Provider from "./provider.js"
import { Effect, Schema } from "effect"
import { Provider } from "@opencode-ai/schema/provider"
import type { ProviderPackageDefinition } from "@opencode-ai/ai"
import { isRecord } from "@opencode-ai/ai/utils/record"
import { Npm } from "@opencode-ai/util/npm"
import type { DeepMutable } from "./schema.js"
import { importModule, resolveModule } from "@opencode-ai/util/runtime-import"
@@ -108,18 +109,7 @@ export function mergeOverlay(
const left = base[key]
const right = overlay[key]
if (right === undefined) return [key, left]
if (
typeof left === "object" &&
left !== null &&
!Array.isArray(left) &&
typeof right === "object" &&
right !== null &&
!Array.isArray(right)
)
return [
key,
mergeOverlay(left as Readonly<Record<string, unknown>>, right as Readonly<Record<string, unknown>>) ?? {},
]
if (isRecord(left) && isRecord(right)) return [key, mergeOverlay(left, right) ?? {}]
return [key, right]
}),
),
+9 -15
View File
@@ -88,6 +88,12 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Ri
const failure = (message: string, cause?: unknown) => new Error({ message, cause })
const normalizePath = (value: string) =>
value
.replace(/^(?:\.[\\/])+/u, "")
.replace(/^[\\/]+/u, "")
.replaceAll("\\", "/")
const isInvalidPattern = (stderr: string) =>
stderr.includes("regex parse error") || stderr.includes("error parsing regex")
@@ -169,13 +175,7 @@ const layer = Layer.effect(
"--glob=!**/.git/**",
".",
],
parse: (line) =>
Effect.succeed(
line
.replace(/^(?:\.[\\/])+/u, "")
.replace(/^[\\/]+/u, "")
.replaceAll("\\", "/"),
),
parse: (line) => Effect.succeed(normalizePath(line)),
}).pipe(
Effect.map((result) =>
result.items.map((relative) =>
@@ -203,10 +203,7 @@ const layer = Layer.effect(
".",
],
parse: (line) => {
const relative = line
.replace(/^(?:\.[\\/])+/u, "")
.replace(/^[\\/]+/u, "")
.replaceAll("\\", "/")
const relative = normalizePath(line)
return Effect.succeed(
Entry.make({
path: RelativePath.make(relative),
@@ -252,10 +249,7 @@ const layer = Layer.effect(
}).pipe(
Effect.map((result) =>
result.items.map((match) => {
const relative = match.path.text
.replace(/^(?:\.[\\/])+/u, "")
.replace(/^[\\/]+/u, "")
.replaceAll("\\", "/")
const relative = normalizePath(match.path.text)
return Match.make({
entry: Entry.make({
path: RelativePath.make(relative),