mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-22 03:45:23 +00:00
sync
This commit is contained in:
@@ -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<typeof Info>
|
||||
const state = State.create(
|
||||
() => Session.use().directory,
|
||||
() => Paths.use().directory,
|
||||
async () => {
|
||||
const cfg = await Config.get()
|
||||
const result: Record<string, Info> = {
|
||||
|
||||
@@ -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<State>(
|
||||
key: any,
|
||||
init: (app: Info) => State,
|
||||
shutdown?: (state: Awaited<State>) => Promise<void>,
|
||||
) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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<any, Subscription[]>()
|
||||
|
||||
return {
|
||||
subscriptions,
|
||||
}
|
||||
})
|
||||
const subscriptions = new Map<any, Subscription[]>()
|
||||
|
||||
export type EventDefinition = ReturnType<typeof event>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<T>(input: App.Input, cb: (app: App.Info) => Prom
|
||||
Share.init()
|
||||
Format.init()
|
||||
Plugin.init()
|
||||
ConfigHooks.init()
|
||||
LSP.init()
|
||||
Snapshot.init()
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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",
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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<string, boolean> = {}
|
||||
const cfg = await Config.get()
|
||||
const state = State.create(
|
||||
() => Paths.use().directory,
|
||||
async () => {
|
||||
const enabled: Record<string, boolean> = {}
|
||||
const cfg = await Config.get()
|
||||
|
||||
const formatters = { ...Formatter } as Record<string, Formatter.Info>
|
||||
for (const [name, item] of Object.entries(cfg.formatter ?? {})) {
|
||||
if (item.disabled) {
|
||||
delete formatters[name]
|
||||
continue
|
||||
const formatters = { ...Formatter } as Record<string, Formatter.Info>
|
||||
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()
|
||||
|
||||
@@ -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<typeof DocumentSymbol>
|
||||
|
||||
const state = State.create(
|
||||
() => Worktree.use(),
|
||||
() => Paths.use().worktree,
|
||||
async () => {
|
||||
const clients: LSPClient.Info[] = []
|
||||
const servers: Record<string, LSPServer.Info> = LSPServer
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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]: {
|
||||
|
||||
@@ -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<PluginInstance>(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<PluginInstance>(mod)) {
|
||||
const init = await fn({
|
||||
client,
|
||||
app,
|
||||
$: Bun.$,
|
||||
})
|
||||
hooks.push(init)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
hooks,
|
||||
}
|
||||
})
|
||||
return {
|
||||
hooks,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export async function trigger<
|
||||
Name extends keyof Required<Hooks>,
|
||||
|
||||
13
packages/opencode/src/project/path.ts
Normal file
13
packages/opencode/src/project/path.ts
Normal file
@@ -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
|
||||
},
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Context } from "../util/context"
|
||||
|
||||
export namespace Worktree {
|
||||
const context = Context.create<string>("worktree")
|
||||
export const use = context.use
|
||||
export const provide = context.provide
|
||||
}
|
||||
@@ -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<any>
|
||||
options: Record<string, any>
|
||||
}
|
||||
} = {}
|
||||
const models = new Map<string, { info: ModelsDev.Model; language: LanguageModel }>()
|
||||
const sdk = new Map<string, SDK>()
|
||||
|
||||
log.info("init")
|
||||
|
||||
function mergeProvider(
|
||||
id: string,
|
||||
options: Record<string, any>,
|
||||
source: Source,
|
||||
getModel?: (sdk: any, modelID: string) => Promise<any>,
|
||||
) {
|
||||
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<any>
|
||||
options: Record<string, any>
|
||||
}
|
||||
return
|
||||
}
|
||||
provider.options = mergeDeep(provider.options, options)
|
||||
provider.source = source
|
||||
provider.getModel = getModel ?? provider.getModel
|
||||
}
|
||||
} = {}
|
||||
const models = new Map<string, { info: ModelsDev.Model; language: LanguageModel }>()
|
||||
const sdk = new Map<string, SDK>()
|
||||
|
||||
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<string, any>,
|
||||
source: Source,
|
||||
getModel?: (sdk: any, modelID: string) => Promise<any>,
|
||||
) {
|
||||
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)
|
||||
|
||||
@@ -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<Info>("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<string, AbortController>()
|
||||
const autoCompacting = new Map<string, boolean>()
|
||||
|
||||
@@ -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<typeof Info>
|
||||
const state = App.state("mode", async () => {
|
||||
const cfg = await Config.get()
|
||||
const model = cfg.model ? Provider.parseModel(cfg.model) : undefined
|
||||
const result: Record<string, Info> = {
|
||||
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<string, Info> = {
|
||||
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])
|
||||
|
||||
@@ -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<void>
|
||||
|
||||
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<T>(key: string) {
|
||||
const dir = await state().then((x) => x.dir)
|
||||
return Bun.file(path.join(dir, key + ".json")).json() as Promise<T>
|
||||
}
|
||||
|
||||
export async function writeJSON<T>(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 []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<typeof TodoInfo>
|
||||
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user