@sw-market/openclaw-opencode-bridge 0.1.3 → 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 CHANGED
@@ -1,44 +1,37 @@
1
1
  # @sw-market/openclaw-opencode-bridge
2
2
 
3
- OpenClaw plugin bridge for OpenCode realtime sessions.
3
+ OpenClaw plugin that bridges gateway methods to a built-in `@opencode-ai/sdk` runtime.
4
4
 
5
- ## Goals
5
+ ## What It Does
6
6
 
7
- - Keep long-lived sessions keyed by `conversationKey`/`sessionKey`.
8
- - Stream OpenCode realtime events back to OpenClaw gateway.
9
- - Support interactive confirmations (`interaction.required` -> `chat.action` -> `interaction.resolved`).
10
- - Emit file change summaries (`file.changed`) for downstream clients (Flutter/Feishu/others).
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 To OpenClaw
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
- After install, verify plugin exists:
24
+ Verify:
19
25
 
20
26
  ```bash
21
- openclaw plugins list
27
+ openclaw plugins info openclaw-opencode-bridge
22
28
  ```
23
29
 
24
- ## Zero-Config Bootstrap (v0.1.3+)
30
+ ## Config
25
31
 
26
- After install, if `sdkAdapterModule` is not configured:
32
+ Plugin no longer requires external adapter modules (`sdkAdapterModule` was removed).
27
33
 
28
- 1. Plugin will auto-discover these common adapter files:
29
- - `<gateway cwd>/opencode_sdk_adapter.mjs`
30
- - `<gateway cwd>/opencode_sdk_adapter.js`
31
- - `~/.openclaw/opencode_sdk_adapter.mjs`
32
- - `~/.openclaw/opencode_sdk_adapter.js`
33
- 2. If none found, plugin falls back to bundled guided adapter.
34
- 3. Bundled guided adapter will auto-generate template file:
35
- - `~/.openclaw/opencode_sdk_adapter.mjs`
36
-
37
- This means plugin install can be simplified: install first, then edit generated template on target machine.
38
-
39
- ## OpenClaw Config
40
-
41
- Optional explicit config in `~/.openclaw/openclaw.json`:
34
+ Configure in `~/.openclaw/openclaw.json`:
42
35
 
43
36
  ```json
44
37
  {
@@ -47,8 +40,12 @@ Optional explicit config in `~/.openclaw/openclaw.json`:
47
40
  "openclaw-opencode-bridge": {
48
41
  "enabled": true,
49
42
  "config": {
50
- "sdkAdapterModule": "/abs/path/to/opencode_sdk_adapter.mjs",
51
- "sdkAdapterExport": "createOpenCodeSdkAdapter",
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": "",
52
49
  "sessionTtlMs": 1800000,
53
50
  "cleanupIntervalMs": 60000,
54
51
  "emitToAllClients": false
@@ -59,63 +56,30 @@ Optional explicit config in `~/.openclaw/openclaw.json`:
59
56
  }
60
57
  ```
61
58
 
62
- You can also set adapter location by environment variable:
59
+ ## Environment Variables
60
+
61
+ Config can also come from env:
63
62
 
64
63
  ```bash
65
- export OPENCODE_SDK_ADAPTER_MODULE=/abs/path/to/opencode_sdk_adapter.mjs
66
- export OPENCODE_SDK_ADAPTER_EXPORT=createOpenCodeSdkAdapter
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=""
67
70
  ```
68
71
 
69
- Adapter module contract:
70
- - export function `createOpenCodeSdkAdapter(ctx)` or export object directly.
71
- - returned object must implement `createSession(args)`.
72
+ ## Adapter Routing (your Python adapter)
72
73
 
73
- Note:
74
- - Plugin installation does not require `sdkAdapterModule` in config.
75
- - If not configured, plugin can auto-discover local adapter file names.
76
- - If still missing, plugin now falls back to guided adapter and generates template at `~/.openclaw/opencode_sdk_adapter.mjs`.
77
-
78
- ## Gateway Methods Provided
79
-
80
- This plugin registers:
81
- - `opencode.chat.send`
82
- - `opencode.chat.action`
83
-
84
- Set adapter side method routing:
74
+ Keep your adapter method mapping:
85
75
 
86
76
  ```env
87
77
  BRIDGE_WS_CHAT_SEND_METHOD=opencode.chat.send
88
78
  BRIDGE_WS_CHAT_ACTION_METHOD=opencode.chat.action
89
79
  ```
90
80
 
91
- ## Library Usage (standalone)
92
-
93
- ```ts
94
- import {
95
- OpenClawOpenCodeBridge,
96
- type OpenCodeSdkAdapter,
97
- } from "@sw-market/openclaw-opencode-bridge";
98
-
99
- const sdk: OpenCodeSdkAdapter = createYourOpenCodeSdkAdapter();
100
- const bridge = new OpenClawOpenCodeBridge(sdk, {
101
- sessionTtlMs: 30 * 60 * 1000,
102
- });
103
-
104
- const started = await bridge.chatSend({
105
- sessionKey: "sw:feishu:chat_001",
106
- message: "list files and update README",
107
- });
108
-
109
- for await (const frame of started.events) {
110
- // forward frame to OpenClaw gateway `event` pipeline
111
- console.log(frame);
112
- }
113
- ```
114
-
115
- ## Contract
116
-
117
- - `chatSend`: start or continue a coding run.
118
- - `chatAction`: handle action replay (interaction reply, cancel run).
119
- - `dispose`: stop cleanup timer and release all in-memory sessions.
81
+ ## Notes
120
82
 
121
- The bridge is SDK-agnostic. You provide an adapter that knows how to invoke OpenCode SDK and return typed async events.
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,12 +1,8 @@
1
- import { existsSync } from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { pathToFileURL } from "node:url";
5
1
  import { OpenClawOpenCodeBridge, } from "./index.js";
2
+ import { createOpenCodeSdkAdapter } from "./sdk-adapter-opencode.js";
6
3
  const CHAT_SEND_METHOD = "opencode.chat.send";
7
4
  const CHAT_ACTION_METHOD = "opencode.chat.action";
8
- const DEFAULT_ADAPTER_EXPORT = "createOpenCodeSdkAdapter";
9
- const DEFAULT_TEMPLATE_FILE = "opencode_sdk_adapter.mjs";
5
+ const DEFAULT_OPENCODE_BASE_URL = "http://127.0.0.1:4096";
10
6
  let bridgePromise = null;
11
7
  let bridgeInstance = null;
12
8
  function asObject(value) {
@@ -51,89 +47,33 @@ function toErrorMessage(error) {
51
47
  }
52
48
  function parseConfig(pluginConfig) {
53
49
  const raw = pluginConfig ?? {};
54
- const sdkAdapterModuleRaw = readString(raw, "sdkAdapterModule") || String(process.env.OPENCODE_SDK_ADAPTER_MODULE || "").trim();
55
- const sdkAdapterModule = sdkAdapterModuleRaw || undefined;
56
- const sdkAdapterExport = readString(raw, "sdkAdapterExport") ||
57
- String(process.env.OPENCODE_SDK_ADAPTER_EXPORT || "").trim() ||
58
- DEFAULT_ADAPTER_EXPORT;
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;
59
62
  const sessionTtlMs = readOptionalPositiveInt(raw, "sessionTtlMs");
60
63
  const cleanupIntervalMs = readOptionalPositiveInt(raw, "cleanupIntervalMs");
61
64
  const emitToAllClients = raw.emitToAllClients === true;
62
65
  return {
63
- sdkAdapterModule,
64
- sdkAdapterExport,
66
+ opencodeBaseUrl,
67
+ opencodeDirectory,
68
+ opencodeAgent,
69
+ opencodeProviderId,
70
+ opencodeModelId,
71
+ opencodeSystemPrompt,
65
72
  sessionTtlMs,
66
73
  cleanupIntervalMs,
67
74
  emitToAllClients,
68
75
  };
69
76
  }
70
- function discoverAdapterPath(api) {
71
- const candidates = [
72
- path.resolve(process.cwd(), DEFAULT_TEMPLATE_FILE),
73
- path.resolve(process.cwd(), "opencode_sdk_adapter.js"),
74
- path.resolve(os.homedir(), ".openclaw", DEFAULT_TEMPLATE_FILE),
75
- path.resolve(os.homedir(), ".openclaw", "opencode_sdk_adapter.js"),
76
- api.resolvePath(`./${DEFAULT_TEMPLATE_FILE}`),
77
- ];
78
- for (const candidate of candidates) {
79
- if (existsSync(candidate)) {
80
- return candidate;
81
- }
82
- }
83
- return undefined;
84
- }
85
- function bundledAdapterSpecifier() {
86
- return new URL("./sdk-adapter-default.js", import.meta.url).href;
87
- }
88
- function resolveModuleSpecifier(api, moduleSpecifier) {
89
- const raw = moduleSpecifier.trim();
90
- if (!raw) {
91
- const discovered = discoverAdapterPath(api);
92
- if (discovered) {
93
- api.logger.info(`[${api.id}] discovered sdkAdapterModule: ${discovered}`);
94
- return pathToFileURL(discovered).href;
95
- }
96
- api.logger.warn(`[${api.id}] sdkAdapterModule is not configured; falling back to bundled guided adapter.`);
97
- return bundledAdapterSpecifier();
98
- }
99
- const looksLikePath = raw.startsWith("./") ||
100
- raw.startsWith("../") ||
101
- raw.startsWith("/") ||
102
- raw.startsWith(".\\") ||
103
- /^[a-zA-Z]:[\\/]/.test(raw);
104
- if (!looksLikePath) {
105
- return raw;
106
- }
107
- const absolutePath = path.isAbsolute(raw) ? raw : api.resolvePath(raw);
108
- if (!existsSync(absolutePath)) {
109
- throw new Error(`sdkAdapterModule path does not exist: ${absolutePath}`);
110
- }
111
- return pathToFileURL(absolutePath).href;
112
- }
113
- function isOpenCodeSdkAdapter(value) {
114
- const obj = asObject(value);
115
- if (!obj) {
116
- return false;
117
- }
118
- return typeof obj.createSession === "function";
119
- }
120
- async function loadSdkAdapter(api, config) {
121
- const moduleSpecifier = resolveModuleSpecifier(api, config.sdkAdapterModule ?? "");
122
- const loaded = await import(moduleSpecifier);
123
- const exported = loaded[config.sdkAdapterExport];
124
- let candidate = exported;
125
- if (typeof exported === "function") {
126
- candidate = await Promise.resolve(exported({
127
- api,
128
- pluginConfig: config,
129
- }));
130
- }
131
- if (!isOpenCodeSdkAdapter(candidate)) {
132
- throw new Error(`Invalid OpenCode SDK adapter from "${config.sdkAdapterModule || moduleSpecifier}" export "${config.sdkAdapterExport}". ` +
133
- "Expected an object with createSession(args) function.");
134
- }
135
- return candidate;
136
- }
137
77
  function respondError(respond, code, message) {
138
78
  respond(false, undefined, {
139
79
  code,
@@ -240,13 +180,21 @@ async function ensureBridge(api) {
240
180
  }
241
181
  if (!bridgePromise) {
242
182
  bridgePromise = (async () => {
243
- const sdk = await loadSdkAdapter(api, config);
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
+ });
244
192
  const bridge = new OpenClawOpenCodeBridge(sdk, {
245
193
  sessionTtlMs: config.sessionTtlMs,
246
194
  cleanupIntervalMs: config.cleanupIntervalMs,
247
195
  });
248
196
  bridgeInstance = bridge;
249
- api.logger.info(`[${CHAT_SEND_METHOD}] bridge initialized`);
197
+ api.logger.info(`[${CHAT_SEND_METHOD}] bridge initialized (baseUrl=${config.opencodeBaseUrl}, directory=${config.opencodeDirectory ?? "<default>"})`);
250
198
  return bridge;
251
199
  })();
252
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;
@@ -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 streams",
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
- "sdkAdapterModule": {
9
+ "opencodeBaseUrl": {
10
10
  "type": "string",
11
- "description": "Node module specifier or path to SDK adapter module (optional; plugin auto-discovers common paths and has a bundled guided fallback)"
11
+ "description": "OpenCode server base URL used by @opencode-ai/sdk (default: http://127.0.0.1:4096)"
12
12
  },
13
- "sdkAdapterExport": {
13
+ "opencodeDirectory": {
14
14
  "type": "string",
15
- "description": "Export name from sdkAdapterModule (default: createOpenCodeSdkAdapter)"
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
- "sdkAdapterModule": {
33
- "label": "SDK Adapter Module",
34
- "help": "Absolute/relative path or npm package that exports OpenCode SDK adapter. If omitted, plugin will auto-discover and then fallback to bundled guided adapter."
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
- "sdkAdapterExport": {
37
- "label": "SDK Adapter Export",
38
- "help": "Export name in the adapter module. Default is createOpenCodeSdkAdapter."
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",
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",
@@ -1,13 +0,0 @@
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
- };
7
- type CreateAdapterContext = {
8
- api?: {
9
- logger?: Logger;
10
- };
11
- };
12
- export declare function createOpenCodeSdkAdapter(ctx?: CreateAdapterContext): OpenCodeSdkAdapter;
13
- export default createOpenCodeSdkAdapter;
@@ -1,133 +0,0 @@
1
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
4
- const DEFAULT_TEMPLATE_PATH = path.join(os.homedir(), ".openclaw", "opencode_sdk_adapter.mjs");
5
- const ADAPTER_TEMPLATE = `// Auto-generated by @sw-market/openclaw-opencode-bridge.
6
- // Fill this file with your real OpenCode SDK wiring.
7
- // Then set in OpenClaw:
8
- // plugins.entries["openclaw-opencode-bridge"].config.sdkAdapterModule = "<this file path>"
9
- // or set env:
10
- // OPENCODE_SDK_ADAPTER_MODULE=<this file path>
11
-
12
- export function createOpenCodeSdkAdapter() {
13
- return {
14
- async createSession({ sessionKey }) {
15
- return {
16
- async *sendMessage({ runId, message, idempotencyKey }) {
17
- // TODO: Replace this with real OpenCode SDK call + stream mapping.
18
- yield {
19
- kind: "assistant_final",
20
- text:
21
- "Template adapter loaded, but real SDK is not connected yet. " +
22
- "Please implement sendMessage/sendAction in opencode_sdk_adapter.mjs.",
23
- summary: "Template adapter loaded",
24
- status: "failed",
25
- };
26
- yield {
27
- kind: "run_completed",
28
- status: "failed",
29
- error: "Template adapter not implemented",
30
- };
31
- },
32
- async *sendAction({ runId, action, idempotencyKey }) {
33
- // TODO: Replace this with real action callback into OpenCode SDK.
34
- if (action.type === "interaction.reply") {
35
- yield {
36
- kind: "interaction_resolved",
37
- interactionId: action.interactionId,
38
- decision: action.decision,
39
- status: "resolved",
40
- source: "template_adapter",
41
- };
42
- }
43
- yield {
44
- kind: "run_completed",
45
- status: "failed",
46
- error: "Template adapter not implemented",
47
- };
48
- },
49
- };
50
- },
51
- };
52
- }
53
- `;
54
- function toErrorMessage(error) {
55
- if (error instanceof Error) {
56
- return error.message;
57
- }
58
- return String(error);
59
- }
60
- function ensureTemplateFile(logger) {
61
- try {
62
- if (!existsSync(DEFAULT_TEMPLATE_PATH)) {
63
- mkdirSync(path.dirname(DEFAULT_TEMPLATE_PATH), { recursive: true });
64
- writeFileSync(DEFAULT_TEMPLATE_PATH, ADAPTER_TEMPLATE, "utf-8");
65
- logger?.info?.(`[openclaw-opencode-bridge] generated SDK adapter template at ${DEFAULT_TEMPLATE_PATH}`);
66
- }
67
- return DEFAULT_TEMPLATE_PATH;
68
- }
69
- catch (error) {
70
- logger?.warn?.(`[openclaw-opencode-bridge] failed to write adapter template: ${toErrorMessage(error)}`);
71
- return undefined;
72
- }
73
- }
74
- function buildGuidanceText(templatePath) {
75
- const lines = [
76
- "OpenCode SDK adapter is not configured yet.",
77
- "Set plugins.entries.openclaw-opencode-bridge.config.sdkAdapterModule",
78
- "or set env OPENCODE_SDK_ADAPTER_MODULE, then restart openclaw-gateway.",
79
- ];
80
- if (templatePath) {
81
- lines.push(`Template generated at: ${templatePath}`);
82
- }
83
- return lines.join("\n");
84
- }
85
- function createGuidanceSession(guidanceText) {
86
- return {
87
- async *sendMessage() {
88
- yield {
89
- kind: "progress",
90
- stage: "configuration_required",
91
- message: "OpenCode SDK adapter module is required.",
92
- };
93
- yield {
94
- kind: "assistant_final",
95
- text: guidanceText,
96
- summary: "SDK adapter not configured",
97
- status: "failed",
98
- };
99
- yield {
100
- kind: "run_completed",
101
- status: "failed",
102
- error: "OpenCode SDK adapter is not configured",
103
- };
104
- },
105
- async *sendAction(args) {
106
- if (args.action.type === "interaction.reply") {
107
- yield {
108
- kind: "interaction_resolved",
109
- interactionId: args.action.interactionId,
110
- decision: args.action.decision,
111
- status: "resolved",
112
- source: "openclaw-opencode-bridge",
113
- };
114
- }
115
- yield {
116
- kind: "run_completed",
117
- status: "failed",
118
- error: "OpenCode SDK adapter is not configured",
119
- };
120
- },
121
- };
122
- }
123
- export function createOpenCodeSdkAdapter(ctx) {
124
- const logger = ctx?.api?.logger;
125
- const templatePath = ensureTemplateFile(logger);
126
- const guidanceText = buildGuidanceText(templatePath);
127
- return {
128
- async createSession() {
129
- return createGuidanceSession(guidanceText);
130
- },
131
- };
132
- }
133
- export default createOpenCodeSdkAdapter;