feat(sdk): prepare v2 packages for publishing

This commit is contained in:
Dax Raad
2026-08-21 17:27:25 -04:00
parent 97ccafce3b
commit f28c10aa4d
62 changed files with 1244 additions and 4510 deletions
File diff suppressed because it is too large Load Diff
+1 -17
View File
@@ -1,19 +1,3 @@
{
"$schema": "https://opencode.ai/tui.json",
"plugin": [
[
"./plugins/tui-smoke.tsx",
{
"enabled": false,
"label": "workspace",
"keybinds": {
"smoke_modal": "ctrl+alt+m",
"smoke_screen": "ctrl+alt+o",
"smoke_screen_home": "escape,ctrl+shift+h",
"smoke_screen_modal": "ctrl+alt+m",
"smoke_dialog_close": "escape,q"
}
}
]
]
"$schema": "https://opencode.ai/tui.json"
}
+1 -1
View File
@@ -1,5 +1,5 @@
- After changing the public Protocol or Server `HttpApi`, run `bun run generate` from `packages/client`. Do not edit generated client files directly.
- Keep runtime dependencies directed from Schema to Core and Protocol, then from Core and Protocol to Server. Client runtime code may depend on Schema and Protocol but never Core or Server; `sdk-next` composes Client, Core, and Server.
- Keep runtime dependencies directed from Schema to Core and Protocol, then from Core and Protocol to Server. Client runtime code may depend on Schema and Protocol but never Core or Server; `sdk` composes Client, Core, and Server.
- Current implementation changes belong in `packages/core`, `packages/cli`, `packages/server`, `packages/protocol`, `packages/schema`, and related generated client surfaces when required.
- The default branch in this repo is `v2`.
- Base all new branches and worktrees on `v2`, or `origin/v2` when the local `v2` ref is unavailable. Do not base them on `dev`.
+664 -1886
View File
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -27,6 +27,7 @@
"upgrade-opentui": "bun run script/upgrade-opentui.ts",
"postinstall": "bun run --cwd packages/core fix-node-pty",
"prepare": "husky",
"reserve-packages": "bun script/reserve-package-names.ts",
"random": "echo 'Random script'",
"sso": "aws sso login --sso-session=opencode --no-browser",
"test": "echo 'do not run tests from root' && exit 1"
@@ -35,8 +36,7 @@
"packages": [
"packages/*",
"packages/console/*",
"packages/stats/*",
"packages/slack"
"packages/stats/*"
],
"catalog": {
"@effect/opentelemetry": "4.0.0-rc.110",
@@ -129,7 +129,6 @@
"@aws-sdk/client-s3": "3.933.0",
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "1.18.5",
"heap-snapshot-toolkit": "1.1.3",
"typescript": "catalog:"
},
+1 -1
View File
@@ -80,7 +80,6 @@
"@types/bun": "catalog:",
"@types/node": "catalog:",
"@types/which": "3.0.4",
"@opencode-ai/shell-scan": "workspace:*",
"@parcel/watcher-darwin-arm64": "2.5.1",
"@parcel/watcher-darwin-x64": "2.5.1",
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
@@ -119,6 +118,7 @@
"@opencode-ai/ai": "workspace:*",
"@opencode-ai/schema": "workspace:*",
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/shell-scan": "workspace:*",
"@opencode-ai/util": "workspace:*",
"@standard-schema/spec": "catalog:",
"@parcel/watcher": "2.5.1",
-10
View File
@@ -20,16 +20,6 @@ const result = await Bun.build({
format: "esm",
packages: "external",
external: ["#sqlite", "#pty", "#fff", "#photon-wasm", "#shell-parser-wasm", "#process-lock-ffi", "#v1-migration"],
plugins: [
{
name: "bundle-shell-scan",
setup(build) {
build.onResolve({ filter: /^@opencode-ai\/shell-scan$/ }, () => ({
path: path.resolve("../shell-scan/src/index.ts"),
}))
},
},
],
splitting: true,
loader: {
".txt": "text",
+1 -2
View File
@@ -2,7 +2,6 @@ export * as PluginHost from "./host.js"
import { Plugin } from "@opencode-ai/plugin/effect"
import type { IntegrationMethodRegistration } from "@opencode-ai/plugin/effect/integration"
import type { CredentialOAuth } from "@opencode-ai/sdk/v2/types"
import { EventManifest } from "@opencode-ai/schema/event-manifest"
import { Mcp } from "@opencode-ai/schema/mcp"
import { App } from "../app.js"
@@ -481,6 +480,6 @@ function methodImplementation(input: IntegrationMethodRegistration): Integration
}
}
function credential(value: CredentialOAuth) {
function credential(value: Credential.OAuth) {
return Credential.OAuth.make({ ...value, methodID: Integration.MethodID.make(value.methodID) })
}
@@ -2,7 +2,6 @@ import { Duration, Effect, Schema, Semaphore, Stream } from "effect"
import type { Scope } from "effect"
import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/effect/integration"
import { define } from "@opencode-ai/plugin/effect/plugin"
import type { CredentialValue } from "@opencode-ai/sdk/v2/types"
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
import { Bus } from "../../bus.js"
import { Credential } from "../../credential.js"
@@ -199,7 +198,7 @@ export const OpencodePlugin = define<HttpClient.HttpClient | Bus.Service | Scope
}),
})
function fetchProviders(http: HttpClient.HttpClient, value: CredentialValue) {
function fetchProviders(http: HttpClient.HttpClient, value: Credential.Value) {
const metadata = value.metadata
const server = typeof metadata?.server === "string" ? metadata.server : defaultServer
const orgID = typeof metadata?.orgID === "string" ? metadata.orgID : undefined
+1 -1
View File
@@ -9,7 +9,7 @@ import type { Versioned } from "../plugin.js"
export const Updated = Bus.ephemeral({ type: "sdk.plugin.updated", schema: {} })
/**
* Holds the plugins an embedder (the `@opencode-ai/sdk-next` host) contributes,
* Holds the plugins an embedder (the `@opencode-ai/sdk` host) contributes,
* so `PluginSupervisor` can add them on every Location boot through the ordinary
* generation path that `PluginSupervisor` uses for plugins discovered from
* config. Registration publishes an unlocated update so every booted Location
+1
View File
@@ -16,6 +16,7 @@
"@opencode-ai/client": "workspace:*",
"@opencode-ai/core": "workspace:*",
"@opencode-ai/schema": "workspace:*",
"@opencode-ai/sdk": "1.18.21",
"@opencode-ai/session-ui": "workspace:*",
"@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*",
-7
View File
@@ -13,7 +13,6 @@
".": "./src/promise/index.ts",
"./effect": "./src/effect/index.ts",
"./tui": "./src/tui/index.ts",
"./v1": "./src/v1/index.ts",
"./*": "./src/*.ts"
},
"files": [
@@ -25,7 +24,6 @@
"@opencode-ai/client": "workspace:*",
"@opencode-ai/protocol": "workspace:*",
"@opencode-ai/schema": "workspace:*",
"@opencode-ai/sdk": "1.18.5",
"@standard-schema/spec": "catalog:",
"effect": "catalog:",
"zod": "catalog:"
@@ -33,7 +31,6 @@
"peerDependencies": {
"@opencode-ai/theme": "workspace:*",
"@opentui/core": ">=0.5.6",
"@opentui/keymap": ">=0.5.6",
"@opentui/solid": ">=0.5.6",
"solid-js": ">=1.9.0"
},
@@ -44,9 +41,6 @@
"@opentui/core": {
"optional": true
},
"@opentui/keymap": {
"optional": true
},
"@opentui/solid": {
"optional": true
},
@@ -57,7 +51,6 @@
"devDependencies": {
"@opencode-ai/theme": "workspace:*",
"@opentui/core": "catalog:",
"@opentui/keymap": "catalog:",
"@opentui/solid": "catalog:",
"@tsconfig/bun": "catalog:",
"@tsconfig/node22": "catalog:",
-335
View File
@@ -1,335 +0,0 @@
import type {
Event,
createOpencodeClient,
Project,
Model,
Provider,
Permission,
UserMessage,
Message,
Part,
Config as SDKConfig,
} from "@opencode-ai/sdk"
import type { Provider as ProviderV2, Model as ModelV2, Auth } from "@opencode-ai/sdk/v2"
import type { BunShell } from "./shell.js"
import { type ToolDefinition } from "./tool.js"
export * from "./tool.js"
export type ProviderContext = {
source: "env" | "config" | "custom" | "api"
info: Provider
options: Record<string, any>
}
export type WorkspaceInfo = {
id: string
type: string
name: string
branch: string | null
directory: string | null
extra: unknown | null
projectID: string
}
export type WorkspaceTarget =
| {
type: "local"
directory: string
}
| {
type: "remote"
url: string | URL
headers?: HeadersInit
}
export type WorkspaceAdapter = {
name: string
description: string
configure(config: WorkspaceInfo): WorkspaceInfo | Promise<WorkspaceInfo>
create(config: WorkspaceInfo, env: Record<string, string | undefined>, from?: WorkspaceInfo): Promise<void>
remove(config: WorkspaceInfo): Promise<void>
target(config: WorkspaceInfo): WorkspaceTarget | Promise<WorkspaceTarget>
}
export type PluginInput = {
client: ReturnType<typeof createOpencodeClient>
project: Project
directory: string
worktree: string
experimental_workspace: {
register(type: string, adapter: WorkspaceAdapter): void
}
serverUrl: URL
$: BunShell
}
export type PluginOptions = Record<string, unknown>
export type Config = Omit<SDKConfig, "plugin"> & {
plugin?: Array<string | [string, PluginOptions]>
}
export type Plugin = (input: PluginInput, options?: PluginOptions) => Promise<Hooks>
export type PluginModule = {
id?: string
server: Plugin
tui?: never
}
type Rule = {
key: string
op: "eq" | "neq"
value: string
}
export type AuthHook = {
provider: string
loader?: (auth: () => Promise<Auth>, provider: Provider) => Promise<Record<string, any>>
methods: (
| {
type: "oauth"
label: string
prompts?: Array<
| {
type: "text"
key: string
message: string
placeholder?: string
validate?: (value: string) => string | undefined
/** @deprecated Use `when` instead */
condition?: (inputs: Record<string, string>) => boolean
when?: Rule
}
| {
type: "select"
key: string
message: string
options: Array<{
label: string
value: string
hint?: string
}>
/** @deprecated Use `when` instead */
condition?: (inputs: Record<string, string>) => boolean
when?: Rule
}
>
authorize(inputs?: Record<string, string>): Promise<AuthOAuthResult>
}
| {
type: "api"
label: string
prompts?: Array<
| {
type: "text"
key: string
message: string
placeholder?: string
validate?: (value: string) => string | undefined
/** @deprecated Use `when` instead */
condition?: (inputs: Record<string, string>) => boolean
when?: Rule
}
| {
type: "select"
key: string
message: string
options: Array<{
label: string
value: string
hint?: string
}>
/** @deprecated Use `when` instead */
condition?: (inputs: Record<string, string>) => boolean
when?: Rule
}
>
authorize?(inputs?: Record<string, string>): Promise<
| {
type: "success"
key: string
provider?: string
metadata?: Record<string, string>
}
| {
type: "failed"
}
>
}
)[]
}
export type AuthOAuthResult = { url: string; instructions: string } & (
| {
method: "auto"
callback(): Promise<
| ({
type: "success"
provider?: string
} & (
| {
refresh: string
access: string
expires: number
accountId?: string
enterpriseUrl?: string
}
| { key: string; metadata?: Record<string, string> }
))
| {
type: "failed"
}
>
}
| {
method: "code"
callback(code: string): Promise<
| ({
type: "success"
provider?: string
} & (
| {
refresh: string
access: string
expires: number
accountId?: string
enterpriseUrl?: string
}
| { key: string; metadata?: Record<string, string> }
))
| {
type: "failed"
}
>
}
)
export type ProviderHookContext = {
auth?: Auth
}
export type ProviderHook = {
id: string
models?: (provider: ProviderV2, ctx: ProviderHookContext) => Promise<Record<string, ModelV2>>
}
/** @deprecated Use AuthOAuthResult instead. */
export type AuthOuathResult = AuthOAuthResult
export interface Hooks {
dispose?: () => Promise<void>
event?: (input: { event: Event }) => Promise<void>
config?: (input: Config) => Promise<void>
tool?: {
[key: string]: ToolDefinition
}
auth?: AuthHook
provider?: ProviderHook
/**
* Called when a new message is received
*/
"chat.message"?: (
input: {
sessionID: string
agent?: string
model?: { providerID: string; modelID: string }
messageID?: string
variant?: string
},
output: { message: UserMessage; parts: Part[] },
) => Promise<void>
/**
* Modify parameters sent to LLM
*/
"chat.params"?: (
input: { sessionID: string; agent: string; model: Model; provider: ProviderContext; message: UserMessage },
output: {
temperature: number
topP: number
topK: number
maxOutputTokens: number | undefined
options: Record<string, any>
},
) => Promise<void>
"chat.headers"?: (
input: { sessionID: string; agent: string; model: Model; provider: ProviderContext; message: UserMessage },
output: { headers: Record<string, string> },
) => Promise<void>
"permission.ask"?: (input: Permission, output: { status: "ask" | "deny" | "allow" }) => Promise<void>
"command.execute.before"?: (
input: { command: string; sessionID: string; arguments: string },
output: { parts: Part[] },
) => Promise<void>
"tool.execute.before"?: (
input: { tool: string; sessionID: string; callID: string },
output: { args: any },
) => Promise<void>
"shell.env"?: (
input: { cwd: string; sessionID?: string; callID?: string },
output: { env: Record<string, string> },
) => Promise<void>
"tool.execute.after"?: (
input: { tool: string; sessionID: string; callID: string; args: any },
output: {
title: string
output: string
metadata: any
},
) => Promise<void>
"experimental.chat.messages.transform"?: (
input: {},
output: {
messages: {
info: Message
parts: Part[]
}[]
},
) => Promise<void>
"experimental.chat.system.transform"?: (
input: { sessionID?: string; model: Model },
output: {
system: string[]
},
) => Promise<void>
"experimental.provider.small_model"?: (input: { provider: ProviderV2 }, output: { model?: ModelV2 }) => Promise<void>
/**
* Called before session compaction starts. Allows plugins to customize
* the compaction prompt.
*
* - `context`: Additional context strings appended to the default prompt
* - `prompt`: If set, replaces the default compaction prompt entirely
*/
"experimental.session.compacting"?: (
input: { sessionID: string },
output: { context: string[]; prompt?: string },
) => Promise<void>
/**
* Called after compaction succeeds and before a synthetic user
* auto-continue message is added.
*
* - `enabled`: Defaults to `true`. Set to `false` to skip the synthetic
* user "continue" turn.
*/
"experimental.compaction.autocontinue"?: (
input: {
sessionID: string
agent: string
model: Model
provider: ProviderContext
message: UserMessage
overflow: boolean
},
output: { enabled: boolean },
) => Promise<void>
"experimental.text.complete"?: (
input: { sessionID: string; messageID: string; partID: string },
output: { text: string },
) => Promise<void>
/**
* Modify tool definitions (description and parameters) sent to LLM
*/
"tool.definition"?: (input: { toolID: string }, output: { description: string; parameters: any }) => Promise<void>
}
-136
View File
@@ -1,136 +0,0 @@
export type ShellFunction = (input: Uint8Array) => Uint8Array
export type ShellExpression =
| { toString(): string }
| Array<ShellExpression>
| string
| { raw: string }
| ReadableStream
export interface BunShell {
(strings: TemplateStringsArray, ...expressions: ShellExpression[]): BunShellPromise
/**
* Perform bash-like brace expansion on the given pattern.
* @param pattern - Brace pattern to expand
*/
braces(pattern: string): string[]
/**
* Escape strings for input into shell commands.
*/
escape(input: string): string
/**
* Change the default environment variables for shells created by this instance.
*/
env(newEnv?: Record<string, string | undefined>): BunShell
/**
* Default working directory to use for shells created by this instance.
*/
cwd(newCwd?: string): BunShell
/**
* Configure the shell to not throw an exception on non-zero exit codes.
*/
nothrow(): BunShell
/**
* Configure whether or not the shell should throw an exception on non-zero exit codes.
*/
throws(shouldThrow: boolean): BunShell
}
export interface BunShellPromise extends Promise<BunShellOutput> {
readonly stdin: WritableStream
/**
* Change the current working directory of the shell.
*/
cwd(newCwd: string): this
/**
* Set environment variables for the shell.
*/
env(newEnv: Record<string, string> | undefined): this
/**
* By default, the shell will write to the current process's stdout and stderr, as well as buffering that output.
* This configures the shell to only buffer the output.
*/
quiet(): this
/**
* Read from stdout as a string, line by line
* Automatically calls quiet() to disable echoing to stdout.
*/
lines(): AsyncIterable<string>
/**
* Read from stdout as a string.
* Automatically calls quiet() to disable echoing to stdout.
*/
text(encoding?: BufferEncoding): Promise<string>
/**
* Read from stdout as a JSON object
* Automatically calls quiet()
*/
json(): Promise<any>
/**
* Read from stdout as an ArrayBuffer
* Automatically calls quiet()
*/
arrayBuffer(): Promise<ArrayBuffer>
/**
* Read from stdout as a Blob
* Automatically calls quiet()
*/
blob(): Promise<Blob>
/**
* Configure the shell to not throw an exception on non-zero exit codes.
*/
nothrow(): this
/**
* Configure whether or not the shell should throw an exception on non-zero exit codes.
*/
throws(shouldThrow: boolean): this
}
export interface BunShellOutput {
readonly stdout: Buffer
readonly stderr: Buffer
readonly exitCode: number
/**
* Read from stdout as a string
*/
text(encoding?: BufferEncoding): string
/**
* Read from stdout as a JSON object
*/
json(): any
/**
* Read from stdout as an ArrayBuffer
*/
arrayBuffer(): ArrayBuffer
/**
* Read from stdout as an Uint8Array
*/
bytes(): Uint8Array
/**
* Read from stdout as a Blob
*/
blob(): Blob
}
export type BunShellError = Error & BunShellOutput
-54
View File
@@ -1,54 +0,0 @@
import { z } from "zod"
export type ToolContext = {
sessionID: string
messageID: string
agent: string
/**
* Current project directory for this session.
* Prefer this over process.cwd() when resolving relative paths.
*/
directory: string
/**
* Project worktree root for this session.
* Useful for generating stable relative paths (e.g. path.relative(worktree, absPath)).
*/
worktree: string
abort: AbortSignal
metadata(input: { title?: string; metadata?: { [key: string]: any } }): void
ask(input: AskInput): Promise<void>
}
type AskInput = {
permission: string
patterns: string[]
always: string[]
metadata: { [key: string]: any }
}
export type ToolAttachment = {
type: "file"
mime: string
url: string
filename?: string
}
export type ToolResult =
| string
| {
title?: string
output: string
metadata?: { [key: string]: any }
attachments?: ToolAttachment[]
}
export function tool<Args extends z.ZodRawShape>(input: {
description: string
args: Args
execute(args: z.infer<z.ZodObject<Args>>, context: ToolContext): Promise<ToolResult>
}) {
return input
}
tool.schema = z
export type ToolDefinition = ReturnType<typeof tool>
-655
View File
@@ -1,655 +0,0 @@
import type {
OpencodeClient,
LspStatus,
McpStatus,
Message,
Part,
Provider,
PermissionRequest,
QuestionRequest,
Session,
SessionStatus,
Config as SdkConfig,
} from "@opencode-ai/sdk/v2"
import type { OpenCodeEvent } from "@opencode-ai/client/promise"
import type { PromptInput } from "@opencode-ai/schema"
import type { Types } from "effect"
import type { CliRenderer, KeyEvent, RGBA, Renderable, SlotMode } from "@opentui/core"
import type { Binding, Keymap } from "@opentui/keymap"
import {
createBindingLookup as createKeymapBindingLookup,
type BindingConfig,
type CreateBindingLookupOptions,
type KeySequenceFormatPart,
type SequenceBindingLike,
} from "@opentui/keymap/extras"
import type { JSX, SolidPlugin } from "@opentui/solid"
import type { Config as PluginConfig, PluginOptions } from "./index.js"
export type { CliRenderer, KeyEvent, Renderable, SlotMode } from "@opentui/core"
export { stringifyKeySequence, stringifyKeyStroke } from "@opentui/keymap"
export type { Binding, KeyLike, KeySequencePart, KeyStringifyInput, StringifyOptions } from "@opentui/keymap"
export { formatCommandBindings, formatKeySequence } from "@opentui/keymap/extras"
export type {
BindingConfig,
BindingLookup,
BindingValue,
CreateBindingLookupOptions,
FormatCommandBindingsOptions,
FormatKeySequenceOptions,
KeySequenceFormatPart,
SequenceBindingLike,
} from "@opentui/keymap/extras"
export function createBindingLookup(
config: BindingConfig<Renderable, KeyEvent> | undefined,
options?: CreateBindingLookupOptions<Renderable, KeyEvent>,
) {
return createKeymapBindingLookup<Renderable, KeyEvent>(config ?? {}, options)
}
export type TuiRouteCurrent =
| {
name: "home"
}
| {
name: "session"
params: {
sessionID: string
prompt?: unknown
}
}
| {
name: string
params?: Record<string, unknown>
}
export type TuiRouteDefinition = {
name: string
render: (input: { params?: Record<string, unknown> }) => JSX.Element
}
export type TuiKeys = {
formatSequence: (parts: readonly KeySequenceFormatPart[] | undefined) => string
formatBindings: (bindings: readonly SequenceBindingLike[] | undefined) => string | undefined
}
export type TuiKeymap = Keymap<Renderable, KeyEvent>
export type TuiModeApi = {
current: () => string
push: (mode: string) => () => void
}
/**
* Legacy `api.command` shape kept so v1 plugins can initialize. Remove in v2.
*
* @deprecated Use `api.keymap.registerLayer({ commands, bindings })` instead.
*/
export type TuiCommand = {
title: string
value: string
description?: string
category?: string
keybind?: string
suggested?: boolean
hidden?: boolean
enabled?: boolean
slash?: {
name: string
aliases?: string[]
}
onSelect?: (dialog?: TuiDialogStack) => void | Promise<void>
}
/**
* Legacy `api.command` API kept so v1 plugins can initialize. Remove in v2.
*
* @deprecated Use `api.keymap.registerLayer`, `api.keymap.dispatchCommand`, and
* `api.keymap.dispatchCommand("command.palette.show")` instead.
*/
export type TuiCommandApi = {
/** @deprecated Use `api.keymap.registerLayer({ commands, bindings })` instead. */
register: (cb: () => TuiCommand[]) => () => void
/** @deprecated Use `api.keymap.dispatchCommand(name)` instead. */
trigger: (value: string) => void
/** @deprecated Use `api.keymap.dispatchCommand("command.palette.show")` instead. */
show: () => void
}
export type TuiDialogProps = {
size?: "medium" | "large" | "xlarge"
onClose: () => void
children?: JSX.Element
}
export type TuiDialogStack = {
replace: (render: () => JSX.Element, onClose?: () => void) => void
clear: () => void
setSize: (size: "medium" | "large" | "xlarge") => void
readonly size: "medium" | "large" | "xlarge"
readonly depth: number
readonly open: boolean
}
export type TuiDialogAlertProps = {
title: string
message: string
onConfirm?: () => void
}
export type TuiDialogConfirmProps = {
title: string
message: string
onConfirm?: () => void
onCancel?: () => void
}
export type TuiDialogPromptProps = {
title: string
description?: () => JSX.Element
placeholder?: string
value?: string
busy?: boolean
busyText?: string
onConfirm?: (value: string) => void
onCancel?: () => void
}
export type TuiDialogSelectOption<Value = unknown> = {
title: string
value: Value
description?: string
footer?: JSX.Element | string
category?: string
disabled?: boolean
onSelect?: () => void
}
export type TuiDialogSelectProps<Value = unknown> = {
title: string
placeholder?: string
options: TuiDialogSelectOption<Value>[]
flat?: boolean
onMove?: (option: TuiDialogSelectOption<Value>) => void
onFilter?: (query: string) => void
onSelect?: (option: TuiDialogSelectOption<Value>) => void
skipFilter?: boolean
current?: Value
}
export type TuiPromptInfo = Types.DeepMutable<PromptInput.Prompt> & {
pasted: {
text: string
source: {
start: number
end: number
text: string
}
}[]
mode?: "normal" | "shell"
}
export type TuiPromptRef = {
focused: boolean
current: TuiPromptInfo
set(prompt: TuiPromptInfo): void
reset(): void
blur(): void
focus(): void
submit(): void
}
export type TuiPromptProps = {
sessionID?: string
visible?: boolean
disabled?: boolean
onSubmit?: () => void
ref?: (ref: TuiPromptRef | undefined) => void
hint?: JSX.Element
right?: JSX.Element
showPlaceholder?: boolean
placeholders?: {
normal?: string[]
shell?: string[]
}
}
export type TuiToast = {
variant?: "info" | "success" | "warning" | "error"
title?: string
message: string
duration?: number
}
export type TuiAttentionWhen = "always" | "focused" | "blurred"
export const TuiAttentionSoundNames = ["default", "question", "permission", "error", "done", "subagent_done"] as const
export type TuiAttentionSoundName = (typeof TuiAttentionSoundNames)[number]
export type TuiAttentionSound =
| boolean
| {
name?: TuiAttentionSoundName
volume?: number
when?: TuiAttentionWhen
}
export type TuiAttentionNotification =
| boolean
| {
when?: TuiAttentionWhen
}
export type TuiAttentionSoundPack = {
id: string
name?: string
sounds: Partial<Record<TuiAttentionSoundName, string>>
}
export type TuiAttentionSoundPackInfo = {
id: string
name?: string
active: boolean
builtin: boolean
}
export type TuiAttentionSoundboardActivateOptions = {
persist?: boolean
}
export type TuiAttentionSoundboard = {
registerPack(pack: TuiAttentionSoundPack): () => void
activate(id: string, options?: TuiAttentionSoundboardActivateOptions): boolean
current(): string
list(): ReadonlyArray<TuiAttentionSoundPackInfo>
}
export type TuiAttentionNotifyInput = {
title?: string
message: string
notification?: TuiAttentionNotification
sound?: TuiAttentionSound
}
export type TuiAttentionNotifySkipReason =
| "attention_disabled"
| "empty_message"
| "blurred"
| "focused"
| "focus_unknown"
| "renderer_destroyed"
export type TuiAttentionNotifyResult = {
ok: boolean
notification: boolean
sound: boolean
skipped?: TuiAttentionNotifySkipReason
}
export type TuiAttention = {
notify(input: TuiAttentionNotifyInput): Promise<TuiAttentionNotifyResult>
soundboard: TuiAttentionSoundboard
}
export type TuiThemeCurrent = {
readonly primary: RGBA
readonly secondary: RGBA
readonly accent: RGBA
readonly error: RGBA
readonly warning: RGBA
readonly success: RGBA
readonly info: RGBA
readonly text: RGBA
readonly textMuted: RGBA
readonly selectedListItemText: RGBA
readonly background: RGBA
readonly backgroundPanel: RGBA
readonly backgroundElement: RGBA
readonly backgroundMenu: RGBA
readonly border: RGBA
readonly borderActive: RGBA
readonly borderSubtle: RGBA
readonly diffAdded: RGBA
readonly diffRemoved: RGBA
readonly diffContext: RGBA
readonly diffHunkHeader: RGBA
readonly diffHighlightAdded: RGBA
readonly diffHighlightRemoved: RGBA
readonly diffAddedBg: RGBA
readonly diffRemovedBg: RGBA
readonly diffContextBg: RGBA
readonly diffLineNumber: RGBA
readonly diffAddedLineNumberBg: RGBA
readonly diffRemovedLineNumberBg: RGBA
readonly markdownText: RGBA
readonly markdownHeading: RGBA
readonly markdownLink: RGBA
readonly markdownLinkText: RGBA
readonly markdownCode: RGBA
readonly markdownBlockQuote: RGBA
readonly markdownEmph: RGBA
readonly markdownStrong: RGBA
readonly markdownHorizontalRule: RGBA
readonly markdownListItem: RGBA
readonly markdownListEnumeration: RGBA
readonly markdownImage: RGBA
readonly markdownImageText: RGBA
readonly markdownCodeBlock: RGBA
readonly syntaxComment: RGBA
readonly syntaxKeyword: RGBA
readonly syntaxFunction: RGBA
readonly syntaxVariable: RGBA
readonly syntaxString: RGBA
readonly syntaxNumber: RGBA
readonly syntaxType: RGBA
readonly syntaxOperator: RGBA
readonly syntaxPunctuation: RGBA
readonly thinkingOpacity: number
}
export type TuiTheme = {
readonly current: TuiThemeCurrent
readonly selected: string
has: (name: string) => boolean
set: (name: string) => boolean
install: (jsonPath: string) => Promise<void>
mode: () => "dark" | "light"
readonly ready: boolean
}
/** @deprecated Persistent TUI KV storage is not supported in V2. */
export type TuiKV = {
get: <Value = unknown>(key: string, fallback?: Value) => Value
set: (key: string, value: unknown) => void
readonly ready: boolean
}
export type TuiState = {
readonly ready: boolean
readonly config: SdkConfig
readonly provider: ReadonlyArray<Provider>
readonly path: {
state: string
config: string
worktree: string
directory: string
}
readonly vcs: { branch?: string; default_branch?: string } | undefined
session: {
count: () => number
get: (sessionID: string) => Session | undefined
diff: (sessionID: string) => ReadonlyArray<TuiSidebarFileItem>
messages: (sessionID: string) => ReadonlyArray<Message>
status: (sessionID: string) => SessionStatus | undefined
permission: (sessionID: string) => ReadonlyArray<PermissionRequest>
question: (sessionID: string) => ReadonlyArray<QuestionRequest>
}
part: (messageID: string) => ReadonlyArray<Part>
lsp: () => ReadonlyArray<TuiSidebarLspItem>
mcp: () => ReadonlyArray<TuiSidebarMcpItem>
}
type TuiBindingLookupView = {
readonly bindings: ReadonlyArray<Binding<Renderable, KeyEvent>>
get: (command: string) => ReadonlyArray<Binding<Renderable, KeyEvent>>
has: (command: string) => boolean
gather: (name: string, commands: readonly string[]) => ReadonlyArray<Binding<Renderable, KeyEvent>>
pick: (name: string, commands: readonly string[]) => Binding<Renderable, KeyEvent>[]
omit: (name: string, commands: readonly string[]) => Binding<Renderable, KeyEvent>[]
}
type TuiAttentionConfigView = {
enabled: boolean
notifications: boolean
sound: boolean
volume: number
sound_pack: string
sounds: Partial<Record<TuiAttentionSoundName, string>>
}
type TuiConfigView = {
$schema?: string
theme?: string | { name?: string; mode?: "system" | "dark" | "light" }
plugin?: PluginConfig["plugin"]
plugins?: ReadonlyArray<string | { package: string; options?: Record<string, any> }>
plugin_enabled?: Record<string, boolean>
leader?: { timeout: number }
leader_timeout?: number
scroll?: { speed?: number; acceleration?: boolean }
scroll_speed?: number
scroll_acceleration?: { enabled: boolean }
attention: TuiAttentionConfigView
diffs?: {
wrap?: "word" | "none"
tree?: boolean
single?: boolean
view?: "auto" | "split" | "unified"
}
diff_style?: "auto" | "stacked"
terminal?: { title?: boolean }
prompt?: { editor?: boolean; paste?: "compact" | "full" } | { max_height?: number; max_width?: number | "auto" }
session?: {
sidebar?: "auto" | "hide"
scrollbar?: boolean
thinking?: "show" | "hide"
markdown?: "source" | "rendered"
grouping?: "auto" | "none"
}
hints?: { onboarding?: boolean }
animations?: boolean
mouse: boolean
keybinds: TuiBindingLookupView
}
export type TuiApp = {
readonly version: string
}
type Frozen<Value> = Value extends (...args: never[]) => unknown
? Value
: Value extends ReadonlyArray<infer Item>
? ReadonlyArray<Frozen<Item>>
: Value extends object
? { readonly [Key in keyof Value]: Frozen<Value[Key]> }
: Value
export type TuiSidebarMcpItem = {
name: string
status: McpStatus["status"]
error?: string
}
export type TuiSidebarLspItem = Pick<LspStatus, "id" | "root" | "status">
export type TuiSidebarFileItem = {
file: string
additions: number
deletions: number
}
export type TuiHostSlotMap = {
app: {}
app_bottom: {}
home_logo: {}
home_prompt: {
ref?: (ref: TuiPromptRef | undefined) => void
}
home_prompt_right: {}
session_prompt: {
session_id: string
visible?: boolean
disabled?: boolean
on_submit?: () => void
ref?: (ref: TuiPromptRef | undefined) => void
}
session_prompt_right: {
session_id: string
}
home_bottom: {}
home_footer: {}
sidebar_title: {
session_id: string
title: string
share_url?: string
}
sidebar_content: {
session_id: string
}
sidebar_footer: {
session_id: string
directory: string
}
}
export type TuiSlotMap<Slots extends Record<string, object> = {}> = TuiHostSlotMap & Slots
type TuiSlotShape<Name extends string, Slots extends Record<string, object>> = Name extends keyof TuiHostSlotMap
? TuiHostSlotMap[Name]
: Name extends keyof Slots
? Slots[Name]
: Record<string, unknown>
export type TuiSlotProps<Name extends string = string, Slots extends Record<string, object> = {}> = {
name: Name
mode?: SlotMode
children?: JSX.Element
} & TuiSlotShape<Name, Slots>
export type TuiSlotContext = {
theme: TuiTheme
}
type SlotCore<Slots extends Record<string, object> = {}> = SolidPlugin<TuiSlotMap<Slots>, TuiSlotContext>
export type TuiSlotPlugin<Slots extends Record<string, object> = {}> = Omit<SlotCore<Slots>, "id"> & {
id?: never
}
export type TuiSlots = {
register: {
(plugin: TuiSlotPlugin): string
<Slots extends Record<string, object>>(plugin: TuiSlotPlugin<Slots>): string
}
}
export type TuiEventBus = {
on: <Type extends OpenCodeEvent["type"]>(
type: Type,
handler: (event: Extract<OpenCodeEvent, { type: Type }>) => void,
) => () => void
}
export type TuiDispose = () => void | Promise<void>
export type TuiLifecycle = {
readonly signal: AbortSignal
onDispose: (fn: TuiDispose) => () => void
}
export type TuiPluginState = "first" | "updated" | "same"
export type TuiPluginEntry = {
id: string
source: "file" | "npm" | "internal"
spec: string
target: string
requested?: string
version?: string
modified?: number
first_time: number
last_time: number
time_changed: number
load_count: number
fingerprint: string
}
export type TuiPluginMeta = TuiPluginEntry & {
state: TuiPluginState
}
export type TuiPluginStatus = {
id: string
source: TuiPluginEntry["source"]
spec: string
target: string
enabled: boolean
active: boolean
}
export type TuiPluginInstallOptions = {
global?: boolean
}
export type TuiPluginInstallResult =
| {
ok: true
dir: string
tui: boolean
}
| {
ok: false
message: string
missing?: boolean
}
export type TuiWorkspace = {
current: () => string | undefined
set: (workspaceID?: string) => void
}
export type TuiPluginApi = {
app: TuiApp
attention: TuiAttention
/**
* Legacy `api.command` API kept so v1 plugins can initialize. Remove in v2.
*
* @deprecated Use `api.keymap.registerLayer`, `api.keymap.dispatchCommand`, and
* `api.keymap.dispatchCommand("command.palette.show")` instead.
*/
command?: TuiCommandApi
keys: TuiKeys
keymap: TuiKeymap
mode: TuiModeApi
route: {
register: (routes: TuiRouteDefinition[]) => () => void
navigate: (name: string, params?: Record<string, unknown>) => void
readonly current: TuiRouteCurrent
}
ui: {
Dialog: (props: TuiDialogProps) => JSX.Element
DialogAlert: (props: TuiDialogAlertProps) => JSX.Element
DialogConfirm: (props: TuiDialogConfirmProps) => JSX.Element
DialogPrompt: (props: TuiDialogPromptProps) => JSX.Element
DialogSelect: <Value = unknown>(props: TuiDialogSelectProps<Value>) => JSX.Element
Slot: <Name extends string>(props: TuiSlotProps<Name>) => JSX.Element | null
Prompt: (props: TuiPromptProps) => JSX.Element
toast: (input: TuiToast) => void
dialog: TuiDialogStack
}
readonly tuiConfig: Frozen<TuiConfigView>
/** @deprecated Persistent TUI KV storage is not supported in V2. */
kv: TuiKV
state: TuiState
theme: TuiTheme
client: OpencodeClient
event: TuiEventBus
renderer: CliRenderer
slots: TuiSlots
plugins: {
list: () => ReadonlyArray<TuiPluginStatus>
activate: (id: string) => Promise<boolean>
deactivate: (id: string) => Promise<boolean>
add: (spec: string) => Promise<boolean>
install: (spec: string, options?: TuiPluginInstallOptions) => Promise<TuiPluginInstallResult>
}
lifecycle: TuiLifecycle
}
export type TuiPlugin = (api: TuiPluginApi, options: PluginOptions | undefined, meta: TuiPluginMeta) => Promise<void>
export type TuiPluginModule = {
id?: string
tui: TuiPlugin
server?: never
}
+1 -1
View File
@@ -16,7 +16,7 @@
- Do not preserve `V2` as the permanent name for the replacement architecture. Remove `V2` from current namespaces, brands, and identifiers as the contracts are normalized.
- Retained V1 contracts live under `src/v1/`. New/current code must not depend on that subtree.
- V1 coexistence is temporary. Keep compatibility entrypoints only where migration requires them, and delete the V1 subtree when the legacy runtime is retired.
- `@opencode-ai/protocol` and `@opencode-ai/sdk-next` are current `/api/...` surfaces.
- `@opencode-ai/protocol` and `@opencode-ai/sdk` are current `/api/...` surfaces.
## Events
@@ -1,11 +1,11 @@
# @opencode-ai/sdk-next
# @opencode-ai/sdk
Effect-native scoped OpenCode host for in-process applications. This transitional package will replace the existing generated `@opencode-ai/sdk` after its consumers migrate.
Effect-native scoped OpenCode host for in-process applications.
The SDK executes Server's assembled HTTP router in memory. It opens no listener and performs no network I/O, while preserving the same routing, middleware, handlers, codecs, and errors as the network client.
```ts
import { OpenCode } from "@opencode-ai/sdk-next"
import { OpenCode } from "@opencode-ai/sdk"
const opencode = yield * OpenCode.create()
const session = yield * opencode.sessions.get({ sessionID })
@@ -1,21 +1,32 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk-next",
"private": true,
"version": "1.18.4",
"name": "@opencode-ai/sdk",
"type": "module",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/anomalyco/opencode.git",
"directory": "packages/sdk"
},
"publishConfig": {
"access": "public"
},
"files": [
"dist"
],
"exports": {
".": "./src/index.ts",
"./workerd": "./src/workerd.ts"
},
"scripts": {
"build": "bun run script/build.ts",
"test": "bun test --timeout 5000",
"typecheck": "tsgo -b"
},
"dependencies": {
"@opencode-ai/client": "workspace:*",
"@opencode-ai/core": "workspace:*",
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/schema": "workspace:*",
"@opencode-ai/server": "workspace:*",
"@opencode-ai/util": "workspace:*",
@@ -23,7 +34,6 @@
},
"devDependencies": {
"@opencode-ai/httpapi-codegen": "workspace:*",
"@opencode-ai/protocol": "workspace:*",
"@tsconfig/bun": "catalog:",
"@types/bun": "catalog:",
"@typescript/native-preview": "catalog:"
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bun
import { $ } from "bun"
import { rm } from "node:fs/promises"
import { fileURLToPath } from "node:url"
process.chdir(fileURLToPath(new URL("..", import.meta.url)))
await rm("dist", { recursive: true, force: true })
await $`bun tsc -p tsconfig.build.json`
const files = await Array.fromAsync(new Bun.Glob("**/*.ts").scan({ cwd: "src" })).then((items) =>
items.map((item) => `src/${item}`),
)
const transpiler = new Bun.Transpiler({ loader: "ts", target: "bun" })
await Promise.all(
files.map(async (file) =>
Bun.write(
file.replace(/^src\//, "dist/").replace(/\.ts$/, ".js"),
await transpiler.transform(await Bun.file(file).text()),
),
),
)
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bun
import { Script } from "@opencode-ai/script"
import { $ } from "bun"
import { rm } from "node:fs/promises"
import { fileURLToPath } from "node:url"
process.chdir(fileURLToPath(new URL("..", import.meta.url)))
const dryRun = Bun.argv.includes("--dry-run")
const originalText = await Bun.file("package.json").text()
const pkg = JSON.parse(originalText) as {
name: string
version: string
exports: Record<string, string | { import: string; types: string }>
}
const tarball = `${pkg.name.replace("@", "").replace("/", "-")}-${pkg.version}.tgz`
const output = (value: string, types = false) =>
value.replace("./src/", "./dist/").replace(/\.ts$/, types ? ".d.ts" : ".js")
if (!dryRun && (await $`npm view ${pkg.name}@${pkg.version} version`.nothrow()).exitCode === 0) {
console.log(`already published ${pkg.name}@${pkg.version}`)
process.exit(0)
}
try {
await $`bun run typecheck`
await $`bun run build`
pkg.exports = Object.fromEntries(
Object.entries(pkg.exports).map(([key, value]) => {
if (typeof value !== "string") return [key, value]
return [key, { import: output(value), types: output(value, true) }]
}),
)
await Bun.write("package.json", JSON.stringify(pkg, null, 2) + "\n")
await rm(tarball, { force: true })
await $`bun pm pack`
if (!dryRun) await $`npm publish ${tarball} --tag ${Script.channel} --access public`
} finally {
await Bun.write("package.json", originalText)
await rm(tarball, { force: true })
}
@@ -1,27 +1,69 @@
import { OpenCode } from "@opencode-ai/client/effect"
import { OpenCode, type OpenCodeClient } from "@opencode-ai/client/effect"
import type { Database } from "@opencode-ai/core/database/database"
import type { ModelsDev } from "@opencode-ai/core/models-dev"
import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
import { SessionRestart } from "@opencode-ai/core/session/execution/restart"
import { Workspace } from "@opencode-ai/core/workspace"
import { WorkspaceDriver } from "@opencode-ai/core/workspace/driver"
import type { ServerFetch } from "@opencode-ai/server/fetch"
import { createEmbeddedRoutes } from "@opencode-ai/server/routes"
import type { ServerOptions } from "@opencode-ai/server/options"
import { Context, Effect, Layer, ManagedRuntime, Scope } from "effect"
import type { LayerNode } from "@opencode-ai/util/effect/layer-node"
import { Config, Context, Effect, Layer, ManagedRuntime, Scope } from "effect"
import { FetchHttpClient, HttpEffect, HttpRouter, HttpServer, HttpServerRequest } from "effect/unstable/http"
import * as Logging from "./logging"
export type { LogEntry, LogLevel, LogOptions, LogWriter } from "./logging"
import type { LogOptions } from "./logging"
export type CreateOptions = ServerOptions & {
export interface CreateOptions {
readonly app?: {
readonly name?: string
readonly version?: string
readonly channel?: string
}
readonly hostname?: string
readonly port?: number
readonly password?: string
readonly simulation?: boolean
readonly database?: Database.Options
readonly events?: { readonly persist?: boolean }
readonly models?: ModelsDev.Options
readonly config?: {
readonly directory?: string
readonly project?: boolean
readonly file?: string
readonly content?: string
}
readonly windows?: { readonly gitbash?: string }
readonly fs?: {
readonly filewatcher?: boolean
readonly fff?: boolean
}
readonly log?: LogOptions
readonly workspaceProviders?: Readonly<Record<string, WorkspaceDriver.Interface>>
}
/** Host hooks for embedding opencode on a non-default runtime profile (e.g. workerd). */
export type EmbedOptions = ServerFetch.BootOptions
export interface EmbedOptions {
readonly overrides?: LayerNode.Replacements
}
export const create = Effect.fn("OpenCode.create")(function* (options: CreateOptions = {}, embed: EmbedOptions = {}) {
export type Interface = Omit<OpenCodeClient, "plugin" | "workspace"> & {
readonly sessions: OpenCodeClient["session"]
readonly events: OpenCodeClient["event"]
readonly workspace: {
readonly create: (options: { readonly provider: string }) => ReturnType<Workspace.Interface["create"]>
readonly destroy: (options: { readonly workspaceID: Workspace.ID }) => ReturnType<Workspace.Interface["destroy"]>
}
readonly plugin: SdkPlugins.Interface["register"] & OpenCodeClient["plugin"]
}
export const create: (
options?: CreateOptions,
embed?: EmbedOptions,
) => Effect.Effect<Interface, Config.ConfigError | Error, Scope.Scope> = Effect.fn("OpenCode.create")(function* (
options: CreateOptions = {},
embed: EmbedOptions = {},
) {
const { log, workspaceProviders, ...server } = options
const runtime = yield* Effect.acquireRelease(
Effect.sync(() =>
@@ -75,8 +117,7 @@ export const create = Effect.fn("OpenCode.create")(function* (options: CreateOpt
}
})
export type Interface = Effect.Success<ReturnType<typeof create>>
export class Service extends Context.Service<Service, Interface>()("@opencode-ai/sdk/OpenCode") {}
export class Service extends Context.Service<Service, Interface>()("@opencode-ai/sdk-next/OpenCode") {}
export const layer = (options: CreateOptions = {}) => Layer.effect(Service, create(options))
export const layer = (options: CreateOptions = {}): Layer.Layer<Service, Config.ConfigError | Error> =>
Layer.effect(Service, create(options))
@@ -1,10 +1,17 @@
export * as OpenCodeWorkerd from "./workerd"
import type { DurableObjectStorage } from "@opencode-ai/core/database/sqlite.workerd"
import { ServerWorkerd } from "@opencode-ai/server/workerd"
import { Layer } from "effect"
import { Config, Effect, Layer, Scope } from "effect"
import * as OpenCode from "./opencode"
export type CreateOptions = ServerWorkerd.Options & Pick<OpenCode.CreateOptions, "log" | "workspaceProviders">
export interface CreateOptions extends Pick<OpenCode.CreateOptions, "log" | "workspaceProviders"> {
readonly storage: DurableObjectStorage
readonly app?: OpenCode.CreateOptions["app"]
readonly password?: string
readonly config?: { readonly content?: string }
readonly models?: OpenCode.CreateOptions["models"]
}
/**
* Boots the embedded opencode SDK on the workerd runtime profile: the full
@@ -20,10 +27,17 @@ export type CreateOptions = ServerWorkerd.Options & Pick<OpenCode.CreateOptions,
* session operations plus the live `events.subscribe()` stream — served over
* an in-process fetch transport, so no request leaves the isolate.
*/
export const create = ({ log, workspaceProviders, ...options }: CreateOptions) =>
export const create: (
options: CreateOptions,
) => Effect.Effect<OpenCode.Interface, Config.ConfigError | Error, Scope.Scope> = ({
log,
workspaceProviders,
...options
}) =>
OpenCode.create(
{ ...ServerWorkerd.serverOptions(options), log, workspaceProviders },
{ overrides: ServerWorkerd.replacements(options) },
)
export const layer = (options: CreateOptions) => Layer.effect(OpenCode.Service, create(options))
export const layer = (options: CreateOptions): Layer.Layer<OpenCode.Service, Config.ConfigError | Error> =>
Layer.effect(OpenCode.Service, create(options))
@@ -20,7 +20,7 @@ async function bundleInputs() {
const entrypoint = join(temporary, "index.ts")
const metafile = join(temporary, "meta.json")
try {
await Bun.write(entrypoint, 'export * from "@opencode-ai/sdk-next"')
await Bun.write(entrypoint, 'export * from "@opencode-ai/sdk"')
const child = Bun.spawn(
[
process.execPath,
+15
View File
@@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./tsconfig.json",
"compilerOptions": {
"composite": false,
"declaration": true,
"emitDeclarationOnly": true,
"incremental": false,
"noEmit": false,
"outDir": "dist",
"rootDir": "src",
"tsBuildInfoFile": null
},
"include": ["src"]
}
+12 -1
View File
@@ -2,13 +2,24 @@
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/server",
"version": "1.18.4",
"private": true,
"type": "module",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/anomalyco/opencode.git",
"directory": "packages/server"
},
"publishConfig": {
"access": "public"
},
"files": [
"dist"
],
"exports": {
"./*": "./src/*.ts"
},
"scripts": {
"build": "bun run script/build.ts",
"test": "bun test --only-failures",
"typecheck": "tsgo -b",
"probe:workerd": "bun run script/workerd-probe.ts"
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bun
import { $ } from "bun"
import { rm } from "node:fs/promises"
import { fileURLToPath } from "node:url"
process.chdir(fileURLToPath(new URL("..", import.meta.url)))
await rm("dist", { recursive: true, force: true })
await $`bun tsc -p tsconfig.build.json`
const files = await Array.fromAsync(new Bun.Glob("**/*.ts").scan({ cwd: "src" })).then((items) =>
items.map((item) => `src/${item}`),
)
const transpiler = new Bun.Transpiler({ loader: "ts", target: "bun" })
await Promise.all(
files.map(async (file) =>
Bun.write(
file.replace(/^src\//, "dist/").replace(/\.ts$/, ".js"),
await transpiler.transform(await Bun.file(file).text()),
),
),
)
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env bun
import { Script } from "@opencode-ai/script"
import { $ } from "bun"
import { rm } from "node:fs/promises"
import { fileURLToPath } from "node:url"
process.chdir(fileURLToPath(new URL("..", import.meta.url)))
const dryRun = Bun.argv.includes("--dry-run")
const originalText = await Bun.file("package.json").text()
const pkg = JSON.parse(originalText) as {
name: string
version: string
exports: Record<string, string | { import: string; types: string }>
}
const tarball = `${pkg.name.replace("@", "").replace("/", "-")}-${pkg.version}.tgz`
if (!dryRun && (await $`npm view ${pkg.name}@${pkg.version} version`.nothrow()).exitCode === 0) {
console.log(`already published ${pkg.name}@${pkg.version}`)
process.exit(0)
}
try {
await $`bun run typecheck`
await $`bun run build`
pkg.exports = Object.fromEntries(
Object.entries(pkg.exports).map(([key, value]) => {
if (typeof value !== "string") return [key, value]
return [
key,
{
import: value.replace("./src/", "./dist/").replace(/\.ts$/, ".js"),
types: value.replace("./src/", "./dist/").replace(/\.ts$/, ".d.ts"),
},
]
}),
)
await Bun.write("package.json", JSON.stringify(pkg, null, 2) + "\n")
await rm(tarball, { force: true })
await $`bun pm pack`
if (!dryRun) await $`npm publish ${tarball} --tag ${Script.channel} --access public`
} finally {
await Bun.write("package.json", originalText)
await rm(tarball, { force: true })
}
+15
View File
@@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./tsconfig.json",
"compilerOptions": {
"composite": false,
"incremental": false,
"rootDir": "src",
"outDir": "dist",
"noEmit": false,
"declaration": true,
"emitDeclarationOnly": true,
"tsBuildInfoFile": null
},
"include": ["src"]
}
+13 -1
View File
@@ -2,12 +2,24 @@
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/shell-scan",
"version": "0.0.0",
"private": true,
"type": "module",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/anomalyco/opencode.git",
"directory": "packages/shell-scan"
},
"publishConfig": {
"access": "public"
},
"files": [
"dist"
],
"exports": {
".": "./src/index.ts"
},
"scripts": {
"build": "bun run script/build.ts",
"test": "bun test --only-failures",
"typecheck": "tsgo --noEmit"
},
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env bun
import { $ } from "bun"
import { rm } from "node:fs/promises"
import { fileURLToPath } from "node:url"
process.chdir(fileURLToPath(new URL("..", import.meta.url)))
await rm("dist", { recursive: true, force: true })
await $`bun tsc -p tsconfig.build.json`
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env bun
import { Script } from "@opencode-ai/script"
import { $ } from "bun"
import { rm } from "node:fs/promises"
import { fileURLToPath } from "node:url"
process.chdir(fileURLToPath(new URL("..", import.meta.url)))
const dryRun = Bun.argv.includes("--dry-run")
const originalText = await Bun.file("package.json").text()
const pkg = JSON.parse(originalText) as {
name: string
version: string
exports: Record<string, string | { import: string; types: string }>
}
const tarball = `${pkg.name.replace("@", "").replace("/", "-")}-${pkg.version}.tgz`
if (!dryRun && (await $`npm view ${pkg.name}@${pkg.version} version`.nothrow()).exitCode === 0) {
console.log(`already published ${pkg.name}@${pkg.version}`)
process.exit(0)
}
try {
await $`bun run typecheck`
await $`bun run build`
pkg.exports = Object.fromEntries(
Object.entries(pkg.exports).map(([key, value]) => {
if (typeof value !== "string") return [key, value]
return [
key,
{
import: value.replace("./src/", "./dist/").replace(/\.ts$/, ".js"),
types: value.replace("./src/", "./dist/").replace(/\.ts$/, ".d.ts"),
},
]
}),
)
await Bun.write("package.json", JSON.stringify(pkg, null, 2) + "\n")
await rm(tarball, { force: true })
await $`bun pm pack`
if (!dryRun) await $`npm publish ${tarball} --tag ${Script.channel} --access public`
} finally {
await Bun.write("package.json", originalText)
await rm(tarball, { force: true })
}
+11
View File
@@ -0,0 +1,11 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"noEmit": false,
"declaration": true
},
"include": ["src"]
}
+12 -1
View File
@@ -2,9 +2,19 @@
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/simulation",
"version": "1.17.13",
"private": true,
"type": "module",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/anomalyco/opencode.git",
"directory": "packages/simulation"
},
"publishConfig": {
"access": "public"
},
"files": [
"dist"
],
"exports": {
"./backend": "./src/backend/index.ts",
"./backend/*": "./src/backend/*.ts",
@@ -14,6 +24,7 @@
"./recording": "./src/recording.ts"
},
"scripts": {
"build": "bun run script/build.ts",
"test": "bun test --timeout 30000 --only-failures",
"typecheck": "tsgo -b"
},
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bun
import { $ } from "bun"
import { rm } from "node:fs/promises"
import { fileURLToPath } from "node:url"
process.chdir(fileURLToPath(new URL("..", import.meta.url)))
await rm("dist", { recursive: true, force: true })
await $`bun tsc -p tsconfig.build.json`
const files = await Array.fromAsync(new Bun.Glob("**/*.ts").scan({ cwd: "src" })).then((items) =>
items.map((item) => `src/${item}`),
)
const transpiler = new Bun.Transpiler({ loader: "ts", target: "bun" })
await Promise.all(
files.map(async (file) =>
Bun.write(
file.replace(/^src\//, "dist/").replace(/\.ts$/, ".js"),
await transpiler.transform(await Bun.file(file).text()),
),
),
)
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env bun
import { Script } from "@opencode-ai/script"
import { $ } from "bun"
import { rm } from "node:fs/promises"
import { fileURLToPath } from "node:url"
process.chdir(fileURLToPath(new URL("..", import.meta.url)))
const dryRun = Bun.argv.includes("--dry-run")
const originalText = await Bun.file("package.json").text()
const pkg = JSON.parse(originalText) as {
name: string
version: string
exports: Record<string, string | { import: string; types: string }>
}
const tarball = `${pkg.name.replace("@", "").replace("/", "-")}-${pkg.version}.tgz`
if (!dryRun && (await $`npm view ${pkg.name}@${pkg.version} version`.nothrow()).exitCode === 0) {
console.log(`already published ${pkg.name}@${pkg.version}`)
process.exit(0)
}
try {
await $`bun run typecheck`
await $`bun run build`
pkg.exports = Object.fromEntries(
Object.entries(pkg.exports).map(([key, value]) => {
if (typeof value !== "string") return [key, value]
return [
key,
{
import: value.replace("./src/", "./dist/").replace(/\.ts$/, ".js"),
types: value.replace("./src/", "./dist/").replace(/\.ts$/, ".d.ts"),
},
]
}),
)
await Bun.write("package.json", JSON.stringify(pkg, null, 2) + "\n")
await rm(tarball, { force: true })
await $`bun pm pack`
if (!dryRun) await $`npm publish ${tarball} --tag ${Script.channel} --access public`
} finally {
await Bun.write("package.json", originalText)
await rm(tarball, { force: true })
}
+4 -2
View File
@@ -2,7 +2,7 @@ import { LayerNode } from "@opencode-ai/util/effect/layer-node"
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
import { httpClient } from "@opencode-ai/util/effect/app-node-platform"
import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
import { Config, Effect, Layer } from "effect"
import { Config, Effect, FileSystem, Layer } from "effect"
import { HttpClient } from "effect/unstable/http"
import { DriveManifest } from "../manifest"
import { SimulationNetwork } from "./network"
@@ -21,7 +21,9 @@ import { SimulatedProvider } from "./simulated-provider"
*
*/
export const simulationReplacements = Effect.fn("Simulation.replacements")(function* (app: {
export const simulationReplacements: (app: {
readonly version: string
}) => Effect.Effect<LayerNode.Replacements, Error, FileSystem.FileSystem> = Effect.fn("Simulation.replacements")(function* (app: {
readonly version: string
}) {
// ModelsDev dies when its catalog fetch fails, so simulation answers it with
+15
View File
@@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./tsconfig.json",
"compilerOptions": {
"composite": false,
"incremental": false,
"rootDir": "src",
"outDir": "dist",
"noEmit": false,
"declaration": true,
"emitDeclarationOnly": true,
"tsBuildInfoFile": null
},
"include": ["src"]
}
-3
View File
@@ -1,3 +0,0 @@
SLACK_BOT_TOKEN=xoxb-your-bot-token
SLACK_SIGNING_SECRET=your-signing-secret
SLACK_APP_TOKEN=xapp-your-app-token
-4
View File
@@ -1,4 +0,0 @@
node_modules
dist
.env
.DS_Store
-27
View File
@@ -1,27 +0,0 @@
# @opencode-ai/slack
Slack bot integration for opencode that creates threaded conversations.
## Setup
1. Create a Slack app at https://api.slack.com/apps
2. Enable Socket Mode
3. Add the following OAuth scopes:
- `chat:write`
- `app_mentions:read`
- `channels:history`
- `groups:history`
4. Install the app to your workspace
5. Set environment variables in `.env`:
- `SLACK_BOT_TOKEN` - Bot User OAuth Token
- `SLACK_SIGNING_SECRET` - Signing Secret from Basic Information
- `SLACK_APP_TOKEN` - App-Level Token from Basic Information
## Usage
```bash
# Edit .env with your Slack app credentials
bun dev
```
The bot will respond to messages in channels where it's added, creating separate opencode sessions for each thread.
-19
View File
@@ -1,19 +0,0 @@
{
"name": "@opencode-ai/slack",
"version": "1.18.15",
"type": "module",
"license": "MIT",
"scripts": {
"dev": "bun run src/index.ts",
"typecheck": "tsgo --noEmit"
},
"dependencies": {
"@opencode-ai/sdk": "1.18.5",
"@slack/bolt": "^3.17.1"
},
"devDependencies": {
"@types/node": "catalog:",
"typescript": "catalog:",
"@typescript/native-preview": "catalog:"
}
}
-139
View File
@@ -1,139 +0,0 @@
import { App } from "@slack/bolt"
import { createOpencode, type ToolPart } from "@opencode-ai/sdk"
const app = new App({
token: process.env.SLACK_BOT_TOKEN,
signingSecret: process.env.SLACK_SIGNING_SECRET,
socketMode: true,
appToken: process.env.SLACK_APP_TOKEN,
})
console.log("🔧 Bot configuration:")
console.log("- Bot token present:", !!process.env.SLACK_BOT_TOKEN)
console.log("- Signing secret present:", !!process.env.SLACK_SIGNING_SECRET)
console.log("- App token present:", !!process.env.SLACK_APP_TOKEN)
console.log("🚀 Starting opencode server...")
const opencode = await createOpencode({
port: 0,
})
console.log("✅ Opencode server ready")
const sessions = new Map<string, { sessionId: string; channel: string; thread: string }>()
void (async () => {
const events = await opencode.client.event.subscribe()
for await (const event of events.stream) {
if (event.type === "message.part.updated") {
const part = event.properties.part
if (part.type === "tool") {
// Find the session for this tool update
for (const session of sessions.values()) {
if (session.sessionId === part.sessionID) {
void handleToolUpdate(part, session.channel, session.thread)
break
}
}
}
}
}
})()
async function handleToolUpdate(part: ToolPart, channel: string, thread: string) {
if (part.state.status !== "completed") return
const toolMessage = `*${part.tool}* - ${part.state.title}`
await app.client.chat
.postMessage({
channel,
thread_ts: thread,
text: toolMessage,
})
.catch(() => {})
}
app.use(async ({ next, context }) => {
console.log("📡 Raw Slack event:", JSON.stringify(context, null, 2))
await next()
})
app.message(async ({ message, say }) => {
console.log("📨 Received message event:", JSON.stringify(message, null, 2))
if (message.subtype || !("text" in message) || !message.text) {
console.log("⏭️ Skipping message - no text or has subtype")
return
}
console.log("✅ Processing message:", message.text)
const channel = message.channel
const thread = ("thread_ts" in message && typeof message.thread_ts === "string" && message.thread_ts) || message.ts
const sessionKey = `${channel}-${thread}`
let session = sessions.get(sessionKey)
if (!session) {
console.log("🆕 Creating new opencode session...")
const createResult = await opencode.client.session.create({
body: { title: `Slack thread ${thread}` },
})
if (createResult.error) {
console.error("❌ Failed to create session:", createResult.error)
await say({
text: "Sorry, I had trouble creating a session. Please try again.",
thread_ts: thread,
})
return
}
console.log("✅ Created opencode session:", createResult.data.id)
session = { sessionId: createResult.data.id, channel, thread }
sessions.set(sessionKey, session)
const shareResult = await opencode.client.session.share({ path: { id: createResult.data.id } })
if (!shareResult.error && shareResult.data) {
const sessionUrl = shareResult.data.share?.url
console.log("🔗 Session shared:", sessionUrl)
await app.client.chat.postMessage({ channel, thread_ts: thread, text: sessionUrl })
}
}
console.log("📝 Sending to opencode:", message.text)
const result = await opencode.client.session.prompt({
path: { id: session.sessionId },
body: { parts: [{ type: "text", text: message.text }] },
})
console.log("📤 Opencode response:", JSON.stringify(result, null, 2))
if (result.error) {
console.error("❌ Failed to send message:", result.error)
await say({
text: "Sorry, I had trouble processing your message. Please try again.",
thread_ts: thread,
})
return
}
// Build response text
const responseText =
result.data.parts
.filter((part) => part.type === "text")
.map((part) => part.text)
.join("\n") || "I received your message but didn't have a response."
console.log("💬 Sending response:", responseText)
// Send main response (tool updates will come via live events)
await say({ text: responseText, thread_ts: thread })
})
app.command("/test", async ({ command, ack, say }) => {
await ack()
console.log("🧪 Test command received:", JSON.stringify(command, null, 2))
await say("🤖 Bot is working! I can hear you loud and clear.")
})
await app.start()
console.log("⚡️ Slack bot is running!")
-10
View File
@@ -1,10 +0,0 @@
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
/* biome-ignore-all lint: auto-generated */
/// <reference path="../../sst-env.d.ts" />
import "sst"
export {}
-8
View File
@@ -1,8 +0,0 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@tsconfig/bun/tsconfig.json",
"compilerOptions": {
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"noUncheckedIndexedAccess": false
}
}
+26 -100
View File
@@ -1,15 +1,13 @@
/// <reference path="./audio.d.ts" />
import type {
TuiAttention,
TuiAttentionNotifyInput,
TuiAttentionNotifyResult,
TuiAttentionNotifySkipReason,
TuiAttentionWhen,
TuiAttentionSoundName,
TuiAttentionSoundPack,
TuiAttentionSoundPackInfo,
} from "@opencode-ai/plugin/v1/tui"
import { AttentionSoundName, type Config } from "./config"
Attention,
AttentionNotifyOptions,
AttentionNotifyResult,
AttentionNotifySkipReason,
AttentionWhen,
AttentionSoundName,
} from "@opencode-ai/plugin/tui/context"
import { Config } from "./config"
import { Schema } from "effect"
import stripAnsi from "strip-ansi"
import * as TuiAudio from "./audio"
@@ -30,33 +28,23 @@ type AttentionRenderer = {
triggerNotification(message: string, title?: string): boolean
}
type RegisteredSoundPack = TuiAttentionSoundPack & {
builtin: boolean
}
type TuiAttentionHost = TuiAttention & {
type AttentionHost = Attention & {
dispose(): void
}
const DEFAULT_TITLE = "OpenCode"
const DEFAULT_PACK_ID = "opencode.default"
const TITLE_LIMIT = 80
const MESSAGE_LIMIT = 240
const BUILTIN_PACK: RegisteredSoundPack = {
id: DEFAULT_PACK_ID,
name: "OpenCode Default",
builtin: true,
sounds: {
default: defaultSoundPath,
question: questionSoundPath,
permission: permissionSoundPath,
error: errorSoundPath,
done: defaultSoundPath,
subagent_done: subagentDoneSoundPath,
},
const BUILTIN_SOUNDS: Record<AttentionSoundName, string> = {
default: defaultSoundPath,
question: questionSoundPath,
permission: permissionSoundPath,
error: errorSoundPath,
done: defaultSoundPath,
subagent_done: subagentDoneSoundPath,
}
function skipped(reason: TuiAttentionNotifySkipReason): TuiAttentionNotifyResult {
function skipped(reason: AttentionNotifySkipReason): AttentionNotifyResult {
return {
ok: false,
notification: false,
@@ -79,7 +67,7 @@ function clampVolume(volume: number) {
return Math.min(1, Math.max(0, volume))
}
function soundVolume(input: TuiAttentionNotifyInput, config: Pick<Config.Resolved, "attention">) {
function soundVolume(input: AttentionNotifyOptions, config: Pick<Config.Resolved, "attention">) {
if (!config.attention.sound) return
if (input.sound === false) return
if (input.sound === undefined) return clampVolume(config.attention.volume)
@@ -87,23 +75,7 @@ function soundVolume(input: TuiAttentionNotifyInput, config: Pick<Config.Resolve
return clampVolume(input.sound.volume ?? config.attention.volume)
}
function normalizePack(pack: TuiAttentionSoundPack): RegisteredSoundPack | undefined {
const id = pack.id.trim()
if (!id) return
return {
id,
name: pack.name?.trim() || undefined,
builtin: false,
sounds: Object.fromEntries(
Object.entries(pack.sounds).filter(
(item): item is [TuiAttentionSoundName, string] =>
Schema.is(AttentionSoundName)(item[0]) && typeof item[1] === "string" && item[1].trim().length > 0,
),
),
}
}
function focusSkip(when: TuiAttentionWhen, focus: FocusState) {
function focusSkip(when: AttentionWhen, focus: FocusState) {
if (when === "always") return
if (focus === "unknown") return "focus_unknown"
if (when === "blurred" && focus === "focused") return "focused"
@@ -113,13 +85,10 @@ function focusSkip(when: TuiAttentionWhen, focus: FocusState) {
export function createTuiAttention(input: {
renderer: AttentionRenderer
config: Pick<Config.Resolved, "attention">
update?: Config.Interface["update"]
audio?: Pick<typeof TuiAudio, "loadSoundFile" | "play">
}): TuiAttentionHost {
}): AttentionHost {
let focus: FocusState = "unknown"
let disposed = false
let activePackID: string | undefined
const packs = new Map<string, RegisteredSoundPack>([[BUILTIN_PACK.id, BUILTIN_PACK]])
const audio = input.audio ?? TuiAudio
const onFocus = () => {
@@ -132,21 +101,13 @@ export function createTuiAttention(input: {
input.renderer.on("focus", onFocus)
input.renderer.on("blur", onBlur)
function configuredPackID() {
return activePackID ?? input.config.attention.sound_pack
}
function currentPack() {
return packs.get(configuredPackID()) ?? BUILTIN_PACK
}
function soundCandidates(name: TuiAttentionSoundName) {
return [input.config.attention.sounds[name], currentPack().sounds[name], BUILTIN_PACK.sounds[name]].filter(
function soundCandidates(name: AttentionSoundName) {
return [input.config.attention.sounds[name], BUILTIN_SOUNDS[name]].filter(
(item, index, list): item is string => typeof item === "string" && list.indexOf(item) === index,
)
}
async function playSound(name: TuiAttentionSoundName, volume: number) {
async function playSound(name: AttentionSoundName, volume: number) {
try {
for (const file of soundCandidates(name)) {
const current = await audio.loadSoundFile(file).catch((error) => {
@@ -194,7 +155,9 @@ export function createTuiAttention(input: {
const requestedSound = typeof request.sound === "object" ? request.sound : undefined
const soundSkip = volume === undefined ? undefined : focusSkip(requestedSound?.when ?? "always", focus)
const soundName =
requestedSound?.name && Schema.is(AttentionSoundName)(requestedSound.name) ? requestedSound.name : "default"
requestedSound?.name && Schema.is(Config.AttentionSoundName)(requestedSound.name)
? requestedSound.name
: "default"
const sound = volume === undefined || soundSkip ? false : await playSound(soundName, volume)
if (!notification && !sound) {
@@ -216,43 +179,6 @@ export function createTuiAttention(input: {
}
}
},
soundboard: {
registerPack(pack) {
const next = normalizePack(pack)
if (!next) return () => {}
packs.set(next.id, next)
let disposed = false
return () => {
if (disposed) return
disposed = true
if (packs.get(next.id) === next) packs.delete(next.id)
}
},
activate(id, options) {
const pack = packs.get(id)
if (!pack) return false
activePackID = pack.id
if (options?.persist)
void input
.update?.((draft) => {
draft.attention = { ...draft.attention, sound_pack: pack.id }
})
.catch(() => {})
return true
},
current() {
return currentPack().id
},
list(): TuiAttentionSoundPackInfo[] {
const current = currentPack().id
return Array.from(packs.values()).map((pack) => ({
id: pack.id,
name: pack.name,
active: pack.id === current,
builtin: pack.builtin,
}))
},
},
dispose() {
if (disposed) return
disposed = true
-1
View File
@@ -11,7 +11,6 @@ export function AttentionProvider(props: ParentProps) {
const attention = createTuiAttention({
renderer: useRenderer(),
config: config.data,
update: config.update,
})
onCleanup(() => attention.dispose())
return <AttentionContext.Provider value={attention}>{props.children}</AttentionContext.Provider>
+10 -9
View File
@@ -6,13 +6,14 @@
// the run footer + scrollback color model. Falls back to a hardcoded dark-mode
// palette if detection fails.
import { RGBA, SyntaxStyle, type CliRenderer, type ColorInput, type TerminalColors } from "@opentui/core"
import type { TuiThemeCurrent } from "@opencode-ai/plugin/v1/tui"
import { ansiToRgba } from "../theme/color"
import { resolveThemeColors } from "../theme/resolve"
import { terminalMode } from "../theme/system"
import type { ThemeV1Json } from "../theme/v1"
import type { Theme, ThemeV1Json } from "../theme/v1"
import type { EntryKind, RunTuiConfig } from "./types"
type ThemeCurrent = Omit<Theme, "_hasSelectedListItemText">
type Tone = {
body: ColorInput
start?: ColorInput
@@ -66,9 +67,9 @@ export type RunTheme = {
block: RunBlockTheme
}
type ThemeColor = Exclude<keyof TuiThemeCurrent, "thinkingOpacity">
type ThemeColor = Exclude<keyof ThemeCurrent, "thinkingOpacity">
type SharedSyntaxTheme = TuiThemeCurrent & {
type SharedSyntaxTheme = ThemeCurrent & {
_hasSelectedListItemText: boolean
}
@@ -184,7 +185,7 @@ function splashShadow(indexed: RGBA[], base: RGBA, overlay: RGBA, value: number)
return nearestIndexed(indexed, mixed)
}
export function resolveTheme(theme: ThemeV1Json, pick: "dark" | "light"): TuiThemeCurrent {
export function resolveTheme(theme: ThemeV1Json, pick: "dark" | "light"): ThemeCurrent {
const resolved = resolveThemeColors(theme, pick, (code) => RGBA.fromIndex(code, ansiToRgba(code)))
return {
...resolved.theme,
@@ -340,7 +341,7 @@ function quantizeColor(indexed: RGBA[], rgba: RGBA): RGBA {
return nearestIndexed(indexed, rgba)
}
function quantizeTheme(theme: TuiThemeCurrent, indexed: RGBA[]): TuiThemeCurrent {
function quantizeTheme(theme: ThemeCurrent, indexed: RGBA[]): ThemeCurrent {
const resolved = Object.fromEntries(
Object.entries(theme)
.filter(([key]) => key !== "thinkingOpacity")
@@ -353,7 +354,7 @@ function quantizeTheme(theme: TuiThemeCurrent, indexed: RGBA[]): TuiThemeCurrent
}
}
function splashTheme(theme: TuiThemeCurrent, indexed: RGBA[]): RunSplashTheme {
function splashTheme(theme: ThemeCurrent, indexed: RGBA[]): RunSplashTheme {
const left = nearestIndexed(indexed, theme.textMuted)
const right = nearestIndexed(indexed, theme.text)
return {
@@ -364,8 +365,8 @@ function splashTheme(theme: TuiThemeCurrent, indexed: RGBA[]): RunSplashTheme {
}
function map(
footerTheme: TuiThemeCurrent,
scrollbackTheme: TuiThemeCurrent,
footerTheme: ThemeCurrent,
scrollbackTheme: ThemeCurrent,
splash: RunSplashTheme,
syntax?: SyntaxStyle,
): RunTheme {
-26
View File
@@ -1,26 +0,0 @@
import type { TuiPluginApi } from "@opencode-ai/plugin/v1/tui"
import { RGBA } from "@opentui/core"
import { createTuiResolvedConfig } from "./tui-runtime"
type Opts = {
client?: TuiPluginApi["client"]
keymap?: TuiPluginApi["keymap"]
attention?: Partial<TuiPluginApi["attention"]>
event?: TuiPluginApi["event"]
state?: { session?: Partial<TuiPluginApi["state"]["session"]> }
}
export function createTuiPluginApi(opts: Opts = {}) {
const color = RGBA.fromInts(200, 200, 200)
const dialog = { clear() {}, replace() {}, setSize() {}, size: "medium" as const, depth: 0, open: false }
return {
attention: { notify: async () => ({ ok: false, notification: false, sound: false }), ...opts.attention },
client: opts.client,
event: opts.event,
keymap: opts.keymap,
state: { session: { get: () => undefined, ...opts.state?.session } },
theme: { current: new Proxy({}, { get: () => color }) },
tuiConfig: createTuiResolvedConfig(),
ui: { dialog },
} as unknown as TuiPluginApi
}
+5 -5
View File
@@ -11,14 +11,14 @@ server and use the [TypeScript client](/build/client) in the meantime.
## Effect
`@opencode-ai/sdk-next` hosts OpenCode in-process. Unlike the
`@opencode-ai/sdk` hosts OpenCode in-process. Unlike the
[network client](/build/client), it assembles the OpenCode server and routes API
calls through its HTTP router in memory. It opens no HTTP listener and adds no
network hop between the client and server.
<Callout type="warning">
The V2 SDK is beta and currently private to the OpenCode workspace. It is not published for external installation yet,
and its package name and API may change before release.
The V2 SDK is beta. Install the current preview with `bun add @opencode-ai/sdk@dev`; its API may change before a
stable release.
</Callout>
## Create a host
@@ -27,7 +27,7 @@ network hop between the client and server.
the router, location services, fibers, and scoped plugin registrations.
```ts
import { AbsolutePath, Location, OpenCode } from "@opencode-ai/sdk-next"
import { AbsolutePath, Location, OpenCode } from "@opencode-ai/sdk"
import { Effect } from "effect"
const program = Effect.scoped(
@@ -57,7 +57,7 @@ Use `OpenCode.layer` when the host should be provided through Effect dependency
injection:
```ts
import { OpenCode } from "@opencode-ai/sdk-next"
import { OpenCode } from "@opencode-ai/sdk"
import { Effect } from "effect"
const program = Effect.gen(function* () {
+2
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env bun
/* TODO: Migrate this script to the new in-process @opencode-ai/sdk.
import path from "path"
import { pathToFileURL } from "bun"
import { createOpencode } from "@opencode-ai/sdk"
@@ -77,3 +78,4 @@ Examples:
}
void main()
*/
+12
View File
@@ -50,6 +50,9 @@ await $`bun ./packages/ai/script/publish.ts`
console.log("\n=== util ===\n")
await $`bun ./packages/util/script/publish.ts`
console.log("\n=== shell scan ===\n")
await $`bun ./packages/shell-scan/script/publish.ts`
console.log("\n=== protocol ===\n")
await $`bun ./packages/protocol/script/publish.ts`
@@ -65,6 +68,15 @@ await $`bun ./packages/plugin/script/publish.ts`
console.log("\n=== core ===\n")
await $`bun ./packages/core/script/publish.ts`
console.log("\n=== simulation ===\n")
await $`bun ./packages/simulation/script/publish.ts`
console.log("\n=== server ===\n")
await $`bun ./packages/server/script/publish.ts`
console.log("\n=== sdk ===\n")
await $`bun ./packages/sdk/script/publish.ts`
console.log("\n=== ui ===\n")
await $`bun ./packages/ui/script/publish.ts`
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env bun
import { mkdir, mkdtemp, rm } from "node:fs/promises"
import { tmpdir } from "node:os"
import path from "node:path"
const packages = ["@opencode-ai/simulation", "@opencode-ai/server"]
const publish = Bun.argv.includes("--publish")
if (!publish) {
console.log("Package names to reserve:")
for (const name of packages) console.log(`- ${name}`)
console.log("\nRun `bun run reserve-packages --publish` to log in and publish reservation placeholders.")
process.exit(0)
}
if (run(["npm", "whoami"]) !== 0) {
if (run(["npm", "login"]) !== 0) throw new Error("npm login failed")
if (run(["npm", "whoami"]) !== 0) throw new Error("npm authentication failed")
}
const directory = await mkdtemp(path.join(tmpdir(), "opencode-package-reservations-"))
try {
for (const name of packages) {
const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(name)}`)
if (response.ok) {
console.log(`Skipping ${name}: already exists on npm`)
continue
}
if (response.status !== 404) throw new Error(`Failed to check ${name}: npm returned ${response.status}`)
const target = path.join(directory, name.slice("@opencode-ai/".length))
await mkdir(target, { recursive: true })
await Bun.write(
path.join(target, "package.json"),
JSON.stringify(
{
name,
version: "0.0.0-reserved",
description: "Reserved for OpenCode",
license: "MIT",
repository: {
type: "git",
url: "git+https://github.com/anomalyco/opencode.git",
},
publishConfig: {
access: "public",
tag: "reserved",
},
},
null,
2,
) + "\n",
)
await Bun.write(path.join(target, "README.md"), `# ${name}\n\nReserved for OpenCode.\n`)
console.log(`Reserving ${name}`)
if (run(["npm", "publish", "--access", "public", "--tag", "reserved"], target) !== 0) {
throw new Error(`Failed to reserve ${name}`)
}
}
} finally {
await rm(directory, { recursive: true, force: true })
}
function run(command: string[], cwd?: string) {
return Bun.spawnSync(command, {
cwd,
stdin: "inherit",
stdout: "inherit",
stderr: "inherit",
}).exitCode
}
+1 -1
View File
@@ -7,7 +7,7 @@
"@opencode-ai/enterprise#typecheck": {
"dependsOn": ["@opencode-ai/core#typecheck"]
},
"@opencode-ai/sdk-next#typecheck": {
"@opencode-ai/sdk#typecheck": {
"dependsOn": ["@opencode-ai/core#typecheck"]
},
"@opencode-ai/server#typecheck": {