mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
feat(sdk): prepare v2 packages for publishing
This commit is contained in:
File diff suppressed because it is too large
Load Diff
+1
-17
@@ -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,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`.
|
||||
|
||||
+2
-3
@@ -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:"
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:*",
|
||||
|
||||
@@ -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:",
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
@@ -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:"
|
||||
@@ -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()),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -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))
|
||||
+1
-1
@@ -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,
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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()),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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`
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"noEmit": false,
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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()),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
SLACK_BOT_TOKEN=xoxb-your-bot-token
|
||||
SLACK_SIGNING_SECRET=your-signing-secret
|
||||
SLACK_APP_TOKEN=xapp-your-app-token
|
||||
@@ -1,4 +0,0 @@
|
||||
node_modules
|
||||
dist
|
||||
.env
|
||||
.DS_Store
|
||||
@@ -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.
|
||||
@@ -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:"
|
||||
}
|
||||
}
|
||||
@@ -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!")
|
||||
Vendored
-10
@@ -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 {}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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* () {
|
||||
|
||||
@@ -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()
|
||||
*/
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user