@sw-market/openclaw-opencode-bridge 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -62
- package/dist/openclaw-extension.js +30 -57
- package/dist/sdk-adapter-opencode.d.ts +18 -0
- package/dist/sdk-adapter-opencode.js +727 -0
- package/openclaw.plugin.json +43 -11
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -1,29 +1,37 @@
|
|
|
1
1
|
# @sw-market/openclaw-opencode-bridge
|
|
2
2
|
|
|
3
|
-
OpenClaw plugin
|
|
3
|
+
OpenClaw plugin that bridges gateway methods to a built-in `@opencode-ai/sdk` runtime.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## What It Does
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
7
|
+
- Registers gateway methods:
|
|
8
|
+
- `opencode.chat.send`
|
|
9
|
+
- `opencode.chat.action`
|
|
10
|
+
- Maintains OpenCode sessions by `sessionKey`.
|
|
11
|
+
- Streams realtime events to OpenClaw:
|
|
12
|
+
- assistant deltas
|
|
13
|
+
- tool start/result
|
|
14
|
+
- interaction required/resolved (permission/question)
|
|
15
|
+
- file change summaries
|
|
16
|
+
- progress and run completion
|
|
11
17
|
|
|
12
|
-
## Install
|
|
18
|
+
## Install
|
|
13
19
|
|
|
14
20
|
```bash
|
|
15
|
-
openclaw plugins install @sw-market/openclaw-opencode-bridge
|
|
21
|
+
openclaw plugins install @sw-market/openclaw-opencode-bridge@0.1.4
|
|
16
22
|
```
|
|
17
23
|
|
|
18
|
-
|
|
24
|
+
Verify:
|
|
19
25
|
|
|
20
26
|
```bash
|
|
21
|
-
openclaw plugins
|
|
27
|
+
openclaw plugins info openclaw-opencode-bridge
|
|
22
28
|
```
|
|
23
29
|
|
|
24
|
-
##
|
|
30
|
+
## Config
|
|
25
31
|
|
|
26
|
-
|
|
32
|
+
Plugin no longer requires external adapter modules (`sdkAdapterModule` was removed).
|
|
33
|
+
|
|
34
|
+
Configure in `~/.openclaw/openclaw.json`:
|
|
27
35
|
|
|
28
36
|
```json
|
|
29
37
|
{
|
|
@@ -32,10 +40,15 @@ In `~/.openclaw/openclaw.json`, enable plugin and set SDK adapter config:
|
|
|
32
40
|
"openclaw-opencode-bridge": {
|
|
33
41
|
"enabled": true,
|
|
34
42
|
"config": {
|
|
35
|
-
"
|
|
36
|
-
"
|
|
43
|
+
"opencodeBaseUrl": "http://127.0.0.1:4096",
|
|
44
|
+
"opencodeDirectory": "/home/xxll-gpu-5080/code/openclaw_workspace",
|
|
45
|
+
"opencodeAgent": "codex",
|
|
46
|
+
"opencodeProviderId": "custom-www-right-codes",
|
|
47
|
+
"opencodeModelId": "gpt-5.2",
|
|
48
|
+
"opencodeSystemPrompt": "",
|
|
37
49
|
"sessionTtlMs": 1800000,
|
|
38
|
-
"cleanupIntervalMs": 60000
|
|
50
|
+
"cleanupIntervalMs": 60000,
|
|
51
|
+
"emitToAllClients": false
|
|
39
52
|
}
|
|
40
53
|
}
|
|
41
54
|
}
|
|
@@ -43,62 +56,30 @@ In `~/.openclaw/openclaw.json`, enable plugin and set SDK adapter config:
|
|
|
43
56
|
}
|
|
44
57
|
```
|
|
45
58
|
|
|
46
|
-
|
|
59
|
+
## Environment Variables
|
|
60
|
+
|
|
61
|
+
Config can also come from env:
|
|
47
62
|
|
|
48
63
|
```bash
|
|
49
|
-
export
|
|
50
|
-
export
|
|
64
|
+
export OPENCODE_BASE_URL=http://127.0.0.1:4096
|
|
65
|
+
export OPENCODE_WORKSPACE_DIR=/home/xxll-gpu-5080/code/openclaw_workspace
|
|
66
|
+
export OPENCODE_AGENT=codex
|
|
67
|
+
export OPENCODE_PROVIDER_ID=custom-www-right-codes
|
|
68
|
+
export OPENCODE_MODEL_ID=gpt-5.2
|
|
69
|
+
export OPENCODE_SYSTEM_PROMPT=""
|
|
51
70
|
```
|
|
52
71
|
|
|
53
|
-
Adapter
|
|
54
|
-
- export function `createOpenCodeSdkAdapter(ctx)` or export object directly.
|
|
55
|
-
- returned object must implement `createSession(args)`.
|
|
56
|
-
|
|
57
|
-
Note:
|
|
58
|
-
- Plugin installation no longer requires `sdkAdapterModule` in config.
|
|
59
|
-
- But runtime calls will fail until adapter module is provided via config or env.
|
|
60
|
-
|
|
61
|
-
## Gateway Methods Provided
|
|
72
|
+
## Adapter Routing (your Python adapter)
|
|
62
73
|
|
|
63
|
-
|
|
64
|
-
- `opencode.chat.send`
|
|
65
|
-
- `opencode.chat.action`
|
|
66
|
-
|
|
67
|
-
Set adapter side method routing:
|
|
74
|
+
Keep your adapter method mapping:
|
|
68
75
|
|
|
69
76
|
```env
|
|
70
77
|
BRIDGE_WS_CHAT_SEND_METHOD=opencode.chat.send
|
|
71
78
|
BRIDGE_WS_CHAT_ACTION_METHOD=opencode.chat.action
|
|
72
79
|
```
|
|
73
80
|
|
|
74
|
-
##
|
|
75
|
-
|
|
76
|
-
```ts
|
|
77
|
-
import {
|
|
78
|
-
OpenClawOpenCodeBridge,
|
|
79
|
-
type OpenCodeSdkAdapter,
|
|
80
|
-
} from "@sw-market/openclaw-opencode-bridge";
|
|
81
|
-
|
|
82
|
-
const sdk: OpenCodeSdkAdapter = createYourOpenCodeSdkAdapter();
|
|
83
|
-
const bridge = new OpenClawOpenCodeBridge(sdk, {
|
|
84
|
-
sessionTtlMs: 30 * 60 * 1000,
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
const started = await bridge.chatSend({
|
|
88
|
-
sessionKey: "sw:feishu:chat_001",
|
|
89
|
-
message: "list files and update README",
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
for await (const frame of started.events) {
|
|
93
|
-
// forward frame to OpenClaw gateway `event` pipeline
|
|
94
|
-
console.log(frame);
|
|
95
|
-
}
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
## Contract
|
|
99
|
-
|
|
100
|
-
- `chatSend`: start or continue a coding run.
|
|
101
|
-
- `chatAction`: handle action replay (interaction reply, cancel run).
|
|
102
|
-
- `dispose`: stop cleanup timer and release all in-memory sessions.
|
|
81
|
+
## Notes
|
|
103
82
|
|
|
104
|
-
|
|
83
|
+
- This plugin is now SDK-first and beta-oriented. It does not keep legacy template compatibility.
|
|
84
|
+
- The OpenCode server must be reachable at `opencodeBaseUrl`.
|
|
85
|
+
- `opencodeProviderId` and `opencodeModelId` are optional. If omitted, OpenCode server defaults are used.
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { pathToFileURL } from "node:url";
|
|
4
1
|
import { OpenClawOpenCodeBridge, } from "./index.js";
|
|
2
|
+
import { createOpenCodeSdkAdapter } from "./sdk-adapter-opencode.js";
|
|
5
3
|
const CHAT_SEND_METHOD = "opencode.chat.send";
|
|
6
4
|
const CHAT_ACTION_METHOD = "opencode.chat.action";
|
|
7
|
-
const
|
|
5
|
+
const DEFAULT_OPENCODE_BASE_URL = "http://127.0.0.1:4096";
|
|
8
6
|
let bridgePromise = null;
|
|
9
7
|
let bridgeInstance = null;
|
|
10
8
|
function asObject(value) {
|
|
@@ -49,66 +47,33 @@ function toErrorMessage(error) {
|
|
|
49
47
|
}
|
|
50
48
|
function parseConfig(pluginConfig) {
|
|
51
49
|
const raw = pluginConfig ?? {};
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
const opencodeBaseUrl = readString(raw, "opencodeBaseUrl") ||
|
|
51
|
+
String(process.env.OPENCODE_BASE_URL || "").trim() ||
|
|
52
|
+
DEFAULT_OPENCODE_BASE_URL;
|
|
53
|
+
const opencodeDirectory = readString(raw, "opencodeDirectory") || String(process.env.OPENCODE_WORKSPACE_DIR || "").trim() || undefined;
|
|
54
|
+
const opencodeAgent = readString(raw, "opencodeAgent") || String(process.env.OPENCODE_AGENT || "").trim() || undefined;
|
|
55
|
+
const opencodeProviderId = readString(raw, "opencodeProviderId") ||
|
|
56
|
+
String(process.env.OPENCODE_PROVIDER_ID || "").trim() ||
|
|
57
|
+
undefined;
|
|
58
|
+
const opencodeModelId = readString(raw, "opencodeModelId") || String(process.env.OPENCODE_MODEL_ID || "").trim() || undefined;
|
|
59
|
+
const opencodeSystemPrompt = readString(raw, "opencodeSystemPrompt") ||
|
|
60
|
+
String(process.env.OPENCODE_SYSTEM_PROMPT || "").trim() ||
|
|
61
|
+
undefined;
|
|
57
62
|
const sessionTtlMs = readOptionalPositiveInt(raw, "sessionTtlMs");
|
|
58
63
|
const cleanupIntervalMs = readOptionalPositiveInt(raw, "cleanupIntervalMs");
|
|
59
64
|
const emitToAllClients = raw.emitToAllClients === true;
|
|
60
65
|
return {
|
|
61
|
-
|
|
62
|
-
|
|
66
|
+
opencodeBaseUrl,
|
|
67
|
+
opencodeDirectory,
|
|
68
|
+
opencodeAgent,
|
|
69
|
+
opencodeProviderId,
|
|
70
|
+
opencodeModelId,
|
|
71
|
+
opencodeSystemPrompt,
|
|
63
72
|
sessionTtlMs,
|
|
64
73
|
cleanupIntervalMs,
|
|
65
74
|
emitToAllClients,
|
|
66
75
|
};
|
|
67
76
|
}
|
|
68
|
-
function resolveModuleSpecifier(api, moduleSpecifier) {
|
|
69
|
-
const raw = moduleSpecifier.trim();
|
|
70
|
-
if (!raw) {
|
|
71
|
-
throw new Error("OpenCode SDK adapter is not configured. Set plugins.entries.openclaw-opencode-bridge.config.sdkAdapterModule " +
|
|
72
|
-
"or env OPENCODE_SDK_ADAPTER_MODULE.");
|
|
73
|
-
}
|
|
74
|
-
const looksLikePath = raw.startsWith("./") ||
|
|
75
|
-
raw.startsWith("../") ||
|
|
76
|
-
raw.startsWith("/") ||
|
|
77
|
-
raw.startsWith(".\\") ||
|
|
78
|
-
/^[a-zA-Z]:[\\/]/.test(raw);
|
|
79
|
-
if (!looksLikePath) {
|
|
80
|
-
return raw;
|
|
81
|
-
}
|
|
82
|
-
const absolutePath = path.isAbsolute(raw) ? raw : api.resolvePath(raw);
|
|
83
|
-
if (!existsSync(absolutePath)) {
|
|
84
|
-
throw new Error(`sdkAdapterModule path does not exist: ${absolutePath}`);
|
|
85
|
-
}
|
|
86
|
-
return pathToFileURL(absolutePath).href;
|
|
87
|
-
}
|
|
88
|
-
function isOpenCodeSdkAdapter(value) {
|
|
89
|
-
const obj = asObject(value);
|
|
90
|
-
if (!obj) {
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
93
|
-
return typeof obj.createSession === "function";
|
|
94
|
-
}
|
|
95
|
-
async function loadSdkAdapter(api, config) {
|
|
96
|
-
const moduleSpecifier = resolveModuleSpecifier(api, config.sdkAdapterModule ?? "");
|
|
97
|
-
const loaded = await import(moduleSpecifier);
|
|
98
|
-
const exported = loaded[config.sdkAdapterExport];
|
|
99
|
-
let candidate = exported;
|
|
100
|
-
if (typeof exported === "function") {
|
|
101
|
-
candidate = await Promise.resolve(exported({
|
|
102
|
-
api,
|
|
103
|
-
pluginConfig: config,
|
|
104
|
-
}));
|
|
105
|
-
}
|
|
106
|
-
if (!isOpenCodeSdkAdapter(candidate)) {
|
|
107
|
-
throw new Error(`Invalid OpenCode SDK adapter from "${config.sdkAdapterModule}" export "${config.sdkAdapterExport}". ` +
|
|
108
|
-
"Expected an object with createSession(args) function.");
|
|
109
|
-
}
|
|
110
|
-
return candidate;
|
|
111
|
-
}
|
|
112
77
|
function respondError(respond, code, message) {
|
|
113
78
|
respond(false, undefined, {
|
|
114
79
|
code,
|
|
@@ -215,13 +180,21 @@ async function ensureBridge(api) {
|
|
|
215
180
|
}
|
|
216
181
|
if (!bridgePromise) {
|
|
217
182
|
bridgePromise = (async () => {
|
|
218
|
-
const sdk =
|
|
183
|
+
const sdk = createOpenCodeSdkAdapter({
|
|
184
|
+
baseUrl: config.opencodeBaseUrl,
|
|
185
|
+
directory: config.opencodeDirectory,
|
|
186
|
+
agent: config.opencodeAgent,
|
|
187
|
+
providerId: config.opencodeProviderId,
|
|
188
|
+
modelId: config.opencodeModelId,
|
|
189
|
+
systemPrompt: config.opencodeSystemPrompt,
|
|
190
|
+
logger: api.logger,
|
|
191
|
+
});
|
|
219
192
|
const bridge = new OpenClawOpenCodeBridge(sdk, {
|
|
220
193
|
sessionTtlMs: config.sessionTtlMs,
|
|
221
194
|
cleanupIntervalMs: config.cleanupIntervalMs,
|
|
222
195
|
});
|
|
223
196
|
bridgeInstance = bridge;
|
|
224
|
-
api.logger.info(`[${CHAT_SEND_METHOD}] bridge initialized`);
|
|
197
|
+
api.logger.info(`[${CHAT_SEND_METHOD}] bridge initialized (baseUrl=${config.opencodeBaseUrl}, directory=${config.opencodeDirectory ?? "<default>"})`);
|
|
225
198
|
return bridge;
|
|
226
199
|
})();
|
|
227
200
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { OpenCodeSdkAdapter } from "./index.js";
|
|
2
|
+
type Logger = {
|
|
3
|
+
info?: (message: string) => void;
|
|
4
|
+
warn?: (message: string) => void;
|
|
5
|
+
error?: (message: string) => void;
|
|
6
|
+
debug?: (message: string) => void;
|
|
7
|
+
};
|
|
8
|
+
export interface OpenCodeSdkRuntimeConfig {
|
|
9
|
+
baseUrl: string;
|
|
10
|
+
directory?: string;
|
|
11
|
+
agent?: string;
|
|
12
|
+
providerId?: string;
|
|
13
|
+
modelId?: string;
|
|
14
|
+
systemPrompt?: string;
|
|
15
|
+
logger?: Logger;
|
|
16
|
+
}
|
|
17
|
+
export declare function createOpenCodeSdkAdapter(config: OpenCodeSdkRuntimeConfig): OpenCodeSdkAdapter;
|
|
18
|
+
export default createOpenCodeSdkAdapter;
|
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
import { createOpencodeClient } from "@opencode-ai/sdk/v2";
|
|
2
|
+
function asObject(value) {
|
|
3
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
4
|
+
return null;
|
|
5
|
+
}
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
function asObjectArray(value) {
|
|
9
|
+
if (!Array.isArray(value)) {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
const out = [];
|
|
13
|
+
for (const item of value) {
|
|
14
|
+
const obj = asObject(item);
|
|
15
|
+
if (obj) {
|
|
16
|
+
out.push(obj);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
function asStringArray(value) {
|
|
22
|
+
if (!Array.isArray(value)) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
const out = [];
|
|
26
|
+
for (const item of value) {
|
|
27
|
+
if (typeof item !== "string") {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const trimmed = item.trim();
|
|
31
|
+
if (trimmed) {
|
|
32
|
+
out.push(trimmed);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
function readString(source, key) {
|
|
38
|
+
const value = source[key];
|
|
39
|
+
return typeof value === "string" ? value.trim() : "";
|
|
40
|
+
}
|
|
41
|
+
function optionIdFromLabel(label, index) {
|
|
42
|
+
const normalized = label
|
|
43
|
+
.toLowerCase()
|
|
44
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
45
|
+
.replace(/^_+|_+$/g, "");
|
|
46
|
+
return normalized || `option_${index + 1}`;
|
|
47
|
+
}
|
|
48
|
+
function toErrorMessage(error) {
|
|
49
|
+
if (error instanceof Error) {
|
|
50
|
+
return error.message;
|
|
51
|
+
}
|
|
52
|
+
return String(error);
|
|
53
|
+
}
|
|
54
|
+
function formatSdkError(error) {
|
|
55
|
+
const obj = asObject(error);
|
|
56
|
+
if (!obj) {
|
|
57
|
+
return toErrorMessage(error);
|
|
58
|
+
}
|
|
59
|
+
const name = readString(obj, "name");
|
|
60
|
+
const message = readString(obj, "message");
|
|
61
|
+
const data = asObject(obj.data);
|
|
62
|
+
const dataMessage = data ? readString(data, "message") : "";
|
|
63
|
+
const text = dataMessage || message || JSON.stringify(obj);
|
|
64
|
+
if (name && text) {
|
|
65
|
+
return `${name}: ${text}`;
|
|
66
|
+
}
|
|
67
|
+
return text || "Unknown SDK error";
|
|
68
|
+
}
|
|
69
|
+
async function requestNoError(label, request) {
|
|
70
|
+
const result = asObject(await request);
|
|
71
|
+
if (!result) {
|
|
72
|
+
throw new Error(`${label} returned invalid response`);
|
|
73
|
+
}
|
|
74
|
+
if (result.error !== undefined) {
|
|
75
|
+
throw new Error(`${label} failed: ${formatSdkError(result.error)}`);
|
|
76
|
+
}
|
|
77
|
+
return result.data;
|
|
78
|
+
}
|
|
79
|
+
function getEventSessionId(eventType, properties) {
|
|
80
|
+
const direct = readString(properties, "sessionID");
|
|
81
|
+
if (direct) {
|
|
82
|
+
return direct;
|
|
83
|
+
}
|
|
84
|
+
const info = asObject(properties.info);
|
|
85
|
+
if (info) {
|
|
86
|
+
const fromInfo = readString(info, "sessionID");
|
|
87
|
+
if (fromInfo) {
|
|
88
|
+
return fromInfo;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const part = asObject(properties.part);
|
|
92
|
+
if (part) {
|
|
93
|
+
const fromPart = readString(part, "sessionID");
|
|
94
|
+
if (fromPart) {
|
|
95
|
+
return fromPart;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (eventType === "message.part.delta") {
|
|
99
|
+
const fromDelta = readString(properties, "sessionID");
|
|
100
|
+
if (fromDelta) {
|
|
101
|
+
return fromDelta;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
function resolveToolEvents(part) {
|
|
107
|
+
const tool = readString(part, "tool") || "tool";
|
|
108
|
+
const state = asObject(part.state);
|
|
109
|
+
if (!state) {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
const status = readString(state, "status");
|
|
113
|
+
if (!status) {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
const input = asObject(state.input) ?? undefined;
|
|
117
|
+
if (status === "pending" || status === "running") {
|
|
118
|
+
return [
|
|
119
|
+
{
|
|
120
|
+
kind: "tool_called",
|
|
121
|
+
tool,
|
|
122
|
+
args: input,
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
if (status === "completed") {
|
|
127
|
+
const output = readString(state, "output") || readString(state, "title") || "Tool completed";
|
|
128
|
+
return [
|
|
129
|
+
{
|
|
130
|
+
kind: "tool_result",
|
|
131
|
+
tool,
|
|
132
|
+
ok: true,
|
|
133
|
+
result: output,
|
|
134
|
+
},
|
|
135
|
+
];
|
|
136
|
+
}
|
|
137
|
+
if (status === "error") {
|
|
138
|
+
const message = readString(state, "error") || "Tool failed";
|
|
139
|
+
return [
|
|
140
|
+
{
|
|
141
|
+
kind: "tool_result",
|
|
142
|
+
tool,
|
|
143
|
+
ok: false,
|
|
144
|
+
result: message,
|
|
145
|
+
},
|
|
146
|
+
];
|
|
147
|
+
}
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
function resolveTextDelta(part, runState) {
|
|
151
|
+
const partId = readString(part, "id");
|
|
152
|
+
const text = readString(part, "text");
|
|
153
|
+
if (!partId || !text) {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
const previous = runState.partTextById.get(partId) ?? "";
|
|
157
|
+
runState.partTextById.set(partId, text);
|
|
158
|
+
const delta = text.startsWith(previous) ? text.slice(previous.length) : text;
|
|
159
|
+
if (!delta) {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
return [
|
|
163
|
+
{
|
|
164
|
+
kind: "assistant_delta",
|
|
165
|
+
delta,
|
|
166
|
+
text,
|
|
167
|
+
},
|
|
168
|
+
];
|
|
169
|
+
}
|
|
170
|
+
function resolvePatchEvents(part) {
|
|
171
|
+
const files = asStringArray(part.files);
|
|
172
|
+
const events = [];
|
|
173
|
+
for (const file of files) {
|
|
174
|
+
events.push({
|
|
175
|
+
kind: "file_changed",
|
|
176
|
+
path: file,
|
|
177
|
+
op: "update",
|
|
178
|
+
summary: "Patch generated by OpenCode",
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return events;
|
|
182
|
+
}
|
|
183
|
+
function resolveQuestionSummary(questions) {
|
|
184
|
+
const lines = [];
|
|
185
|
+
const defaultAnswers = [];
|
|
186
|
+
const firstQuestionOptionById = {};
|
|
187
|
+
let firstQuestionOptions = [];
|
|
188
|
+
let firstQuestionDefaultOption;
|
|
189
|
+
questions.forEach((question, index) => {
|
|
190
|
+
const text = readString(question, "question") || readString(question, "header") || `Question ${index + 1}`;
|
|
191
|
+
lines.push(`${index + 1}. ${text}`);
|
|
192
|
+
const options = asObjectArray(question.options);
|
|
193
|
+
const labels = options
|
|
194
|
+
.map((opt) => readString(opt, "label"))
|
|
195
|
+
.filter((label) => label.length > 0);
|
|
196
|
+
defaultAnswers.push(labels[0] ? [labels[0]] : [""]);
|
|
197
|
+
if (index === 0) {
|
|
198
|
+
firstQuestionOptions = options.map((opt, optIndex) => {
|
|
199
|
+
const title = readString(opt, "label") || `Option ${optIndex + 1}`;
|
|
200
|
+
const id = optionIdFromLabel(title, optIndex);
|
|
201
|
+
firstQuestionOptionById[id] = title;
|
|
202
|
+
const description = readString(opt, "description") || undefined;
|
|
203
|
+
return {
|
|
204
|
+
id,
|
|
205
|
+
title,
|
|
206
|
+
description,
|
|
207
|
+
};
|
|
208
|
+
});
|
|
209
|
+
firstQuestionDefaultOption = firstQuestionOptions[0]?.id;
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
return {
|
|
213
|
+
message: lines.join("\n"),
|
|
214
|
+
options: firstQuestionOptions,
|
|
215
|
+
defaultOption: firstQuestionDefaultOption,
|
|
216
|
+
defaultAnswers,
|
|
217
|
+
optionById: firstQuestionOptionById,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function resolveQuestionAnswers(payload, pending) {
|
|
221
|
+
const fallback = pending.defaultAnswers.map((row) => [...row]);
|
|
222
|
+
if (!payload) {
|
|
223
|
+
return fallback;
|
|
224
|
+
}
|
|
225
|
+
const explicitAnswers = payload.answers;
|
|
226
|
+
if (Array.isArray(explicitAnswers)) {
|
|
227
|
+
const rows = [];
|
|
228
|
+
for (const row of explicitAnswers) {
|
|
229
|
+
if (!Array.isArray(row)) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const values = row
|
|
233
|
+
.map((item) => (typeof item === "string" ? item.trim() : ""))
|
|
234
|
+
.filter((item) => item.length > 0);
|
|
235
|
+
rows.push(values.length > 0 ? values : [""]);
|
|
236
|
+
}
|
|
237
|
+
if (rows.length > 0) {
|
|
238
|
+
return rows;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const answerValue = payload.answer;
|
|
242
|
+
if (typeof answerValue === "string" && answerValue.trim()) {
|
|
243
|
+
fallback[0] = [answerValue.trim()];
|
|
244
|
+
return fallback;
|
|
245
|
+
}
|
|
246
|
+
if (Array.isArray(answerValue)) {
|
|
247
|
+
const first = answerValue
|
|
248
|
+
.map((item) => (typeof item === "string" ? item.trim() : ""))
|
|
249
|
+
.filter((item) => item.length > 0);
|
|
250
|
+
if (first.length > 0) {
|
|
251
|
+
fallback[0] = first;
|
|
252
|
+
return fallback;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const optionId = typeof payload.optionId === "string" ? payload.optionId.trim() : "";
|
|
256
|
+
if (optionId && pending.firstQuestionOptionById[optionId]) {
|
|
257
|
+
fallback[0] = [pending.firstQuestionOptionById[optionId]];
|
|
258
|
+
return fallback;
|
|
259
|
+
}
|
|
260
|
+
return fallback;
|
|
261
|
+
}
|
|
262
|
+
function mapPermissionDecision(decision, payload) {
|
|
263
|
+
if (decision !== "approve") {
|
|
264
|
+
return "reject";
|
|
265
|
+
}
|
|
266
|
+
return payload?.always === true ? "always" : "once";
|
|
267
|
+
}
|
|
268
|
+
function extractNestedErrorMessage(value) {
|
|
269
|
+
const obj = asObject(value);
|
|
270
|
+
if (!obj) {
|
|
271
|
+
return toErrorMessage(value);
|
|
272
|
+
}
|
|
273
|
+
const data = asObject(obj.data);
|
|
274
|
+
const dataMessage = data ? readString(data, "message") : "";
|
|
275
|
+
const message = readString(obj, "message");
|
|
276
|
+
const name = readString(obj, "name");
|
|
277
|
+
const text = dataMessage || message || JSON.stringify(obj);
|
|
278
|
+
return name ? `${name}: ${text}` : text;
|
|
279
|
+
}
|
|
280
|
+
function mapSdkEvent(args) {
|
|
281
|
+
const event = asObject(args.rawEvent);
|
|
282
|
+
if (!event) {
|
|
283
|
+
return [];
|
|
284
|
+
}
|
|
285
|
+
const type = readString(event, "type");
|
|
286
|
+
if (!type) {
|
|
287
|
+
return [];
|
|
288
|
+
}
|
|
289
|
+
const properties = asObject(event.properties) ?? {};
|
|
290
|
+
const eventSessionId = getEventSessionId(type, properties);
|
|
291
|
+
if (eventSessionId && eventSessionId !== args.sessionState.sessionId) {
|
|
292
|
+
return [];
|
|
293
|
+
}
|
|
294
|
+
const out = [];
|
|
295
|
+
if (type === "message.part.delta") {
|
|
296
|
+
if (readString(properties, "field") === "text") {
|
|
297
|
+
const delta = readString(properties, "delta");
|
|
298
|
+
if (delta) {
|
|
299
|
+
out.push({
|
|
300
|
+
kind: "assistant_delta",
|
|
301
|
+
delta,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return out;
|
|
306
|
+
}
|
|
307
|
+
if (type === "message.part.updated") {
|
|
308
|
+
const part = asObject(properties.part);
|
|
309
|
+
if (!part) {
|
|
310
|
+
return out;
|
|
311
|
+
}
|
|
312
|
+
const partType = readString(part, "type");
|
|
313
|
+
if (partType === "text") {
|
|
314
|
+
out.push(...resolveTextDelta(part, args.runState));
|
|
315
|
+
}
|
|
316
|
+
else if (partType === "tool") {
|
|
317
|
+
out.push(...resolveToolEvents(part));
|
|
318
|
+
}
|
|
319
|
+
else if (partType === "patch") {
|
|
320
|
+
out.push(...resolvePatchEvents(part));
|
|
321
|
+
}
|
|
322
|
+
return out;
|
|
323
|
+
}
|
|
324
|
+
if (type === "permission.asked") {
|
|
325
|
+
const interactionId = readString(properties, "id");
|
|
326
|
+
if (!interactionId) {
|
|
327
|
+
return out;
|
|
328
|
+
}
|
|
329
|
+
const permission = readString(properties, "permission") || "permission";
|
|
330
|
+
const patterns = asStringArray(properties.patterns);
|
|
331
|
+
const patternText = patterns.length > 0 ? patterns.join(", ") : "*";
|
|
332
|
+
args.sessionState.pendingInteractions.set(interactionId, {
|
|
333
|
+
kind: "permission",
|
|
334
|
+
requestId: interactionId,
|
|
335
|
+
});
|
|
336
|
+
out.push({
|
|
337
|
+
kind: "interaction_required",
|
|
338
|
+
interactionId,
|
|
339
|
+
interactionType: "permission",
|
|
340
|
+
title: "Permission required",
|
|
341
|
+
message: `${permission} on ${patternText}`,
|
|
342
|
+
options: [
|
|
343
|
+
{ id: "approve_once", title: "Approve once" },
|
|
344
|
+
{ id: "approve_always", title: "Approve always" },
|
|
345
|
+
{ id: "reject", title: "Reject" },
|
|
346
|
+
],
|
|
347
|
+
defaultOption: "approve_once",
|
|
348
|
+
});
|
|
349
|
+
return out;
|
|
350
|
+
}
|
|
351
|
+
if (type === "question.asked") {
|
|
352
|
+
const interactionId = readString(properties, "id");
|
|
353
|
+
if (!interactionId) {
|
|
354
|
+
return out;
|
|
355
|
+
}
|
|
356
|
+
const questions = asObjectArray(properties.questions);
|
|
357
|
+
const summary = resolveQuestionSummary(questions);
|
|
358
|
+
args.sessionState.pendingInteractions.set(interactionId, {
|
|
359
|
+
kind: "question",
|
|
360
|
+
requestId: interactionId,
|
|
361
|
+
defaultAnswers: summary.defaultAnswers,
|
|
362
|
+
firstQuestionOptionById: summary.optionById,
|
|
363
|
+
});
|
|
364
|
+
out.push({
|
|
365
|
+
kind: "interaction_required",
|
|
366
|
+
interactionId,
|
|
367
|
+
interactionType: "question",
|
|
368
|
+
title: "Question from OpenCode",
|
|
369
|
+
message: summary.message || "Answer is required.",
|
|
370
|
+
options: summary.options.length > 0
|
|
371
|
+
? summary.options
|
|
372
|
+
: [
|
|
373
|
+
{
|
|
374
|
+
id: "answer",
|
|
375
|
+
title: "Reply",
|
|
376
|
+
description: "Provide an answer in payload.answer or payload.answers",
|
|
377
|
+
},
|
|
378
|
+
],
|
|
379
|
+
defaultOption: summary.defaultOption,
|
|
380
|
+
});
|
|
381
|
+
return out;
|
|
382
|
+
}
|
|
383
|
+
if (type === "permission.replied") {
|
|
384
|
+
const interactionId = readString(properties, "requestID");
|
|
385
|
+
if (!interactionId) {
|
|
386
|
+
return out;
|
|
387
|
+
}
|
|
388
|
+
const reply = readString(properties, "reply");
|
|
389
|
+
args.sessionState.pendingInteractions.delete(interactionId);
|
|
390
|
+
out.push({
|
|
391
|
+
kind: "interaction_resolved",
|
|
392
|
+
interactionId,
|
|
393
|
+
decision: reply === "reject" ? "reject" : "approve",
|
|
394
|
+
status: "resolved",
|
|
395
|
+
source: "opencode",
|
|
396
|
+
});
|
|
397
|
+
return out;
|
|
398
|
+
}
|
|
399
|
+
if (type === "question.replied" || type === "question.rejected") {
|
|
400
|
+
const interactionId = readString(properties, "requestID");
|
|
401
|
+
if (!interactionId) {
|
|
402
|
+
return out;
|
|
403
|
+
}
|
|
404
|
+
args.sessionState.pendingInteractions.delete(interactionId);
|
|
405
|
+
out.push({
|
|
406
|
+
kind: "interaction_resolved",
|
|
407
|
+
interactionId,
|
|
408
|
+
decision: type === "question.rejected" ? "reject" : "approve",
|
|
409
|
+
status: "resolved",
|
|
410
|
+
source: "opencode",
|
|
411
|
+
});
|
|
412
|
+
return out;
|
|
413
|
+
}
|
|
414
|
+
if (type === "session.status") {
|
|
415
|
+
const status = asObject(properties.status);
|
|
416
|
+
const statusType = status ? readString(status, "type") : "";
|
|
417
|
+
if (!statusType) {
|
|
418
|
+
return out;
|
|
419
|
+
}
|
|
420
|
+
if (statusType === "busy") {
|
|
421
|
+
args.runState.seenBusy = true;
|
|
422
|
+
out.push({
|
|
423
|
+
kind: "progress",
|
|
424
|
+
stage: "session_busy",
|
|
425
|
+
message: "OpenCode is processing the request",
|
|
426
|
+
});
|
|
427
|
+
return out;
|
|
428
|
+
}
|
|
429
|
+
if (statusType === "retry") {
|
|
430
|
+
const attemptValue = status?.attempt;
|
|
431
|
+
const attempt = typeof attemptValue === "number" && Number.isFinite(attemptValue) ? Math.floor(attemptValue) : undefined;
|
|
432
|
+
const message = readString(status ?? {}, "message") || "OpenCode is retrying";
|
|
433
|
+
out.push({
|
|
434
|
+
kind: "progress",
|
|
435
|
+
stage: "session_retry",
|
|
436
|
+
message: attempt ? `${message} (attempt ${attempt})` : message,
|
|
437
|
+
});
|
|
438
|
+
return out;
|
|
439
|
+
}
|
|
440
|
+
if (statusType === "idle" && args.runState.seenBusy) {
|
|
441
|
+
args.runState.completed = true;
|
|
442
|
+
out.push({
|
|
443
|
+
kind: "run_completed",
|
|
444
|
+
status: "succeeded",
|
|
445
|
+
});
|
|
446
|
+
return out;
|
|
447
|
+
}
|
|
448
|
+
return out;
|
|
449
|
+
}
|
|
450
|
+
if (type === "session.idle" && args.runState.seenBusy) {
|
|
451
|
+
args.runState.completed = true;
|
|
452
|
+
out.push({
|
|
453
|
+
kind: "run_completed",
|
|
454
|
+
status: "succeeded",
|
|
455
|
+
});
|
|
456
|
+
return out;
|
|
457
|
+
}
|
|
458
|
+
if (type === "session.error") {
|
|
459
|
+
const errorMessage = extractNestedErrorMessage(properties.error ?? properties);
|
|
460
|
+
args.runState.completed = true;
|
|
461
|
+
args.runState.lastError = errorMessage;
|
|
462
|
+
out.push({
|
|
463
|
+
kind: "assistant_final",
|
|
464
|
+
text: errorMessage,
|
|
465
|
+
summary: "OpenCode run failed",
|
|
466
|
+
status: "failed",
|
|
467
|
+
});
|
|
468
|
+
out.push({
|
|
469
|
+
kind: "run_completed",
|
|
470
|
+
status: "failed",
|
|
471
|
+
error: errorMessage,
|
|
472
|
+
});
|
|
473
|
+
return out;
|
|
474
|
+
}
|
|
475
|
+
if (type === "session.diff") {
|
|
476
|
+
const diffs = asObjectArray(properties.diff);
|
|
477
|
+
for (const diff of diffs) {
|
|
478
|
+
const file = readString(diff, "file");
|
|
479
|
+
if (!file) {
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
const status = readString(diff, "status");
|
|
483
|
+
const op = status === "added" ? "create" : status === "deleted" ? "delete" : "update";
|
|
484
|
+
const additions = typeof diff.additions === "number" ? diff.additions : 0;
|
|
485
|
+
const deletions = typeof diff.deletions === "number" ? diff.deletions : 0;
|
|
486
|
+
out.push({
|
|
487
|
+
kind: "file_changed",
|
|
488
|
+
path: file,
|
|
489
|
+
op,
|
|
490
|
+
summary: `Diff +${additions} -${deletions}`,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
return out;
|
|
494
|
+
}
|
|
495
|
+
if (type === "file.watcher.updated") {
|
|
496
|
+
const file = readString(properties, "file");
|
|
497
|
+
if (!file) {
|
|
498
|
+
return out;
|
|
499
|
+
}
|
|
500
|
+
const watcherEvent = readString(properties, "event");
|
|
501
|
+
const op = watcherEvent === "add" ? "create" : watcherEvent === "unlink" ? "delete" : "update";
|
|
502
|
+
out.push({
|
|
503
|
+
kind: "file_changed",
|
|
504
|
+
path: file,
|
|
505
|
+
op,
|
|
506
|
+
summary: `File watcher event: ${watcherEvent || "change"}`,
|
|
507
|
+
});
|
|
508
|
+
return out;
|
|
509
|
+
}
|
|
510
|
+
if (type === "file.edited") {
|
|
511
|
+
const file = readString(properties, "file");
|
|
512
|
+
if (!file) {
|
|
513
|
+
return out;
|
|
514
|
+
}
|
|
515
|
+
out.push({
|
|
516
|
+
kind: "file_changed",
|
|
517
|
+
path: file,
|
|
518
|
+
op: "update",
|
|
519
|
+
summary: "File edited",
|
|
520
|
+
});
|
|
521
|
+
return out;
|
|
522
|
+
}
|
|
523
|
+
return out;
|
|
524
|
+
}
|
|
525
|
+
class OpenCodeSessionRuntime {
|
|
526
|
+
client;
|
|
527
|
+
config;
|
|
528
|
+
state;
|
|
529
|
+
constructor(client, config, sessionId) {
|
|
530
|
+
this.client = client;
|
|
531
|
+
this.config = config;
|
|
532
|
+
this.state = {
|
|
533
|
+
sessionId,
|
|
534
|
+
pendingInteractions: new Map(),
|
|
535
|
+
activeRunAbort: null,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
async *sendMessage(args) {
|
|
539
|
+
if (this.state.activeRunAbort) {
|
|
540
|
+
throw new Error("A run is already active for this session. Wait for completion before sending another message.");
|
|
541
|
+
}
|
|
542
|
+
const runAbort = new AbortController();
|
|
543
|
+
this.state.activeRunAbort = runAbort;
|
|
544
|
+
const runState = {
|
|
545
|
+
seenBusy: false,
|
|
546
|
+
completed: false,
|
|
547
|
+
partTextById: new Map(),
|
|
548
|
+
};
|
|
549
|
+
try {
|
|
550
|
+
const stream = await this.client.event.subscribe({
|
|
551
|
+
directory: this.config.directory,
|
|
552
|
+
}, {
|
|
553
|
+
signal: runAbort.signal,
|
|
554
|
+
sseMaxRetryAttempts: 0,
|
|
555
|
+
});
|
|
556
|
+
const body = {
|
|
557
|
+
messageID: args.idempotencyKey,
|
|
558
|
+
parts: [
|
|
559
|
+
{
|
|
560
|
+
type: "text",
|
|
561
|
+
text: args.message,
|
|
562
|
+
},
|
|
563
|
+
],
|
|
564
|
+
};
|
|
565
|
+
if (this.config.agent) {
|
|
566
|
+
body.agent = this.config.agent;
|
|
567
|
+
}
|
|
568
|
+
if (this.config.systemPrompt) {
|
|
569
|
+
body.system = this.config.systemPrompt;
|
|
570
|
+
}
|
|
571
|
+
if (this.config.providerId && this.config.modelId) {
|
|
572
|
+
body.model = {
|
|
573
|
+
providerID: this.config.providerId,
|
|
574
|
+
modelID: this.config.modelId,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
await requestNoError("session.promptAsync", this.client.session.promptAsync({
|
|
578
|
+
sessionID: this.state.sessionId,
|
|
579
|
+
directory: this.config.directory,
|
|
580
|
+
...body,
|
|
581
|
+
}));
|
|
582
|
+
yield {
|
|
583
|
+
kind: "progress",
|
|
584
|
+
stage: "submitted",
|
|
585
|
+
message: "Message submitted to OpenCode",
|
|
586
|
+
};
|
|
587
|
+
for await (const rawEvent of stream.stream) {
|
|
588
|
+
const events = mapSdkEvent({
|
|
589
|
+
rawEvent,
|
|
590
|
+
sessionState: this.state,
|
|
591
|
+
runState,
|
|
592
|
+
});
|
|
593
|
+
for (const event of events) {
|
|
594
|
+
yield event;
|
|
595
|
+
}
|
|
596
|
+
if (runState.completed) {
|
|
597
|
+
runAbort.abort();
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
if (!runState.completed) {
|
|
602
|
+
const finalError = runState.lastError || "OpenCode event stream closed before completion";
|
|
603
|
+
yield {
|
|
604
|
+
kind: "run_completed",
|
|
605
|
+
status: "failed",
|
|
606
|
+
error: finalError,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
catch (error) {
|
|
611
|
+
const message = `OpenCode streaming failed: ${toErrorMessage(error)}`;
|
|
612
|
+
this.config.logger?.error?.(message);
|
|
613
|
+
yield {
|
|
614
|
+
kind: "assistant_final",
|
|
615
|
+
text: message,
|
|
616
|
+
summary: "OpenCode runtime error",
|
|
617
|
+
status: "failed",
|
|
618
|
+
};
|
|
619
|
+
yield {
|
|
620
|
+
kind: "run_completed",
|
|
621
|
+
status: "failed",
|
|
622
|
+
error: message,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
finally {
|
|
626
|
+
runAbort.abort();
|
|
627
|
+
if (this.state.activeRunAbort === runAbort) {
|
|
628
|
+
this.state.activeRunAbort = null;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
async *sendAction(args) {
|
|
633
|
+
if (args.action.type === "cancel_run") {
|
|
634
|
+
const reason = args.action.reason?.trim() || "Run cancelled by user";
|
|
635
|
+
if (this.state.activeRunAbort) {
|
|
636
|
+
this.state.activeRunAbort.abort();
|
|
637
|
+
this.state.activeRunAbort = null;
|
|
638
|
+
}
|
|
639
|
+
await requestNoError("session.abort", this.client.session.abort({
|
|
640
|
+
sessionID: this.state.sessionId,
|
|
641
|
+
directory: this.config.directory,
|
|
642
|
+
}));
|
|
643
|
+
yield {
|
|
644
|
+
kind: "progress",
|
|
645
|
+
stage: "cancelled",
|
|
646
|
+
message: reason,
|
|
647
|
+
};
|
|
648
|
+
yield {
|
|
649
|
+
kind: "run_completed",
|
|
650
|
+
status: "failed",
|
|
651
|
+
error: reason,
|
|
652
|
+
};
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
const interactionId = args.action.interactionId;
|
|
656
|
+
const pending = this.state.pendingInteractions.get(interactionId);
|
|
657
|
+
if (!pending) {
|
|
658
|
+
throw new Error(`Unknown interactionId: ${interactionId}`);
|
|
659
|
+
}
|
|
660
|
+
const payload = asObject(args.action.payload);
|
|
661
|
+
if (pending.kind === "permission") {
|
|
662
|
+
const reply = mapPermissionDecision(args.action.decision, payload);
|
|
663
|
+
await requestNoError("permission.reply", this.client.permission.reply({
|
|
664
|
+
requestID: pending.requestId,
|
|
665
|
+
directory: this.config.directory,
|
|
666
|
+
reply,
|
|
667
|
+
message: payload ? readString(payload, "message") || undefined : undefined,
|
|
668
|
+
}));
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
if (args.action.decision === "reject" || args.action.decision === "cancel") {
|
|
672
|
+
await requestNoError("question.reject", this.client.question.reject({
|
|
673
|
+
requestID: pending.requestId,
|
|
674
|
+
directory: this.config.directory,
|
|
675
|
+
}));
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
const answers = resolveQuestionAnswers(payload, pending);
|
|
679
|
+
await requestNoError("question.reply", this.client.question.reply({
|
|
680
|
+
requestID: pending.requestId,
|
|
681
|
+
directory: this.config.directory,
|
|
682
|
+
answers,
|
|
683
|
+
}));
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
this.state.pendingInteractions.delete(interactionId);
|
|
687
|
+
yield {
|
|
688
|
+
kind: "interaction_resolved",
|
|
689
|
+
interactionId,
|
|
690
|
+
decision: args.action.decision,
|
|
691
|
+
status: "resolved",
|
|
692
|
+
source: args.action.source || "openclaw-opencode-bridge",
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
async close() {
|
|
696
|
+
if (this.state.activeRunAbort) {
|
|
697
|
+
this.state.activeRunAbort.abort();
|
|
698
|
+
this.state.activeRunAbort = null;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
async function createSessionId(client, config, sessionKey) {
|
|
703
|
+
const data = await requestNoError("session.create", client.session.create({
|
|
704
|
+
directory: config.directory,
|
|
705
|
+
title: `openclaw:${sessionKey}`,
|
|
706
|
+
}));
|
|
707
|
+
const session = data ? asObject(data) : null;
|
|
708
|
+
const sessionId = session ? readString(session, "id") : "";
|
|
709
|
+
if (!sessionId) {
|
|
710
|
+
throw new Error("session.create failed: missing session id");
|
|
711
|
+
}
|
|
712
|
+
return sessionId;
|
|
713
|
+
}
|
|
714
|
+
export function createOpenCodeSdkAdapter(config) {
|
|
715
|
+
const client = createOpencodeClient({
|
|
716
|
+
baseUrl: config.baseUrl,
|
|
717
|
+
directory: config.directory,
|
|
718
|
+
});
|
|
719
|
+
return {
|
|
720
|
+
async createSession(args) {
|
|
721
|
+
const sessionId = await createSessionId(client, config, args.sessionKey);
|
|
722
|
+
config.logger?.info?.(`[openclaw-opencode-bridge] OpenCode session created: key=${args.sessionKey} id=${sessionId}`);
|
|
723
|
+
return new OpenCodeSessionRuntime(client, config, sessionId);
|
|
724
|
+
},
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
export default createOpenCodeSdkAdapter;
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,18 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "openclaw-opencode-bridge",
|
|
3
3
|
"name": "OpenCode Bridge",
|
|
4
|
-
"description": "Bridge OpenClaw gateway chat.send/chat.action with OpenCode SDK
|
|
4
|
+
"description": "Bridge OpenClaw gateway chat.send/chat.action with built-in OpenCode SDK runtime",
|
|
5
5
|
"configSchema": {
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"properties": {
|
|
9
|
-
"
|
|
9
|
+
"opencodeBaseUrl": {
|
|
10
10
|
"type": "string",
|
|
11
|
-
"description": "
|
|
11
|
+
"description": "OpenCode server base URL used by @opencode-ai/sdk (default: http://127.0.0.1:4096)"
|
|
12
12
|
},
|
|
13
|
-
"
|
|
13
|
+
"opencodeDirectory": {
|
|
14
14
|
"type": "string",
|
|
15
|
-
"description": "
|
|
15
|
+
"description": "Workspace directory passed to OpenCode SDK requests"
|
|
16
|
+
},
|
|
17
|
+
"opencodeAgent": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"description": "Optional OpenCode agent id used by session.promptAsync"
|
|
20
|
+
},
|
|
21
|
+
"opencodeProviderId": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"description": "Optional provider id for model override"
|
|
24
|
+
},
|
|
25
|
+
"opencodeModelId": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"description": "Optional model id for model override (requires opencodeProviderId)"
|
|
28
|
+
},
|
|
29
|
+
"opencodeSystemPrompt": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"description": "Optional system prompt sent with each chat message"
|
|
16
32
|
},
|
|
17
33
|
"sessionTtlMs": {
|
|
18
34
|
"type": "integer",
|
|
@@ -29,13 +45,29 @@
|
|
|
29
45
|
}
|
|
30
46
|
},
|
|
31
47
|
"uiHints": {
|
|
32
|
-
"
|
|
33
|
-
"label": "
|
|
34
|
-
"help": "
|
|
48
|
+
"opencodeBaseUrl": {
|
|
49
|
+
"label": "OpenCode Base URL",
|
|
50
|
+
"help": "Defaults to http://127.0.0.1:4096. You can also use env OPENCODE_BASE_URL."
|
|
51
|
+
},
|
|
52
|
+
"opencodeDirectory": {
|
|
53
|
+
"label": "OpenCode Directory",
|
|
54
|
+
"help": "You can also use env OPENCODE_WORKSPACE_DIR."
|
|
55
|
+
},
|
|
56
|
+
"opencodeAgent": {
|
|
57
|
+
"label": "OpenCode Agent",
|
|
58
|
+
"help": "You can also use env OPENCODE_AGENT."
|
|
59
|
+
},
|
|
60
|
+
"opencodeProviderId": {
|
|
61
|
+
"label": "OpenCode Provider",
|
|
62
|
+
"help": "You can also use env OPENCODE_PROVIDER_ID."
|
|
63
|
+
},
|
|
64
|
+
"opencodeModelId": {
|
|
65
|
+
"label": "OpenCode Model",
|
|
66
|
+
"help": "You can also use env OPENCODE_MODEL_ID."
|
|
35
67
|
},
|
|
36
|
-
"
|
|
37
|
-
"label": "
|
|
38
|
-
"help": "
|
|
68
|
+
"opencodeSystemPrompt": {
|
|
69
|
+
"label": "OpenCode System Prompt",
|
|
70
|
+
"help": "You can also use env OPENCODE_SYSTEM_PROMPT."
|
|
39
71
|
},
|
|
40
72
|
"sessionTtlMs": {
|
|
41
73
|
"label": "Session TTL (ms)"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sw-market/openclaw-opencode-bridge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "OpenClaw plugin bridge for OpenCode realtime streaming and interaction actions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -29,6 +29,9 @@
|
|
|
29
29
|
"./dist/openclaw-extension.js"
|
|
30
30
|
]
|
|
31
31
|
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@opencode-ai/sdk": "^1.2.15"
|
|
34
|
+
},
|
|
32
35
|
"devDependencies": {
|
|
33
36
|
"@types/node": "^22.15.0",
|
|
34
37
|
"rimraf": "^6.0.1",
|