From 323ce486a54866960621f9cc004ef7ff8439aaaf Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Wed, 6 Aug 2025 20:20:20 -0400 Subject: [PATCH] sync --- packages/opencode/src/agent/agent.ts | 4 +- packages/opencode/src/app/app.ts | 49 ++-- packages/opencode/src/bus/index.ts | 12 +- packages/opencode/src/cli/bootstrap.ts | 2 - packages/opencode/src/config/config.ts | 40 +--- packages/opencode/src/config/hooks.ts | 55 ----- packages/opencode/src/file/watch.ts | 3 +- packages/opencode/src/format/index.ts | 49 ++-- packages/opencode/src/lsp/index.ts | 4 +- packages/opencode/src/mcp/index.ts | 3 +- packages/opencode/src/permission/index.ts | 7 +- packages/opencode/src/plugin/index.ts | 58 ++--- packages/opencode/src/project/path.ts | 13 ++ packages/opencode/src/project/state.ts | 2 +- packages/opencode/src/project/worktree.ts | 7 - packages/opencode/src/provider/provider.ts | 260 +++++++++++---------- packages/opencode/src/session/index.ts | 12 +- packages/opencode/src/session/mode.ts | 84 +++---- packages/opencode/src/storage/storage.ts | 155 ------------ packages/opencode/src/tool/todo.ts | 18 +- 20 files changed, 303 insertions(+), 534 deletions(-) delete mode 100644 packages/opencode/src/config/hooks.ts create mode 100644 packages/opencode/src/project/path.ts delete mode 100644 packages/opencode/src/project/worktree.ts delete mode 100644 packages/opencode/src/storage/storage.ts diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 5117957625..96fdb62207 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -5,7 +5,7 @@ import { generateObject, type ModelMessage } from "ai" import PROMPT_GENERATE from "./generate.txt" import { SystemPrompt } from "../session/system" import { State } from "../project/state" -import { Session } from "../session" +import { Paths } from "../project/path" export namespace Agent { export const Info = z @@ -26,7 +26,7 @@ export namespace Agent { }) export type Info = z.infer const state = State.create( - () => Session.use().directory, + () => Paths.use().directory, async () => { const cfg = await Config.get() const result: Record = { diff --git a/packages/opencode/src/app/app.ts b/packages/opencode/src/app/app.ts index 504d726cc9..acc438a9b0 100644 --- a/packages/opencode/src/app/app.ts +++ b/packages/opencode/src/app/app.ts @@ -7,7 +7,7 @@ import path from "path" import os from "os" import { z } from "zod" import { Project } from "../project/project" -import { Worktree } from "../project/worktree" +import { Paths } from "../project/path" export namespace App { const log = Log.create({ service: "app" }) @@ -100,41 +100,28 @@ export namespace App { return ctx.provide(app, async () => { return Project.provide(project, async () => { - return Worktree.provide(project.worktree, async () => { - try { - const result = await cb(app.info) - return result - } finally { - for (const [key, entry] of app.services.entries()) { - if (!entry.shutdown) continue - log.info("shutdown", { name: key }) - await entry.shutdown?.(await entry.state) + return Paths.provide( + { + worktree: app.info.path.root, + directory: app.info.path.cwd, + }, + async () => { + try { + const result = await cb(app.info) + return result + } finally { + for (const [key, entry] of app.services.entries()) { + if (!entry.shutdown) continue + log.info("shutdown", { name: key }) + await entry.shutdown?.(await entry.state) + } } - } - }) + }, + ) }) }) } - export function state( - key: any, - init: (app: Info) => State, - shutdown?: (state: Awaited) => Promise, - ) { - return () => { - const app = ctx.use() - const services = app.services - if (!services.has(key)) { - log.info("registering service", { name: key }) - services.set(key, { - state: init(app.info), - shutdown, - }) - } - return services.get(key)?.state as State - } - } - export function info() { return ctx.use().info } diff --git a/packages/opencode/src/bus/index.ts b/packages/opencode/src/bus/index.ts index c13649aff4..0d810edeeb 100644 --- a/packages/opencode/src/bus/index.ts +++ b/packages/opencode/src/bus/index.ts @@ -1,18 +1,11 @@ import { z, type ZodType } from "zod" -import { App } from "../app/app" import { Log } from "../util/log" export namespace Bus { const log = Log.create({ service: "bus" }) type Subscription = (event: any) => void - const state = App.state("bus", () => { - const subscriptions = new Map() - - return { - subscriptions, - } - }) + const subscriptions = new Map() export type EventDefinition = ReturnType @@ -63,7 +56,7 @@ export namespace Bus { }) const pending = [] for (const key of [def.type, "*"]) { - const match = state().subscriptions.get(key) + const match = subscriptions.get(key) for (const sub of match ?? []) { pending.push(sub(payload)) } @@ -96,7 +89,6 @@ export namespace Bus { function raw(type: string, callback: (event: any) => void) { log.info("subscribing", { type }) - const subscriptions = state().subscriptions let match = subscriptions.get(type) ?? [] match.push(callback) subscriptions.set(type, match) diff --git a/packages/opencode/src/cli/bootstrap.ts b/packages/opencode/src/cli/bootstrap.ts index 0096650224..d26694d11f 100644 --- a/packages/opencode/src/cli/bootstrap.ts +++ b/packages/opencode/src/cli/bootstrap.ts @@ -1,5 +1,4 @@ import { App } from "../app/app" -import { ConfigHooks } from "../config/hooks" import { Format } from "../format" import { LSP } from "../lsp" import { Plugin } from "../plugin" @@ -11,7 +10,6 @@ export async function bootstrap(input: App.Input, cb: (app: App.Info) => Prom Share.init() Format.init() Plugin.init() - ConfigHooks.init() LSP.init() Snapshot.init() diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 91f5236b7e..62a65ec5dc 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -14,18 +14,19 @@ import { Flag } from "../flag/flag" import { Auth } from "../auth" import { type ParseError as JsoncParseError, parse as parseJsonc, printParseErrorCode } from "jsonc-parser" import { State } from "../project/state" -import { Project } from "../project/project" +import { Paths } from "../project/path" export namespace Config { const log = Log.create({ service: "config" }) export const state = State.create( - () => Project.use().worktree, + () => Paths.use().directory, async () => { + const paths = Paths.use() const auth = await Auth.all() let result = await global() for (const file of ["opencode.jsonc", "opencode.json"]) { - const found = await Filesystem.findUp(file, app.path.cwd, app.path.root) + const found = await Filesystem.findUp(file, paths.directory, paths.worktree) for (const resolved of found.toReversed()) { result = mergeDeep(result, await loadFile(resolved)) } @@ -48,7 +49,7 @@ export namespace Config { result.agent = result.agent || {} const markdownAgents = [ ...(await Filesystem.globUp("agent/*.md", Global.Path.config, Global.Path.config)), - ...(await Filesystem.globUp(".opencode/agent/*.md", app.path.cwd, app.path.root)), + ...(await Filesystem.globUp(".opencode/agent/*.md", paths.directory, paths.worktree)), ] for (const item of markdownAgents) { const content = await Bun.file(item).text() @@ -74,7 +75,7 @@ export namespace Config { result.mode = result.mode || {} const markdownModes = [ ...(await Filesystem.globUp("mode/*.md", Global.Path.config, Global.Path.config)), - ...(await Filesystem.globUp(".opencode/mode/*.md", app.path.cwd, app.path.root)), + ...(await Filesystem.globUp(".opencode/mode/*.md", paths.directory, paths.worktree)), ] for (const item of markdownModes) { const content = await Bun.file(item).text() @@ -100,7 +101,7 @@ export namespace Config { result.plugin.push( ...[ ...(await Filesystem.globUp("plugin/*.ts", Global.Path.config, Global.Path.config)), - ...(await Filesystem.globUp(".opencode/plugin/*.ts", app.path.cwd, app.path.root)), + ...(await Filesystem.globUp(".opencode/plugin/*.ts", paths.directory, paths.worktree)), ].map((x) => "file://" + x), ) @@ -328,32 +329,7 @@ export namespace Config { bash: z.union([Permission, z.record(z.string(), Permission)]).optional(), }) .optional(), - experimental: z - .object({ - hook: z - .object({ - file_edited: z - .record( - z.string(), - z - .object({ - command: z.string().array(), - environment: z.record(z.string(), z.string()).optional(), - }) - .array(), - ) - .optional(), - session_completed: z - .object({ - command: z.string().array(), - environment: z.record(z.string(), z.string()).optional(), - }) - .array() - .optional(), - }) - .optional(), - }) - .optional(), + experimental: z.object({}).optional(), }) .strict() .openapi({ diff --git a/packages/opencode/src/config/hooks.ts b/packages/opencode/src/config/hooks.ts deleted file mode 100644 index acb7f11171..0000000000 --- a/packages/opencode/src/config/hooks.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { App } from "../app/app" -import { Bus } from "../bus" -import { File } from "../file" -import { Session } from "../session" -import { Log } from "../util/log" -import { Config } from "./config" -import path from "path" - -export namespace ConfigHooks { - const log = Log.create({ service: "config.hooks" }) - - export function init() { - log.info("init") - - Bus.subscribe(File.Event.Edited, async (payload) => { - const cfg = await Config.get() - const ext = path.extname(payload.properties.file) - for (const item of cfg.experimental?.hook?.file_edited?.[ext] ?? []) { - log.info("file_edited", { - file: payload.properties.file, - command: item.command, - }) - Bun.spawn({ - cmd: item.command.map((x) => x.replace("$FILE", payload.properties.file)), - env: item.environment, - cwd: app.path.cwd, - stdout: "ignore", - stderr: "ignore", - }) - } - }) - - Bus.subscribe(Session.Event.Idle, async (payload) => { - const cfg = await Config.get() - if (cfg.experimental?.hook?.session_completed) { - const session = await Session.get(payload.properties.sessionID) - // Only fire hook for top-level sessions (not subagent sessions) - if (session.parentID) return - - for (const item of cfg.experimental.hook.session_completed) { - log.info("session_completed", { - command: item.command, - }) - Bun.spawn({ - cmd: item.command, - cwd: App.info().path.cwd, - env: item.environment, - stdout: "ignore", - stderr: "ignore", - }) - } - } - }) - } -} diff --git a/packages/opencode/src/file/watch.ts b/packages/opencode/src/file/watch.ts index 383ad6f362..5a84aa354c 100644 --- a/packages/opencode/src/file/watch.ts +++ b/packages/opencode/src/file/watch.ts @@ -4,6 +4,7 @@ import fs from "fs" import { App } from "../app/app" import { Log } from "../util/log" import { Flag } from "../flag/flag" +import { Paths } from "../project/path" export namespace FileWatcher { const log = Log.create({ service: "file.watcher" }) @@ -18,7 +19,7 @@ export namespace FileWatcher { ), } const state = App.state( - "file.watcher", + () => Paths.use().worktree, () => { const app = App.use() if (!app.info.git) return {} diff --git a/packages/opencode/src/format/index.ts b/packages/opencode/src/format/index.ts index d4f73c38a4..3863ae88e2 100644 --- a/packages/opencode/src/format/index.ts +++ b/packages/opencode/src/format/index.ts @@ -7,35 +7,40 @@ import path from "path" import * as Formatter from "./formatter" import { Config } from "../config/config" import { mergeDeep } from "remeda" +import { State } from "../project/state" +import { Paths } from "../project/path" export namespace Format { const log = Log.create({ service: "format" }) - const state = App.state("format", async () => { - const enabled: Record = {} - const cfg = await Config.get() + const state = State.create( + () => Paths.use().directory, + async () => { + const enabled: Record = {} + const cfg = await Config.get() - const formatters = { ...Formatter } as Record - for (const [name, item] of Object.entries(cfg.formatter ?? {})) { - if (item.disabled) { - delete formatters[name] - continue + const formatters = { ...Formatter } as Record + for (const [name, item] of Object.entries(cfg.formatter ?? {})) { + if (item.disabled) { + delete formatters[name] + continue + } + const result: Formatter.Info = mergeDeep(formatters[name] ?? {}, { + command: [], + extensions: [], + ...item, + }) + result.enabled = async () => true + result.name = name + formatters[name] = result } - const result: Formatter.Info = mergeDeep(formatters[name] ?? {}, { - command: [], - extensions: [], - ...item, - }) - result.enabled = async () => true - result.name = name - formatters[name] = result - } - return { - enabled, - formatters, - } - }) + return { + enabled, + formatters, + } + }, + ) async function isEnabled(item: Formatter.Info) { const s = await state() diff --git a/packages/opencode/src/lsp/index.ts b/packages/opencode/src/lsp/index.ts index e07004e20d..5260a5a26e 100644 --- a/packages/opencode/src/lsp/index.ts +++ b/packages/opencode/src/lsp/index.ts @@ -7,7 +7,7 @@ import { z } from "zod" import { Config } from "../config/config" import { spawn } from "child_process" import { State } from "../project/state" -import { Worktree } from "../project/worktree" +import { Paths } from "../project/path" export namespace LSP { const log = Log.create({ service: "lsp" }) @@ -56,7 +56,7 @@ export namespace LSP { export type DocumentSymbol = z.infer const state = State.create( - () => Worktree.use(), + () => Paths.use().worktree, async () => { const clients: LSPClient.Info[] = [] const servers: Record = LSPServer diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 7057be511e..763b7b3367 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -9,6 +9,7 @@ import { NamedError } from "../util/error" import { z } from "zod" import { Session } from "../session" import { Bus } from "../bus" +import { State } from "../project/state" export namespace MCP { const log = Log.create({ service: "mcp" }) @@ -20,7 +21,7 @@ export namespace MCP { }), ) - const state = App.state( + const state = State.create( "mcp", async () => { const cfg = await Config.get() diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts index 53c49696e5..37101a74ac 100644 --- a/packages/opencode/src/permission/index.ts +++ b/packages/opencode/src/permission/index.ts @@ -1,9 +1,10 @@ -import { App } from "../app/app" import { z } from "zod" import { Bus } from "../bus" import { Log } from "../util/log" import { Identifier } from "../id/id" import { Plugin } from "../plugin" +import { State } from "../project/state" +import { Project } from "../project/project" export namespace Permission { const log = Log.create({ service: "permission" }) @@ -35,8 +36,8 @@ export namespace Permission { ), } - const state = App.state( - "permission", + const state = State.create( + () => Project.use().id, () => { const pending: { [sessionID: string]: { diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index 3ffa301910..5c254a0795 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -1,43 +1,47 @@ import type { Hooks, Plugin as PluginInstance } from "@opencode-ai/plugin" -import { App } from "../app/app" import { Config } from "../config/config" import { Bus } from "../bus" import { Log } from "../util/log" import { createOpencodeClient } from "@opencode-ai/sdk" import { Server } from "../server/server" import { BunProc } from "../bun" +import { State } from "../project/state" +import { Paths } from "../project/path" export namespace Plugin { const log = Log.create({ service: "plugin" }) - const state = App.state("plugin", async (app) => { - const client = createOpencodeClient({ - baseUrl: "http://localhost:4096", - fetch: async (...args) => Server.app().fetch(...args), - }) - const config = await Config.get() - const hooks = [] - for (let plugin of config.plugin ?? []) { - log.info("loading plugin", { path: plugin }) - if (!plugin.startsWith("file://")) { - const [pkg, version] = plugin.split("@") - plugin = await BunProc.install(pkg, version ?? "latest") + const state = State.create( + () => Paths.directory, + async () => { + const client = createOpencodeClient({ + baseUrl: "http://localhost:4096", + fetch: async (...args) => Server.app().fetch(...args), + }) + const config = await Config.get() + const hooks = [] + for (let plugin of config.plugin ?? []) { + log.info("loading plugin", { path: plugin }) + if (!plugin.startsWith("file://")) { + const [pkg, version] = plugin.split("@") + plugin = await BunProc.install(pkg, version ?? "latest") + } + const mod = await import(plugin) + for (const [_name, fn] of Object.entries(mod)) { + const init = await fn({ + client, + app: null as any, + $: Bun.$, + }) + hooks.push(init) + } } - const mod = await import(plugin) - for (const [_name, fn] of Object.entries(mod)) { - const init = await fn({ - client, - app, - $: Bun.$, - }) - hooks.push(init) - } - } - return { - hooks, - } - }) + return { + hooks, + } + }, + ) export async function trigger< Name extends keyof Required, diff --git a/packages/opencode/src/project/path.ts b/packages/opencode/src/project/path.ts new file mode 100644 index 0000000000..47575935ef --- /dev/null +++ b/packages/opencode/src/project/path.ts @@ -0,0 +1,13 @@ +import { Context } from "../util/context" + +const context = Context.create<{ directory: string; worktree: string }>("path") + +export const Paths = { + provider: context.provide, + get directory() { + return context.use().directory + }, + get worktree() { + return context.use().worktree + }, +} diff --git a/packages/opencode/src/project/state.ts b/packages/opencode/src/project/state.ts index ad38136332..542c6b776b 100644 --- a/packages/opencode/src/project/state.ts +++ b/packages/opencode/src/project/state.ts @@ -21,7 +21,7 @@ export namespace State { } export async function dispose() { - for (const [key, entry] of entries.entries()) { + for (const [_, entry] of entries.entries()) { if (!entry.dispose) continue await entry.dispose(await entry.state) } diff --git a/packages/opencode/src/project/worktree.ts b/packages/opencode/src/project/worktree.ts deleted file mode 100644 index d1ac14586f..0000000000 --- a/packages/opencode/src/project/worktree.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Context } from "../util/context" - -export namespace Worktree { - const context = Context.create("worktree") - export const use = context.use - export const provide = context.provide -} diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 6e3ea85fee..00f577946f 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -1,5 +1,4 @@ import z from "zod" -import { App } from "../app/app" import { Config } from "../config/config" import { mergeDeep, sortBy } from "remeda" import { NoSuchModelError, type LanguageModel, type Provider as SDK } from "ai" @@ -10,6 +9,8 @@ import { AuthCopilot } from "../auth/copilot" import { ModelsDev } from "./models" import { NamedError } from "../util/error" import { Auth } from "../auth" +import { State } from "../project/state" +import { Paths } from "../project/path" export namespace Provider { const log = Log.create({ service: "provider" }) @@ -216,149 +217,152 @@ export namespace Provider { }, } - const state = App.state("provider", async () => { - const config = await Config.get() - const database = await ModelsDev.get() + const state = State.create( + () => Paths.directory, + async () => { + const config = await Config.get() + const database = await ModelsDev.get() - const providers: { - [providerID: string]: { - source: Source - info: ModelsDev.Provider - getModel?: (sdk: any, modelID: string) => Promise - options: Record - } - } = {} - const models = new Map() - const sdk = new Map() - - log.info("init") - - function mergeProvider( - id: string, - options: Record, - source: Source, - getModel?: (sdk: any, modelID: string) => Promise, - ) { - const provider = providers[id] - if (!provider) { - const info = database[id] - if (!info) return - if (info.api && !options["baseURL"]) options["baseURL"] = info.api - providers[id] = { - source, - info, - options, - getModel, + const providers: { + [providerID: string]: { + source: Source + info: ModelsDev.Provider + getModel?: (sdk: any, modelID: string) => Promise + options: Record } - return - } - provider.options = mergeDeep(provider.options, options) - provider.source = source - provider.getModel = getModel ?? provider.getModel - } + } = {} + const models = new Map() + const sdk = new Map() - const configProviders = Object.entries(config.provider ?? {}) + log.info("init") - for (const [providerID, provider] of configProviders) { - const existing = database[providerID] - const parsed: ModelsDev.Provider = { - id: providerID, - npm: provider.npm ?? existing?.npm, - name: provider.name ?? existing?.name ?? providerID, - env: provider.env ?? existing?.env ?? [], - api: provider.api ?? existing?.api, - models: existing?.models ?? {}, + function mergeProvider( + id: string, + options: Record, + source: Source, + getModel?: (sdk: any, modelID: string) => Promise, + ) { + const provider = providers[id] + if (!provider) { + const info = database[id] + if (!info) return + if (info.api && !options["baseURL"]) options["baseURL"] = info.api + providers[id] = { + source, + info, + options, + getModel, + } + return + } + provider.options = mergeDeep(provider.options, options) + provider.source = source + provider.getModel = getModel ?? provider.getModel } - for (const [modelID, model] of Object.entries(provider.models ?? {})) { - const existing = parsed.models[modelID] - const parsedModel: ModelsDev.Model = { - id: modelID, - name: model.name ?? existing?.name ?? modelID, - release_date: model.release_date ?? existing?.release_date, - attachment: model.attachment ?? existing?.attachment ?? false, - reasoning: model.reasoning ?? existing?.reasoning ?? false, - temperature: model.temperature ?? existing?.temperature ?? false, - tool_call: model.tool_call ?? existing?.tool_call ?? true, - cost: - !model.cost && !existing?.cost - ? { - input: 0, - output: 0, - cache_read: 0, - cache_write: 0, - } - : { - cache_read: 0, - cache_write: 0, - ...existing?.cost, - ...model.cost, - }, - options: { - ...existing?.options, - ...model.options, - }, - limit: model.limit ?? - existing?.limit ?? { - context: 0, - output: 0, + const configProviders = Object.entries(config.provider ?? {}) + + for (const [providerID, provider] of configProviders) { + const existing = database[providerID] + const parsed: ModelsDev.Provider = { + id: providerID, + npm: provider.npm ?? existing?.npm, + name: provider.name ?? existing?.name ?? providerID, + env: provider.env ?? existing?.env ?? [], + api: provider.api ?? existing?.api, + models: existing?.models ?? {}, + } + + for (const [modelID, model] of Object.entries(provider.models ?? {})) { + const existing = parsed.models[modelID] + const parsedModel: ModelsDev.Model = { + id: modelID, + name: model.name ?? existing?.name ?? modelID, + release_date: model.release_date ?? existing?.release_date, + attachment: model.attachment ?? existing?.attachment ?? false, + reasoning: model.reasoning ?? existing?.reasoning ?? false, + temperature: model.temperature ?? existing?.temperature ?? false, + tool_call: model.tool_call ?? existing?.tool_call ?? true, + cost: + !model.cost && !existing?.cost + ? { + input: 0, + output: 0, + cache_read: 0, + cache_write: 0, + } + : { + cache_read: 0, + cache_write: 0, + ...existing?.cost, + ...model.cost, + }, + options: { + ...existing?.options, + ...model.options, }, + limit: model.limit ?? + existing?.limit ?? { + context: 0, + output: 0, + }, + } + parsed.models[modelID] = parsedModel } - parsed.models[modelID] = parsedModel + database[providerID] = parsed } - database[providerID] = parsed - } - const disabled = await Config.get().then((cfg) => new Set(cfg.disabled_providers ?? [])) - // load env - for (const [providerID, provider] of Object.entries(database)) { - if (disabled.has(providerID)) continue - const apiKey = provider.env.map((item) => process.env[item]).at(0) - if (!apiKey) continue - mergeProvider( - providerID, - // only include apiKey if there's only one potential option - provider.env.length === 1 ? { apiKey } : {}, - "env", - ) - } - - // load apikeys - for (const [providerID, provider] of Object.entries(await Auth.all())) { - if (disabled.has(providerID)) continue - if (provider.type === "api") { - mergeProvider(providerID, { apiKey: provider.key }, "api") + const disabled = await Config.get().then((cfg) => new Set(cfg.disabled_providers ?? [])) + // load env + for (const [providerID, provider] of Object.entries(database)) { + if (disabled.has(providerID)) continue + const apiKey = provider.env.map((item) => process.env[item]).at(0) + if (!apiKey) continue + mergeProvider( + providerID, + // only include apiKey if there's only one potential option + provider.env.length === 1 ? { apiKey } : {}, + "env", + ) } - } - // load custom - for (const [providerID, fn] of Object.entries(CUSTOM_LOADERS)) { - if (disabled.has(providerID)) continue - const result = await fn(database[providerID]) - if (result && (result.autoload || providers[providerID])) { - mergeProvider(providerID, result.options ?? {}, "custom", result.getModel) + // load apikeys + for (const [providerID, provider] of Object.entries(await Auth.all())) { + if (disabled.has(providerID)) continue + if (provider.type === "api") { + mergeProvider(providerID, { apiKey: provider.key }, "api") + } } - } - // load config - for (const [providerID, provider] of configProviders) { - mergeProvider(providerID, provider.options ?? {}, "config") - } - - for (const [providerID, provider] of Object.entries(providers)) { - if (Object.keys(provider.info.models).length === 0) { - delete providers[providerID] - continue + // load custom + for (const [providerID, fn] of Object.entries(CUSTOM_LOADERS)) { + if (disabled.has(providerID)) continue + const result = await fn(database[providerID]) + if (result && (result.autoload || providers[providerID])) { + mergeProvider(providerID, result.options ?? {}, "custom", result.getModel) + } } - log.info("found", { providerID }) - } - return { - models, - providers, - sdk, - } - }) + // load config + for (const [providerID, provider] of configProviders) { + mergeProvider(providerID, provider.options ?? {}, "config") + } + + for (const [providerID, provider] of Object.entries(providers)) { + if (Object.keys(provider.info.models).length === 0) { + delete providers[providerID] + continue + } + log.info("found", { providerID }) + } + + return { + models, + providers, + sdk, + } + }, + ) export async function list() { return state().then((state) => state.providers) diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 6c68e96511..38d8ef6ff8 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -42,16 +42,12 @@ import { mergeDeep, pipe, splitWhen } from "remeda" import { ToolRegistry } from "../tool/registry" import { Plugin } from "../plugin" import { Project } from "../project/project" -import { Context } from "../util/context" +import { State } from "../project/state" +import { Paths } from "../project/path" export namespace Session { const log = Log.create({ service: "session" }) - const context = Context.create("session") - - export const use = context.use - export const provide = context.provide - const OUTPUT_TOKEN_MAX = 32_000 const parentSessionTitlePrefix = "New session - " @@ -135,8 +131,8 @@ export namespace Session { ), } - const state = App.state( - "session", + const state = State.create( + () => Paths.directory, () => { const pending = new Map() const autoCompacting = new Map() diff --git a/packages/opencode/src/session/mode.ts b/packages/opencode/src/session/mode.ts index baf2ba242e..5a36004663 100644 --- a/packages/opencode/src/session/mode.ts +++ b/packages/opencode/src/session/mode.ts @@ -1,7 +1,8 @@ -import { App } from "../app/app" import { Config } from "../config/config" import z from "zod" import { Provider } from "../provider/provider" +import { State } from "../project/state" +import { Paths } from "../project/path" export namespace Mode { export const Info = z @@ -22,47 +23,50 @@ export namespace Mode { ref: "Mode", }) export type Info = z.infer - const state = App.state("mode", async () => { - const cfg = await Config.get() - const model = cfg.model ? Provider.parseModel(cfg.model) : undefined - const result: Record = { - build: { - model, - name: "build", - tools: {}, - }, - plan: { - name: "plan", - model, - tools: { - write: false, - edit: false, - patch: false, - }, - }, - } - for (const [key, value] of Object.entries(cfg.mode ?? {})) { - if (value.disable) continue - let item = result[key] - if (!item) - item = result[key] = { - name: key, + const state = State.create( + () => Paths.directory, + async () => { + const cfg = await Config.get() + const model = cfg.model ? Provider.parseModel(cfg.model) : undefined + const result: Record = { + build: { + model, + name: "build", tools: {}, - } - item.name = key - if (value.model) item.model = Provider.parseModel(value.model) - if (value.prompt) item.prompt = value.prompt - if (value.temperature != undefined) item.temperature = value.temperature - if (value.top_p != undefined) item.topP = value.top_p - if (value.tools) - item.tools = { - ...value.tools, - ...item.tools, - } - } + }, + plan: { + name: "plan", + model, + tools: { + write: false, + edit: false, + patch: false, + }, + }, + } + for (const [key, value] of Object.entries(cfg.mode ?? {})) { + if (value.disable) continue + let item = result[key] + if (!item) + item = result[key] = { + name: key, + tools: {}, + } + item.name = key + if (value.model) item.model = Provider.parseModel(value.model) + if (value.prompt) item.prompt = value.prompt + if (value.temperature != undefined) item.temperature = value.temperature + if (value.top_p != undefined) item.topP = value.top_p + if (value.tools) + item.tools = { + ...value.tools, + ...item.tools, + } + } - return result - }) + return result + }, + ) export async function get(mode: string) { return state().then((x) => x[mode]) diff --git a/packages/opencode/src/storage/storage.ts b/packages/opencode/src/storage/storage.ts deleted file mode 100644 index f4efbfdfe5..0000000000 --- a/packages/opencode/src/storage/storage.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { Log } from "../util/log" -import { App } from "../app/app" -import { Bus } from "../bus" -import path from "path" -import z from "zod" -import fs from "fs/promises" -import { MessageV2 } from "../session/message-v2" -import { Identifier } from "../id/id" - -export namespace Storage { - const log = Log.create({ service: "storage" }) - - export const Event = { - Write: Bus.event("storage.write", z.object({ key: z.string(), content: z.any() })), - } - - type Migration = (dir: string) => Promise - - const MIGRATIONS: Migration[] = [ - async (dir: string) => { - try { - const files = new Bun.Glob("session/message/*/*.json").scanSync({ - cwd: dir, - absolute: true, - }) - for (const file of files) { - const content = await Bun.file(file).json() - if (!content.metadata) continue - log.info("migrating to v2 message", { file }) - try { - const result = MessageV2.fromV1(content) - await Bun.write( - file, - JSON.stringify( - { - ...result.info, - parts: result.parts, - }, - null, - 2, - ), - ) - } catch (e) { - await fs.rename(file, file.replace("storage", "broken")) - } - } - } catch {} - }, - async (dir: string) => { - const files = new Bun.Glob("session/message/*/*.json").scanSync({ - cwd: dir, - absolute: true, - }) - for (const file of files) { - try { - const { parts, ...info } = await Bun.file(file).json() - if (!parts) continue - for (const part of parts) { - const id = Identifier.ascending("part") - await Bun.write( - [dir, "session", "part", info.sessionID, info.id, id + ".json"].join("/"), - JSON.stringify({ - ...part, - id, - sessionID: info.sessionID, - messageID: info.id, - ...(part.type === "tool" ? { callID: part.id } : {}), - }), - ) - } - await Bun.write(file, JSON.stringify(info, null, 2)) - } catch (e) {} - } - }, - async (dir: string) => { - const files = new Bun.Glob("session/message/*/*.json").scanSync({ - cwd: dir, - absolute: true, - }) - for (const file of files) { - try { - const content = await Bun.file(file).json() - if (content.role === "assistant" && !content.mode) { - log.info("adding mode field to message", { file }) - content.mode = "build" - await Bun.write(file, JSON.stringify(content, null, 2)) - } - } catch (e) {} - } - }, - ] - - const state = App.state("storage", async () => { - const app = App.info() - const dir = path.normalize(path.join(app.path.data, "storage")) - await fs.mkdir(dir, { recursive: true }) - const migration = await Bun.file(path.join(dir, "migration")) - .json() - .then((x) => parseInt(x)) - .catch(() => 0) - for (let index = migration; index < MIGRATIONS.length; index++) { - log.info("running migration", { index }) - const migration = MIGRATIONS[index] - await migration(dir) - await Bun.write(path.join(dir, "migration"), (index + 1).toString()) - } - return { - dir, - } - }) - - export async function remove(key: string) { - const dir = await state().then((x) => x.dir) - const target = path.join(dir, key + ".json") - await fs.unlink(target).catch(() => {}) - } - - export async function removeDir(key: string) { - const dir = await state().then((x) => x.dir) - const target = path.join(dir, key) - await fs.rm(target, { recursive: true, force: true }).catch(() => {}) - } - - export async function readJSON(key: string) { - const dir = await state().then((x) => x.dir) - return Bun.file(path.join(dir, key + ".json")).json() as Promise - } - - export async function writeJSON(key: string, content: T) { - const dir = await state().then((x) => x.dir) - const target = path.join(dir, key + ".json") - const tmp = target + Date.now() + ".tmp" - await Bun.write(tmp, JSON.stringify(content, null, 2)) - await fs.rename(tmp, target).catch(() => {}) - await fs.unlink(tmp).catch(() => {}) - Bus.publish(Event.Write, { key, content }) - } - - const glob = new Bun.Glob("**/*") - export async function list(prefix: string) { - const dir = await state().then((x) => x.dir) - try { - const result = await Array.fromAsync( - glob.scan({ - cwd: path.join(dir, prefix), - onlyFiles: true, - }), - ).then((items) => items.map((item) => path.join(prefix, item.slice(0, -5)))) - result.sort() - return result - } catch { - return [] - } - } -} diff --git a/packages/opencode/src/tool/todo.ts b/packages/opencode/src/tool/todo.ts index adb0c5099e..fa8351ced6 100644 --- a/packages/opencode/src/tool/todo.ts +++ b/packages/opencode/src/tool/todo.ts @@ -1,7 +1,8 @@ import { z } from "zod" import { Tool } from "./tool" import DESCRIPTION_WRITE from "./todowrite.txt" -import { App } from "../app/app" +import { State } from "../project/state" +import { Project } from "../project/project" const TodoInfo = z.object({ content: z.string().describe("Brief description of the task"), @@ -11,12 +12,15 @@ const TodoInfo = z.object({ }) type TodoInfo = z.infer -const state = App.state("todo-tool", () => { - const todos: { - [sessionId: string]: TodoInfo[] - } = {} - return todos -}) +const state = State.create( + () => Project.use().id, + () => { + const todos: { + [sessionId: string]: TodoInfo[] + } = {} + return todos + }, +) export const TodoWriteTool = Tool.define("todowrite", { description: DESCRIPTION_WRITE,