From 8a97cb55d362eb1757cae8f421e6c772758aae6d Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Wed, 3 Jun 2026 18:02:57 -0400 Subject: [PATCH] feat(core): register built-in skill --- packages/core/src/config/plugin/skill.ts | 5 +++ packages/core/src/markdown.d.ts | 4 ++ packages/core/src/plugin/boot.ts | 2 + packages/core/src/plugin/skill.ts | 30 +++++++++++++ .../src/plugin/skill}/customize-opencode.md | 2 +- packages/core/src/skill.ts | 15 +++++-- packages/core/test/config/skill.test.ts | 3 ++ packages/core/test/plugin/skill.test.ts | 32 +++++++++++++ .../instance/httpapi/groups/v2/message.ts | 5 ++- .../instance/httpapi/groups/v2/permission.ts | 5 ++- .../instance/httpapi/groups/v2/response.ts | 9 ++++ .../instance/httpapi/groups/v2/session.ts | 9 ++-- .../instance/httpapi/handlers/v2/message.ts | 5 ++- .../httpapi/handlers/v2/permission.ts | 5 ++- .../instance/httpapi/handlers/v2/session.ts | 13 +++--- packages/opencode/src/skill/index.ts | 2 +- .../test/server/httpapi-exercise/index.ts | 45 +++++++++++++------ .../test/server/httpapi-session.test.ts | 9 ++-- 18 files changed, 159 insertions(+), 41 deletions(-) create mode 100644 packages/core/src/markdown.d.ts create mode 100644 packages/core/src/plugin/skill.ts rename packages/{opencode/src/skill/prompt => core/src/plugin/skill}/customize-opencode.md (99%) create mode 100644 packages/core/test/plugin/skill.test.ts create mode 100644 packages/opencode/src/server/routes/instance/httpapi/groups/v2/response.ts diff --git a/packages/core/src/config/plugin/skill.ts b/packages/core/src/config/plugin/skill.ts index a6c1bccdec..c4c4b6c0fd 100644 --- a/packages/core/src/config/plugin/skill.ts +++ b/packages/core/src/config/plugin/skill.ts @@ -18,9 +18,14 @@ export const Plugin = PluginV2.define({ const skill = yield* SkillV2.Service const transform = yield* skill.transform() const entries = yield* config.entries() + const directories = entries.flatMap((entry) => (entry.type === "directory" ? [entry.path] : [])) const items = entries.flatMap((entry) => (entry.type === "document" ? (entry.info.skills ?? []) : [])) yield* transform((editor) => { + for (const directory of directories) { + editor.source(new SkillV2.DirectorySource({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) })) + editor.source(new SkillV2.DirectorySource({ type: "directory", path: AbsolutePath.make(path.join(directory, "skills")) })) + } for (const item of items) { if (URL.canParse(item) && /^(https?:)$/.test(new URL(item).protocol)) { editor.source(new SkillV2.UrlSource({ type: "url", url: item })) diff --git a/packages/core/src/markdown.d.ts b/packages/core/src/markdown.d.ts new file mode 100644 index 0000000000..eb3e3b92d6 --- /dev/null +++ b/packages/core/src/markdown.d.ts @@ -0,0 +1,4 @@ +declare module "*.md" { + const content: string + export default content +} diff --git a/packages/core/src/plugin/boot.ts b/packages/core/src/plugin/boot.ts index d7695f7785..6a589a1ef0 100644 --- a/packages/core/src/plugin/boot.ts +++ b/packages/core/src/plugin/boot.ts @@ -19,6 +19,7 @@ import { PluginV2 } from "../plugin" import { AccountPlugin } from "./account" import { AgentPlugin } from "./agent" import { CommandPlugin } from "./command" +import { SkillPlugin } from "./skill" import { ConfigProviderPlugin } from "../config/plugin/provider" import { EnvPlugin } from "./env" import { ModelsDevPlugin } from "./models-dev" @@ -94,6 +95,7 @@ export const layer = Layer.effect( yield* add(AccountPlugin) yield* add(AgentPlugin.Plugin) yield* add(CommandPlugin.Plugin) + yield* add(SkillPlugin.Plugin) for (const item of ProviderPlugins) { yield* add(item) } diff --git a/packages/core/src/plugin/skill.ts b/packages/core/src/plugin/skill.ts new file mode 100644 index 0000000000..8ce22373da --- /dev/null +++ b/packages/core/src/plugin/skill.ts @@ -0,0 +1,30 @@ +export * as SkillPlugin from "./skill" + +import { Effect } from "effect" +import { PluginV2 } from "../plugin" +import { AbsolutePath } from "../schema" +import { SkillV2 } from "../skill" +import CUSTOMIZE_OPENCODE_SKILL_BODY from "./skill/customize-opencode.md" with { type: "text" } + +export const Plugin = PluginV2.define({ + id: PluginV2.ID.make("skill"), + effect: Effect.gen(function* () { + const skill = yield* SkillV2.Service + const transform = yield* skill.transform() + + yield* transform((editor) => { + editor.source( + new SkillV2.EmbeddedSource({ + type: "embedded", + skill: new SkillV2.Info({ + name: "customize-opencode", + 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.", + location: AbsolutePath.make("/builtin/customize-opencode.md"), + content: CUSTOMIZE_OPENCODE_SKILL_BODY, + }), + }), + ) + }) + }), +}) diff --git a/packages/opencode/src/skill/prompt/customize-opencode.md b/packages/core/src/plugin/skill/customize-opencode.md similarity index 99% rename from packages/opencode/src/skill/prompt/customize-opencode.md rename to packages/core/src/plugin/skill/customize-opencode.md index a3bc44a1fc..5b51f8f2ab 100644 --- a/packages/opencode/src/skill/prompt/customize-opencode.md +++ b/packages/core/src/plugin/skill/customize-opencode.md @@ -1,6 +1,6 @@ diff --git a/packages/core/src/skill.ts b/packages/core/src/skill.ts index d1a8c7d6b5..577fd8a624 100644 --- a/packages/core/src/skill.ts +++ b/packages/core/src/skill.ts @@ -21,17 +21,23 @@ export class UrlSource extends Schema.Class("SkillV2.UrlSource")({ url: Schema.String, }) {} -export const Source = Schema.Union([DirectorySource, UrlSource]).pipe( +export class EmbeddedSource extends Schema.Class("SkillV2.EmbeddedSource")({ + type: Schema.Literal("embedded"), + skill: Schema.suspend(() => Info), +}) {} + +export const Source = Schema.Union([DirectorySource, UrlSource, EmbeddedSource]).pipe( Schema.toTaggedUnion("type"), withStatics(() => ({ - equals: (a: DirectorySource | UrlSource, b: DirectorySource | UrlSource) => { + equals: (a: DirectorySource | UrlSource | EmbeddedSource, b: DirectorySource | UrlSource | EmbeddedSource) => { if (a.type !== b.type) return false if (a.type === "directory" && b.type === "directory") return a.path === b.path if (a.type === "url" && b.type === "url") return a.url === b.url + if (a.type === "embedded" && b.type === "embedded") return a.skill.name === b.skill.name return false }, - key: (source: DirectorySource | UrlSource) => - source.type === "directory" ? `directory:${source.path}` : `url:${source.url}`, + key: (source: DirectorySource | UrlSource | EmbeddedSource) => + source.type === "directory" ? `directory:${source.path}` : source.type === "url" ? `url:${source.url}` : `embedded:${source.skill.name}`, })), ) export type Source = typeof Source.Type @@ -89,6 +95,7 @@ export const layer = Layer.effect( const load = Effect.fn("SkillV2.load")(function* (source: Source) { const skills: Info[] = [] + if (source.type === "embedded") return [source.skill] const directories = source.type === "directory" ? [source.path] : yield* discovery.pull(source.url) for (const directory of directories) { const files = yield* fs diff --git a/packages/core/test/config/skill.test.ts b/packages/core/test/config/skill.test.ts index a4841cb957..4f999c3173 100644 --- a/packages/core/test/config/skill.test.ts +++ b/packages/core/test/config/skill.test.ts @@ -33,6 +33,7 @@ describe("ConfigSkillPlugin.Plugin", () => { Config.Service.of({ entries: () => Effect.succeed([ + new Config.Directory({ type: "directory", path: AbsolutePath.make("/repo/.opencode") }), new Config.Document({ type: "document", info: decode({ @@ -56,6 +57,8 @@ describe("ConfigSkillPlugin.Plugin", () => { ) expect(sources).toEqual([ + new SkillV2.DirectorySource({ type: "directory", path: AbsolutePath.make(path.join("/repo/.opencode", "skill")) }), + new SkillV2.DirectorySource({ type: "directory", path: AbsolutePath.make(path.join("/repo/.opencode", "skills")) }), new SkillV2.DirectorySource({ type: "directory", path: AbsolutePath.make(path.join(directory, "skills")) }), new SkillV2.DirectorySource({ type: "directory", diff --git a/packages/core/test/plugin/skill.test.ts b/packages/core/test/plugin/skill.test.ts new file mode 100644 index 0000000000..63d028e4ec --- /dev/null +++ b/packages/core/test/plugin/skill.test.ts @@ -0,0 +1,32 @@ +import { describe, expect } from "bun:test" +import { Effect, Layer } from "effect" +import { AgentV2 } from "@opencode-ai/core/agent" +import { FSUtil } from "@opencode-ai/core/fs-util" +import { SkillPlugin } from "@opencode-ai/core/plugin/skill" +import { SkillV2 } from "@opencode-ai/core/skill" +import { SkillDiscovery } from "@opencode-ai/core/skill/discovery" +import { testEffect } from "../lib/effect" + +const it = testEffect( + SkillV2.layer.pipe( + Layer.provide(FSUtil.defaultLayer), + Layer.provide(SkillDiscovery.defaultLayer), + Layer.provideMerge(AgentV2.locationLayer), + ), +) + +describe("SkillPlugin.Plugin", () => { + it.effect("registers the built-in customize-opencode skill", () => + Effect.gen(function* () { + const skill = yield* SkillV2.Service + yield* SkillPlugin.Plugin.effect.pipe(Effect.provideService(SkillV2.Service, skill)) + + expect(yield* skill.list()).toContainEqual( + expect.objectContaining({ + name: "customize-opencode", + description: expect.stringContaining("opencode's own configuration"), + }), + ) + }), + ) +}) diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/message.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/v2/message.ts index be2fdb5ba4..109b63f97d 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/message.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/v2/message.ts @@ -5,6 +5,7 @@ import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { InvalidCursorError, SessionNotFoundError, UnknownError } from "../../errors" import { V2Authorization } from "../../middleware/authorization" import { WorkspaceRoutingQueryFields } from "../../middleware/workspace-routing" +import { data } from "./response" export const MessagesQuery = Schema.Struct({ ...WorkspaceRoutingQueryFields, @@ -29,13 +30,13 @@ export const MessageGroup = HttpApiGroup.make("v2.message") HttpApiEndpoint.get("messages", "/api/session/:sessionID/message", { params: { sessionID: SessionID }, query: MessagesQuery, - success: Schema.Struct({ + success: data(Schema.Struct({ items: Schema.Array(SessionMessage.Message), cursor: Schema.Struct({ previous: Schema.String.pipe(Schema.optional), next: Schema.String.pipe(Schema.optional), }), - }).annotate({ identifier: "V2SessionMessagesResponse" }), + }).annotate({ identifier: "V2SessionMessagesResponse" })), error: [InvalidCursorError, SessionNotFoundError, UnknownError], }).annotateMerge( OpenApi.annotations({ diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/permission.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/v2/permission.ts index 7a143d6828..8e07eef4aa 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/permission.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/v2/permission.ts @@ -8,6 +8,7 @@ import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/un import { PermissionNotFoundError, SessionNotFoundError } from "../../errors" import { V2Authorization } from "../../middleware/authorization" import { LocationQuery, locationQueryOpenApi, V2LocationMiddleware } from "./location" +import { data } from "./response" export const PermissionGroup = HttpApiGroup.make("v2.permission") .add( @@ -32,7 +33,7 @@ export const SessionPermissionGroup = HttpApiGroup.make("v2.session.permission") .add( HttpApiEndpoint.get("sessionPermissionRequests", "/api/session/:sessionID/permission/request", { params: { sessionID: SessionV2.ID }, - success: Schema.Array(PermissionV2.Request), + success: data(Schema.Array(PermissionV2.Request)), error: SessionNotFoundError, }).annotateMerge( OpenApi.annotations({ @@ -68,7 +69,7 @@ export const PermissionSavedGroup = HttpApiGroup.make("v2.permission.saved") .add( HttpApiEndpoint.get("savedPermissions", "/api/permission/saved", { query: Schema.Struct({ projectID: ProjectV2.ID.pipe(Schema.optional) }), - success: Schema.Array(PermissionSaved.Info), + success: data(Schema.Array(PermissionSaved.Info)), }).annotateMerge( OpenApi.annotations({ identifier: "v2.permission.saved.list", diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/response.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/v2/response.ts new file mode 100644 index 0000000000..f1a5a8d5fe --- /dev/null +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/v2/response.ts @@ -0,0 +1,9 @@ +import { Schema } from "effect" + +export function data(schema: S) { + return Schema.Struct({ data: schema }) +} + +export function make(data: A) { + return { data } +} diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/session.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/v2/session.ts index 7ec6b9875a..fdf6b26dd2 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/session.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/v2/session.ts @@ -16,6 +16,7 @@ import { } from "../../errors" import { V2Authorization } from "../../middleware/authorization" import { WorkspaceRoutingQuery } from "../../middleware/workspace-routing" +import { data } from "./response" const SessionsQueryFields = { workspace: WorkspaceV2.ID.pipe(Schema.optional), @@ -88,13 +89,13 @@ export const SessionGroup = HttpApiGroup.make("v2.session") .add( HttpApiEndpoint.get("sessions", "/api/session", { query: SessionsQuery, - success: Schema.Struct({ + success: data(Schema.Struct({ items: Schema.Array(SessionV2.Info), cursor: Schema.Struct({ previous: SessionsCursor.pipe(Schema.optional), next: SessionsCursor.pipe(Schema.optional), }), - }).annotate({ identifier: "V2SessionsResponse" }), + }).annotate({ identifier: "V2SessionsResponse" })), error: [InvalidCursorError, InvalidRequestError], }).annotateMerge( OpenApi.annotations({ @@ -113,7 +114,7 @@ export const SessionGroup = HttpApiGroup.make("v2.session") prompt: Prompt, delivery: SessionV2.Delivery.pipe(Schema.optional), }), - success: SessionMessage.Message, + success: data(SessionMessage.Message), error: [SessionNotFoundError, ServiceUnavailableError], }).annotateMerge( OpenApi.annotations({ @@ -155,7 +156,7 @@ export const SessionGroup = HttpApiGroup.make("v2.session") HttpApiEndpoint.get("context", "/api/session/:sessionID/context", { params: { sessionID: SessionID }, query: WorkspaceRoutingQuery, - success: Schema.Array(SessionMessage.Message), + success: data(Schema.Array(SessionMessage.Message)), error: [SessionNotFoundError, UnknownError], }).annotateMerge( OpenApi.annotations({ diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/message.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/message.ts index c9cfe33bc8..e0d9228170 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/message.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/message.ts @@ -5,6 +5,7 @@ import * as DateTime from "effect/DateTime" import { HttpApiBuilder } from "effect/unstable/httpapi" import { InstanceHttpApi } from "../../api" import { InvalidCursorError, SessionNotFoundError, UnknownError } from "../../errors" +import { make } from "../../groups/v2/response" const DefaultMessagesLimit = 50 @@ -75,13 +76,13 @@ export const messageHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.message ) const first = messages[0] const last = messages.at(-1) - return { + return make({ items: messages, cursor: { previous: first ? cursor.encode(first, order, "previous") : undefined, next: last ? cursor.encode(last, order, "next") : undefined, }, - } + }) }), ) }), diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/permission.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/permission.ts index e697ef314f..d241bcea7f 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/permission.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/permission.ts @@ -10,6 +10,7 @@ import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi" import { InstanceHttpApi } from "../../api" import { PermissionNotFoundError, SessionNotFoundError } from "../../errors" import { response } from "../../groups/v2/location" +import { make } from "../../groups/v2/response" function missingRequest(id: PermissionV2.ID) { return new PermissionNotFoundError({ requestID: id, message: `Permission request not found: ${id}` }) @@ -62,7 +63,7 @@ export const sessionPermissionHandlers = HttpApiBuilder.group(InstanceHttpApi, " "sessionPermissionRequests", Effect.fn(function* (ctx) { return yield* withSessionPermission(ctx.params.sessionID, (permission) => - permission.forSession(ctx.params.sessionID), + permission.forSession(ctx.params.sessionID).pipe(Effect.map(make)), ) }), ) @@ -92,7 +93,7 @@ export const savedPermissionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2 .handle( "savedPermissions", Effect.fn(function* (ctx) { - return yield* saved.list({ projectID: ctx.query.projectID }) + return make(yield* saved.list({ projectID: ctx.query.projectID })) }), ) .handle( diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts index 6befb471ad..77d7265131 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts @@ -4,6 +4,7 @@ import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi" import { InstanceHttpApi } from "../../api" import { SessionsCursor } from "../../groups/v2/session" import { InvalidCursorError, ServiceUnavailableError, SessionNotFoundError, UnknownError } from "../../errors" +import { make } from "../../groups/v2/response" const DefaultSessionsLimit = 50 @@ -28,7 +29,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.session }) const first = sessions[0] const last = sessions.at(-1) - return { + return make({ items: sessions, cursor: { previous: first @@ -52,13 +53,13 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.session }) : undefined, }, - } + }) }), ) .handle( "prompt", Effect.fn(function* (ctx) { - return yield* session + return make(yield* session .prompt({ sessionID: ctx.params.sessionID, prompt: ctx.payload.prompt, @@ -81,7 +82,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.session }), ), ), - ) + )) }), ) .handle( @@ -135,7 +136,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.session .handle( "context", Effect.fn(function* (ctx) { - return yield* session.context(ctx.params.sessionID).pipe( + return make(yield* session.context(ctx.params.sessionID).pipe( Effect.catchTag("Session.NotFoundError", (error) => Effect.fail( new SessionNotFoundError({ @@ -158,7 +159,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.session ), ) }), - ) + )) }), ) }), diff --git a/packages/opencode/src/skill/index.ts b/packages/opencode/src/skill/index.ts index 34e5edba83..b128a19da2 100644 --- a/packages/opencode/src/skill/index.ts +++ b/packages/opencode/src/skill/index.ts @@ -15,7 +15,7 @@ import { RuntimeFlags } from "@/effect/runtime-flags" import { Glob } from "@opencode-ai/core/util/glob" import * as Log from "@opencode-ai/core/util/log" import { Discovery } from "./discovery" -import CUSTOMIZE_OPENCODE_SKILL_BODY from "./prompt/customize-opencode.md" with { type: "text" } +import CUSTOMIZE_OPENCODE_SKILL_BODY from "../../../core/src/plugin/skill/customize-opencode.md" with { type: "text" } import { isRecord } from "@/util/record" const log = Log.create({ service: "skill" }) diff --git a/packages/opencode/test/server/httpapi-exercise/index.ts b/packages/opencode/test/server/httpapi-exercise/index.ts index f8e8db75f7..6d9a99ffcf 100644 --- a/packages/opencode/test/server/httpapi-exercise/index.ts +++ b/packages/opencode/test/server/httpapi-exercise/index.ts @@ -42,6 +42,22 @@ function cursor(input: Record) { return Buffer.from(JSON.stringify(input)).toString("base64url") } +function data(validate: (value: any) => void) { + return (body: any) => { + object(body) + validate(body.data) + } +} + +function locationData(validate: (value: any) => void) { + return (body: any) => { + object(body) + object(body.location) + object(body.location.project) + validate(body.data) + } +} + const scenarios: Scenario[] = [ http.protected .get("/global/health", "global.health") @@ -608,19 +624,19 @@ const scenarios: Scenario[] = [ check(auth.test === undefined, "auth remove should delete provider from isolated auth file") }), ), - http.protected.get("/api/model", "v2.model.list").json(200, array), - http.protected.get("/api/provider", "v2.provider.list").json(200, array), + http.protected.get("/api/model", "v2.model.list").json(200, locationData(array)), + http.protected.get("/api/provider", "v2.provider.list").json(200, locationData(array)), http.protected .get("/api/fs/read", "v2.fs.read") .seeded((ctx) => ctx.file("hello.txt", "hello\n")) .at((ctx) => ({ path: "/api/fs/read?path=hello.txt", headers: ctx.headers() })) - .json(200, object), - http.protected.get("/api/fs/list", "v2.fs.list").json(200, array), + .json(200, locationData(object)), + http.protected.get("/api/fs/list", "v2.fs.list").json(200, locationData(array)), http.protected .get("/api/provider/{providerID}", "v2.provider.get") .at((ctx) => ({ path: route("/api/provider/{providerID}", { providerID: "missing" }), headers: ctx.headers() })) .json(404, object, "status"), - http.protected.get("/api/permission/request", "v2.permission.request.list").json(200, array), + http.protected.get("/api/permission/request", "v2.permission.request.list").json(200, locationData(array)), http.protected .get("/api/session/{sessionID}/permission/request", "v2.session.permission.list") .seeded((ctx) => ctx.session({ title: "Permission list owner" })) @@ -628,7 +644,7 @@ const scenarios: Scenario[] = [ path: route("/api/session/{sessionID}/permission/request", { sessionID: ctx.state.id }), headers: ctx.headers(), })) - .json(200, array), + .json(200, data(array)), http.protected .post("/api/session/{sessionID}/permission/request/{requestID}/reply", "v2.session.permission.reply") .seeded((ctx) => ctx.session({ title: "Permission owner" })) @@ -641,7 +657,7 @@ const scenarios: Scenario[] = [ body: { reply: "once" }, })) .json(404, object, "status"), - http.protected.get("/api/permission/saved", "v2.permission.saved.list").json(200, array), + http.protected.get("/api/permission/saved", "v2.permission.saved.list").json(200, data(array)), http.protected .delete("/api/permission/saved/{id}", "v2.permission.saved.remove") .at((ctx) => ({ path: route("/api/permission/saved/{id}", { id: "psv_httpapi_missing" }), headers: ctx.headers() })) @@ -653,8 +669,9 @@ const scenarios: Scenario[] = [ 200, (body) => { object(body) - array(body.items) - object(body.cursor) + object(body.data) + array(body.data.items) + object(body.data.cursor) }, "none", ), @@ -676,8 +693,9 @@ const scenarios: Scenario[] = [ 200, (body) => { object(body) - array(body.items) - object(body.cursor) + object(body.data) + array(body.data.items) + object(body.data.cursor) }, "none", ), @@ -698,8 +716,9 @@ const scenarios: Scenario[] = [ 200, (body) => { object(body) - array(body.items) - object(body.cursor) + object(body.data) + array(body.data.items) + object(body.data.cursor) }, "none", ), diff --git a/packages/opencode/test/server/httpapi-session.test.ts b/packages/opencode/test/server/httpapi-session.test.ts index 5763cf3b00..6a3eb664f1 100644 --- a/packages/opencode/test/server/httpapi-session.test.ts +++ b/packages/opencode/test/server/httpapi-session.test.ts @@ -388,8 +388,9 @@ describe("session HttpApi", () => { yield* insertLegacyAssistantMessage(parent.id) expect( - (yield* requestJson<{ items: SessionMessage.Message[] }>(`/api/session/${parent.id}/message`, { headers })) - .items, + (yield* requestJson<{ data: { items: SessionMessage.Message[] } }>(`/api/session/${parent.id}/message`, { + headers, + })).data.items, ).toMatchObject([{ type: "assistant" }]) }), { git: true, config: { formatter: false, lsp: false } }, @@ -453,7 +454,7 @@ describe("session HttpApi", () => { })}`, { headers }, ) - const sessionCursor = (yield* json<{ cursor: { next?: string } }>(sessionPage)).cursor.next + const sessionCursor = (yield* json<{ data: { cursor: { next?: string } } }>(sessionPage)).data.cursor.next expect(sessionCursor).toBeTruthy() expect(JSON.parse(Buffer.from(sessionCursor!, "base64url").toString("utf8"))).toMatchObject({ order: "asc", @@ -480,7 +481,7 @@ describe("session HttpApi", () => { }) const messagePage = yield* request(`/api/session/${session.id}/message?limit=1`, { headers }) - const messageCursor = (yield* json<{ cursor: { next?: string } }>(messagePage)).cursor.next + const messageCursor = (yield* json<{ data: { cursor: { next?: string } } }>(messagePage)).data.cursor.next expect(messageCursor).toBeTruthy() const messageCursorWithOrder = yield* request(