From 8c09baceda92f128b43f77ff4fd08deebb1dfaab Mon Sep 17 00:00:00 2001 From: Caplets Test Date: Mon, 29 Jun 2026 16:56:55 -0400 Subject: [PATCH 1/3] fix opencode array item schemas --- .changeset/sharp-array-schemas.md | 5 ++++ packages/opencode/src/schema.ts | 35 ++++++++++++++++++++----- packages/opencode/test/opencode.test.ts | 14 +++++++--- 3 files changed, 44 insertions(+), 10 deletions(-) create mode 100644 .changeset/sharp-array-schemas.md diff --git a/.changeset/sharp-array-schemas.md b/.changeset/sharp-array-schemas.md new file mode 100644 index 00000000..35b06a42 --- /dev/null +++ b/.changeset/sharp-array-schemas.md @@ -0,0 +1,5 @@ +--- +"@caplets/opencode": patch +--- + +Preserve JSON Schema array item types when registering direct native tools with OpenCode. diff --git a/packages/opencode/src/schema.ts b/packages/opencode/src/schema.ts index 180ea3a6..1bb15130 100644 --- a/packages/opencode/src/schema.ts +++ b/packages/opencode/src/schema.ts @@ -1,6 +1,8 @@ import { tool } from "@opencode-ai/plugin"; import { operations } from "@caplets/core/generated-tool-input-schema"; +type OpenCodeSchema = Parameters[0]["args"][string]; + export function capletsOpenCodeArgs(operationNames: string[] = [...operations]) { const enumValues = operationNames.length > 0 ? operationNames : [...operations]; return { @@ -52,28 +54,47 @@ export function capletsOpenCodeJsonSchemaArgs(schema: Record | return {}; } return Object.fromEntries( - Object.entries(properties).map(([key, value]) => [key, jsonSchemaPropertyToOpenCode(value)]), + Object.entries(properties).map(([key, value]) => [ + key, + jsonSchemaPropertyToOpenCode(value, { optional: true }), + ]), ); } -function jsonSchemaPropertyToOpenCode(value: unknown) { +function jsonSchemaPropertyToOpenCode( + value: unknown, + options: { optional: boolean }, +): OpenCodeSchema { if (!value || typeof value !== "object" || Array.isArray(value)) return tool.schema.unknown(); const schema = value as Record; if (Array.isArray(schema.enum) && schema.enum.every((item) => typeof item === "string")) { return tool.schema.enum(schema.enum as [string, ...string[]]); } - if (schema.type === "string") return tool.schema.string().optional(); + if (schema.type === "string") { + const stringSchema = tool.schema.string(); + return options.optional ? stringSchema.optional() : stringSchema; + } if (schema.type === "number" || schema.type === "integer") { - return tool.schema.number().int().positive().optional(); + const numberSchema = tool.schema.number().int().positive(); + return options.optional ? numberSchema.optional() : numberSchema; } if (schema.type === "boolean" && "boolean" in tool.schema) { - return (tool.schema as typeof tool.schema & { boolean: () => unknown }).boolean(); + return (tool.schema as typeof tool.schema & { boolean: () => OpenCodeSchema }).boolean(); } if (schema.type === "object") { - return tool.schema.record(tool.schema.string(), tool.schema.unknown()).optional(); + const objectSchema = tool.schema.record(tool.schema.string(), tool.schema.unknown()); + return options.optional ? objectSchema.optional() : objectSchema; } if (schema.type === "array") { - return tool.schema.array(tool.schema.unknown()).min(1).optional(); + const itemSchema = + schema.items && + typeof schema.items === "object" && + !Array.isArray(schema.items) && + Object.keys(schema.items).length > 0 + ? jsonSchemaPropertyToOpenCode(schema.items, { optional: false }) + : tool.schema.string(); + const arraySchema = tool.schema.array(itemSchema).min(1); + return options.optional ? arraySchema.optional() : arraySchema; } return tool.schema.unknown(); } diff --git a/packages/opencode/test/opencode.test.ts b/packages/opencode/test/opencode.test.ts index b2a62686..bc05ed2f 100644 --- a/packages/opencode/test/opencode.test.ts +++ b/packages/opencode/test/opencode.test.ts @@ -31,7 +31,9 @@ vi.mock("@opencode-ai/plugin", () => ({ options, optional: () => ({ type: "union", options, optional: true }), }), - array: () => ({ min: () => ({ optional: () => ({ type: "array", optional: true }) }) }), + array: (item: unknown) => ({ + min: () => ({ optional: () => ({ type: "array", item, optional: true }) }), + }), }, }), })); @@ -184,7 +186,10 @@ describe("@caplets/opencode", () => { promptGuidance: ["Use caplets__status__ping."], inputSchema: { type: "object", - properties: { verbose: { type: "boolean" } }, + properties: { + verbose: { type: "boolean" }, + tags: { type: "array", items: { type: "string" } }, + }, }, }, ], @@ -200,7 +205,10 @@ describe("@caplets/opencode", () => { execute(args: unknown, context: unknown): Promise; }; - expect(directTool.args).toEqual({ verbose: { type: "boolean" } }); + expect(directTool.args).toMatchObject({ + verbose: { type: "boolean" }, + tags: { type: "array", item: { type: "string" }, optional: true }, + }); await directTool.execute({ verbose: true }, {} as never); expect(service.execute).toHaveBeenCalledWith("status__ping", { verbose: true }); }); From a5d1f3aa7237e9ba1c47e7c4935a32921ba85b4e Mon Sep 17 00:00:00 2001 From: Caplets Test Date: Mon, 29 Jun 2026 18:51:31 -0400 Subject: [PATCH 2/3] fix opencode boolean schema optionality --- packages/opencode/src/schema.ts | 7 ++++++- packages/opencode/test/opencode.test.ts | 7 +++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/schema.ts b/packages/opencode/src/schema.ts index 1bb15130..deda0fed 100644 --- a/packages/opencode/src/schema.ts +++ b/packages/opencode/src/schema.ts @@ -79,7 +79,12 @@ function jsonSchemaPropertyToOpenCode( return options.optional ? numberSchema.optional() : numberSchema; } if (schema.type === "boolean" && "boolean" in tool.schema) { - return (tool.schema as typeof tool.schema & { boolean: () => OpenCodeSchema }).boolean(); + const booleanSchema = ( + tool.schema as typeof tool.schema & { + boolean: () => OpenCodeSchema & { optional: () => OpenCodeSchema }; + } + ).boolean(); + return options.optional ? booleanSchema.optional() : booleanSchema; } if (schema.type === "object") { const objectSchema = tool.schema.record(tool.schema.string(), tool.schema.unknown()); diff --git a/packages/opencode/test/opencode.test.ts b/packages/opencode/test/opencode.test.ts index bc05ed2f..2bbce329 100644 --- a/packages/opencode/test/opencode.test.ts +++ b/packages/opencode/test/opencode.test.ts @@ -9,7 +9,10 @@ vi.mock("@opencode-ai/plugin", () => ({ optional: () => ({ type: "string", optional: true }), min: () => ({ type: "string" }), }), - boolean: () => ({ type: "boolean" }), + boolean: () => ({ + type: "boolean", + optional: () => ({ type: "boolean", optional: true }), + }), number: () => ({ int: () => ({ positive: () => ({ optional: () => ({ type: "number", optional: true }) }) }), }), @@ -206,7 +209,7 @@ describe("@caplets/opencode", () => { }; expect(directTool.args).toMatchObject({ - verbose: { type: "boolean" }, + verbose: { type: "boolean", optional: true }, tags: { type: "array", item: { type: "string" }, optional: true }, }); await directTool.execute({ verbose: true }, {} as never); From 413c34679be9456569219b701bfdebace185ae0d Mon Sep 17 00:00:00 2001 From: Caplets Test Date: Mon, 29 Jun 2026 19:22:02 -0400 Subject: [PATCH 3/3] fix opencode enum schema optionality --- packages/opencode/src/schema.ts | 29 ++++++++++++++++++------- packages/opencode/test/opencode.test.ts | 16 +++++++++++++- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/schema.ts b/packages/opencode/src/schema.ts index deda0fed..9c3f2252 100644 --- a/packages/opencode/src/schema.ts +++ b/packages/opencode/src/schema.ts @@ -68,7 +68,8 @@ function jsonSchemaPropertyToOpenCode( if (!value || typeof value !== "object" || Array.isArray(value)) return tool.schema.unknown(); const schema = value as Record; if (Array.isArray(schema.enum) && schema.enum.every((item) => typeof item === "string")) { - return tool.schema.enum(schema.enum as [string, ...string[]]); + const enumSchema = tool.schema.enum(schema.enum as [string, ...string[]]); + return options.optional ? enumSchema.optional() : enumSchema; } if (schema.type === "string") { const stringSchema = tool.schema.string(); @@ -91,15 +92,27 @@ function jsonSchemaPropertyToOpenCode( return options.optional ? objectSchema.optional() : objectSchema; } if (schema.type === "array") { - const itemSchema = - schema.items && - typeof schema.items === "object" && - !Array.isArray(schema.items) && - Object.keys(schema.items).length > 0 - ? jsonSchemaPropertyToOpenCode(schema.items, { optional: false }) - : tool.schema.string(); + const itemSchema = isSupportedOpenCodeJsonSchema(schema.items) + ? jsonSchemaPropertyToOpenCode(schema.items, { optional: false }) + : tool.schema.string(); const arraySchema = tool.schema.array(itemSchema).min(1); return options.optional ? arraySchema.optional() : arraySchema; } return tool.schema.unknown(); } + +function isSupportedOpenCodeJsonSchema(value: unknown): boolean { + if (!value || typeof value !== "object" || Array.isArray(value)) return false; + const schema = value as Record; + if (Array.isArray(schema.enum) && schema.enum.every((item) => typeof item === "string")) { + return schema.enum.length > 0; + } + return ( + schema.type === "string" || + schema.type === "number" || + schema.type === "integer" || + schema.type === "boolean" || + schema.type === "object" || + schema.type === "array" + ); +} diff --git a/packages/opencode/test/opencode.test.ts b/packages/opencode/test/opencode.test.ts index 2bbce329..5a159ce8 100644 --- a/packages/opencode/test/opencode.test.ts +++ b/packages/opencode/test/opencode.test.ts @@ -3,7 +3,11 @@ import { describe, expect, it, vi } from "vitest"; vi.mock("@opencode-ai/plugin", () => ({ tool: Object.assign((definition: unknown) => definition, { schema: { - enum: () => ({ type: "enum" }), + enum: (values: string[]) => ({ + type: "enum", + values, + optional: () => ({ type: "enum", values, optional: true }), + }), string: () => ({ type: "string", optional: () => ({ type: "string", optional: true }), @@ -190,8 +194,11 @@ describe("@caplets/opencode", () => { inputSchema: { type: "object", properties: { + mode: { enum: ["fast", "safe"] }, verbose: { type: "boolean" }, tags: { type: "array", items: { type: "string" } }, + priorityTags: { type: "array", items: { enum: ["high", "low"] } }, + looseTags: { type: "array", items: { anyOf: [{ type: "string" }] } }, }, }, }, @@ -209,8 +216,15 @@ describe("@caplets/opencode", () => { }; expect(directTool.args).toMatchObject({ + mode: { type: "enum", values: ["fast", "safe"], optional: true }, verbose: { type: "boolean", optional: true }, tags: { type: "array", item: { type: "string" }, optional: true }, + priorityTags: { + type: "array", + item: { type: "enum", values: ["high", "low"] }, + optional: true, + }, + looseTags: { type: "array", item: { type: "string" }, optional: true }, }); await directTool.execute({ verbose: true }, {} as never); expect(service.execute).toHaveBeenCalledWith("status__ping", { verbose: true });