1import {
2 type ModelCapabilities,
3 type ServerProviderModel,
4 type ServerProviderSkill,
5 type ServerProviderSlashCommand,
6} from "@t3tools/contracts";
7import type { OpenCodeSettings } from "../settings.ts";
8import * as Cause from "effect/Cause";
9import * as Data from "effect/Data";
10import * as DateTime from "effect/DateTime";
11import * as Effect from "effect/Effect";
12import * as Schedule from "effect/Schedule";
13
14import { createModelCapabilities } from "@t3tools/shared/model";
15import { compareSemverVersions } from "@t3tools/shared/semver";
16import {
17 buildServerProvider,
18 COMPACT_SLASH_COMMAND,
19 nonEmptyTrimmed,
20 providerModelsFromSettings,
21 type ServerProviderDraft,
22} from "@t3tools/provider-core/server/snapshotProbe";
23import * as OpenCodeRuntime from "./OpenCodeRuntime.ts";
24import type { ProbedOpenCode } from "./versionProbe.ts";
25import type { Agent, ProviderListResponse } from "@opencode-ai/sdk/v2";
26import * as OpenCodeServerOwner from "./OpenCodeServerOwner.ts";
27
28const OPENCODE_PRESENTATION = {
29 displayName: "OpenCode",
30 showInteractionModeToggle: false,
31} as const;
32
33class OpenCodeProbeError extends Data.TaggedError("OpenCodeProbeError")<{
34 readonly cause?: unknown;
35 readonly detail: string;
36}> {}
37
38function normalizeProbeMessage(message: string): string | undefined {
39 const trimmed = message.trim();
40 if (trimmed.length === 0) {
41 return undefined;
42 }
43 if (
44 trimmed === "An error occurred in Effect.tryPromise" ||
45 trimmed === "An error occurred in Effect.try"
46 ) {
47 return undefined;
48 }
49 return trimmed;
50}
51
52function normalizedErrorMessage(cause: unknown): string | undefined {
53 if (cause instanceof OpenCodeProbeError) {
54 return normalizeProbeMessage(cause.detail);
55 }
56
57 if (!(cause instanceof Error)) {
58 return undefined;
59 }
60
61 return normalizeProbeMessage(cause.message);
62}
63
64function formatOpenCodeProbeError(input: {
65 readonly cause: unknown;
66 readonly isExternalServer: boolean;
67 readonly phase: "version" | "inventory";
68 readonly serverUrl: string;
69}): { readonly installed: boolean; readonly message: string } {
70 const detail = normalizedErrorMessage(input.cause);
71 const lower = detail?.toLowerCase() ?? "";
72
73 if (input.isExternalServer) {
74 if (
75 lower.includes("401") ||
76 lower.includes("403") ||
77 lower.includes("unauthorized") ||
78 lower.includes("forbidden")
79 ) {
80 return {
81 installed: true,
82 message: "OpenCode server rejected authentication. Check the server URL and password.",
83 };
84 }
85
86 if (
87 lower.includes("econnrefused") ||
88 lower.includes("enotfound") ||
89 lower.includes("fetch failed") ||
90 lower.includes("networkerror") ||
91 lower.includes("timed out") ||
92 lower.includes("timeout") ||
93 lower.includes("socket hang up")
94 ) {
95 return {
96 installed: true,
97 message: `Couldn't reach the configured OpenCode server at ${input.serverUrl}. Check that the server is running and the URL is correct.`,
98 };
99 }
100
101 return {
102 installed: true,
103 message: detail ?? "Failed to connect to the configured OpenCode server.",
104 };
105 }
106
107 if (lower.includes("enoent") || lower.includes("notfound")) {
108 return {
109 installed: false,
110 message: "OpenCode CLI (`opencode`) is not installed or not on PATH.",
111 };
112 }
113
114 if (lower.includes("quarantine")) {
115 return {
116 installed: true,
117 message:
118 "macOS is blocking the OpenCode binary (quarantine). Run `xattr -d com.apple.quarantine $(which opencode)` to fix this.",
119 };
120 }
121
122 if (lower.includes("invalid code signature") || lower.includes("corrupted")) {
123 return {
124 installed: true,
125 message:
126 "macOS killed the OpenCode process due to an invalid code signature. The binary may be corrupted — try reinstalling OpenCode.",
127 };
128 }
129
130 const failureLabel =
131 input.phase === "inventory"
132 ? "Failed to load OpenCode provider inventory"
133 : "Failed to execute OpenCode CLI health check";
134 return {
135 installed: true,
136 message: detail ? `${failureLabel}: ${detail}` : `${failureLabel}.`,
137 };
138}
139
140function titleCaseSlug(value: string): string {
141 const segments: Array<string> = [];
142 for (const segment of value.split(/[-_/]+/)) {
143 if (segment.length > 0) {
144 segments.push(segment.charAt(0).toUpperCase() + segment.slice(1));
145 }
146 }
147 return segments.join(" ");
148}
149
150function inferDefaultVariant(
151 providerID: string,
152 variants: ReadonlyArray<string>,
153): string | undefined {
154 if (variants.length === 1) {
155 return variants[0];
156 }
157 if (providerID === "anthropic" || providerID.startsWith("google")) {
158 return variants.includes("high") ? "high" : undefined;
159 }
160 if (providerID === "openai" || providerID === "opencode") {
161 return variants.includes("medium") ? "medium" : variants.includes("high") ? "high" : undefined;
162 }
163 return undefined;
164}
165
166function inferDefaultAgent(agents: ReadonlyArray<Agent>): string | undefined {
167 return agents.find((agent) => agent.name === "build")?.name ?? agents[0]?.name ?? undefined;
168}
169
170const DEFAULT_OPENCODE_MODEL_CAPABILITIES: ModelCapabilities = createModelCapabilities({
171 optionDescriptors: [
172 {
173 id: "variant",
174 label: "Reasoning",
175 type: "select",
176 options: [
177 { id: "low", label: "Low" },
178 { id: "medium", label: "Medium", isDefault: true },
179 { id: "high", label: "High" },
180 { id: "xhigh", label: "Extra High" },
181 ],
182 currentValue: "medium",
183 },
184 {
185 id: "agent",
186 label: "Agent",
187 type: "select",
188 options: [
189 { id: "build", label: "Build", isDefault: true },
190 { id: "plan", label: "Plan" },
191 ],
192 currentValue: "build",
193 },
194 ],
195});
196
197function openCodeCapabilitiesForModel(input: {
198 readonly providerID: string;
199 readonly model: ProviderListResponse["all"][number]["models"][string];
200 readonly agents: ReadonlyArray<Agent>;
201}): ModelCapabilities {
202 const rawVariantValues = Object.keys(input.model.variants ?? {});
203 // When a model advertises no variants, synthesize the standard reasoning
204 // levels so the composer still offers a Reasoning selector (mirrors the
205 // Codex/Grok experience where reasoning is always configurable). The set
206 // covers the common OpenCode variant spectrum; `inferDefaultVariant`
207 // picks the provider-appropriate default (e.g. medium for openai/opencode).
208 const variantValues =
209 rawVariantValues.length > 0 ? rawVariantValues : ["low", "medium", "high", "xhigh"];
210 const defaultVariant = inferDefaultVariant(input.providerID, variantValues);
211 const variantOptions = variantValues.map((value) =>
212 defaultVariant === value
213 ? { id: value, label: titleCaseSlug(value), isDefault: true as const }
214 : { id: value, label: titleCaseSlug(value) },
215 );
216 const primaryAgents = input.agents.filter(
217 (agent) => !agent.hidden && (agent.mode === "primary" || agent.mode === "all"),
218 );
219 const defaultAgent = inferDefaultAgent(primaryAgents);
220 const agentOptions = primaryAgents.map((agent) =>
221 defaultAgent === agent.name
222 ? { id: agent.name, label: titleCaseSlug(agent.name), isDefault: true as const }
223 : { id: agent.name, label: titleCaseSlug(agent.name) },
224 );
225 return createModelCapabilities({
226 optionDescriptors: [
227 ...(variantOptions.length > 0
228 ? [
229 {
230 id: "variant",
231 label: "Reasoning",
232 type: "select" as const,
233 options: variantOptions,
234 ...(defaultVariant ? { currentValue: defaultVariant } : {}),
235 },
236 ]
237 : []),
238 ...(agentOptions.length > 0
239 ? [
240 {
241 id: "agent",
242 label: "Agent",
243 type: "select" as const,
244 options: agentOptions,
245 ...(defaultAgent ? { currentValue: defaultAgent } : {}),
246 },
247 ]
248 : []),
249 ],
250 });
251}
252
253function flattenOpenCodeModels(
254 input: OpenCodeRuntime.OpenCodeInventory,
255): ReadonlyArray<ServerProviderModel> {
256 const connected = new Set(input.providerList.connected);
257 const models: Array<ServerProviderModel> = [];
258
259 for (const provider of input.providerList.all) {
260 if (!connected.has(provider.id)) {
261 continue;
262 }
263
264 for (const model of Object.values(provider.models)) {
265 const name = nonEmptyTrimmed(model.name);
266 if (!name) {
267 continue;
268 }
269
270 const subProvider = nonEmptyTrimmed(provider.name);
271 models.push({
272 slug: `${provider.id}/${model.id}`,
273 name,
274 ...(subProvider ? { subProvider } : {}),
275 isCustom: false,
276 capabilities: openCodeCapabilitiesForModel({
277 providerID: provider.id,
278 model,
279 agents: input.agents,
280 }),
281 });
282 }
283 }
284
285 return models.toSorted((left, right) => left.name.localeCompare(right.name));
286}
287
288function trimOptional(value: string | null | undefined): string | undefined {
289 const trimmed = value?.trim();
290 return trimmed && trimmed.length > 0 ? trimmed : undefined;
291}
292
293export function openCodeSkillsToServerProviderSkills(
294 input: OpenCodeRuntime.OpenCodeInventory["skills"] | undefined,
295): ReadonlyArray<ServerProviderSkill> {
296 const skills: ServerProviderSkill[] = [];
297 for (const skill of input ?? []) {
298 const name = trimOptional(skill.name);
299 const path = trimOptional(skill.location);
300 if (!name || !path) {
301 continue;
302 }
303
304 const description = trimOptional(skill.description);
305 skills.push({
306 name,
307 path,
308 enabled: true,
309 ...(description ? { description, shortDescription: description } : {}),
310 });
311 }
312
313 return skills.toSorted((left, right) => left.name.localeCompare(right.name));
314}
315
316export function openCodeCommandsToServerProviderSlashCommands(
317 input: OpenCodeRuntime.OpenCodeInventory["commands"],
318): ReadonlyArray<ServerProviderSlashCommand> {
319 const commands: ServerProviderSlashCommand[] = [COMPACT_SLASH_COMMAND];
320 const names = new Set([COMPACT_SLASH_COMMAND.name]);
321 for (const command of input ?? []) {
322 const name = trimOptional(command.name);
323 if (!name || names.has(name) || command.source === "skill") continue;
324 names.add(name);
325 const description = trimOptional(command.description);
326 const hint = trimOptional(command.hints.join(" "));
327 commands.push({
328 name,
329 ...(description ? { description } : {}),
330 ...(hint ? { input: { hint } } : {}),
331 });
332 }
333 return commands;
334}
335
336export const makePendingOpenCodeProvider = (
337 openCodeSettings: OpenCodeSettings,
338): Effect.Effect<ServerProviderDraft> =>
339 Effect.gen(function* () {
340 const checkedAt = yield* Effect.map(DateTime.now, DateTime.formatIso);
341 const models = providerModelsFromSettings(
342 [],
343 openCodeSettings.customModels,
344 DEFAULT_OPENCODE_MODEL_CAPABILITIES,
345 );
346
347 if (!openCodeSettings.enabled) {
348 return buildServerProvider({
349 presentation: OPENCODE_PRESENTATION,
350 enabled: false,
351 checkedAt,
352 models,
353 probe: {
354 installed: false,
355 version: null,
356 status: "warning",
357 auth: { status: "unknown" },
358 message:
359 openCodeSettings.serverUrl.trim().length > 0
360 ? "OpenCode is disabled in T3 Code settings. A server URL is configured."
361 : "OpenCode is disabled in T3 Code settings.",
362 },
363 });
364 }
365
366 return buildServerProvider({
367 presentation: OPENCODE_PRESENTATION,
368 enabled: true,
369 checkedAt,
370 models,
371 probe: {
372 installed: false,
373 version: null,
374 status: "warning",
375 auth: { status: "unknown" },
376 message: "OpenCode provider status has not been checked in this session yet.",
377 },
378 });
379 });
380
381/** One model an OpenCode 2 server lists, as the status check reads it. */
382export interface OpenCode2Model {
383 readonly providerID: string;
384 readonly id: string;
385 readonly name: string;
386 /**
387 * Name of the provider the model belongs to, when the server names it. One
388 * OpenCode instance fronts several: OpenCode Zen, OpenCode Go, and whatever
389 * the user added themselves.
390 */
391 readonly providerName?: string;
392 readonly variants: ReadonlyArray<{ readonly id: string }>;
393}
394
395/**
396 * One OpenCode instance fronts several providers, and their catalogues
397 * overlap: OpenCode Zen and OpenCode Go both list Step 5 Preview Free. Naming
398 * the sub-provider is what tells two such rows apart. The label only earns its
399 * place when it adds to the instance name the row already shows, which is why a
400 * provider called plain "OpenCode" keeps none, and the id stands in while the
401 * server has not named its providers yet.
402 */
403function subProviderLabelFor(input: {
404 readonly providerID: string;
405 readonly providerName?: string;
406}): string | undefined {
407 const label = nonEmptyTrimmed(input.providerName) ?? input.providerID;
408 return label.toLowerCase() === OPENCODE_PRESENTATION.displayName.toLowerCase()
409 ? undefined
410 : label;
411}
412
413/**
414 * A server loads its catalog lazily and lists nothing for its first few
415 * hundred milliseconds, and a local instance starts a fresh server for each
416 * status check. So an empty list is read again for a while, and the last
417 * non-empty list stands in if the catalog still has not loaded.
418 */
419export const makeOpenCode2ModelLoader = <E>(
420 list: Effect.Effect<ReadonlyArray<OpenCode2Model>, E>,
421) =>
422 Effect.sync(() => {
423 let lastLoaded: ReadonlyArray<OpenCode2Model> = [];
424 return list.pipe(
425 Effect.repeat({
426 until: (models) => models.length > 0,
427 schedule: Schedule.spaced("250 millis"),
428 }),
429 Effect.timeoutOption("5 seconds"),
430 Effect.map((loaded) => {
431 if (loaded._tag === "Some" && loaded.value.length > 0) lastLoaded = loaded.value;
432 return lastLoaded;
433 }),
434 );
435 });
436
437/** A workspace's skills and commands, as an OpenCode 2 server lists them for its directory. */
438export interface OpenCode2Workspace {
439 readonly skills: ReadonlyArray<{
440 readonly id: string;
441 readonly name: string;
442 readonly path: string;
443 readonly description?: string | undefined;
444 }>;
445 readonly commands: ReadonlyArray<{
446 readonly name: string;
447 readonly description?: string | undefined;
448 }>;
449}
450
451/**
452 * A server scans a directory it has not served yet in the background: its
453 * first read lists no commands at all (not even the built-ins), and
454 * `command.updated` plus `skill.updated` for the directory end the scan. A
455 * directory it already serves lists everything at once. `scanned` must be
456 * listening before `read` runs, since the read is what starts the scan.
457 */
458export const loadOpenCode2Workspace = <E>(
459 read: Effect.Effect<OpenCode2Workspace, E>,
460 scanned: Effect.Effect<void>,
461) =>
462 Effect.gen(function* () {
463 const first = yield* read;
464 if (first.commands.length > 0) return first;
465 yield* scanned.pipe(Effect.timeoutOption("10 seconds"));
466 return yield* read;
467 });
468
469export function openCode2SkillsToServerProviderSkills(
470 skills: OpenCode2Workspace["skills"],
471): ReadonlyArray<ServerProviderSkill> {
472 return skills
473 .map((skill) => {
474 const description = trimOptional(skill.description);
475 return {
476 name: skill.id,
477 path: skill.path,
478 enabled: true,
479 ...(skill.name === skill.id ? {} : { displayName: skill.name }),
480 ...(description ? { description, shortDescription: description } : {}),
481 };
482 })
483 .toSorted((left, right) => left.name.localeCompare(right.name));
484}
485
486export function openCode2CommandsToServerProviderSlashCommands(
487 commands: OpenCode2Workspace["commands"],
488): ReadonlyArray<ServerProviderSlashCommand> {
489 const slashCommands: ServerProviderSlashCommand[] = [COMPACT_SLASH_COMMAND];
490 const names = new Set([COMPACT_SLASH_COMMAND.name]);
491 for (const command of commands) {
492 const name = trimOptional(command.name);
493 if (!name || names.has(name)) continue;
494 names.add(name);
495 const description = trimOptional(command.description);
496 slashCommands.push({ name, ...(description ? { description } : {}) });
497 }
498 return slashCommands;
499}
500
501/**
502 * Every mode maps onto OpenCode 2 session rules. Auto asks like Supervised:
503 * OpenCode has no reviewer that approves routine actions. Plan mode is its
504 * `plan` agent, so the composer's mode toggle drives it.
505 */
506const OPENCODE_2_PRESENTATION = {
507 ...OPENCODE_PRESENTATION,
508 showInteractionModeToggle: true,
509 supportedRuntimeModes: ["approval-required", "auto-accept-edits", "auto", "full-access"],
510} as const;
511
512function openCode2ModelCapabilities(model: OpenCode2Model): ModelCapabilities {
513 const variants = model.variants.map((variant) => variant.id);
514 const defaultVariant = inferDefaultVariant(model.providerID, variants);
515 return createModelCapabilities({
516 optionDescriptors:
517 variants.length === 0
518 ? []
519 : [
520 {
521 id: "variant",
522 label: "Reasoning",
523 type: "select",
524 options: variants.map((id) => ({
525 id,
526 label: titleCaseSlug(id),
527 ...(id === defaultVariant ? { isDefault: true } : {}),
528 })),
529 ...(defaultVariant === undefined ? {} : { currentValue: defaultVariant }),
530 },
531 ],
532 });
533}
534
535const checkOpenCode2 = Effect.fn("checkOpenCode2")(function* (
536 settings: OpenCodeSettings,
537 version: string,
538 checkedAt: string,
539 loadModels: Effect.Effect<ReadonlyArray<OpenCode2Model>, OpenCodeRuntime.OpenCodeRuntimeError>,
540) {
541 const result = yield* Effect.exit(loadModels);
542 const probe = (status: "ready" | "warning" | "error", message: string) => ({
543 installed: true,
544 version,
545 status,
546 auth: { status: status === "ready" ? ("authenticated" as const) : ("unknown" as const) },
547 message,
548 });
549 if (result._tag === "Failure") {
550 // The detail can carry a response body; it stays in the log, not the status.
551 yield* Effect.logWarning("OpenCode 2 model list failed", result.cause);
552 return buildServerProvider({
553 presentation: OPENCODE_2_PRESENTATION,
554 enabled: true,
555 checkedAt,
556 models: providerModelsFromSettings(
557 [],
558 settings.customModels,
559 DEFAULT_OPENCODE_MODEL_CAPABILITIES,
560 ),
561 // Compaction does not depend on the model catalog.
562 slashCommands: [COMPACT_SLASH_COMMAND],
563 probe: probe("error", "OpenCode could not load its model list."),
564 });
565 }
566 const models = providerModelsFromSettings(
567 result.value
568 .map((model) => {
569 const subProvider = subProviderLabelFor(model);
570 return {
571 slug: `${model.providerID}/${model.id}`,
572 name: model.name,
573 ...(subProvider ? { subProvider } : {}),
574 isCustom: false,
575 capabilities: openCode2ModelCapabilities(model),
576 };
577 })
578 .toSorted((left, right) => left.name.localeCompare(right.name)),
579 settings.customModels,
580 DEFAULT_OPENCODE_MODEL_CAPABILITIES,
581 );
582 return buildServerProvider({
583 presentation: OPENCODE_2_PRESENTATION,
584 enabled: true,
585 checkedAt,
586 models,
587 // Every session can compact; a workspace snapshot adds that directory's commands.
588 slashCommands: [COMPACT_SLASH_COMMAND],
589 probe:
590 result.value.length > 0
591 ? probe(
592 "ready",
593 `OpenCode ${version} lists ${result.value.length} model${result.value.length === 1 ? "" : "s"}.`,
594 )
595 : probe("warning", "OpenCode 2 is running, but it did not list any models yet."),
596 });
597});
598
599/**
600 * `probeRuntime` is the driver's memoized version probe: `opencode --version` for a local binary,
601 * the version endpoints for a configured server. The status check refreshes it, so an in-place
602 * upgrade re-routes the instance.
603 */
604export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatus")(function* (
605 openCodeSettings: OpenCodeSettings,
606 cwd: string,
607 probeRuntime: Effect.Effect<ProbedOpenCode, OpenCodeRuntime.OpenCodeRuntimeError>,
608 loadOpenCode2Models: Effect.Effect<
609 ReadonlyArray<OpenCode2Model>,
610 OpenCodeRuntime.OpenCodeRuntimeError
611 >,
612): Effect.fn.Return<
613 ServerProviderDraft,
614 never,
615 OpenCodeRuntime.OpenCodeRuntime | OpenCodeServerOwner.OpenCodeServerOwner
616> {
617 const openCodeRuntime = yield* OpenCodeRuntime.OpenCodeRuntime;
618 const serverOwner = yield* OpenCodeServerOwner.OpenCodeServerOwner;
619 const checkedAt = DateTime.formatIso(yield* DateTime.now);
620 const customModels = openCodeSettings.customModels;
621 const isExternalServer = openCodeSettings.serverUrl.trim().length > 0;
622
623 const fallback = (
624 cause: unknown,
625 version: string | null = null,
626 phase: "version" | "inventory" = "version",
627 ) => {
628 const failure = formatOpenCodeProbeError({
629 cause,
630 isExternalServer,
631 phase,
632 serverUrl: openCodeSettings.serverUrl,
633 });
634 return buildServerProvider({
635 presentation: OPENCODE_PRESENTATION,
636 enabled: openCodeSettings.enabled,
637 checkedAt,
638 models: providerModelsFromSettings([], customModels, DEFAULT_OPENCODE_MODEL_CAPABILITIES),
639 probe: {
640 installed: failure.installed,
641 version,
642 status: "error",
643 auth: { status: "unknown" },
644 message: failure.message,
645 },
646 });
647 };
648
649 if (!openCodeSettings.enabled) {
650 return buildServerProvider({
651 presentation: OPENCODE_PRESENTATION,
652 enabled: false,
653 checkedAt,
654 models: providerModelsFromSettings([], customModels, DEFAULT_OPENCODE_MODEL_CAPABILITIES),
655 probe: {
656 installed: false,
657 version: null,
658 status: "warning",
659 auth: { status: "unknown" },
660 message: isExternalServer
661 ? "OpenCode is disabled in T3 Code settings. A server URL is configured."
662 : "OpenCode is disabled in T3 Code settings.",
663 },
664 });
665 }
666
667 const probedExit = yield* Effect.exit(
668 probeRuntime.pipe(
669 Effect.mapError(
670 (cause) =>
671 new OpenCodeProbeError({
672 cause,
673 detail: OpenCodeRuntime.openCodeRuntimeErrorDetail(cause),
674 }),
675 ),
676 ),
677 );
678 if (probedExit._tag === "Failure") return fallback(Cause.squash(probedExit.cause));
679 const probed = probedExit.value;
680 if (probed.generation === "v2") {
681 return yield* checkOpenCode2(openCodeSettings, probed.version, checkedAt, loadOpenCode2Models);
682 }
683 let version: string | null = probed.version;
684 if (compareSemverVersions(probed.version, OpenCodeRuntime.MINIMUM_OPENCODE_VERSION) < 0) {
685 return buildServerProvider({
686 presentation: OPENCODE_PRESENTATION,
687 enabled: openCodeSettings.enabled,
688 checkedAt,
689 models: providerModelsFromSettings([], customModels, DEFAULT_OPENCODE_MODEL_CAPABILITIES),
690 probe: {
691 installed: true,
692 version: probed.version,
693 status: "error",
694 auth: { status: "unknown" },
695 message: `OpenCode v${probed.version} is too old. Upgrade to v${OpenCodeRuntime.MINIMUM_OPENCODE_VERSION} or newer.`,
696 },
697 });
698 }
699
700 const loadInventory = (server: {
701 readonly url: string;
702 readonly serverPassword?: string;
703 readonly version: string;
704 }) =>
705 openCodeRuntime
706 .loadOpenCodeInventory(
707 openCodeRuntime.createOpenCodeSdkClient({
708 baseUrl: server.url,
709 directory: cwd,
710 ...(server.serverPassword !== undefined ? { serverPassword: server.serverPassword } : {}),
711 }),
712 )
713 .pipe(Effect.map((inventory) => ({ inventory, version: server.version })));
714 const inventoryEffect = isExternalServer
715 ? openCodeRuntime
716 .connectToOpenCodeServer({
717 binaryPath: openCodeSettings.binaryPath,
718 directory: cwd,
719 serverUrl: openCodeSettings.serverUrl,
720 ...(openCodeSettings.serverPassword
721 ? { serverPassword: openCodeSettings.serverPassword }
722 : {}),
723 })
724 .pipe(Effect.flatMap(loadInventory), Effect.scoped)
725 : serverOwner.withServer(loadInventory);
726 const inventoryExit = yield* Effect.exit(
727 inventoryEffect.pipe(
728 Effect.mapError(
729 (cause) =>
730 new OpenCodeProbeError({
731 cause,
732 detail: OpenCodeRuntime.openCodeRuntimeErrorDetail(cause),
733 }),
734 ),
735 ),
736 );
737 if (inventoryExit._tag === "Failure") {
738 return fallback(Cause.squash(inventoryExit.cause), version, "inventory");
739 }
740
741 version = inventoryExit.value.version;
742
743 const models = providerModelsFromSettings(
744 flattenOpenCodeModels(inventoryExit.value.inventory),
745 customModels,
746 DEFAULT_OPENCODE_MODEL_CAPABILITIES,
747 );
748 const skills = openCodeSkillsToServerProviderSkills(inventoryExit.value.inventory.skills);
749 const connectedCount = inventoryExit.value.inventory.providerList.connected.length;
750 return buildServerProvider({
751 presentation: OPENCODE_PRESENTATION,
752 enabled: true,
753 checkedAt,
754 models,
755 skills,
756 slashCommands: openCodeCommandsToServerProviderSlashCommands(
757 inventoryExit.value.inventory.commands,
758 ),
759 probe: {
760 installed: true,
761 version,
762 status: connectedCount > 0 ? "ready" : "warning",
763 auth: {
764 status: connectedCount > 0 ? "authenticated" : "unknown",
765 type: "opencode",
766 },
767 message:
768 connectedCount > 0
769 ? `${connectedCount} upstream provider${connectedCount === 1 ? "" : "s"} connected through ${isExternalServer ? "the configured OpenCode server" : "OpenCode"}.`
770 : isExternalServer
771 ? "Connected to the configured OpenCode server, but it did not report any connected upstream providers."
772 : "OpenCode is available, but it did not report any connected upstream providers.",
773 },
774 });
775});