sync
This commit is contained in:
parent
390b660c81
commit
323ce486a5
20 changed files with 303 additions and 534 deletions
|
|
@ -5,7 +5,7 @@ import { generateObject, type ModelMessage } from "ai"
|
||||||
import PROMPT_GENERATE from "./generate.txt"
|
import PROMPT_GENERATE from "./generate.txt"
|
||||||
import { SystemPrompt } from "../session/system"
|
import { SystemPrompt } from "../session/system"
|
||||||
import { State } from "../project/state"
|
import { State } from "../project/state"
|
||||||
import { Session } from "../session"
|
import { Paths } from "../project/path"
|
||||||
|
|
||||||
export namespace Agent {
|
export namespace Agent {
|
||||||
export const Info = z
|
export const Info = z
|
||||||
|
|
@ -26,7 +26,7 @@ export namespace Agent {
|
||||||
})
|
})
|
||||||
export type Info = z.infer<typeof Info>
|
export type Info = z.infer<typeof Info>
|
||||||
const state = State.create(
|
const state = State.create(
|
||||||
() => Session.use().directory,
|
() => Paths.use().directory,
|
||||||
async () => {
|
async () => {
|
||||||
const cfg = await Config.get()
|
const cfg = await Config.get()
|
||||||
const result: Record<string, Info> = {
|
const result: Record<string, Info> = {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import path from "path"
|
||||||
import os from "os"
|
import os from "os"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { Project } from "../project/project"
|
import { Project } from "../project/project"
|
||||||
import { Worktree } from "../project/worktree"
|
import { Paths } from "../project/path"
|
||||||
|
|
||||||
export namespace App {
|
export namespace App {
|
||||||
const log = Log.create({ service: "app" })
|
const log = Log.create({ service: "app" })
|
||||||
|
|
@ -100,41 +100,28 @@ export namespace App {
|
||||||
|
|
||||||
return ctx.provide(app, async () => {
|
return ctx.provide(app, async () => {
|
||||||
return Project.provide(project, async () => {
|
return Project.provide(project, async () => {
|
||||||
return Worktree.provide(project.worktree, async () => {
|
return Paths.provide(
|
||||||
try {
|
{
|
||||||
const result = await cb(app.info)
|
worktree: app.info.path.root,
|
||||||
return result
|
directory: app.info.path.cwd,
|
||||||
} finally {
|
},
|
||||||
for (const [key, entry] of app.services.entries()) {
|
async () => {
|
||||||
if (!entry.shutdown) continue
|
try {
|
||||||
log.info("shutdown", { name: key })
|
const result = await cb(app.info)
|
||||||
await entry.shutdown?.(await entry.state)
|
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() {
|
export function info() {
|
||||||
return ctx.use().info
|
return ctx.use().info
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,11 @@
|
||||||
import { z, type ZodType } from "zod"
|
import { z, type ZodType } from "zod"
|
||||||
import { App } from "../app/app"
|
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
|
|
||||||
export namespace Bus {
|
export namespace Bus {
|
||||||
const log = Log.create({ service: "bus" })
|
const log = Log.create({ service: "bus" })
|
||||||
type Subscription = (event: any) => void
|
type Subscription = (event: any) => void
|
||||||
|
|
||||||
const state = App.state("bus", () => {
|
const subscriptions = new Map<any, Subscription[]>()
|
||||||
const subscriptions = new Map<any, Subscription[]>()
|
|
||||||
|
|
||||||
return {
|
|
||||||
subscriptions,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export type EventDefinition = ReturnType<typeof event>
|
export type EventDefinition = ReturnType<typeof event>
|
||||||
|
|
||||||
|
|
@ -63,7 +56,7 @@ export namespace Bus {
|
||||||
})
|
})
|
||||||
const pending = []
|
const pending = []
|
||||||
for (const key of [def.type, "*"]) {
|
for (const key of [def.type, "*"]) {
|
||||||
const match = state().subscriptions.get(key)
|
const match = subscriptions.get(key)
|
||||||
for (const sub of match ?? []) {
|
for (const sub of match ?? []) {
|
||||||
pending.push(sub(payload))
|
pending.push(sub(payload))
|
||||||
}
|
}
|
||||||
|
|
@ -96,7 +89,6 @@ export namespace Bus {
|
||||||
|
|
||||||
function raw(type: string, callback: (event: any) => void) {
|
function raw(type: string, callback: (event: any) => void) {
|
||||||
log.info("subscribing", { type })
|
log.info("subscribing", { type })
|
||||||
const subscriptions = state().subscriptions
|
|
||||||
let match = subscriptions.get(type) ?? []
|
let match = subscriptions.get(type) ?? []
|
||||||
match.push(callback)
|
match.push(callback)
|
||||||
subscriptions.set(type, match)
|
subscriptions.set(type, match)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { App } from "../app/app"
|
import { App } from "../app/app"
|
||||||
import { ConfigHooks } from "../config/hooks"
|
|
||||||
import { Format } from "../format"
|
import { Format } from "../format"
|
||||||
import { LSP } from "../lsp"
|
import { LSP } from "../lsp"
|
||||||
import { Plugin } from "../plugin"
|
import { Plugin } from "../plugin"
|
||||||
|
|
@ -11,7 +10,6 @@ export async function bootstrap<T>(input: App.Input, cb: (app: App.Info) => Prom
|
||||||
Share.init()
|
Share.init()
|
||||||
Format.init()
|
Format.init()
|
||||||
Plugin.init()
|
Plugin.init()
|
||||||
ConfigHooks.init()
|
|
||||||
LSP.init()
|
LSP.init()
|
||||||
Snapshot.init()
|
Snapshot.init()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,18 +14,19 @@ import { Flag } from "../flag/flag"
|
||||||
import { Auth } from "../auth"
|
import { Auth } from "../auth"
|
||||||
import { type ParseError as JsoncParseError, parse as parseJsonc, printParseErrorCode } from "jsonc-parser"
|
import { type ParseError as JsoncParseError, parse as parseJsonc, printParseErrorCode } from "jsonc-parser"
|
||||||
import { State } from "../project/state"
|
import { State } from "../project/state"
|
||||||
import { Project } from "../project/project"
|
import { Paths } from "../project/path"
|
||||||
|
|
||||||
export namespace Config {
|
export namespace Config {
|
||||||
const log = Log.create({ service: "config" })
|
const log = Log.create({ service: "config" })
|
||||||
|
|
||||||
export const state = State.create(
|
export const state = State.create(
|
||||||
() => Project.use().worktree,
|
() => Paths.use().directory,
|
||||||
async () => {
|
async () => {
|
||||||
|
const paths = Paths.use()
|
||||||
const auth = await Auth.all()
|
const auth = await Auth.all()
|
||||||
let result = await global()
|
let result = await global()
|
||||||
for (const file of ["opencode.jsonc", "opencode.json"]) {
|
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()) {
|
for (const resolved of found.toReversed()) {
|
||||||
result = mergeDeep(result, await loadFile(resolved))
|
result = mergeDeep(result, await loadFile(resolved))
|
||||||
}
|
}
|
||||||
|
|
@ -48,7 +49,7 @@ export namespace Config {
|
||||||
result.agent = result.agent || {}
|
result.agent = result.agent || {}
|
||||||
const markdownAgents = [
|
const markdownAgents = [
|
||||||
...(await Filesystem.globUp("agent/*.md", Global.Path.config, Global.Path.config)),
|
...(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) {
|
for (const item of markdownAgents) {
|
||||||
const content = await Bun.file(item).text()
|
const content = await Bun.file(item).text()
|
||||||
|
|
@ -74,7 +75,7 @@ export namespace Config {
|
||||||
result.mode = result.mode || {}
|
result.mode = result.mode || {}
|
||||||
const markdownModes = [
|
const markdownModes = [
|
||||||
...(await Filesystem.globUp("mode/*.md", Global.Path.config, Global.Path.config)),
|
...(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) {
|
for (const item of markdownModes) {
|
||||||
const content = await Bun.file(item).text()
|
const content = await Bun.file(item).text()
|
||||||
|
|
@ -100,7 +101,7 @@ export namespace Config {
|
||||||
result.plugin.push(
|
result.plugin.push(
|
||||||
...[
|
...[
|
||||||
...(await Filesystem.globUp("plugin/*.ts", Global.Path.config, Global.Path.config)),
|
...(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),
|
].map((x) => "file://" + x),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -328,32 +329,7 @@ export namespace Config {
|
||||||
bash: z.union([Permission, z.record(z.string(), Permission)]).optional(),
|
bash: z.union([Permission, z.record(z.string(), Permission)]).optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
experimental: z
|
experimental: z.object({}).optional(),
|
||||||
.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(),
|
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.openapi({
|
.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 { App } from "../app/app"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { Flag } from "../flag/flag"
|
import { Flag } from "../flag/flag"
|
||||||
|
import { Paths } from "../project/path"
|
||||||
|
|
||||||
export namespace FileWatcher {
|
export namespace FileWatcher {
|
||||||
const log = Log.create({ service: "file.watcher" })
|
const log = Log.create({ service: "file.watcher" })
|
||||||
|
|
@ -18,7 +19,7 @@ export namespace FileWatcher {
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
const state = App.state(
|
const state = App.state(
|
||||||
"file.watcher",
|
() => Paths.use().worktree,
|
||||||
() => {
|
() => {
|
||||||
const app = App.use()
|
const app = App.use()
|
||||||
if (!app.info.git) return {}
|
if (!app.info.git) return {}
|
||||||
|
|
|
||||||
|
|
@ -7,35 +7,40 @@ import path from "path"
|
||||||
import * as Formatter from "./formatter"
|
import * as Formatter from "./formatter"
|
||||||
import { Config } from "../config/config"
|
import { Config } from "../config/config"
|
||||||
import { mergeDeep } from "remeda"
|
import { mergeDeep } from "remeda"
|
||||||
|
import { State } from "../project/state"
|
||||||
|
import { Paths } from "../project/path"
|
||||||
|
|
||||||
export namespace Format {
|
export namespace Format {
|
||||||
const log = Log.create({ service: "format" })
|
const log = Log.create({ service: "format" })
|
||||||
|
|
||||||
const state = App.state("format", async () => {
|
const state = State.create(
|
||||||
const enabled: Record<string, boolean> = {}
|
() => Paths.use().directory,
|
||||||
const cfg = await Config.get()
|
async () => {
|
||||||
|
const enabled: Record<string, boolean> = {}
|
||||||
|
const cfg = await Config.get()
|
||||||
|
|
||||||
const formatters = { ...Formatter } as Record<string, Formatter.Info>
|
const formatters = { ...Formatter } as Record<string, Formatter.Info>
|
||||||
for (const [name, item] of Object.entries(cfg.formatter ?? {})) {
|
for (const [name, item] of Object.entries(cfg.formatter ?? {})) {
|
||||||
if (item.disabled) {
|
if (item.disabled) {
|
||||||
delete formatters[name]
|
delete formatters[name]
|
||||||
continue
|
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 {
|
return {
|
||||||
enabled,
|
enabled,
|
||||||
formatters,
|
formatters,
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
)
|
||||||
|
|
||||||
async function isEnabled(item: Formatter.Info) {
|
async function isEnabled(item: Formatter.Info) {
|
||||||
const s = await state()
|
const s = await state()
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { z } from "zod"
|
||||||
import { Config } from "../config/config"
|
import { Config } from "../config/config"
|
||||||
import { spawn } from "child_process"
|
import { spawn } from "child_process"
|
||||||
import { State } from "../project/state"
|
import { State } from "../project/state"
|
||||||
import { Worktree } from "../project/worktree"
|
import { Paths } from "../project/path"
|
||||||
|
|
||||||
export namespace LSP {
|
export namespace LSP {
|
||||||
const log = Log.create({ service: "lsp" })
|
const log = Log.create({ service: "lsp" })
|
||||||
|
|
@ -56,7 +56,7 @@ export namespace LSP {
|
||||||
export type DocumentSymbol = z.infer<typeof DocumentSymbol>
|
export type DocumentSymbol = z.infer<typeof DocumentSymbol>
|
||||||
|
|
||||||
const state = State.create(
|
const state = State.create(
|
||||||
() => Worktree.use(),
|
() => Paths.use().worktree,
|
||||||
async () => {
|
async () => {
|
||||||
const clients: LSPClient.Info[] = []
|
const clients: LSPClient.Info[] = []
|
||||||
const servers: Record<string, LSPServer.Info> = LSPServer
|
const servers: Record<string, LSPServer.Info> = LSPServer
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { NamedError } from "../util/error"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { Session } from "../session"
|
import { Session } from "../session"
|
||||||
import { Bus } from "../bus"
|
import { Bus } from "../bus"
|
||||||
|
import { State } from "../project/state"
|
||||||
|
|
||||||
export namespace MCP {
|
export namespace MCP {
|
||||||
const log = Log.create({ service: "mcp" })
|
const log = Log.create({ service: "mcp" })
|
||||||
|
|
@ -20,7 +21,7 @@ export namespace MCP {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const state = App.state(
|
const state = State.create(
|
||||||
"mcp",
|
"mcp",
|
||||||
async () => {
|
async () => {
|
||||||
const cfg = await Config.get()
|
const cfg = await Config.get()
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { App } from "../app/app"
|
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { Bus } from "../bus"
|
import { Bus } from "../bus"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { Identifier } from "../id/id"
|
import { Identifier } from "../id/id"
|
||||||
import { Plugin } from "../plugin"
|
import { Plugin } from "../plugin"
|
||||||
|
import { State } from "../project/state"
|
||||||
|
import { Project } from "../project/project"
|
||||||
|
|
||||||
export namespace Permission {
|
export namespace Permission {
|
||||||
const log = Log.create({ service: "permission" })
|
const log = Log.create({ service: "permission" })
|
||||||
|
|
@ -35,8 +36,8 @@ export namespace Permission {
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = App.state(
|
const state = State.create(
|
||||||
"permission",
|
() => Project.use().id,
|
||||||
() => {
|
() => {
|
||||||
const pending: {
|
const pending: {
|
||||||
[sessionID: string]: {
|
[sessionID: string]: {
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,47 @@
|
||||||
import type { Hooks, Plugin as PluginInstance } from "@opencode-ai/plugin"
|
import type { Hooks, Plugin as PluginInstance } from "@opencode-ai/plugin"
|
||||||
import { App } from "../app/app"
|
|
||||||
import { Config } from "../config/config"
|
import { Config } from "../config/config"
|
||||||
import { Bus } from "../bus"
|
import { Bus } from "../bus"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { createOpencodeClient } from "@opencode-ai/sdk"
|
import { createOpencodeClient } from "@opencode-ai/sdk"
|
||||||
import { Server } from "../server/server"
|
import { Server } from "../server/server"
|
||||||
import { BunProc } from "../bun"
|
import { BunProc } from "../bun"
|
||||||
|
import { State } from "../project/state"
|
||||||
|
import { Paths } from "../project/path"
|
||||||
|
|
||||||
export namespace Plugin {
|
export namespace Plugin {
|
||||||
const log = Log.create({ service: "plugin" })
|
const log = Log.create({ service: "plugin" })
|
||||||
|
|
||||||
const state = App.state("plugin", async (app) => {
|
const state = State.create(
|
||||||
const client = createOpencodeClient({
|
() => Paths.directory,
|
||||||
baseUrl: "http://localhost:4096",
|
async () => {
|
||||||
fetch: async (...args) => Server.app().fetch(...args),
|
const client = createOpencodeClient({
|
||||||
})
|
baseUrl: "http://localhost:4096",
|
||||||
const config = await Config.get()
|
fetch: async (...args) => Server.app().fetch(...args),
|
||||||
const hooks = []
|
})
|
||||||
for (let plugin of config.plugin ?? []) {
|
const config = await Config.get()
|
||||||
log.info("loading plugin", { path: plugin })
|
const hooks = []
|
||||||
if (!plugin.startsWith("file://")) {
|
for (let plugin of config.plugin ?? []) {
|
||||||
const [pkg, version] = plugin.split("@")
|
log.info("loading plugin", { path: plugin })
|
||||||
plugin = await BunProc.install(pkg, version ?? "latest")
|
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 {
|
return {
|
||||||
hooks,
|
hooks,
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
)
|
||||||
|
|
||||||
export async function trigger<
|
export async function trigger<
|
||||||
Name extends keyof Required<Hooks>,
|
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() {
|
export async function dispose() {
|
||||||
for (const [key, entry] of entries.entries()) {
|
for (const [_, entry] of entries.entries()) {
|
||||||
if (!entry.dispose) continue
|
if (!entry.dispose) continue
|
||||||
await entry.dispose(await entry.state)
|
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 z from "zod"
|
||||||
import { App } from "../app/app"
|
|
||||||
import { Config } from "../config/config"
|
import { Config } from "../config/config"
|
||||||
import { mergeDeep, sortBy } from "remeda"
|
import { mergeDeep, sortBy } from "remeda"
|
||||||
import { NoSuchModelError, type LanguageModel, type Provider as SDK } from "ai"
|
import { NoSuchModelError, type LanguageModel, type Provider as SDK } from "ai"
|
||||||
|
|
@ -10,6 +9,8 @@ import { AuthCopilot } from "../auth/copilot"
|
||||||
import { ModelsDev } from "./models"
|
import { ModelsDev } from "./models"
|
||||||
import { NamedError } from "../util/error"
|
import { NamedError } from "../util/error"
|
||||||
import { Auth } from "../auth"
|
import { Auth } from "../auth"
|
||||||
|
import { State } from "../project/state"
|
||||||
|
import { Paths } from "../project/path"
|
||||||
|
|
||||||
export namespace Provider {
|
export namespace Provider {
|
||||||
const log = Log.create({ service: "provider" })
|
const log = Log.create({ service: "provider" })
|
||||||
|
|
@ -216,149 +217,152 @@ export namespace Provider {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = App.state("provider", async () => {
|
const state = State.create(
|
||||||
const config = await Config.get()
|
() => Paths.directory,
|
||||||
const database = await ModelsDev.get()
|
async () => {
|
||||||
|
const config = await Config.get()
|
||||||
|
const database = await ModelsDev.get()
|
||||||
|
|
||||||
const providers: {
|
const providers: {
|
||||||
[providerID: string]: {
|
[providerID: string]: {
|
||||||
source: Source
|
source: Source
|
||||||
info: ModelsDev.Provider
|
info: ModelsDev.Provider
|
||||||
getModel?: (sdk: any, modelID: string) => Promise<any>
|
getModel?: (sdk: any, modelID: string) => Promise<any>
|
||||||
options: Record<string, 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,
|
|
||||||
}
|
}
|
||||||
return
|
} = {}
|
||||||
}
|
const models = new Map<string, { info: ModelsDev.Model; language: LanguageModel }>()
|
||||||
provider.options = mergeDeep(provider.options, options)
|
const sdk = new Map<string, SDK>()
|
||||||
provider.source = source
|
|
||||||
provider.getModel = getModel ?? provider.getModel
|
|
||||||
}
|
|
||||||
|
|
||||||
const configProviders = Object.entries(config.provider ?? {})
|
log.info("init")
|
||||||
|
|
||||||
for (const [providerID, provider] of configProviders) {
|
function mergeProvider(
|
||||||
const existing = database[providerID]
|
id: string,
|
||||||
const parsed: ModelsDev.Provider = {
|
options: Record<string, any>,
|
||||||
id: providerID,
|
source: Source,
|
||||||
npm: provider.npm ?? existing?.npm,
|
getModel?: (sdk: any, modelID: string) => Promise<any>,
|
||||||
name: provider.name ?? existing?.name ?? providerID,
|
) {
|
||||||
env: provider.env ?? existing?.env ?? [],
|
const provider = providers[id]
|
||||||
api: provider.api ?? existing?.api,
|
if (!provider) {
|
||||||
models: existing?.models ?? {},
|
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 configProviders = Object.entries(config.provider ?? {})
|
||||||
const existing = parsed.models[modelID]
|
|
||||||
const parsedModel: ModelsDev.Model = {
|
for (const [providerID, provider] of configProviders) {
|
||||||
id: modelID,
|
const existing = database[providerID]
|
||||||
name: model.name ?? existing?.name ?? modelID,
|
const parsed: ModelsDev.Provider = {
|
||||||
release_date: model.release_date ?? existing?.release_date,
|
id: providerID,
|
||||||
attachment: model.attachment ?? existing?.attachment ?? false,
|
npm: provider.npm ?? existing?.npm,
|
||||||
reasoning: model.reasoning ?? existing?.reasoning ?? false,
|
name: provider.name ?? existing?.name ?? providerID,
|
||||||
temperature: model.temperature ?? existing?.temperature ?? false,
|
env: provider.env ?? existing?.env ?? [],
|
||||||
tool_call: model.tool_call ?? existing?.tool_call ?? true,
|
api: provider.api ?? existing?.api,
|
||||||
cost:
|
models: existing?.models ?? {},
|
||||||
!model.cost && !existing?.cost
|
}
|
||||||
? {
|
|
||||||
input: 0,
|
for (const [modelID, model] of Object.entries(provider.models ?? {})) {
|
||||||
output: 0,
|
const existing = parsed.models[modelID]
|
||||||
cache_read: 0,
|
const parsedModel: ModelsDev.Model = {
|
||||||
cache_write: 0,
|
id: modelID,
|
||||||
}
|
name: model.name ?? existing?.name ?? modelID,
|
||||||
: {
|
release_date: model.release_date ?? existing?.release_date,
|
||||||
cache_read: 0,
|
attachment: model.attachment ?? existing?.attachment ?? false,
|
||||||
cache_write: 0,
|
reasoning: model.reasoning ?? existing?.reasoning ?? false,
|
||||||
...existing?.cost,
|
temperature: model.temperature ?? existing?.temperature ?? false,
|
||||||
...model.cost,
|
tool_call: model.tool_call ?? existing?.tool_call ?? true,
|
||||||
},
|
cost:
|
||||||
options: {
|
!model.cost && !existing?.cost
|
||||||
...existing?.options,
|
? {
|
||||||
...model.options,
|
input: 0,
|
||||||
},
|
output: 0,
|
||||||
limit: model.limit ??
|
cache_read: 0,
|
||||||
existing?.limit ?? {
|
cache_write: 0,
|
||||||
context: 0,
|
}
|
||||||
output: 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 ?? []))
|
const disabled = await Config.get().then((cfg) => new Set(cfg.disabled_providers ?? []))
|
||||||
// load env
|
// load env
|
||||||
for (const [providerID, provider] of Object.entries(database)) {
|
for (const [providerID, provider] of Object.entries(database)) {
|
||||||
if (disabled.has(providerID)) continue
|
if (disabled.has(providerID)) continue
|
||||||
const apiKey = provider.env.map((item) => process.env[item]).at(0)
|
const apiKey = provider.env.map((item) => process.env[item]).at(0)
|
||||||
if (!apiKey) continue
|
if (!apiKey) continue
|
||||||
mergeProvider(
|
mergeProvider(
|
||||||
providerID,
|
providerID,
|
||||||
// only include apiKey if there's only one potential option
|
// only include apiKey if there's only one potential option
|
||||||
provider.env.length === 1 ? { apiKey } : {},
|
provider.env.length === 1 ? { apiKey } : {},
|
||||||
"env",
|
"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")
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// load custom
|
// load apikeys
|
||||||
for (const [providerID, fn] of Object.entries(CUSTOM_LOADERS)) {
|
for (const [providerID, provider] of Object.entries(await Auth.all())) {
|
||||||
if (disabled.has(providerID)) continue
|
if (disabled.has(providerID)) continue
|
||||||
const result = await fn(database[providerID])
|
if (provider.type === "api") {
|
||||||
if (result && (result.autoload || providers[providerID])) {
|
mergeProvider(providerID, { apiKey: provider.key }, "api")
|
||||||
mergeProvider(providerID, result.options ?? {}, "custom", result.getModel)
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// load config
|
// load custom
|
||||||
for (const [providerID, provider] of configProviders) {
|
for (const [providerID, fn] of Object.entries(CUSTOM_LOADERS)) {
|
||||||
mergeProvider(providerID, provider.options ?? {}, "config")
|
if (disabled.has(providerID)) continue
|
||||||
}
|
const result = await fn(database[providerID])
|
||||||
|
if (result && (result.autoload || providers[providerID])) {
|
||||||
for (const [providerID, provider] of Object.entries(providers)) {
|
mergeProvider(providerID, result.options ?? {}, "custom", result.getModel)
|
||||||
if (Object.keys(provider.info.models).length === 0) {
|
}
|
||||||
delete providers[providerID]
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
log.info("found", { providerID })
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
// load config
|
||||||
models,
|
for (const [providerID, provider] of configProviders) {
|
||||||
providers,
|
mergeProvider(providerID, provider.options ?? {}, "config")
|
||||||
sdk,
|
}
|
||||||
}
|
|
||||||
})
|
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() {
|
export async function list() {
|
||||||
return state().then((state) => state.providers)
|
return state().then((state) => state.providers)
|
||||||
|
|
|
||||||
|
|
@ -42,16 +42,12 @@ import { mergeDeep, pipe, splitWhen } from "remeda"
|
||||||
import { ToolRegistry } from "../tool/registry"
|
import { ToolRegistry } from "../tool/registry"
|
||||||
import { Plugin } from "../plugin"
|
import { Plugin } from "../plugin"
|
||||||
import { Project } from "../project/project"
|
import { Project } from "../project/project"
|
||||||
import { Context } from "../util/context"
|
import { State } from "../project/state"
|
||||||
|
import { Paths } from "../project/path"
|
||||||
|
|
||||||
export namespace Session {
|
export namespace Session {
|
||||||
const log = Log.create({ service: "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 OUTPUT_TOKEN_MAX = 32_000
|
||||||
|
|
||||||
const parentSessionTitlePrefix = "New session - "
|
const parentSessionTitlePrefix = "New session - "
|
||||||
|
|
@ -135,8 +131,8 @@ export namespace Session {
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = App.state(
|
const state = State.create(
|
||||||
"session",
|
() => Paths.directory,
|
||||||
() => {
|
() => {
|
||||||
const pending = new Map<string, AbortController>()
|
const pending = new Map<string, AbortController>()
|
||||||
const autoCompacting = new Map<string, boolean>()
|
const autoCompacting = new Map<string, boolean>()
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { App } from "../app/app"
|
|
||||||
import { Config } from "../config/config"
|
import { Config } from "../config/config"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Provider } from "../provider/provider"
|
import { Provider } from "../provider/provider"
|
||||||
|
import { State } from "../project/state"
|
||||||
|
import { Paths } from "../project/path"
|
||||||
|
|
||||||
export namespace Mode {
|
export namespace Mode {
|
||||||
export const Info = z
|
export const Info = z
|
||||||
|
|
@ -22,47 +23,50 @@ export namespace Mode {
|
||||||
ref: "Mode",
|
ref: "Mode",
|
||||||
})
|
})
|
||||||
export type Info = z.infer<typeof Info>
|
export type Info = z.infer<typeof Info>
|
||||||
const state = App.state("mode", async () => {
|
const state = State.create(
|
||||||
const cfg = await Config.get()
|
() => Paths.directory,
|
||||||
const model = cfg.model ? Provider.parseModel(cfg.model) : undefined
|
async () => {
|
||||||
const result: Record<string, Info> = {
|
const cfg = await Config.get()
|
||||||
build: {
|
const model = cfg.model ? Provider.parseModel(cfg.model) : undefined
|
||||||
model,
|
const result: Record<string, Info> = {
|
||||||
name: "build",
|
build: {
|
||||||
tools: {},
|
model,
|
||||||
},
|
name: "build",
|
||||||
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: {},
|
tools: {},
|
||||||
}
|
},
|
||||||
item.name = key
|
plan: {
|
||||||
if (value.model) item.model = Provider.parseModel(value.model)
|
name: "plan",
|
||||||
if (value.prompt) item.prompt = value.prompt
|
model,
|
||||||
if (value.temperature != undefined) item.temperature = value.temperature
|
tools: {
|
||||||
if (value.top_p != undefined) item.topP = value.top_p
|
write: false,
|
||||||
if (value.tools)
|
edit: false,
|
||||||
item.tools = {
|
patch: false,
|
||||||
...value.tools,
|
},
|
||||||
...item.tools,
|
},
|
||||||
}
|
}
|
||||||
}
|
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) {
|
export async function get(mode: string) {
|
||||||
return state().then((x) => x[mode])
|
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 { z } from "zod"
|
||||||
import { Tool } from "./tool"
|
import { Tool } from "./tool"
|
||||||
import DESCRIPTION_WRITE from "./todowrite.txt"
|
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({
|
const TodoInfo = z.object({
|
||||||
content: z.string().describe("Brief description of the task"),
|
content: z.string().describe("Brief description of the task"),
|
||||||
|
|
@ -11,12 +12,15 @@ const TodoInfo = z.object({
|
||||||
})
|
})
|
||||||
type TodoInfo = z.infer<typeof TodoInfo>
|
type TodoInfo = z.infer<typeof TodoInfo>
|
||||||
|
|
||||||
const state = App.state("todo-tool", () => {
|
const state = State.create(
|
||||||
const todos: {
|
() => Project.use().id,
|
||||||
[sessionId: string]: TodoInfo[]
|
() => {
|
||||||
} = {}
|
const todos: {
|
||||||
return todos
|
[sessionId: string]: TodoInfo[]
|
||||||
})
|
} = {}
|
||||||
|
return todos
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
export const TodoWriteTool = Tool.define("todowrite", {
|
export const TodoWriteTool = Tool.define("todowrite", {
|
||||||
description: DESCRIPTION_WRITE,
|
description: DESCRIPTION_WRITE,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue