Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/client/script/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NodeFileSystem } from "@effect/platform-node"
import { compile, emitEffectImported, emitEffectShape, emitPromise, write } from "@opencode-ai/httpapi-codegen"
import { ClientApi, effectOmitEndpoints, groupNames, promiseOmitEndpoints } from "@opencode-ai/protocol/client"
import { Agent } from "@opencode-ai/schema/agent"
import { Capability } from "@opencode-ai/schema/capability"
import { Command } from "@opencode-ai/schema/command"
import { Config } from "@opencode-ai/schema/config"
import { Credential } from "@opencode-ai/schema/credential"
Expand Down Expand Up @@ -43,6 +44,7 @@ const promiseContract = compile(ClientApi, { groupNames, omitEndpoints: promiseO
const effectContract = compile(ClientApi, { groupNames, omitEndpoints: effectOmitEndpoints })
const effectTypeReferences = [
...namespaceTypes("Agent", "@opencode-ai/schema/agent", Agent),
...namespaceTypes("Capability", "@opencode-ai/schema/capability", Capability),
...namespaceTypes("Command", "@opencode-ai/schema/command", Command),
...namespaceTypes("Config", "@opencode-ai/schema/config", Config),
...namespaceTypes("Credential", "@opencode-ai/schema/credential", Credential),
Expand Down
21 changes: 21 additions & 0 deletions packages/client/src/effect/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import type { Vcs } from "@opencode-ai/schema/vcs"
import type { FileDiff } from "@opencode-ai/schema/file-diff"
import type { WebSearch } from "@opencode-ai/schema/websearch"
import type { Config } from "@opencode-ai/schema/config"
import type { Capability } from "@opencode-ai/schema/capability"

export type Endpoint0_0Output = { readonly healthy: true; readonly version: string; readonly pid: number }
export type HealthGetOperation<E = never> = () => Effect.Effect<Endpoint0_0Output, E>
Expand Down Expand Up @@ -1631,6 +1632,25 @@ export interface ConfigApi<E = never> {
readonly get: ConfigGetOperation<E>
}

export type Endpoint29_0Input = {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint29_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Capability.Info> }
export type CapabilityListOperation<E = never> = (input?: Endpoint29_0Input) => Effect.Effect<Endpoint29_0Output, E>

export type Endpoint29_1Input = {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
readonly ref: Capability.Ref
readonly state: "enabled" | "disabled" | "inherit"
}
export type Endpoint29_1Output = void
export type CapabilityUpdateOperation<E = never> = (input: Endpoint29_1Input) => Effect.Effect<Endpoint29_1Output, E>

export interface CapabilityApi<E = never> {
readonly list: CapabilityListOperation<E>
readonly update: CapabilityUpdateOperation<E>
}

export interface AppApi<E = never> {
readonly health: HealthApi<E>
readonly server: ServerApi<E>
Expand Down Expand Up @@ -1661,4 +1681,5 @@ export interface AppApi<E = never> {
readonly migration: MigrationApi<E>
readonly websearch: WebsearchApi<E>
readonly config: ConfigApi<E>
readonly capability: CapabilityApi<E>
}
20 changes: 20 additions & 0 deletions packages/client/src/effect/generated/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,10 @@ import type {
Endpoint27_1Output,
Endpoint28_0Input,
Endpoint28_0Output,
Endpoint29_0Input,
Endpoint29_0Output,
Endpoint29_1Input,
Endpoint29_1Output,
} from "../api/api.js"
import { ClientError } from "./client-error.js"

Expand Down Expand Up @@ -1259,6 +1263,21 @@ const Endpoint28_0 = (raw: RawClient["server.config"]) => (input?: Endpoint28_0I

const adaptGroup28 = (raw: RawClient["server.config"]) => ({ get: Endpoint28_0(raw) })

const Endpoint29_0 = (raw: RawClient["server.capability"]) => (input?: Endpoint29_0Input) =>
preserveEffect<Endpoint29_0Output>()(
raw["capability.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)),
)

const Endpoint29_1 = (raw: RawClient["server.capability"]) => (input: Endpoint29_1Input) =>
preserveEffect<Endpoint29_1Output>()(
raw["capability.update"]({
query: { location: input["location"] },
payload: { ref: input["ref"], state: input["state"] },
}).pipe(Effect.mapError(mapClientError)),
)

const adaptGroup29 = (raw: RawClient["server.capability"]) => ({ list: Endpoint29_0(raw), update: Endpoint29_1(raw) })

const adaptClient = (raw: RawClient) => ({
health: adaptGroup0(raw["server.health"]),
server: adaptGroup1(raw["server.server"]),
Expand Down Expand Up @@ -1289,6 +1308,7 @@ const adaptClient = (raw: RawClient) => ({
migration: adaptGroup26(raw["server.migration"]),
websearch: adaptGroup27(raw["server.websearch"]),
config: adaptGroup28(raw["server.config"]),
capability: adaptGroup29(raw["server.capability"]),
})

export const make = (options?: { readonly baseUrl?: URL | string }) =>
Expand Down
31 changes: 31 additions & 0 deletions packages/client/src/promise/generated/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,10 @@ import type {
WebsearchQueryOutput,
ConfigGetInput,
ConfigGetOutput,
CapabilityListInput,
CapabilityListOutput,
CapabilityUpdateInput,
CapabilityUpdateOutput,
} from "./types.js"
import { ClientError } from "./client-error.js"

Expand Down Expand Up @@ -1840,6 +1844,33 @@ export function make(options: ClientOptions) {
requestOptions,
),
},
capability: {
list: (input?: CapabilityListInput, requestOptions?: RequestOptions) =>
request<CapabilityListOutput>(
{
method: "GET",
path: `/api/capability`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
update: (input: CapabilityUpdateInput, requestOptions?: RequestOptions) =>
request<CapabilityUpdateOutput>(
{
method: "PUT",
path: `/api/capability`,
query: { location: input["location"] },
body: { ref: input["ref"], state: input["state"] },
successStatus: 204,
declaredStatuses: [401, 400],
empty: true,
},
requestOptions,
),
},
}
}

Expand Down
53 changes: 49 additions & 4 deletions packages/client/src/promise/generated/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ export type SkillInfo = {

export type PermissionReply = "once" | "always" | "reject"

export type CapabilityRef = { kind: "skill"; key: [string, ...Array<string>] }

export type Pty = {
id: string
title: string
Expand Down Expand Up @@ -244,7 +246,7 @@ export type PromptFileAttachment = {

export type PromptAgentAttachment = { name: string; mention?: PromptMention }

export type PromptSkillAttachment = { id: string; name: string; text: string; mention?: PromptMention }
export type PromptSkillAttachment = { id: string; name: string; mention?: PromptMention }

export type ToolFileContent = { type: "file"; uri: string; mime: string; name?: string | null }

Expand Down Expand Up @@ -1062,6 +1064,24 @@ export type PermissionReplied = {
data: { sessionID: string; requestID: string; reply: PermissionReply }
}

export type CapabilityUpdated = {
id: string
created: number
metadata?: { [x: string]: any }
type: "capability.updated"
location?: LocationRef
data: { ref: CapabilityRef }
}

export type CapabilityInfo = {
ref: CapabilityRef
name: string
description?: string
defaultState: "enabled" | "disabled"
state: "enabled" | "disabled"
preference?: "enabled" | "disabled"
}

export type PtyCreated = {
id: string
created: number
Expand Down Expand Up @@ -2077,6 +2097,7 @@ export type V2Event =
| WorktreeResolved
| CommandUpdated
| ConfigUpdated
| CapabilityUpdated
| SkillUpdated
| PtyCreated
| PtyUpdated
Expand Down Expand Up @@ -2567,7 +2588,6 @@ export type SessionImportInput = {
readonly skills?: ReadonlyArray<{
readonly id: string
readonly name: string
readonly text: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly type: "user"
Expand Down Expand Up @@ -2836,7 +2856,6 @@ export type SessionImportInput = {
readonly skills?: ReadonlyArray<{
readonly id: string
readonly name: string
readonly text: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly type: "user"
Expand Down Expand Up @@ -3105,7 +3124,6 @@ export type SessionImportInput = {
readonly skills?: ReadonlyArray<{
readonly id: string
readonly name: string
readonly text: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly type: "user"
Expand Down Expand Up @@ -5722,3 +5740,30 @@ export type ConfigGetInput = {
}

export type ConfigGetOutput = Array<ConfigEntry>

export type CapabilityListInput = {
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
}

export type CapabilityListOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } }
data: Array<CapabilityInfo>
}

export type CapabilityUpdateInput = {
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly ref: {
readonly ref: { readonly kind: "skill"; readonly key: readonly [string, ...Array<string>] }
readonly state: "enabled" | "disabled" | "inherit"
}["ref"]
readonly state: {
readonly ref: { readonly kind: "skill"; readonly key: readonly [string, ...Array<string>] }
readonly state: "enabled" | "disabled" | "inherit"
}["state"]
}

export type CapabilityUpdateOutput = void
71 changes: 71 additions & 0 deletions packages/core/src/capability.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
export * as Capability from "./capability.js"

import { Capability } from "@opencode-ai/schema/capability"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { Context, Effect, Layer, Option, Schema } from "effect"
import { Bus } from "./bus.js"
import { KV } from "./kv.js"

export const Ref = Capability.Ref
export type Ref = Capability.Ref
export const State = Capability.State
export type State = Capability.State
export const Preference = Capability.Preference
export type Preference = Capability.Preference
export const Info = Capability.Info
export type Info = Capability.Info
export const Update = Capability.Update
export type Update = Capability.Update
export const Event = Capability.Event

export const skill = (id: string) => Ref.make({ kind: "skill", key: [id] })

const Key = "capability:preferences"
const Preferences = Schema.Array(Preference)
const equals = Schema.toEquivalence(Ref)

export interface Interface {
readonly list: () => Effect.Effect<ReadonlyArray<Preference>>
readonly get: (ref: Ref) => Effect.Effect<State | undefined>
readonly resolve: (ref: Ref, fallback?: boolean) => Effect.Effect<State>
readonly set: (update: Update) => Effect.Effect<void>
}

export class Service extends Context.Service<Service, Interface>()("@opencode/Capability") {}

const layer = Layer.effect(
Service,
Effect.gen(function* () {
const bus = yield* Bus.Service
const kv = yield* KV.Service

const load = Effect.fn("Capability.load")(function* () {
const stored = yield* kv.get(Key)
const decoded = Schema.decodeUnknownOption(Preferences)(stored)
if (stored !== undefined && Option.isNone(decoded)) yield* kv.remove(Key)
return Option.getOrElse(decoded, () => [])
})

const get = Effect.fn("Capability.get")(function* (ref: Ref) {
return (yield* load()).find((item) => equals(item.ref, ref))?.state
})

return Service.of({
list: load,
get,
resolve: Effect.fn("Capability.resolve")(function* (ref, fallback = true) {
return (yield* get(ref)) ?? (fallback ? "enabled" : "disabled")
}),
set: Effect.fn("Capability.set")(function* (update) {
const preferences = (yield* load()).filter((item) => !equals(item.ref, update.ref))
yield* kv.set(
Key,
update.state === "inherit" ? preferences : [...preferences, { ref: update.ref, state: update.state }],
)
yield* bus.publish(Event.Updated, { ref: update.ref })
}),
})
}),
)

export const node = makeLocationNode({ service: Service, layer, deps: [Bus.node, KV.node] })
2 changes: 2 additions & 0 deletions packages/core/src/location-services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,15 @@ import { Tool } from "./tool.js"
import { ToolOutput } from "./tool-output.js"
import { Vcs } from "./vcs.js"
import { AbsolutePath } from "./schema.js"
import { Capability } from "./capability.js"

export { LocationServiceMap } from "./location-service-map.js"

const locationServiceNodes = [
Location.node,
Environment.node,
Config.node,
Capability.node,
Agent.node,
Command.node,
Reference.node,
Expand Down
1 change: 0 additions & 1 deletion packages/core/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -982,7 +982,6 @@ const resolvePrompt = Effect.fn("Session.resolvePrompt")(function* (
return Effect.succeed({
id: skill.id,
name: skill.name,
text: Skill.toModelOutput(skill, []),
mention: attachment.mention,
})
})
Expand Down
3 changes: 1 addition & 2 deletions packages/core/src/session/compaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,7 @@ const serialize = (message: SessionMessage.Info) => {
(file) =>
`[Attached ${file.mime}: ${file.name ?? (file.source.type === "uri" ? file.source.uri : "inline attachment")}]`,
) ?? []
const skills = message.skills?.map((skill) => `[Attached skill: ${skill.name}]\n${skill.text}`) ?? []
return [`[User]: ${message.text}`, ...skills, ...files].join("\n")
return [`[User]: ${message.text}`, ...files].join("\n")
}
if (message.type === "location-switched")
return `[User]: The working directory has been changed to ${message.location.directory}.`
Expand Down
1 change: 0 additions & 1 deletion packages/core/src/session/runner/to-llm-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,6 @@ function toLLMMessage(message: SessionMessage.Info, model: Model.Ref, providerMe
]
case "user":
const content = [
...(message.skills ?? []).map((skill) => Message.text(skill.text)),
...(message.text === "" ? [] : [Message.text(message.text)]),
...userAttachmentContent(message.files ?? []),
]
Expand Down
1 change: 0 additions & 1 deletion packages/core/src/session/transfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,6 @@ function sanitizeMessage(message: SessionMessage.Info): SessionMessage.Info {
skills: message.skills?.map((skill, index) => ({
...skill,
name: Skill.Name.make(redact("skill-name", String(index), skill.name)),
text: redact("skill", String(index), skill.text),
mention: skill.mention
? { ...skill.mention, text: redact("skill-mention", String(index), skill.mention.text) }
: undefined,
Expand Down
Loading
Loading