@polderlabs/bizar 4.3.0 → 4.4.0

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.
Files changed (57) hide show
  1. package/bizar-dash/src/server/opencode-sdk.mjs +72 -0
  2. package/bizar-dash/src/server/routes/background.mjs +92 -0
  3. package/bizar-dash/src/server/routes/chat.mjs +300 -123
  4. package/bizar-dash/src/server/routes/opencode-session-detail.mjs +26 -0
  5. package/bizar-dash/src/server/routes/tasks.mjs +59 -1
  6. package/bizar-dash/src/server/task-delegator.mjs +154 -8
  7. package/bizar-dash/src/server/tasks-store.mjs +50 -2
  8. package/bizar-dash/src/web/components/background/AttachButton.tsx +96 -0
  9. package/bizar-dash/src/web/components/background/TmuxAttachCard.tsx +122 -0
  10. package/bizar-dash/src/web/components/chat/AgentNode.tsx +127 -0
  11. package/bizar-dash/src/web/components/chat/AgentTree.tsx +80 -0
  12. package/bizar-dash/src/web/components/chat/ChatComposer.tsx +76 -0
  13. package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +144 -0
  14. package/bizar-dash/src/web/components/chat/ChatRail.tsx +387 -0
  15. package/bizar-dash/src/web/components/chat/ChatThread.tsx +28 -7
  16. package/bizar-dash/src/web/components/chat/JumpToLatest.tsx +58 -0
  17. package/bizar-dash/src/web/components/chat/MessageBlock.tsx +260 -0
  18. package/bizar-dash/src/web/components/chat/SessionRowMenu.tsx +206 -0
  19. package/bizar-dash/src/web/components/chat/StreamingIndicator.tsx +33 -5
  20. package/bizar-dash/src/web/components/chat/_legacy.ts +30 -0
  21. package/bizar-dash/src/web/components/chat/index.ts +11 -0
  22. package/bizar-dash/src/web/components/chat/useChat.ts +345 -167
  23. package/bizar-dash/src/web/components/tasks/BacklogPanel.css +109 -0
  24. package/bizar-dash/src/web/components/tasks/BacklogPanel.tsx +209 -0
  25. package/bizar-dash/src/web/styles/chat.css +1536 -133
  26. package/bizar-dash/src/web/views/BackgroundAgents.tsx +3 -0
  27. package/bizar-dash/src/web/views/Chat.tsx +147 -57
  28. package/bizar-dash/src/web/views/Tasks.tsx +23 -1
  29. package/cli/bg.mjs +94 -71
  30. package/cli/bin.mjs +19 -7
  31. package/cli/service-controller.mjs +587 -0
  32. package/cli/service-controller.test.mjs +92 -0
  33. package/cli/service.mjs +162 -14
  34. package/config/agents/baldr.md +2 -0
  35. package/config/agents/browser-harness.md +2 -0
  36. package/config/agents/forseti.md +2 -0
  37. package/config/agents/frigg.md +2 -0
  38. package/config/agents/heimdall.md +2 -0
  39. package/config/agents/hermod.md +2 -0
  40. package/config/agents/mimir.md +2 -0
  41. package/config/agents/odin.md +2 -0
  42. package/config/agents/quick.md +2 -0
  43. package/config/agents/semble-search.md +2 -0
  44. package/config/agents/thor.md +2 -0
  45. package/config/agents/tyr.md +2 -0
  46. package/config/agents/vidarr.md +2 -0
  47. package/config/agents/vor.md +2 -0
  48. package/config/opencode.json.template +1 -0
  49. package/install.sh +448 -787
  50. package/package.json +1 -1
  51. package/packages/sdk/package.json +20 -0
  52. package/packages/sdk/src/client.ts +5 -0
  53. package/packages/sdk/src/errors.ts +11 -2
  54. package/packages/sdk/src/index.ts +19 -0
  55. package/packages/sdk/src/opencode-events.ts +134 -0
  56. package/packages/sdk/src/opencode-types.ts +66 -0
  57. package/packages/sdk/src/opencode.ts +335 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polderlabs/bizar",
3
- "version": "4.3.0",
3
+ "version": "4.4.0",
4
4
  "description": "Norse-pantheon multi-agent system for opencode — 13 agents across 4 cost tiers with cost-aware routing, plans, and a configurable agent harness. v4 ships as a single npm package bundling the dashboard server, opencode plugin, and typed SDK.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@polderlabs/bizar-sdk",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": "./dist/index.js",
9
+ "./opencode": "./dist/opencode.js"
10
+ },
11
+ "files": ["dist"],
12
+ "peerDependencies": {
13
+ "@opencode-ai/sdk": "*"
14
+ },
15
+ "peerDependenciesMeta": {
16
+ "@opencode-ai/sdk": {
17
+ "optional": true
18
+ }
19
+ }
20
+ }
@@ -67,6 +67,8 @@ export interface BizarClient {
67
67
  plans: PlansResource;
68
68
  events: EventsResource;
69
69
  health: HealthResource;
70
+ /** Change the base URL after the client was created. */
71
+ setBaseUrl(url: string): void;
70
72
  }
71
73
 
72
74
  export function createBizarClient(config: BizarClientConfig): BizarClient {
@@ -168,6 +170,9 @@ export function createBizarClient(config: BizarClientConfig): BizarClient {
168
170
  }
169
171
  },
170
172
  },
173
+ setBaseUrl(url: string) {
174
+ config.baseUrl = url;
175
+ },
171
176
  };
172
177
  }
173
178
 
@@ -51,7 +51,15 @@ export type APIError = {
51
51
  };
52
52
  };
53
53
 
54
- export type BizarError = PluginError | DashboardError | ConnectionError | APIError;
54
+ export type OpencodeConnectionError = {
55
+ name: "OpencodeConnectionError";
56
+ data: {
57
+ message: string;
58
+ cause?: unknown;
59
+ };
60
+ };
61
+
62
+ export type BizarError = PluginError | DashboardError | ConnectionError | APIError | OpencodeConnectionError;
55
63
 
56
64
  /**
57
65
  * Type guard: narrows `unknown` to `BizarError`.
@@ -64,7 +72,8 @@ export function isBizarError(value: unknown): value is BizarError {
64
72
  v.name === "PluginError" ||
65
73
  v.name === "DashboardError" ||
66
74
  v.name === "ConnectionError" ||
67
- v.name === "APIError"
75
+ v.name === "APIError" ||
76
+ v.name === "OpencodeConnectionError"
68
77
  );
69
78
  }
70
79
 
@@ -27,6 +27,7 @@ export type {
27
27
  DashboardError,
28
28
  ConnectionError,
29
29
  APIError,
30
+ OpencodeConnectionError,
30
31
  } from "./errors.js";
31
32
 
32
33
  export type {
@@ -61,3 +62,21 @@ export type {
61
62
  } from "./types.js";
62
63
 
63
64
  export { SDK_VERSION } from "./version.js";
65
+
66
+ // Opencode SDK — wraps the opencode serve child with a typed interface.
67
+ // Tries `@opencode-ai/sdk` first; falls back to a thin fetch wrapper.
68
+ export { createOpencodeSdk } from "./opencode.js";
69
+ export type { OpencodeSdk, OpencodeSdkConfig } from "./opencode.js";
70
+
71
+ export { subscribeOpencodeEvents } from "./opencode-events.js";
72
+ export type {
73
+ OpencodeEventEnvelope,
74
+ OpencodeEventSubscribeOptions,
75
+ } from "./opencode-events.js";
76
+
77
+ export type {
78
+ OpencodeSession,
79
+ OpencodeMessage,
80
+ OpencodePart,
81
+ OpencodeEvent,
82
+ } from "./opencode-types.js";
@@ -0,0 +1,134 @@
1
+ /**
2
+ * SSE event iteration helper for the opencode serve child.
3
+ *
4
+ * Subscribes to the opencode SSE stream at `/event?directory=…`, filters
5
+ * by `sessionID` when provided, and yields parsed typed events.
6
+ *
7
+ * This module is consumed by the SDK's `events.subscribe()` and by the
8
+ * dashboard's chat SSE bridge.
9
+ */
10
+
11
+ import { parseSseStream } from "./events.js";
12
+ import type { EventSubscription } from "./events.js";
13
+
14
+ /**
15
+ * Options for subscribing to the opencode event stream.
16
+ */
17
+ export interface OpencodeEventSubscribeOptions {
18
+ /** AbortSignal to cancel the subscription early. */
19
+ signal?: AbortSignal;
20
+ /**
21
+ * When provided, only events whose `sessionID` matches are yielded.
22
+ * When omitted, all events are yielded.
23
+ */
24
+ sessionID?: string;
25
+ /**
26
+ * Optional baseUrl override (defaults to the SDK's configured baseUrl).
27
+ */
28
+ baseUrl?: string;
29
+ /**
30
+ * Optional auth header override.
31
+ */
32
+ authHeader?: string;
33
+ /**
34
+ * Injectable fetch. Defaults to global `fetch`.
35
+ */
36
+ fetch?: typeof fetch;
37
+ }
38
+
39
+ /**
40
+ * A single parsed opencode SSE event envelope.
41
+ */
42
+ export interface OpencodeEventEnvelope {
43
+ type: string;
44
+ sessionID?: string;
45
+ messageID?: string;
46
+ part?: object;
47
+ data?: object;
48
+ }
49
+
50
+ /**
51
+ * Subscribe to opencode's SSE `/event` endpoint.
52
+ *
53
+ * @param baseUrl e.g. "http://127.0.0.1:4097"
54
+ * @param authHeader `Authorization: Basic …` value
55
+ * @param opts
56
+ */
57
+ export async function subscribeOpencodeEvents(
58
+ baseUrl: string,
59
+ authHeader: string,
60
+ opts: OpencodeEventSubscribeOptions = {},
61
+ ): Promise<EventSubscription> {
62
+ const fetchImpl = opts.fetch ?? fetch;
63
+ const controller = new AbortController();
64
+ if (opts.signal) {
65
+ opts.signal.addEventListener("abort", () => controller.abort(), { once: true });
66
+ }
67
+
68
+ const url = new URL("/event", baseUrl);
69
+
70
+ const response = await fetchImpl(url.toString(), {
71
+ method: "GET",
72
+ headers: {
73
+ Authorization: authHeader,
74
+ Accept: "text/event-stream",
75
+ },
76
+ signal: controller.signal,
77
+ });
78
+
79
+ if (!response.ok || !response.body) {
80
+ throw new Error(`opencode event subscribe failed: ${response.status} ${response.statusText}`);
81
+ }
82
+
83
+ // Cast through unknown so the AsyncIterable<OpencodeEventEnvelope> satisfies
84
+ // the AsyncIterable<DashboardEvent> constraint from the existing EventSubscription type.
85
+ // The actual events flowing through are OpencodeEventEnvelope-shaped.
86
+ const filteredStream = filterOpencodeSseStream(response.body, controller, opts.sessionID);
87
+ return {
88
+ stream: filteredStream as unknown as EventSubscription["stream"],
89
+ close: () => controller.abort(),
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Wrap `parseSseStream` with optional sessionID filtering.
95
+ */
96
+ async function* filterOpencodeSseStream(
97
+ body: ReadableStream<Uint8Array>,
98
+ controller: AbortController,
99
+ sessionID?: string,
100
+ ): AsyncIterable<OpencodeEventEnvelope> {
101
+ for await (const raw of parseSseStream<OpencodeEventEnvelope>(body, controller)) {
102
+ if (!raw || typeof raw !== "object") continue;
103
+ const evt = raw as unknown as Record<string, unknown>;
104
+ // Unwrap sync envelope if present (mirrors serve-info.mjs:unwrapOpencodeSseEvent)
105
+ let unwrapped = evt;
106
+ if (evt.type === "sync" && evt.syncEvent && typeof evt.syncEvent === "object") {
107
+ const se = evt.syncEvent as Record<string, unknown>;
108
+ if (typeof se.type === "string") {
109
+ if (se.data && typeof se.data === "object") {
110
+ unwrapped = se.data as Record<string, unknown>;
111
+ }
112
+ }
113
+ }
114
+ const type = typeof unwrapped.type === "string" ? unwrapped.type : null;
115
+ if (!type) continue;
116
+ const evtSessionID = pickString(unwrapped, ["sessionID", "sessionId", "session_id"]);
117
+ if (sessionID && evtSessionID !== sessionID) continue;
118
+ yield {
119
+ type,
120
+ sessionID: evtSessionID,
121
+ messageID: pickString(unwrapped, ["messageID", "messageId", "message_id"]),
122
+ part: unwrapped.part && typeof unwrapped.part === "object" ? (unwrapped.part as object) : undefined,
123
+ data: unwrapped,
124
+ };
125
+ }
126
+ }
127
+
128
+ function pickString(obj: Record<string, unknown>, keys: string[]): string | undefined {
129
+ for (const k of keys) {
130
+ const v = obj[k];
131
+ if (typeof v === "string" && v.length > 0) return v;
132
+ }
133
+ return undefined;
134
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Typed wrappers for opencode SDK entities.
3
+ *
4
+ * We declare shapes structurally so that:
5
+ * 1. The package compiles even when `@opencode-ai/sdk` is not installed.
6
+ * 2. The shapes are stable regardless of which version of the upstream
7
+ * SDK is installed (the upstream types are very complex and couple us
8
+ * to internal class structure we don't need).
9
+ */
10
+
11
+ export interface OpencodeSession {
12
+ id: string;
13
+ slug?: string;
14
+ projectID?: string;
15
+ workspaceID?: string;
16
+ directory?: string;
17
+ path?: string;
18
+ parentID?: string;
19
+ title?: string;
20
+ agent?: string;
21
+ model?: {
22
+ id: string;
23
+ providerID: string;
24
+ variant?: string;
25
+ };
26
+ time?: {
27
+ created: number;
28
+ updated: number;
29
+ compacting?: number;
30
+ archived?: number;
31
+ };
32
+ }
33
+
34
+ export interface OpencodeMessage {
35
+ info?: {
36
+ id?: string;
37
+ role?: string;
38
+ time?: {
39
+ created?: number;
40
+ };
41
+ };
42
+ id?: string;
43
+ role?: string;
44
+ parts?: OpencodePart[];
45
+ text?: string;
46
+ content?: string;
47
+ }
48
+
49
+ export type OpencodePart =
50
+ | { type: "text"; text: string }
51
+ | { type: "reasoning"; text: string }
52
+ | { type: "tool_call"; name: string; input: unknown }
53
+ | { type: "tool_result"; toolCallId: string; result: unknown }
54
+ | { type: "agent"; text: string }
55
+ | { type: "subtask"; text: string }
56
+ | { type: "file"; path: string; content?: string }
57
+ | { type: "step_start" | "step_finish" | "snapshot" | "patch" | "retry" | "compaction"; [key: string]: unknown };
58
+
59
+ export interface OpencodeEvent {
60
+ type: string;
61
+ sessionID?: string;
62
+ messageID?: string;
63
+ part?: OpencodePart;
64
+ data?: Record<string, unknown>;
65
+ properties?: Record<string, unknown>;
66
+ }
@@ -0,0 +1,335 @@
1
+ /**
2
+ * Opencode SDK factory.
3
+ *
4
+ * Tries to load `@opencode-ai/sdk` dynamically. If the package is
5
+ * installed, `createOpencodeClient` from that package is used and its
6
+ * result is re-wrapped. If the import fails (package not installed), a
7
+ * thin fetch-based fallback is returned that hits the same REST endpoints.
8
+ *
9
+ * Auth: `Basic base64("opencode:<password>")` — matches the convention
10
+ * used by the opencode serve child.
11
+ *
12
+ * Reference: https://opencode.ai/docs/sdk/
13
+ */
14
+
15
+ import { parseSseStream } from "./events.js";
16
+ import { connectionErrorFrom, type BizarError } from "./errors.js";
17
+ import type { EventSubscription } from "./events.js";
18
+ import type { OpencodeEventEnvelope } from "./opencode-events.js";
19
+
20
+ export interface OpencodeSdkConfig {
21
+ baseUrl: string;
22
+ /** Password for the opencode serve child. */
23
+ password?: string;
24
+ /** Injectable fetch for testing. */
25
+ fetch?: typeof fetch;
26
+ /** When true, errors are thrown; default false (return BizarError). */
27
+ throwOnError?: boolean;
28
+ }
29
+
30
+ /**
31
+ * Thin fetch-based opencode client, used when `@opencode-ai/sdk` is not
32
+ * installed in the consumer's project.
33
+ */
34
+ interface FallbackOpencodeClient {
35
+ sessions: {
36
+ list(): Promise<unknown[]>;
37
+ get(input: { sessionId: string }): Promise<unknown>;
38
+ create(input: { title: string; agent: string }): Promise<{ id: string }>;
39
+ abort(input: { sessionId: string }): Promise<null>;
40
+ messages(input: { sessionId: string }): Promise<unknown[]>;
41
+ prompt(input: {
42
+ sessionId: string;
43
+ body: { text: string };
44
+ messageId?: string;
45
+ }): Promise<{ messageId: string }>;
46
+ promptAsync(input: { sessionId: string; body: { text: string } }): Promise<null>;
47
+ };
48
+ events: {
49
+ subscribe(opts?: {
50
+ signal?: AbortSignal;
51
+ sessionID?: string;
52
+ since?: number;
53
+ }): Promise<EventSubscription & { sessionID?: string }>;
54
+ };
55
+ health: {
56
+ check(): Promise<{ ok: boolean } | BizarError>;
57
+ };
58
+ server: {
59
+ close?(): void;
60
+ };
61
+ }
62
+
63
+ /**
64
+ * The SDK instance returned by `createOpencodeSdk`.
65
+ */
66
+ export interface OpencodeSdk {
67
+ sessions: FallbackOpencodeClient["sessions"];
68
+ events: FallbackOpencodeClient["events"];
69
+ health: FallbackOpencodeClient["health"];
70
+ server: FallbackOpencodeClient["server"];
71
+ }
72
+
73
+ function makeAuthHeader(password: string): string {
74
+ const encoded = Buffer.from(`opencode:${password}`, "utf-8").toString("base64");
75
+ return `Basic ${encoded}`;
76
+ }
77
+
78
+ async function fetchJson<T>(
79
+ url: string,
80
+ init: RequestInit,
81
+ fetchImpl: typeof fetch,
82
+ throwOnError: boolean,
83
+ ): Promise<T | BizarError> {
84
+ let response: Response;
85
+ try {
86
+ response = await fetchImpl(url, init);
87
+ } catch (err) {
88
+ const e = connectionErrorFrom(err);
89
+ if (throwOnError) throw e;
90
+ return e;
91
+ }
92
+ if (response.status === 204) return null as T;
93
+ const text = await response.text();
94
+ if (!response.ok) {
95
+ const err: BizarError = {
96
+ name: "APIError",
97
+ data: {
98
+ statusCode: response.status,
99
+ isRetryable: response.status >= 500 || response.status === 429,
100
+ message: `opencode API returned ${response.status}`,
101
+ responseBody: text,
102
+ },
103
+ };
104
+ if (throwOnError) throw err;
105
+ return err;
106
+ }
107
+ try {
108
+ return JSON.parse(text) as T;
109
+ } catch {
110
+ return null as T;
111
+ }
112
+ }
113
+
114
+ function createFallbackClient(config: OpencodeSdkConfig): FallbackOpencodeClient {
115
+ const { baseUrl, password = "", fetch: fetchImpl = fetch, throwOnError = false } = config;
116
+ const auth = makeAuthHeader(password);
117
+
118
+ async function request<T>(
119
+ method: string,
120
+ path: string,
121
+ body?: unknown,
122
+ ): Promise<T | BizarError> {
123
+ const url = `${baseUrl}${path}`;
124
+ const headers: Record<string, string> = {
125
+ Authorization: auth,
126
+ "Content-Type": "application/json",
127
+ Accept: "application/json",
128
+ };
129
+ return fetchJson<T>(
130
+ url,
131
+ {
132
+ method,
133
+ headers,
134
+ body: body != null ? JSON.stringify(body) : undefined,
135
+ },
136
+ fetchImpl,
137
+ throwOnError,
138
+ );
139
+ }
140
+
141
+ return {
142
+ sessions: {
143
+ list: () => request<unknown[]>("GET", "/api/session") as Promise<unknown[]>,
144
+ get: ({ sessionId }) =>
145
+ request<unknown>("GET", `/api/session/${encodeURIComponent(sessionId)}`),
146
+ create: (input) =>
147
+ request<{ id: string }>("POST", "/api/session", input) as Promise<{ id: string }>,
148
+ abort: ({ sessionId }) =>
149
+ request<null>(
150
+ "POST",
151
+ `/api/session/${encodeURIComponent(sessionId)}/abort`,
152
+ ) as Promise<null>,
153
+ messages: ({ sessionId }) =>
154
+ request<unknown[]>(
155
+ "GET",
156
+ `/api/session/${encodeURIComponent(sessionId)}/message`,
157
+ ) as Promise<unknown[]>,
158
+ prompt: ({ sessionId, body, messageId }) =>
159
+ request<{ messageId: string }>(
160
+ "POST",
161
+ `/api/session/${encodeURIComponent(sessionId)}/prompt`,
162
+ { id: messageId, prompt: body },
163
+ ) as Promise<{ messageId: string }>,
164
+ promptAsync: ({ sessionId, body }) =>
165
+ request<null>(
166
+ "POST",
167
+ `/api/session/${encodeURIComponent(sessionId)}/prompt_async`,
168
+ body,
169
+ ) as Promise<null>,
170
+ },
171
+ events: {
172
+ subscribe: async (opts = {}) => {
173
+ const { signal, sessionID, since } = opts;
174
+ const url = new URL("/event", baseUrl);
175
+ if (typeof since === "number") {
176
+ url.searchParams.set("since", String(since));
177
+ }
178
+ const controller = new AbortController();
179
+ if (signal) {
180
+ signal.addEventListener("abort", () => controller.abort(), { once: true });
181
+ }
182
+ const response = await fetchImpl(url.toString(), {
183
+ method: "GET",
184
+ headers: {
185
+ Authorization: auth,
186
+ Accept: "text/event-stream",
187
+ },
188
+ signal: controller.signal,
189
+ });
190
+ if (!response.ok || !response.body) {
191
+ throw new Error(`event subscribe failed: ${response.status}`);
192
+ }
193
+ const sub: EventSubscription & { sessionID?: string } = {
194
+ sessionID,
195
+ // Cast through unknown to satisfy the AsyncIterable<DashboardEvent> constraint
196
+ // from the existing events.ts EventSubscription type. The actual yielded
197
+ // values are OpencodeEventEnvelope objects which are compatible.
198
+ stream: parseSseStream<OpencodeEventEnvelope>(
199
+ response.body,
200
+ controller,
201
+ ) as unknown as EventSubscription["stream"],
202
+ close: () => controller.abort(),
203
+ };
204
+ return sub;
205
+ },
206
+ },
207
+ health: {
208
+ check: async () => {
209
+ const url = `${baseUrl}/health`;
210
+ try {
211
+ const r = await fetchImpl(url, { method: "GET" });
212
+ if (!r.ok) {
213
+ const text = await r.text().catch(() => "");
214
+ const err: BizarError = {
215
+ name: "APIError",
216
+ data: {
217
+ statusCode: r.status,
218
+ isRetryable: r.status >= 500 || r.status === 429,
219
+ message: `opencode health check returned ${r.status}`,
220
+ responseBody: text,
221
+ },
222
+ };
223
+ if (throwOnError) throw err;
224
+ return err;
225
+ }
226
+ return { ok: true } as const;
227
+ } catch (err) {
228
+ const e = connectionErrorFrom(err);
229
+ if (throwOnError) throw e;
230
+ return e;
231
+ }
232
+ },
233
+ },
234
+ server: {},
235
+ };
236
+ }
237
+
238
+ /**
239
+ * Create an opencode SDK instance.
240
+ *
241
+ * Tries to use `@opencode-ai/sdk`'s `createOpencodeClient` if installed;
242
+ * falls back to a thin fetch-based client otherwise.
243
+ *
244
+ * @param opts `{ baseUrl, password?, fetch?, throwOnError? }`
245
+ */
246
+ export async function createOpencodeSdk(
247
+ opts: OpencodeSdkConfig,
248
+ ): Promise<OpencodeSdk> {
249
+ try {
250
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
251
+ const mod = await import("@opencode-ai/sdk/v2/client") as any;
252
+ const createClient = mod.createOpencodeClient;
253
+ if (typeof createClient !== "function") throw new Error("not a function");
254
+
255
+ const headers: Record<string, string> = {};
256
+ if (opts.password) {
257
+ headers["Authorization"] = makeAuthHeader(opts.password);
258
+ }
259
+
260
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
261
+ const inner = createClient({
262
+ baseUrl: opts.baseUrl,
263
+ fetch: opts.fetch as any,
264
+ headers,
265
+ // The upstream SDK accepts different options; ignore unknown ones.
266
+ } as any) as any;
267
+
268
+ // The @opencode-ai/sdk client has a very different internal structure
269
+ // (HeyApi pattern with .session.list(), .session.create(), etc.).
270
+ // We wrap it to match the FallbackOpencodeClient interface expected
271
+ // by the dashboard.
272
+ const wrapped: OpencodeSdk = {
273
+ sessions: {
274
+ list: async () => {
275
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
276
+ const r = await (inner.session?.list?.() ?? inner.sessions?.list?.()) as any;
277
+ return r ?? [];
278
+ },
279
+ get: async ({ sessionId }) => {
280
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
281
+ return (await (inner.session?.get?.({ sessionID: sessionId } as any) ?? inner.sessions?.get?.({ sessionId } as any))) as unknown;
282
+ },
283
+ create: async (input) => {
284
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
285
+ const r = await (inner.session?.create?.(input as any) ?? inner.sessions?.create?.(input as any)) as any;
286
+ return { id: r?.data?.id ?? r?.id ?? "" };
287
+ },
288
+ abort: async ({ sessionId }) => {
289
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
290
+ return (await (inner.session?.abort?.({ sessionID: sessionId } as any) ?? inner.sessions?.abort?.({ sessionId } as any))) as null;
291
+ },
292
+ messages: async ({ sessionId }) => {
293
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
294
+ const r = await (inner.session?.messages?.({ sessionID: sessionId } as any) ?? inner.sessions?.messages?.({ sessionId } as any)) as any;
295
+ return r?.data ?? r ?? [];
296
+ },
297
+ prompt: async ({ sessionId, body, messageId }) => {
298
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
299
+ const r = await (inner.session?.prompt?.({ sessionID: sessionId, prompt: body, id: messageId } as any) ?? inner.sessions?.prompt?.({ sessionId, body, messageId } as any)) as any;
300
+ return { messageId: r?.messageId ?? messageId ?? "" };
301
+ },
302
+ promptAsync: async ({ sessionId, body }) => {
303
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
304
+ return (await (inner.session?.promptAsync?.({ sessionID: sessionId, prompt: body } as any) ?? inner.sessions?.promptAsync?.({ sessionId, body } as any))) as null;
305
+ },
306
+ },
307
+ events: {
308
+ subscribe: async (opts = {}) => {
309
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
310
+ const sub = await (inner.event?.subscribe?.(opts as any) ?? inner.events?.subscribe?.(opts as any)) as any;
311
+ return {
312
+ stream: sub?.stream as EventSubscription["stream"],
313
+ close: () => sub?.close?.(),
314
+ };
315
+ },
316
+ },
317
+ health: {
318
+ check: async () => {
319
+ try {
320
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
321
+ const r = await (inner.global?.health?.() ?? inner.health?.check?.()) as any;
322
+ return { ok: r?.ok !== false };
323
+ } catch {
324
+ return { ok: false };
325
+ }
326
+ },
327
+ },
328
+ server: inner.server ?? {},
329
+ };
330
+ return wrapped;
331
+ } catch {
332
+ // `@opencode-ai/sdk` not installed or incompatible — use the fallback.
333
+ return createFallbackClient(opts);
334
+ }
335
+ }