diff --git a/bun.lock b/bun.lock
index 8992576c8f..c52d9e6b17 100644
--- a/bun.lock
+++ b/bun.lock
@@ -968,7 +968,6 @@
"@effect/platform-node": "catalog:",
"@npmcli/arborist": "catalog:",
"@npmcli/config": "10.8.1",
- "@opencode-ai/ai": "workspace:*",
"@opentelemetry/api": "1.9.0",
"@opentelemetry/context-async-hooks": "2.6.1",
"@opentelemetry/exporter-trace-otlp-http": "0.214.0",
diff --git a/packages/core/src/effect/app-node-platform.ts b/packages/core/src/effect/app-node-platform.ts
new file mode 100644
index 0000000000..4ad725fcbb
--- /dev/null
+++ b/packages/core/src/effect/app-node-platform.ts
@@ -0,0 +1,13 @@
+import { LLMClient, RequestExecutor } from "@opencode-ai/ai/route"
+import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
+import { httpClient } from "@opencode-ai/util/effect/app-node-platform"
+
+export const requestExecutor = makeGlobalNode({
+ service: RequestExecutor.Service,
+ layer: RequestExecutor.layer,
+ deps: [httpClient],
+})
+
+export const llmClient = makeGlobalNode({ service: LLMClient.Service, layer: LLMClient.layer, deps: [requestExecutor] })
+
+export * as LayerNodePlatform from "./app-node-platform"
diff --git a/packages/core/src/generate.ts b/packages/core/src/generate.ts
index 9304bbb001..f4a79e9aa2 100644
--- a/packages/core/src/generate.ts
+++ b/packages/core/src/generate.ts
@@ -4,7 +4,7 @@ import { LLM, LLMClient, LLMError } from "@opencode-ai/ai"
import { Context, Effect, Layer, Schema } from "effect"
import { Catalog } from "./catalog"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
-import { llmClient } from "@opencode-ai/util/effect/app-node-platform"
+import { llmClient } from "./effect/app-node-platform"
import { Integration } from "./integration"
import { ModelV2 } from "./model"
import { SessionRunnerModel } from "./session/runner/model"
diff --git a/packages/core/src/models-dev.ts b/packages/core/src/models-dev.ts
index fa30001477..3bc763882e 100644
--- a/packages/core/src/models-dev.ts
+++ b/packages/core/src/models-dev.ts
@@ -649,7 +649,7 @@ export const layer = (options?: Options) =>
if (fetch && !process.argv.includes("--get-yargs-completions")) {
// Schedule.spaced runs the effect once, then waits between completions.
- yield* Effect.forkScoped(refresh().pipe(Effect.repeat(Schedule.spaced("60 minutes")), Effect.ignore))
+ yield* Effect.forkScoped(refresh().pipe(Effect.repeat(Schedule.spaced(ttl)), Effect.ignore))
}
return Service.of({ get, refresh })
diff --git a/packages/core/src/session/compaction.ts b/packages/core/src/session/compaction.ts
index 4f225b7b8e..7a18f55f38 100644
--- a/packages/core/src/session/compaction.ts
+++ b/packages/core/src/session/compaction.ts
@@ -6,7 +6,7 @@ import { Context, Effect, Layer, Stream } from "effect"
import { Config } from "../config"
import { EventV2 } from "../event"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
-import { llmClient } from "@opencode-ai/util/effect/app-node-platform"
+import { llmClient } from "../effect/app-node-platform"
import { SessionEvent } from "./event"
import type { SessionMessage } from "./message"
import { SessionModelHeaders } from "./model-headers"
diff --git a/packages/core/src/session/generate-node.ts b/packages/core/src/session/generate-node.ts
index 4c28b67172..24be6f835c 100644
--- a/packages/core/src/session/generate-node.ts
+++ b/packages/core/src/session/generate-node.ts
@@ -4,8 +4,8 @@ import { LLM, LLMClient, Message, SystemPart } from "@opencode-ai/ai"
import { Effect, Layer } from "effect"
import { Database } from "../database/database"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
-import { llmClient } from "@opencode-ai/util/effect/app-node-platform"
import { Client } from "@opencode-ai/util/client"
+import { llmClient } from "../effect/app-node-platform"
import { PluginHooks } from "../plugin/hooks"
import { SessionContext } from "./context"
import { SessionGenerate } from "./generate"
diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts
index 2885a24c14..2a8aee08cc 100644
--- a/packages/core/src/session/runner/llm.ts
+++ b/packages/core/src/session/runner/llm.ts
@@ -22,7 +22,7 @@ import { Service } from "./index"
import { createLLMEventPublisher } from "./publish-llm-event"
import { Snapshot } from "../../snapshot"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
-import { llmClient } from "@opencode-ai/util/effect/app-node-platform"
+import { llmClient } from "../../effect/app-node-platform"
import { StepFailedError } from "../error"
import { toSessionError } from "../to-session-error"
import { SessionRunnerRetry } from "./retry"
diff --git a/packages/core/src/session/title.ts b/packages/core/src/session/title.ts
index 737319d589..a506cb1cc1 100644
--- a/packages/core/src/session/title.ts
+++ b/packages/core/src/session/title.ts
@@ -6,8 +6,8 @@ import { AgentV2 } from "../agent"
import { Database } from "../database/database"
import { EventV2 } from "../event"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
-import { llmClient } from "@opencode-ai/util/effect/app-node-platform"
import { Client } from "@opencode-ai/util/client"
+import { llmClient } from "../effect/app-node-platform"
import { SessionEvent } from "./event"
import { SessionHistory } from "./history"
import { SessionModelHeaders } from "./model-headers"
diff --git a/packages/core/test/session-compaction.test.ts b/packages/core/test/session-compaction.test.ts
index a4959badf6..0634e42224 100644
--- a/packages/core/test/session-compaction.test.ts
+++ b/packages/core/test/session-compaction.test.ts
@@ -4,7 +4,7 @@ import { OpenAIChat } from "@opencode-ai/ai/protocols"
import { Config } from "@opencode-ai/core/config"
import { Database } from "@opencode-ai/core/database/database"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
-import { llmClient } from "@opencode-ai/util/effect/app-node-platform"
+import { llmClient } from "@opencode-ai/core/effect/app-node-platform"
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
import { EventV2 } from "@opencode-ai/core/event"
import { EventTable } from "@opencode-ai/core/event/sql"
diff --git a/packages/core/test/session-generate.test.ts b/packages/core/test/session-generate.test.ts
index 647122ef34..b8be85433f 100644
--- a/packages/core/test/session-generate.test.ts
+++ b/packages/core/test/session-generate.test.ts
@@ -4,7 +4,7 @@ import { OpenAIChat } from "@opencode-ai/ai/protocols"
import { AgentV2 } from "@opencode-ai/core/agent"
import { Database } from "@opencode-ai/core/database/database"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
-import { llmClient } from "@opencode-ai/util/effect/app-node-platform"
+import { llmClient } from "@opencode-ai/core/effect/app-node-platform"
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
import { EventV2 } from "@opencode-ai/core/event"
import { EventTable } from "@opencode-ai/core/event/sql"
diff --git a/packages/core/test/session-runner-recorded.test.ts b/packages/core/test/session-runner-recorded.test.ts
index bf5b538122..ba4329ca78 100644
--- a/packages/core/test/session-runner-recorded.test.ts
+++ b/packages/core/test/session-runner-recorded.test.ts
@@ -4,7 +4,7 @@ import { Auth, LLMClient, RequestExecutor } from "@opencode-ai/ai/route"
import { Catalog } from "@opencode-ai/core/catalog"
import { Database } from "@opencode-ai/core/database/database"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
-import { LayerNodePlatform } from "@opencode-ai/util/effect/app-node-platform"
+import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
import { EventV2 } from "@opencode-ai/core/event"
import { EventTable } from "@opencode-ai/core/event/sql"
diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts
index e38875cffa..6de3b0d070 100644
--- a/packages/core/test/session-runner.test.ts
+++ b/packages/core/test/session-runner.test.ts
@@ -19,7 +19,7 @@ import { Catalog } from "@opencode-ai/core/catalog"
import { Database } from "@opencode-ai/core/database/database"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
-import { LayerNodePlatform } from "@opencode-ai/util/effect/app-node-platform"
+import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
import { EventV2 } from "@opencode-ai/core/event"
import { InstallationVersion } from "@opencode-ai/util/installation/version"
diff --git a/packages/core/test/session-title.test.ts b/packages/core/test/session-title.test.ts
index d1cebe3d40..3263930e1e 100644
--- a/packages/core/test/session-title.test.ts
+++ b/packages/core/test/session-title.test.ts
@@ -4,7 +4,7 @@ import { OpenAIChat } from "@opencode-ai/ai/protocols"
import { AgentV2 } from "@opencode-ai/core/agent"
import { Database } from "@opencode-ai/core/database/database"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
-import { llmClient } from "@opencode-ai/util/effect/app-node-platform"
+import { llmClient } from "@opencode-ai/core/effect/app-node-platform"
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
import { EventV2 } from "@opencode-ai/core/event"
import { SessionEvent } from "@opencode-ai/core/session/event"
diff --git a/packages/tui/src/mini/footer.view.tsx b/packages/tui/src/mini/footer.view.tsx
index 2ddeef1ec0..ff5199e89f 100644
--- a/packages/tui/src/mini/footer.view.tsx
+++ b/packages/tui/src/mini/footer.view.tsx
@@ -29,6 +29,7 @@ import { RunFormBody } from "./footer.form"
import { createFormBodyState, type FormBodyState } from "./form.shared"
import { footerWidthPolicy } from "./footer.width"
import { Keymap } from "../context/keymap"
+import { modelInfo } from "./variant.shared"
import type {
FooterPromptRoute,
@@ -157,6 +158,10 @@ export function RunFooterView(props: RunFooterViewProps) {
return tabs().findIndex((item) => item.sessionID === sessionID) + 1
})
const foregroundSubagents = createMemo(() => activeTabs().some((item) => !item.background))
+ const model = createMemo(() => {
+ const current = props.currentModel()
+ return current ? modelInfo(props.providers(), current).model : undefined
+ })
const detail = createMemo(() => {
const current = route()
return current.type === "subagent" ? subagent().details[current.sessionID] : undefined
@@ -364,6 +369,14 @@ export function RunFooterView(props: RunFooterViewProps) {
return usage()
})
+ const modelStatus = createMemo(() => {
+ const current = model()
+ if (!prompt() || !responsive().statusline.showModel || !current) return
+ return {
+ model: current,
+ variant: responsive().statusline.showModelVariant ? props.currentVariant() : undefined,
+ }
+ })
const statusColor = createMemo(() => {
if (exiting()) {
return theme().error
@@ -381,6 +394,7 @@ export function RunFooterView(props: RunFooterViewProps) {
})
const statuslineBackground = createMemo(() => theme().status)
const hasActivityMeta = createMemo(() => activityMeta().length > 0)
+ const hasModelStatus = createMemo(() => Boolean(modelStatus()))
const contextHints = createMemo(() => {
if (!prompt() || shell() || !responsive().statusline.showContextHints) {
return []
@@ -819,11 +833,29 @@ export function RunFooterView(props: RunFooterViewProps) {
+
+ {(info) => (
+
+
+ {info().model}
+
+ {(variant) => {variant()}}
+
+
+
+ )}
+
+
{(hint, index) => (
- 0 || (hasActivityMeta() && index() === 0)}>
+ 0 || ((hasActivityMeta() || hasModelStatus()) && index() === 0)}>
{sectionSeparator()}
{hint.key}{" "}
@@ -837,7 +869,7 @@ export function RunFooterView(props: RunFooterViewProps) {
{(hint) => (
-
+
{sectionSeparator()}
{hint().key}{" "}
diff --git a/packages/tui/src/mini/footer.width.ts b/packages/tui/src/mini/footer.width.ts
index b0979858e4..35670d0ef7 100644
--- a/packages/tui/src/mini/footer.width.ts
+++ b/packages/tui/src/mini/footer.width.ts
@@ -1,8 +1,10 @@
// Shared responsive width policy
const FOOTER_WIDTH_BREAKPOINTS = {
+ commandHint: 24,
+ model: 32,
+ modelVariant: 40,
compact: 80,
- commandHint: 66,
context: 120,
spacious: 150,
} as const
@@ -19,6 +21,8 @@ export function footerWidthPolicy(width: number) {
statusline: {
showActivityMeta: compact,
showCommandHint: width >= FOOTER_WIDTH_BREAKPOINTS.commandHint,
+ showModel: width >= FOOTER_WIDTH_BREAKPOINTS.model,
+ showModelVariant: width >= FOOTER_WIDTH_BREAKPOINTS.modelVariant,
showContextHints: compact,
contextHintLimit: !compact ? 0 : spacious ? undefined : context ? 2 : 1,
},
diff --git a/packages/tui/test/mini/footer.view.test.tsx b/packages/tui/test/mini/footer.view.test.tsx
index b8c191ceb0..41e3e13568 100644
--- a/packages/tui/test/mini/footer.view.test.tsx
+++ b/packages/tui/test/mini/footer.view.test.tsx
@@ -1074,8 +1074,8 @@ test("direct footer shows authoritative pending work while running", async () =>
const statusItems = statusline.getChildren().filter((item): item is BoxRenderable => item instanceof BoxRenderable)
const main = statusItems[0]
const spinner = main.getChildren()[0]
- const background = statusItems[1]
- const queued = statusItems[2]
+ const background = statusItems[2]
+ const queued = statusItems[3]
const hint = statusItems.at(-1)!
expect(spinner).toBeDefined()
@@ -1098,11 +1098,36 @@ test("direct footer shows authoritative pending work while running", async () =>
}
})
+test("direct footer progressively adds model details after the command hint", async () => {
+ for (const expected of [
+ { width: 24, model: false, variant: false },
+ { width: 32, model: true, variant: false },
+ { width: 40, model: true, variant: true },
+ ]) {
+ const app = await renderFooter({
+ providers: [provider()],
+ currentModel: { providerID: "opencode", modelID: "gpt-5" },
+ currentVariant: "xhigh",
+ width: expected.width,
+ })
+
+ try {
+ await app.renderOnce()
+ const frame = app.captureCharFrame()
+ expect({
+ width: expected.width,
+ command: frame.includes("ctrl+p cmd"),
+ model: frame.includes("GPT-5"),
+ variant: frame.includes("xhigh"),
+ }).toEqual({ ...expected, command: true })
+ } finally {
+ app.cleanup()
+ }
+ }
+})
+
test("direct footer always offers backgrounding for a foreground subagent", async () => {
const app = await renderFooter({
- providers: [provider()],
- currentModel: { providerID: "opencode", modelID: "gpt-5" },
- currentVariant: "xhigh",
subagents: {
tabs: [subagent({ sessionID: "s-1", label: "Explore", description: "Inspect auth flow" })],
details: {},
@@ -1125,9 +1150,6 @@ test("direct footer always offers backgrounding for a foreground subagent", asyn
test("direct footer hides the subagent hint when only completed subagents remain", async () => {
const app = await renderFooter({
- providers: [provider()],
- currentModel: { providerID: "opencode", modelID: "gpt-5" },
- currentVariant: "xhigh",
subagents: {
tabs: [subagent({ sessionID: "s-1", label: "Explore", description: "Inspect auth flow", status: "completed" })],
details: {},
diff --git a/packages/tui/test/mini/footer.width.test.ts b/packages/tui/test/mini/footer.width.test.ts
index 3f9809d356..fa8ec5d31a 100644
--- a/packages/tui/test/mini/footer.width.test.ts
+++ b/packages/tui/test/mini/footer.width.test.ts
@@ -3,19 +3,16 @@ import { footerWidthPolicy } from "../../src/mini/footer.width"
describe("run footer width", () => {
test("preserves shared dialog and statusline breakpoints", () => {
+ expect([23, 24].map((width) => footerWidthPolicy(width).statusline.showCommandHint)).toEqual([false, true])
+ expect([31, 32].map((width) => footerWidthPolicy(width).statusline.showModel)).toEqual([false, true])
+ expect([39, 40].map((width) => footerWidthPolicy(width).statusline.showModelVariant)).toEqual([false, true])
+
const narrow = footerWidthPolicy(79)
expect(narrow.dialog.narrow).toBe(true)
expect(narrow.statusline.showActivityMeta).toBe(false)
- expect(narrow.statusline.showCommandHint).toBe(true)
expect(narrow.statusline.showContextHints).toBe(false)
expect(narrow.statusline.contextHintLimit).toBe(0)
- const command = footerWidthPolicy(65)
- expect(command.statusline.showCommandHint).toBe(false)
-
- const commandHint = footerWidthPolicy(66)
- expect(commandHint.statusline.showCommandHint).toBe(true)
-
const compact = footerWidthPolicy(80)
expect(compact.dialog.narrow).toBe(false)
expect(compact.statusline.showActivityMeta).toBe(true)
diff --git a/packages/util/package.json b/packages/util/package.json
index 817c94328f..70dab458df 100644
--- a/packages/util/package.json
+++ b/packages/util/package.json
@@ -2,9 +2,19 @@
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/util",
"version": "1.18.3",
- "private": true,
"type": "module",
"license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/anomalyco/opencode.git",
+ "directory": "packages/util"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "files": [
+ "dist"
+ ],
"exports": {
"./effect/layer-node": "./src/effect/layer-node.ts",
"./*": "./src/*.ts"
@@ -17,6 +27,7 @@
}
},
"scripts": {
+ "build": "bun run script/build.ts",
"typecheck": "tsgo --noEmit"
},
"dependencies": {
@@ -24,7 +35,6 @@
"@effect/platform-node": "catalog:",
"@npmcli/arborist": "catalog:",
"@npmcli/config": "10.8.1",
- "@opencode-ai/ai": "workspace:*",
"@opentelemetry/api": "1.9.0",
"@opentelemetry/context-async-hooks": "2.6.1",
"@opentelemetry/exporter-trace-otlp-http": "0.214.0",
diff --git a/packages/util/script/build.ts b/packages/util/script/build.ts
new file mode 100644
index 0000000000..323a63ddf9
--- /dev/null
+++ b/packages/util/script/build.ts
@@ -0,0 +1,9 @@
+#!/usr/bin/env bun
+
+import { $ } from "bun"
+import { fileURLToPath } from "node:url"
+
+process.chdir(fileURLToPath(new URL("..", import.meta.url)))
+
+await $`rm -rf dist`
+await $`bun tsc -p tsconfig.build.json`
diff --git a/packages/util/script/publish.ts b/packages/util/script/publish.ts
new file mode 100644
index 0000000000..ec6cff16c4
--- /dev/null
+++ b/packages/util/script/publish.ts
@@ -0,0 +1,49 @@
+#!/usr/bin/env bun
+
+import { Script } from "@opencode-ai/script"
+import { $ } from "bun"
+import { rm } from "node:fs/promises"
+import { fileURLToPath } from "node:url"
+
+process.chdir(fileURLToPath(new URL("..", import.meta.url)))
+
+const originalText = await Bun.file("package.json").text()
+const pkg = JSON.parse(originalText) as {
+ name: string
+ version: string
+ exports: Record
+ imports: Record>
+}
+const tarball = `${pkg.name.replace("@", "").replace("/", "-")}-${pkg.version}.tgz`
+
+if ((await $`npm view ${pkg.name}@${pkg.version} version`.nothrow()).exitCode === 0) {
+ console.log(`already published ${pkg.name}@${pkg.version}`)
+ process.exit(0)
+}
+
+const output = (value: string, extension: ".js" | ".d.ts") =>
+ value.replace("./src/", "./dist/").replace(/\.ts$/, extension)
+
+try {
+ await $`bun run typecheck`
+ await $`bun run build`
+ pkg.exports = Object.fromEntries(
+ Object.entries(pkg.exports).map(([key, value]) => {
+ if (typeof value !== "string") return [key, value]
+ return [key, { import: output(value, ".js"), types: output(value, ".d.ts") }]
+ }),
+ )
+ pkg.imports = Object.fromEntries(
+ Object.entries(pkg.imports).map(([key, conditions]) => [
+ key,
+ Object.fromEntries(Object.entries(conditions).map(([condition, value]) => [condition, output(value, ".js")])),
+ ]),
+ )
+ await Bun.write("package.json", JSON.stringify(pkg, null, 2) + "\n")
+ await rm(tarball, { force: true })
+ await $`bun pm pack`
+ await $`npm publish ${tarball} --tag ${Script.channel} --access public`
+} finally {
+ await Bun.write("package.json", originalText)
+ await rm(tarball, { force: true })
+}
diff --git a/packages/util/src/client.ts b/packages/util/src/client.ts
index 7d9721abba..8375e23616 100644
--- a/packages/util/src/client.ts
+++ b/packages/util/src/client.ts
@@ -1,7 +1,7 @@
-export * as Client from "./client"
+export * as Client from "./client.js"
import { Context, Layer } from "effect"
-import { makeGlobalNode } from "./effect/app-node"
+import { makeGlobalNode } from "./effect/app-node.js"
export const Name = Context.Reference("@opencode/Client/Name", {
defaultValue: () => "cli",
diff --git a/packages/util/src/cross-spawn-spawner.ts b/packages/util/src/cross-spawn-spawner.ts
index f6a04ffd93..306c72bc5c 100644
--- a/packages/util/src/cross-spawn-spawner.ts
+++ b/packages/util/src/cross-spawn-spawner.ts
@@ -15,8 +15,8 @@ import {
import * as NodeChildProcess from "node:child_process"
import { PassThrough } from "node:stream"
import launch from "cross-spawn"
-import { makeGlobalNode } from "./effect/app-node"
-import { filesystem, path } from "./effect/app-node-platform"
+import { makeGlobalNode } from "./effect/app-node.js"
+import { filesystem, path } from "./effect/app-node-platform.js"
const toError = (err: unknown): Error => (err instanceof globalThis.Error ? err : new globalThis.Error(String(err)))
@@ -495,4 +495,4 @@ const layer: Layer.Layer = LayerNode.Node(service: Context.Service, layer: Layer.Layer) {
let rt: ManagedRuntime.ManagedRuntime | undefined
diff --git a/packages/util/src/flock.ts b/packages/util/src/flock.ts
index 958bd9fd1d..3868cf1ed9 100644
--- a/packages/util/src/flock.ts
+++ b/packages/util/src/flock.ts
@@ -2,7 +2,7 @@ import path from "path"
import os from "os"
import { randomBytes, randomUUID } from "crypto"
import { mkdir, readFile, rm, stat, utimes, writeFile } from "fs/promises"
-import { Hash } from "./hash"
+import { Hash } from "./hash.js"
import { Effect } from "effect"
export type FlockGlobal = {
diff --git a/packages/util/src/fs-util.ts b/packages/util/src/fs-util.ts
index 152d2599fd..46d408a827 100644
--- a/packages/util/src/fs-util.ts
+++ b/packages/util/src/fs-util.ts
@@ -5,10 +5,10 @@ import { readdir } from "fs/promises"
import { lookup } from "mime-types"
import { Context, Effect, FileSystem, Layer, Schema } from "effect"
import type { PlatformError } from "effect/PlatformError"
-import { Glob } from "./glob"
-import { serviceUse } from "./effect/service-use"
-import { makeGlobalNode } from "./effect/app-node"
-import { filesystem } from "./effect/app-node-platform"
+import { Glob } from "./glob.js"
+import { serviceUse } from "./effect/service-use.js"
+import { makeGlobalNode } from "./effect/app-node.js"
+import { filesystem } from "./effect/app-node-platform.js"
export namespace FSUtil {
export class FileSystemError extends Schema.TaggedErrorClass()("FileSystemError", {
diff --git a/packages/util/src/global.ts b/packages/util/src/global.ts
index bced17b5b3..3edbde5ce2 100644
--- a/packages/util/src/global.ts
+++ b/packages/util/src/global.ts
@@ -3,8 +3,8 @@ import fs from "fs/promises"
import { xdgData, xdgCache, xdgConfig, xdgState } from "xdg-basedir"
import os from "os"
import { Context, Effect, Layer } from "effect"
-import { Flock } from "./flock"
-import { makeGlobalNode } from "./effect/app-node"
+import { Flock } from "./flock.js"
+import { makeGlobalNode } from "./effect/app-node.js"
const app = "opencode"
const data = path.join(xdgData!, app)
@@ -83,4 +83,4 @@ export const layerWith = (input: Partial) =>
Effect.sync(() => Service.of(make(input))),
)
-export * as Global from "./global"
+export * as Global from "./global.js"
diff --git a/packages/util/src/installation/version.ts b/packages/util/src/installation/version.ts
index 25d9cd99aa..d142b46a8b 100644
--- a/packages/util/src/installation/version.ts
+++ b/packages/util/src/installation/version.ts
@@ -1,7 +1,5 @@
-declare global {
- const OPENCODE_VERSION: string
- const OPENCODE_CHANNEL: string
-}
+declare const OPENCODE_VERSION: string
+declare const OPENCODE_CHANNEL: string
export const InstallationVersion = typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "local"
export const InstallationChannel = typeof OPENCODE_CHANNEL === "string" ? OPENCODE_CHANNEL : "local"
diff --git a/packages/util/src/npm-config.ts b/packages/util/src/npm-config.ts
index da0f0f2632..bb127edb35 100644
--- a/packages/util/src/npm-config.ts
+++ b/packages/util/src/npm-config.ts
@@ -1,4 +1,4 @@
-export * as NpmConfig from "./npm-config"
+export * as NpmConfig from "./npm-config.js"
import { fileURLToPath } from "url"
// @ts-expect-error npm does not publish types for this internal config API.
diff --git a/packages/util/src/npm.ts b/packages/util/src/npm.ts
index fa4e40332b..8a3fc77ec1 100644
--- a/packages/util/src/npm.ts
+++ b/packages/util/src/npm.ts
@@ -1,17 +1,17 @@
-export * as Npm from "./npm"
+export * as Npm from "./npm.js"
import path from "path"
import npa from "npm-package-arg"
import { Effect, Schema, Context, Layer, Option, FileSystem } from "effect"
import { NodeFileSystem } from "@effect/platform-node"
-import { FSUtil } from "./fs-util"
-import { Global } from "./global"
-import { EffectFlock } from "./effect-flock"
-import { makeGlobalNode } from "./effect/app-node"
-import { filesystem } from "./effect/app-node-platform"
-import { LayerNode } from "./effect/layer-node"
-import { makeRuntime } from "./effect/runtime"
-import { NpmConfig } from "./npm-config"
+import { FSUtil } from "./fs-util.js"
+import { Global } from "./global.js"
+import { EffectFlock } from "./effect-flock.js"
+import { makeGlobalNode } from "./effect/app-node.js"
+import { filesystem } from "./effect/app-node-platform.js"
+import { LayerNode } from "./effect/layer-node.js"
+import { makeRuntime } from "./effect/runtime.js"
+import { NpmConfig } from "./npm-config.js"
import { resolveModule } from "#runtime-import"
export class InstallFailedError extends Schema.TaggedErrorClass()("NpmInstallFailedError", {
diff --git a/packages/util/src/observability.ts b/packages/util/src/observability.ts
index 5d32791c2a..f356aa03fb 100644
--- a/packages/util/src/observability.ts
+++ b/packages/util/src/observability.ts
@@ -1,13 +1,13 @@
-export * as Observability from "./observability"
+export * as Observability from "./observability.js"
import { NodeFileSystem } from "@effect/platform-node"
-import { LayerNode } from "./effect/layer-node"
+import { LayerNode } from "./effect/layer-node.js"
import { Effect, Layer, Logger, References, Schema } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
import { OtlpSerialization } from "effect/unstable/observability"
-import { Logging } from "./observability/logging"
-import { Otlp } from "./observability/otlp"
-import { Client } from "./client"
+import { Logging } from "./observability/logging.js"
+import { Otlp } from "./observability/otlp.js"
+import { Client } from "./client.js"
export const Options = Schema.Struct({
endpoint: Schema.optional(Schema.String),
diff --git a/packages/util/src/observability/logging.ts b/packages/util/src/observability/logging.ts
index 1588c6f764..b83c0cc150 100644
--- a/packages/util/src/observability/logging.ts
+++ b/packages/util/src/observability/logging.ts
@@ -1,8 +1,8 @@
import { Formatter, Logger, type LogLevel } from "effect"
import path from "path"
-import { Global } from "../global"
-import { InstallationChannel, InstallationLocal } from "../installation/version"
-import { runID } from "./shared"
+import { Global } from "../global.js"
+import { InstallationChannel, InstallationLocal } from "../installation/version.js"
+import { runID } from "./shared.js"
function formatter(id: string = runID) {
return Logger.map(Logger.formatStructured, (output) => {
@@ -74,4 +74,4 @@ export function loggers() {
return process.env.OPENCODE_PRINT_LOGS === "1" ? [fileLogger(), stderrLogger] : [fileLogger()]
}
-export * as Logging from "./logging"
+export * as Logging from "./logging.js"
diff --git a/packages/util/src/observability/otlp.ts b/packages/util/src/observability/otlp.ts
index 14b781e5fa..9ac4b7ce6d 100644
--- a/packages/util/src/observability/otlp.ts
+++ b/packages/util/src/observability/otlp.ts
@@ -1,7 +1,7 @@
import { Layer } from "effect"
import { OtlpLogger } from "effect/unstable/observability"
-import { InstallationChannel, InstallationVersion } from "../installation/version"
-import { runID } from "./shared"
+import { InstallationChannel, InstallationVersion } from "../installation/version.js"
+import { runID } from "./shared.js"
export interface Options {
readonly endpoint?: string
@@ -90,4 +90,4 @@ export async function tracingLayer(options: Options | undefined, client: string)
}))
}
-export * as Otlp from "./otlp"
+export * as Otlp from "./otlp.js"
diff --git a/packages/util/src/patch.ts b/packages/util/src/patch.ts
index 3ab103fa97..600c604401 100644
--- a/packages/util/src/patch.ts
+++ b/packages/util/src/patch.ts
@@ -1,4 +1,4 @@
-export * as Patch from "./patch"
+export * as Patch from "./patch.js"
import { Result, Schema } from "effect"
diff --git a/packages/util/src/process.ts b/packages/util/src/process.ts
index 16e9118d1f..790bfa3be4 100644
--- a/packages/util/src/process.ts
+++ b/packages/util/src/process.ts
@@ -2,8 +2,8 @@ import { Context, Duration, Effect, Fiber, Layer, Schema, Stream } from "effect"
import type { PlatformError } from "effect/PlatformError"
import { ChildProcess } from "effect/unstable/process"
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
-import { CrossSpawnSpawner } from "./cross-spawn-spawner"
-import { makeGlobalNode } from "./effect/app-node"
+import { CrossSpawnSpawner } from "./cross-spawn-spawner.js"
+import { makeGlobalNode } from "./effect/app-node.js"
export class AppProcessError extends Schema.TaggedErrorClass()("AppProcessError", {
command: Schema.String,
@@ -258,4 +258,4 @@ const layer = Layer.effect(
export const node = makeGlobalNode({ service: Service, layer: layer, deps: [CrossSpawnSpawner.node] })
-export * as AppProcess from "./process"
+export * as AppProcess from "./process.js"
diff --git a/packages/util/tsconfig.build.json b/packages/util/tsconfig.build.json
new file mode 100644
index 0000000000..e235ae78cf
--- /dev/null
+++ b/packages/util/tsconfig.build.json
@@ -0,0 +1,11 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "rootDir": "src",
+ "outDir": "dist",
+ "noEmit": false,
+ "declaration": true
+ },
+ "include": ["src"]
+}
diff --git a/packages/util/tsconfig.json b/packages/util/tsconfig.json
index fe5c4d217b..189db6f3e9 100644
--- a/packages/util/tsconfig.json
+++ b/packages/util/tsconfig.json
@@ -2,6 +2,11 @@
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@tsconfig/bun/tsconfig.json",
"compilerOptions": {
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "allowImportingTsExtensions": false,
+ "allowJs": false,
"noUncheckedIndexedAccess": false
}
}
diff --git a/script/publish.ts b/script/publish.ts
index 7b5ca8a39d..cfb3aace13 100755
--- a/script/publish.ts
+++ b/script/publish.ts
@@ -41,6 +41,9 @@ await $`bun ./packages/schema/script/publish.ts`
console.log("\n=== ai ===\n")
await $`bun ./packages/ai/script/publish.ts`
+console.log("\n=== util ===\n")
+await $`bun ./packages/util/script/publish.ts`
+
console.log("\n=== protocol ===\n")
await $`bun ./packages/protocol/script/publish.ts`