This commit is contained in:
Dax Raad 2025-08-07 15:01:44 -04:00
parent 6015a47dae
commit f82f29cd24
38 changed files with 324 additions and 362 deletions

View file

@ -4,8 +4,7 @@ import { Provider } from "../provider/provider"
import { generateObject, type ModelMessage } from "ai" 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 { Instance } from "../project/instance"
import { Paths } from "../project/path"
export namespace Agent { export namespace Agent {
export const Info = z export const Info = z
@ -25,8 +24,7 @@ export namespace Agent {
ref: "Agent", ref: "Agent",
}) })
export type Info = z.infer<typeof Info> export type Info = z.infer<typeof Info>
const state = State.create( const state = Instance.state(
() => Paths.directory,
async () => { async () => {
const cfg = await Config.get() const cfg = await Config.get()
const result: Record<string, Info> = { const result: Record<string, Info> = {

View file

@ -7,8 +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 { Paths } from "../project/path" import { Instance } from "../project/instance"
import { State } from "../project/state"
export namespace App { export namespace App {
const log = Log.create({ service: "app" }) const log = Log.create({ service: "app" })
@ -101,7 +100,7 @@ export namespace App {
return ctx.provide(app, async () => { return ctx.provide(app, async () => {
return Project.provide(project, async () => { return Project.provide(project, async () => {
const result = await Paths.provide( const result = await Instance.provide(
{ {
worktree: app.info.path.root, worktree: app.info.path.root,
directory: app.info.path.cwd, directory: app.info.path.cwd,
@ -119,7 +118,7 @@ export namespace App {
} }
}, },
) )
await State.dispose(app.info.path.cwd) await Instance.dispose()
return result return result
}) })
}) })

View file

@ -1,15 +1,12 @@
import { z, type ZodType } from "zod" import { z, type ZodType } from "zod"
import { Log } from "../util/log" import { Log } from "../util/log"
import { State } from "../project/state" import { Instance } from "../project/instance"
import { Paths } from "../project/path"
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 = State.create( const state = Instance.state(() => {
() => Paths.directory,
() => {
const subscriptions = new Map<any, Subscription[]>() const subscriptions = new Map<any, Subscription[]>()
return { return {

View file

@ -1,5 +1,5 @@
import { Ripgrep } from "../../../file/ripgrep" import { Ripgrep } from "../../../file/ripgrep"
import { Paths } from "../../../project/path" import { Instance } from "../../../project/instance"
import { bootstrap } from "../../bootstrap" import { bootstrap } from "../../bootstrap"
import { cmd } from "../cmd" import { cmd } from "../cmd"
@ -17,7 +17,7 @@ const TreeCommand = cmd({
}), }),
async handler(args) { async handler(args) {
await bootstrap({ cwd: process.cwd() }, async () => { await bootstrap({ cwd: process.cwd() }, async () => {
console.log(await Ripgrep.tree({ cwd: Paths.directory, limit: args.limit })) console.log(await Ripgrep.tree({ cwd: Instance.directory, limit: args.limit }))
}) })
}, },
}) })
@ -41,7 +41,7 @@ const FilesCommand = cmd({
async handler(args) { async handler(args) {
await bootstrap({ cwd: process.cwd() }, async () => { await bootstrap({ cwd: process.cwd() }, async () => {
const files = await Ripgrep.files({ const files = await Ripgrep.files({
cwd: Paths.directory, cwd: Instance.directory,
query: args.query, query: args.query,
glob: args.glob ? [args.glob] : undefined, glob: args.glob ? [args.glob] : undefined,
limit: args.limit, limit: args.limit,

View file

@ -20,7 +20,7 @@ import { Provider } from "../../provider/provider"
import { Bus } from "../../bus" import { Bus } from "../../bus"
import { MessageV2 } from "../../session/message-v2" import { MessageV2 } from "../../session/message-v2"
import { Project } from "../../project/project" import { Project } from "../../project/project"
import { Paths } from "../../project/path" import { Instance } from "../../project/instance"
type GitHubAuthor = { type GitHubAuthor = {
login: string login: string
@ -197,7 +197,7 @@ export const GithubInstallCommand = cmd({
throw new UI.CancelledError() throw new UI.CancelledError()
} }
const [owner, repo] = parsed[1].split("/") const [owner, repo] = parsed[1].split("/")
return { owner, repo, root: Paths.worktree } return { owner, repo, root: Instance.worktree }
} }
async function promptProvider() { async function promptProvider() {

View file

@ -13,19 +13,17 @@ import matter from "gray-matter"
import { Flag } from "../flag/flag" 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 { Instance } from "../project/instance"
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 = Instance.state(
() => Paths.directory,
async () => { async () => {
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, Paths.directory, Paths.worktree) const found = await Filesystem.findUp(file, Instance.directory, Instance.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 +46,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", Paths.directory, Paths.worktree)), ...(await Filesystem.globUp(".opencode/agent/*.md", Instance.directory, Instance.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 +72,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", Paths.directory, Paths.worktree)), ...(await Filesystem.globUp(".opencode/mode/*.md", Instance.directory, Instance.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 +98,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", Paths.directory, Paths.worktree)), ...(await Filesystem.globUp(".opencode/plugin/*.ts", Instance.directory, Instance.worktree)),
].map((x) => "file://" + x), ].map((x) => "file://" + x),
) )

View file

@ -6,7 +6,7 @@ import path from "path"
import * as git from "isomorphic-git" import * as git from "isomorphic-git"
import fs from "fs" import fs from "fs"
import { Log } from "../util/log" import { Log } from "../util/log"
import { Paths } from "../project/path" import { Instance } from "../project/instance"
import { Project } from "../project/project" import { Project } from "../project/project"
export namespace File { export namespace File {
@ -38,7 +38,7 @@ export namespace File {
const project = Project.use() const project = Project.use()
if (project.vcs !== "git") return [] if (project.vcs !== "git") return []
const diffOutput = await $`git diff --numstat HEAD`.cwd(Paths.directory).quiet().nothrow().text() const diffOutput = await $`git diff --numstat HEAD`.cwd(Instance.directory).quiet().nothrow().text()
const changedFiles: Info[] = [] const changedFiles: Info[] = []
@ -56,7 +56,7 @@ export namespace File {
} }
const untrackedOutput = await $`git ls-files --others --exclude-standard` const untrackedOutput = await $`git ls-files --others --exclude-standard`
.cwd(Paths.directory) .cwd(Instance.directory)
.quiet() .quiet()
.nothrow() .nothrow()
.text() .text()
@ -65,7 +65,7 @@ export namespace File {
const untrackedFiles = untrackedOutput.trim().split("\n") const untrackedFiles = untrackedOutput.trim().split("\n")
for (const filepath of untrackedFiles) { for (const filepath of untrackedFiles) {
try { try {
const content = await Bun.file(path.join(Paths.worktree, filepath)).text() const content = await Bun.file(path.join(Instance.worktree, filepath)).text()
const lines = content.split("\n").length const lines = content.split("\n").length
changedFiles.push({ changedFiles.push({
path: filepath, path: filepath,
@ -81,7 +81,7 @@ export namespace File {
// Get deleted files // Get deleted files
const deletedOutput = await $`git diff --name-only --diff-filter=D HEAD` const deletedOutput = await $`git diff --name-only --diff-filter=D HEAD`
.cwd(Paths.directory) .cwd(Instance.directory)
.quiet() .quiet()
.nothrow() .nothrow()
.text() .text()
@ -100,27 +100,27 @@ export namespace File {
return changedFiles.map((x) => ({ return changedFiles.map((x) => ({
...x, ...x,
path: path.relative(Paths.directory, path.join(Paths.worktree, x.path)), path: path.relative(Instance.directory, path.join(Instance.worktree, x.path)),
})) }))
} }
export async function read(file: string) { export async function read(file: string) {
using _ = log.time("read", { file }) using _ = log.time("read", { file })
const project = Project.use() const project = Project.use()
const full = path.join(Paths.directory, file) const full = path.join(Instance.directory, file)
const content = await Bun.file(full) const content = await Bun.file(full)
.text() .text()
.catch(() => "") .catch(() => "")
.then((x) => x.trim()) .then((x) => x.trim())
if (project.vcs === "git") { if (project.vcs === "git") {
const rel = path.relative(Paths.worktree, full) const rel = path.relative(Instance.worktree, full)
const diff = await git.status({ const diff = await git.status({
fs, fs,
dir: Paths.worktree, dir: Instance.worktree,
filepath: rel, filepath: rel,
}) })
if (diff !== "unmodified") { if (diff !== "unmodified") {
const original = await $`git show HEAD:${rel}`.cwd(Paths.worktree).quiet().nothrow().text() const original = await $`git show HEAD:${rel}`.cwd(Instance.worktree).quiet().nothrow().text()
const patch = createPatch(file, original, content, "old", "new", { const patch = createPatch(file, original, content, "old", "new", {
context: Infinity, context: Infinity,
}) })

View file

@ -1,11 +1,9 @@
import { Paths } from "../project/path" import { Instance } from "../project/instance"
import { State } from "../project/state"
import { Log } from "../util/log" import { Log } from "../util/log"
export namespace FileTime { export namespace FileTime {
const log = Log.create({ service: "file.time" }) const log = Log.create({ service: "file.time" })
export const state = State.create( export const state = Instance.state(
() => Paths.directory,
() => { () => {
const read: { const read: {
[sessionID: string]: { [sessionID: string]: {

View file

@ -4,8 +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" import { Instance } from "../project/instance"
import { State } from "../project/state"
export namespace FileWatcher { export namespace FileWatcher {
const log = Log.create({ service: "file.watcher" }) const log = Log.create({ service: "file.watcher" })
@ -19,8 +18,7 @@ export namespace FileWatcher {
}), }),
), ),
} }
const state = State.create( const state = Instance.state(
() => Paths.directory,
() => { () => {
const app = App.use() const app = App.use()
if (!app.info.git) return {} if (!app.info.git) return {}

View file

@ -1,5 +1,5 @@
import { BunProc } from "../bun" import { BunProc } from "../bun"
import { Paths } from "../project/path" import { Instance } from "../project/instance"
import { Filesystem } from "../util/filesystem" import { Filesystem } from "../util/filesystem"
export interface Info { export interface Info {
@ -63,7 +63,7 @@ export const prettier: Info = {
".gql", ".gql",
], ],
async enabled() { async enabled() {
const items = await Filesystem.findUp("package.json", Paths.directory, Paths.worktree) const items = await Filesystem.findUp("package.json", Instance.directory, Instance.worktree)
for (const item of items) { for (const item of items) {
const json = await Bun.file(item).json() const json = await Bun.file(item).json()
if (json.dependencies?.prettier) return true if (json.dependencies?.prettier) return true
@ -108,7 +108,7 @@ export const biome: Info = {
".gql", ".gql",
], ],
async enabled() { async enabled() {
const items = await Filesystem.findUp("biome.json", Paths.directory, Paths.worktree) const items = await Filesystem.findUp("biome.json", Instance.directory, Instance.worktree)
return items.length > 0 return items.length > 0
}, },
} }
@ -127,7 +127,7 @@ export const clang: Info = {
command: ["clang-format", "-i", "$FILE"], command: ["clang-format", "-i", "$FILE"],
extensions: [".c", ".cc", ".cpp", ".cxx", ".c++", ".h", ".hh", ".hpp", ".hxx", ".h++", ".ino", ".C", ".H"], extensions: [".c", ".cc", ".cpp", ".cxx", ".c++", ".h", ".hh", ".hpp", ".hxx", ".h++", ".ino", ".C", ".H"],
async enabled() { async enabled() {
const items = await Filesystem.findUp(".clang-format", Paths.directory, Paths.worktree) const items = await Filesystem.findUp(".clang-format", Instance.directory, Instance.worktree)
return items.length > 0 return items.length > 0
}, },
} }
@ -149,7 +149,7 @@ export const ruff: Info = {
if (!Bun.which("ruff")) return false if (!Bun.which("ruff")) return false
const configs = ["pyproject.toml", "ruff.toml", ".ruff.toml"] const configs = ["pyproject.toml", "ruff.toml", ".ruff.toml"]
for (const config of configs) { for (const config of configs) {
const found = await Filesystem.findUp(config, Paths.directory, Paths.worktree) const found = await Filesystem.findUp(config, Instance.directory, Instance.worktree)
if (found.length > 0) { if (found.length > 0) {
if (config === "pyproject.toml") { if (config === "pyproject.toml") {
const content = await Bun.file(found[0]).text() const content = await Bun.file(found[0]).text()
@ -161,7 +161,7 @@ export const ruff: Info = {
} }
const deps = ["requirements.txt", "pyproject.toml", "Pipfile"] const deps = ["requirements.txt", "pyproject.toml", "Pipfile"]
for (const dep of deps) { for (const dep of deps) {
const found = await Filesystem.findUp(dep, Paths.directory, Paths.worktree) const found = await Filesystem.findUp(dep, Instance.directory, Instance.worktree)
if (found.length > 0) { if (found.length > 0) {
const content = await Bun.file(found[0]).text() const content = await Bun.file(found[0]).text()
if (content.includes("ruff")) return true if (content.includes("ruff")) return true

View file

@ -6,14 +6,12 @@ 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 { Instance } from "../project/instance"
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 = State.create( const state = Instance.state(
() => Paths.directory,
async () => { async () => {
const enabled: Record<string, boolean> = {} const enabled: Record<string, boolean> = {}
const cfg = await Config.get() const cfg = await Config.get()
@ -74,7 +72,7 @@ export namespace Format {
log.info("running", { command: item.command }) log.info("running", { command: item.command })
const proc = Bun.spawn({ const proc = Bun.spawn({
cmd: item.command.map((x) => x.replace("$FILE", file)), cmd: item.command.map((x) => x.replace("$FILE", file)),
cwd: Paths.directory, cwd: Instance.directory,
env: item.environment, env: item.environment,
stdout: "ignore", stdout: "ignore",
stderr: "ignore", stderr: "ignore",

View file

@ -8,7 +8,7 @@ import z from "zod"
import type { LSPServer } from "./server" import type { LSPServer } from "./server"
import { NamedError } from "../util/error" import { NamedError } from "../util/error"
import { withTimeout } from "../util/timeout" import { withTimeout } from "../util/timeout"
import { Paths } from "../project/path" import { Instance } from "../project/instance"
export namespace LSPClient { export namespace LSPClient {
const log = Log.create({ service: "lsp.client" }) const log = Log.create({ service: "lsp.client" })
@ -122,7 +122,7 @@ export namespace LSPClient {
}, },
notify: { notify: {
async open(input: { path: string }) { async open(input: { path: string }) {
input.path = path.isAbsolute(input.path) ? input.path : path.resolve(Paths.directory, input.path) input.path = path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path)
const file = Bun.file(input.path) const file = Bun.file(input.path)
const text = await file.text() const text = await file.text()
const version = files[input.path] const version = files[input.path]
@ -154,7 +154,7 @@ export namespace LSPClient {
return diagnostics return diagnostics
}, },
async waitForDiagnostics(input: { path: string }) { async waitForDiagnostics(input: { path: string }) {
input.path = path.isAbsolute(input.path) ? input.path : path.resolve(Paths.directory, input.path) input.path = path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path)
log.info("waiting for diagnostics", input) log.info("waiting for diagnostics", input)
let unsub: () => void let unsub: () => void
return await withTimeout( return await withTimeout(

View file

@ -5,8 +5,7 @@ import { LSPServer } from "./server"
import { z } from "zod" 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 { Instance } from "../project/instance"
import { Paths } from "../project/path"
export namespace LSP { export namespace LSP {
const log = Log.create({ service: "lsp" }) const log = Log.create({ service: "lsp" })
@ -54,8 +53,7 @@ export namespace LSP {
}) })
export type DocumentSymbol = z.infer<typeof DocumentSymbol> export type DocumentSymbol = z.infer<typeof DocumentSymbol>
const state = State.create( const state = Instance.state(
() => Paths.directory,
async () => { async () => {
const clients: LSPClient.Info[] = [] const clients: LSPClient.Info[] = []
const servers: Record<string, LSPServer.Info> = LSPServer const servers: Record<string, LSPServer.Info> = LSPServer
@ -68,7 +66,7 @@ export namespace LSP {
} }
servers[name] = { servers[name] = {
...existing, ...existing,
root: existing?.root ?? (async () => Paths.directory), root: existing?.root ?? (async () => Instance.directory),
extensions: item.extensions ?? existing.extensions, extensions: item.extensions ?? existing.extensions,
spawn: async (root) => { spawn: async (root) => {
return { return {

View file

@ -6,7 +6,7 @@ import { BunProc } from "../bun"
import { $ } from "bun" import { $ } from "bun"
import fs from "fs/promises" import fs from "fs/promises"
import { Filesystem } from "../util/filesystem" import { Filesystem } from "../util/filesystem"
import { Paths } from "../project/path" import { Instance } from "../project/instance"
export namespace LSPServer { export namespace LSPServer {
const log = Log.create({ service: "lsp.server" }) const log = Log.create({ service: "lsp.server" })
@ -23,11 +23,11 @@ export namespace LSPServer {
const files = Filesystem.up({ const files = Filesystem.up({
targets: patterns, targets: patterns,
start: path.dirname(file), start: path.dirname(file),
stop: Paths.worktree, stop: Instance.worktree,
}) })
const first = await files.next() const first = await files.next()
await files.return() await files.return()
if (!first.value) return Paths.worktree if (!first.value) return Instance.worktree
return path.dirname(first.value) return path.dirname(first.value)
} }
} }
@ -45,7 +45,7 @@ export namespace LSPServer {
root: NearestRoot(["tsconfig.json", "package.json", "jsconfig.json"]), root: NearestRoot(["tsconfig.json", "package.json", "jsconfig.json"]),
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"], extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
async spawn(root) { async spawn(root) {
const tsserver = await Bun.resolve("typescript/lib/tsserver.js", Paths.directory).catch(() => {}) const tsserver = await Bun.resolve("typescript/lib/tsserver.js", Instance.directory).catch(() => {})
if (!tsserver) return if (!tsserver) return
const proc = spawn(BunProc.which(), ["x", "typescript-language-server", "--stdio"], { const proc = spawn(BunProc.which(), ["x", "typescript-language-server", "--stdio"], {
cwd: root, cwd: root,

View file

@ -8,8 +8,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" import { Instance } from "../project/instance"
import { Paths } from "../project/path"
export namespace MCP { export namespace MCP {
const log = Log.create({ service: "mcp" }) const log = Log.create({ service: "mcp" })
@ -21,8 +20,7 @@ export namespace MCP {
}), }),
) )
const state = State.create( const state = Instance.state(
() => Paths.directory,
async () => { async () => {
const cfg = await Config.get() const cfg = await Config.get()
const clients: { const clients: {

View file

@ -3,8 +3,7 @@ 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 { Instance } from "../project/instance"
import { Paths } from "../project/path"
export namespace Permission { export namespace Permission {
const log = Log.create({ service: "permission" }) const log = Log.create({ service: "permission" })
@ -36,8 +35,7 @@ export namespace Permission {
), ),
} }
const state = State.create( const state = Instance.state(
() => Paths.directory,
() => { () => {
const pending: { const pending: {
[sessionID: string]: { [sessionID: string]: {

View file

@ -5,14 +5,12 @@ 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 { Instance } from "../project/instance"
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 = State.create( const state = Instance.state(
() => Paths.directory,
async () => { async () => {
const client = createOpencodeClient({ const client = createOpencodeClient({
baseUrl: "http://localhost:4096", baseUrl: "http://localhost:4096",

View file

@ -0,0 +1,20 @@
import { Context } from "../util/context"
import { State } from "./state"
const context = Context.create<{ directory: string; worktree: string }>("path")
export const Instance = {
provide: context.provide,
get directory() {
return context.use().directory
},
get worktree() {
return context.use().worktree
},
state<S>(init: () => S, dispose?: (state: Awaited<S>) => Promise<void>): () => S {
return State.create(() => Instance.directory, init, dispose)
},
async dispose() {
await State.dispose(Instance.directory)
},
}

View file

@ -1,13 +0,0 @@
import { Context } from "../util/context"
const context = Context.create<{ directory: string; worktree: string }>("path")
export const Paths = {
provide: context.provide,
get directory() {
return context.use().directory
},
get worktree() {
return context.use().worktree
},
}

View file

@ -9,8 +9,7 @@ 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 { Instance } from "../project/instance"
import { Paths } from "../project/path"
export namespace Provider { export namespace Provider {
const log = Log.create({ service: "provider" }) const log = Log.create({ service: "provider" })
@ -217,152 +216,149 @@ export namespace Provider {
}, },
} }
const state = State.create( const state = Instance.state(async () => {
() => Paths.directory, const config = await Config.get()
async () => { const database = await ModelsDev.get()
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 }>() }
const sdk = new Map<string, SDK>() provider.options = mergeDeep(provider.options, options)
provider.source = source
provider.getModel = getModel ?? provider.getModel
}
log.info("init") const configProviders = Object.entries(config.provider ?? {})
function mergeProvider( for (const [providerID, provider] of configProviders) {
id: string, const existing = database[providerID]
options: Record<string, any>, const parsed: ModelsDev.Provider = {
source: Source, id: providerID,
getModel?: (sdk: any, modelID: string) => Promise<any>, npm: provider.npm ?? existing?.npm,
) { name: provider.name ?? existing?.name ?? providerID,
const provider = providers[id] env: provider.env ?? existing?.env ?? [],
if (!provider) { api: provider.api ?? existing?.api,
const info = database[id] models: existing?.models ?? {},
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
} }
const configProviders = Object.entries(config.provider ?? {}) for (const [modelID, model] of Object.entries(provider.models ?? {})) {
const existing = parsed.models[modelID]
for (const [providerID, provider] of configProviders) { const parsedModel: ModelsDev.Model = {
const existing = database[providerID] id: modelID,
const parsed: ModelsDev.Provider = { name: model.name ?? existing?.name ?? modelID,
id: providerID, release_date: model.release_date ?? existing?.release_date,
npm: provider.npm ?? existing?.npm, attachment: model.attachment ?? existing?.attachment ?? false,
name: provider.name ?? existing?.name ?? providerID, reasoning: model.reasoning ?? existing?.reasoning ?? false,
env: provider.env ?? existing?.env ?? [], temperature: model.temperature ?? existing?.temperature ?? false,
api: provider.api ?? existing?.api, tool_call: model.tool_call ?? existing?.tool_call ?? true,
models: existing?.models ?? {}, cost:
} !model.cost && !existing?.cost
? {
for (const [modelID, model] of Object.entries(provider.models ?? {})) { input: 0,
const existing = parsed.models[modelID] output: 0,
const parsedModel: ModelsDev.Model = { cache_read: 0,
id: modelID, cache_write: 0,
name: model.name ?? existing?.name ?? modelID, }
release_date: model.release_date ?? existing?.release_date, : {
attachment: model.attachment ?? existing?.attachment ?? false, cache_read: 0,
reasoning: model.reasoning ?? existing?.reasoning ?? false, cache_write: 0,
temperature: model.temperature ?? existing?.temperature ?? false, ...existing?.cost,
tool_call: model.tool_call ?? existing?.tool_call ?? true, ...model.cost,
cost: },
!model.cost && !existing?.cost options: {
? { ...existing?.options,
input: 0, ...model.options,
output: 0, },
cache_read: 0, limit: model.limit ??
cache_write: 0, existing?.limit ?? {
} 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
} }
database[providerID] = parsed parsed.models[modelID] = parsedModel
} }
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 // load apikeys
for (const [providerID, provider] of Object.entries(await Auth.all())) { for (const [providerID, provider] of Object.entries(await Auth.all())) {
if (disabled.has(providerID)) continue if (disabled.has(providerID)) continue
if (provider.type === "api") { if (provider.type === "api") {
mergeProvider(providerID, { apiKey: provider.key }, "api") mergeProvider(providerID, { apiKey: provider.key }, "api")
}
} }
}
// load custom // load custom
for (const [providerID, fn] of Object.entries(CUSTOM_LOADERS)) { for (const [providerID, fn] of Object.entries(CUSTOM_LOADERS)) {
if (disabled.has(providerID)) continue if (disabled.has(providerID)) continue
const result = await fn(database[providerID]) const result = await fn(database[providerID])
if (result && (result.autoload || providers[providerID])) { if (result && (result.autoload || providers[providerID])) {
mergeProvider(providerID, result.options ?? {}, "custom", result.getModel) mergeProvider(providerID, result.options ?? {}, "custom", result.getModel)
}
} }
}
// load config // load config
for (const [providerID, provider] of configProviders) { for (const [providerID, provider] of configProviders) {
mergeProvider(providerID, provider.options ?? {}, "config") mergeProvider(providerID, provider.options ?? {}, "config")
} }
for (const [providerID, provider] of Object.entries(providers)) { for (const [providerID, provider] of Object.entries(providers)) {
if (Object.keys(provider.info.models).length === 0) { if (Object.keys(provider.info.models).length === 0) {
delete providers[providerID] delete providers[providerID]
continue continue
}
log.info("found", { providerID })
} }
log.info("found", { providerID })
}
return { return {
models, models,
providers, providers,
sdk, sdk,
} }
}, })
)
export async function list() { export async function list() {
return state().then((state) => state.providers) return state().then((state) => state.providers)

View file

@ -20,7 +20,7 @@ import { Mode } from "../session/mode"
import { callTui, TuiRoute } from "./tui" import { callTui, TuiRoute } from "./tui"
import { Permission } from "../permission" import { Permission } from "../permission"
import { lazy } from "../util/lazy" import { lazy } from "../util/lazy"
import { Paths } from "../project/path" import { Instance } from "../project/instance"
const ERRORS = { const ERRORS = {
400: { 400: {
@ -695,7 +695,7 @@ export namespace Server {
async (c) => { async (c) => {
const pattern = c.req.valid("query").pattern const pattern = c.req.valid("query").pattern
const result = await Ripgrep.search({ const result = await Ripgrep.search({
cwd: Paths.directory, cwd: Instance.directory,
pattern, pattern,
limit: 10, limit: 10,
}) })
@ -727,7 +727,7 @@ export namespace Server {
async (c) => { async (c) => {
const query = c.req.valid("query").query const query = c.req.valid("query").query
const result = await Ripgrep.files({ const result = await Ripgrep.files({
cwd: Paths.directory, cwd: Instance.directory,
query, query,
limit: 10, limit: 10,
}) })

View file

@ -41,8 +41,8 @@ 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 { State } from "../project/state"
import { Paths } from "../project/path" import { Instance } from "../project/instance"
export namespace Session { export namespace Session {
const log = Log.create({ service: "session" }) const log = Log.create({ service: "session" })
@ -65,7 +65,6 @@ export namespace Session {
id: Identifier.schema("session"), id: Identifier.schema("session"),
projectID: z.string(), projectID: z.string(),
directory: z.string(), directory: z.string(),
worktree: z.string(),
parentID: Identifier.schema("session").optional(), parentID: Identifier.schema("session").optional(),
share: z share: z
.object({ .object({
@ -130,8 +129,7 @@ export namespace Session {
), ),
} }
const state = State.create( const state = Instance.state(
() => 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>()
@ -162,7 +160,7 @@ export namespace Session {
export async function create(parentID?: string) { export async function create(parentID?: string) {
return createNext({ return createNext({
parentID, parentID,
directory: Paths.directory, directory: Instance.directory,
}) })
} }
@ -172,7 +170,6 @@ export namespace Session {
id: Identifier.descending("session", input.id), id: Identifier.descending("session", input.id),
version: Installation.VERSION, version: Installation.VERSION,
projectID: project.id, projectID: project.id,
worktree: project.worktree,
directory: input.directory, directory: input.directory,
parentID: input.parentID, parentID: input.parentID,
title: createDefaultTitle(!!input.parentID), title: createDefaultTitle(!!input.parentID),
@ -711,8 +708,8 @@ export namespace Session {
system, system,
mode: inputMode, mode: inputMode,
path: { path: {
cwd: Paths.directory, cwd: Instance.directory,
root: Paths.worktree, root: Instance.worktree,
}, },
cost: 0, cost: 0,
tokens: { tokens: {
@ -838,6 +835,7 @@ export namespace Session {
}, },
params, params,
) )
console.log(outputLimit)
const stream = streamText({ const stream = streamText({
onError(e) { onError(e) {
log.error("streamText error", { log.error("streamText error", {
@ -866,8 +864,8 @@ export namespace Session {
role: "assistant", role: "assistant",
system, system,
path: { path: {
cwd: Paths.directory, cwd: Instance.directory,
root: Paths.worktree, root: Instance.worktree,
}, },
cost: 0, cost: 0,
tokens: { tokens: {
@ -1276,8 +1274,8 @@ export namespace Session {
system, system,
mode: "build", mode: "build",
path: { path: {
cwd: Paths.directory, cwd: Instance.directory,
root: Paths.worktree, root: Instance.worktree,
}, },
summary: true, summary: true,
cost: 0, cost: 0,
@ -1400,7 +1398,7 @@ export namespace Session {
{ {
id: Identifier.ascending("part"), id: Identifier.ascending("part"),
type: "text", type: "text",
text: PROMPT_INITIALIZE.replace("${path}", Paths.worktree), text: PROMPT_INITIALIZE.replace("${path}", Instance.worktree),
}, },
], ],
}) })

View file

@ -1,8 +1,7 @@
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 { Instance } from "../project/instance"
import { Paths } from "../project/path"
export namespace Mode { export namespace Mode {
export const Info = z export const Info = z
@ -23,50 +22,47 @@ export namespace Mode {
ref: "Mode", ref: "Mode",
}) })
export type Info = z.infer<typeof Info> export type Info = z.infer<typeof Info>
const state = State.create( const state = Instance.state(async () => {
() => Paths.directory, const cfg = await Config.get()
async () => { const model = cfg.model ? Provider.parseModel(cfg.model) : undefined
const cfg = await Config.get() const result: Record<string, Info> = {
const model = cfg.model ? Provider.parseModel(cfg.model) : undefined build: {
const result: Record<string, Info> = { model,
build: { name: "build",
model, tools: {},
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: {},
}, }
plan: { item.name = key
name: "plan", if (value.model) item.model = Provider.parseModel(value.model)
model, if (value.prompt) item.prompt = value.prompt
tools: { if (value.temperature != undefined) item.temperature = value.temperature
write: false, if (value.top_p != undefined) item.topP = value.top_p
edit: false, if (value.tools)
patch: false, item.tools = {
}, ...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])

View file

@ -13,7 +13,7 @@ import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt"
import PROMPT_SUMMARIZE from "./prompt/summarize.txt" import PROMPT_SUMMARIZE from "./prompt/summarize.txt"
import PROMPT_TITLE from "./prompt/title.txt" import PROMPT_TITLE from "./prompt/title.txt"
import { Project } from "../project/project" import { Project } from "../project/project"
import { Paths } from "../project/path" import { Instance } from "../project/instance"
export namespace SystemPrompt { export namespace SystemPrompt {
export function header(providerID: string) { export function header(providerID: string) {
@ -33,7 +33,7 @@ export namespace SystemPrompt {
[ [
`Here is some useful information about the environment you are running in:`, `Here is some useful information about the environment you are running in:`,
`<env>`, `<env>`,
` Working directory: ${Paths.directory}`, ` Working directory: ${Instance.directory}`,
` Is directory a git repo: ${project.vcs === "git" ? "yes" : "no"}`, ` Is directory a git repo: ${project.vcs === "git" ? "yes" : "no"}`,
` Platform: ${process.platform}`, ` Platform: ${process.platform}`,
` Today's date: ${new Date().toDateString()}`, ` Today's date: ${new Date().toDateString()}`,
@ -42,7 +42,7 @@ export namespace SystemPrompt {
` ${ ` ${
project.vcs === "git" project.vcs === "git"
? await Ripgrep.tree({ ? await Ripgrep.tree({
cwd: Paths.directory, cwd: Instance.directory,
limit: 200, limit: 200,
}) })
: "" : ""
@ -63,7 +63,7 @@ export namespace SystemPrompt {
const paths = new Set<string>() const paths = new Set<string>()
for (const item of CUSTOM_FILES) { for (const item of CUSTOM_FILES) {
const matches = await Filesystem.findUp(item, Paths.directory, Paths.worktree) const matches = await Filesystem.findUp(item, Instance.directory, Instance.worktree)
matches.forEach((path) => paths.add(path)) matches.forEach((path) => paths.add(path))
} }
@ -72,7 +72,7 @@ export namespace SystemPrompt {
if (config.instructions) { if (config.instructions) {
for (const instruction of config.instructions) { for (const instruction of config.instructions) {
const matches = await Filesystem.globUp(instruction, Paths.directory, Paths.worktree).catch(() => []) const matches = await Filesystem.globUp(instruction, Instance.directory, Instance.worktree).catch(() => [])
matches.forEach((path) => paths.add(path)) matches.forEach((path) => paths.add(path))
} }
} }

View file

@ -6,7 +6,7 @@ import { Global } from "../global"
import { z } from "zod" import { z } from "zod"
import { Config } from "../config/config" import { Config } from "../config/config"
import { Project } from "../project/project" import { Project } from "../project/project"
import { Paths } from "../project/path" import { Instance } from "../project/instance"
export namespace Snapshot { export namespace Snapshot {
const log = Log.create({ service: "snapshot" }) const log = Log.create({ service: "snapshot" })
@ -36,14 +36,14 @@ export namespace Snapshot {
.env({ .env({
...process.env, ...process.env,
GIT_DIR: git, GIT_DIR: git,
GIT_WORK_TREE: Paths.worktree, GIT_WORK_TREE: Instance.worktree,
}) })
.quiet() .quiet()
.nothrow() .nothrow()
log.info("initialized") log.info("initialized")
} }
await $`git --git-dir ${git} add .`.quiet().cwd(Paths.directory).nothrow() await $`git --git-dir ${git} add .`.quiet().cwd(Instance.directory).nothrow()
const hash = await $`git --git-dir ${git} write-tree`.quiet().cwd(Paths.directory).nothrow().text() const hash = await $`git --git-dir ${git} write-tree`.quiet().cwd(Instance.directory).nothrow().text()
return hash.trim() return hash.trim()
} }
@ -55,8 +55,8 @@ export namespace Snapshot {
export async function patch(hash: string): Promise<Patch> { export async function patch(hash: string): Promise<Patch> {
const git = gitdir() const git = gitdir()
await $`git --git-dir ${git} add .`.quiet().cwd(Paths.directory).nothrow() await $`git --git-dir ${git} add .`.quiet().cwd(Instance.directory).nothrow()
const files = await $`git --git-dir ${git} diff --name-only ${hash} -- .`.cwd(Paths.directory).text() const files = await $`git --git-dir ${git} diff --name-only ${hash} -- .`.cwd(Instance.directory).text()
return { return {
hash, hash,
files: files files: files
@ -64,7 +64,7 @@ export namespace Snapshot {
.split("\n") .split("\n")
.map((x) => x.trim()) .map((x) => x.trim())
.filter(Boolean) .filter(Boolean)
.map((x) => path.join(Paths.directory, x)), .map((x) => path.join(Instance.directory, x)),
} }
} }
@ -73,7 +73,7 @@ export namespace Snapshot {
const git = gitdir() const git = gitdir()
await $`git --git-dir=${git} read-tree ${snapshot} && git --git-dir=${git} checkout-index -a -f` await $`git --git-dir=${git} read-tree ${snapshot} && git --git-dir=${git} checkout-index -a -f`
.quiet() .quiet()
.cwd(Paths.worktree) .cwd(Instance.worktree)
} }
export async function revert(patches: Patch[]) { export async function revert(patches: Patch[]) {
@ -85,7 +85,7 @@ export namespace Snapshot {
log.info("reverting", { file, hash: item.hash }) log.info("reverting", { file, hash: item.hash })
const result = await $`git --git-dir=${git} checkout ${item.hash} -- ${file}` const result = await $`git --git-dir=${git} checkout ${item.hash} -- ${file}`
.quiet() .quiet()
.cwd(Paths.worktree) .cwd(Instance.worktree)
.nothrow() .nothrow()
if (result.exitCode !== 0) { if (result.exitCode !== 0) {
log.info("file not found in history, deleting", { file }) log.info("file not found in history, deleting", { file })
@ -98,7 +98,7 @@ export namespace Snapshot {
export async function diff(hash: string) { export async function diff(hash: string) {
const git = gitdir() const git = gitdir()
const result = await $`git --git-dir=${git} diff ${hash} -- .`.quiet().cwd(Paths.worktree).text() const result = await $`git --git-dir=${git} diff ${hash} -- .`.quiet().cwd(Instance.worktree).text()
return result.trim() return result.trim()
} }

View file

@ -10,7 +10,7 @@ import { lazy } from "../util/lazy"
import { Log } from "../util/log" import { Log } from "../util/log"
import { Wildcard } from "../util/wildcard" import { Wildcard } from "../util/wildcard"
import { $ } from "bun" import { $ } from "bun"
import { Paths } from "../project/path" import { Instance } from "../project/instance"
const MAX_OUTPUT_LENGTH = 30000 const MAX_OUTPUT_LENGTH = 30000
const DEFAULT_TIMEOUT = 1 * 60 * 1000 const DEFAULT_TIMEOUT = 1 * 60 * 1000
@ -82,9 +82,9 @@ export const BashTool = Tool.define("bash", {
.text() .text()
.then((x) => x.trim()) .then((x) => x.trim())
log.info("resolved path", { arg, resolved }) log.info("resolved path", { arg, resolved })
if (resolved && !Filesystem.contains(Paths.directory, resolved)) { if (resolved && !Filesystem.contains(Instance.directory, resolved)) {
throw new Error( throw new Error(
`This command references paths outside of ${Paths.directory} so it is not allowed to be executed.`, `This command references paths outside of ${Instance.directory} so it is not allowed to be executed.`,
) )
} }
} }
@ -123,7 +123,7 @@ export const BashTool = Tool.define("bash", {
} }
const process = exec(params.command, { const process = exec(params.command, {
cwd: Paths.directory, cwd: Instance.directory,
signal: ctx.abort, signal: ctx.abort,
maxBuffer: MAX_OUTPUT_LENGTH, maxBuffer: MAX_OUTPUT_LENGTH,
timeout, timeout,

View file

@ -15,7 +15,7 @@ import { Bus } from "../bus"
import { FileTime } from "../file/time" import { FileTime } from "../file/time"
import { Config } from "../config/config" import { Config } from "../config/config"
import { Filesystem } from "../util/filesystem" import { Filesystem } from "../util/filesystem"
import { Paths } from "../project/path" import { Instance } from "../project/instance"
export const EditTool = Tool.define("edit", { export const EditTool = Tool.define("edit", {
description: DESCRIPTION, description: DESCRIPTION,
@ -34,8 +34,8 @@ export const EditTool = Tool.define("edit", {
throw new Error("oldString and newString must be different") throw new Error("oldString and newString must be different")
} }
const filePath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Paths.directory, params.filePath) const filePath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath)
if (!Filesystem.contains(Paths.directory, filePath)) { if (!Filesystem.contains(Instance.directory, filePath)) {
throw new Error(`File ${filePath} is not in the current working directory`) throw new Error(`File ${filePath} is not in the current working directory`)
} }
@ -120,7 +120,7 @@ export const EditTool = Tool.define("edit", {
diagnostics, diagnostics,
diff, diff,
}, },
title: `${path.relative(Paths.worktree, filePath)}`, title: `${path.relative(Instance.worktree, filePath)}`,
output, output,
} }
}, },

View file

@ -3,7 +3,7 @@ import path from "path"
import { Tool } from "./tool" import { Tool } from "./tool"
import DESCRIPTION from "./glob.txt" import DESCRIPTION from "./glob.txt"
import { Ripgrep } from "../file/ripgrep" import { Ripgrep } from "../file/ripgrep"
import { Paths } from "../project/path" import { Instance } from "../project/instance"
export const GlobTool = Tool.define("glob", { export const GlobTool = Tool.define("glob", {
description: DESCRIPTION, description: DESCRIPTION,
@ -17,8 +17,8 @@ export const GlobTool = Tool.define("glob", {
), ),
}), }),
async execute(params) { async execute(params) {
let search = params.path ?? Paths.directory let search = params.path ?? Instance.directory
search = path.isAbsolute(search) ? search : path.resolve(Paths.directory, search) search = path.isAbsolute(search) ? search : path.resolve(Instance.directory, search)
const limit = 100 const limit = 100
const files = [] const files = []
@ -54,7 +54,7 @@ export const GlobTool = Tool.define("glob", {
} }
return { return {
title: path.relative(Paths.worktree, search), title: path.relative(Instance.worktree, search),
metadata: { metadata: {
count: files.length, count: files.length,
truncated, truncated,

View file

@ -3,7 +3,7 @@ import { Tool } from "./tool"
import { Ripgrep } from "../file/ripgrep" import { Ripgrep } from "../file/ripgrep"
import DESCRIPTION from "./grep.txt" import DESCRIPTION from "./grep.txt"
import { Paths } from "../project/path" import { Instance } from "../project/instance"
export const GrepTool = Tool.define("grep", { export const GrepTool = Tool.define("grep", {
description: DESCRIPTION, description: DESCRIPTION,
@ -17,7 +17,7 @@ export const GrepTool = Tool.define("grep", {
throw new Error("pattern is required") throw new Error("pattern is required")
} }
const searchPath = params.path || Paths.directory const searchPath = params.path || Instance.directory
const rgPath = await Ripgrep.filepath() const rgPath = await Ripgrep.filepath()
const args = ["-n", params.pattern] const args = ["-n", params.pattern]

View file

@ -2,7 +2,7 @@ import { z } from "zod"
import { Tool } from "./tool" import { Tool } from "./tool"
import * as path from "path" import * as path from "path"
import DESCRIPTION from "./ls.txt" import DESCRIPTION from "./ls.txt"
import { Paths } from "../project/path" import { Instance } from "../project/instance"
export const IGNORE_PATTERNS = [ export const IGNORE_PATTERNS = [
"node_modules/", "node_modules/",
@ -40,7 +40,7 @@ export const ListTool = Tool.define("list", {
ignore: z.array(z.string()).describe("List of glob patterns to ignore").optional(), ignore: z.array(z.string()).describe("List of glob patterns to ignore").optional(),
}), }),
async execute(params) { async execute(params) {
const searchPath = path.resolve(Paths.directory, params.path || ".") const searchPath = path.resolve(Instance.directory, params.path || ".")
const glob = new Bun.Glob("**/*") const glob = new Bun.Glob("**/*")
const files = [] const files = []
@ -101,7 +101,7 @@ export const ListTool = Tool.define("list", {
const output = `${searchPath}/\n` + renderDir(".", 0) const output = `${searchPath}/\n` + renderDir(".", 0)
return { return {
title: path.relative(Paths.worktree, searchPath), title: path.relative(Instance.worktree, searchPath),
metadata: { metadata: {
count: files.length, count: files.length,
truncated: files.length >= LIMIT, truncated: files.length >= LIMIT,

View file

@ -3,7 +3,7 @@ import { Tool } from "./tool"
import path from "path" import path from "path"
import { LSP } from "../lsp" import { LSP } from "../lsp"
import DESCRIPTION from "./lsp-diagnostics.txt" import DESCRIPTION from "./lsp-diagnostics.txt"
import { Paths } from "../project/path" import { Instance } from "../project/instance"
export const LspDiagnosticTool = Tool.define("lsp_diagnostics", { export const LspDiagnosticTool = Tool.define("lsp_diagnostics", {
description: DESCRIPTION, description: DESCRIPTION,
@ -11,12 +11,12 @@ export const LspDiagnosticTool = Tool.define("lsp_diagnostics", {
path: z.string().describe("The path to the file to get diagnostics."), path: z.string().describe("The path to the file to get diagnostics."),
}), }),
execute: async (args) => { execute: async (args) => {
const normalized = path.isAbsolute(args.path) ? args.path : path.join(Paths.directory, args.path) const normalized = path.isAbsolute(args.path) ? args.path : path.join(Instance.directory, args.path)
await LSP.touchFile(normalized, true) await LSP.touchFile(normalized, true)
const diagnostics = await LSP.diagnostics() const diagnostics = await LSP.diagnostics()
const file = diagnostics[normalized] const file = diagnostics[normalized]
return { return {
title: path.relative(Paths.worktree, normalized), title: path.relative(Instance.worktree, normalized),
metadata: { metadata: {
diagnostics, diagnostics,
}, },

View file

@ -3,7 +3,7 @@ import { Tool } from "./tool"
import path from "path" import path from "path"
import { LSP } from "../lsp" import { LSP } from "../lsp"
import DESCRIPTION from "./lsp-hover.txt" import DESCRIPTION from "./lsp-hover.txt"
import { Paths } from "../project/path" import { Instance } from "../project/instance"
export const LspHoverTool = Tool.define("lsp_hover", { export const LspHoverTool = Tool.define("lsp_hover", {
description: DESCRIPTION, description: DESCRIPTION,
@ -13,7 +13,7 @@ export const LspHoverTool = Tool.define("lsp_hover", {
character: z.number().describe("The character number to get diagnostics."), character: z.number().describe("The character number to get diagnostics."),
}), }),
execute: async (args) => { execute: async (args) => {
const file = path.isAbsolute(args.file) ? args.file : path.join(Paths.directory, args.file) const file = path.isAbsolute(args.file) ? args.file : path.join(Instance.directory, args.file)
await LSP.touchFile(file, true) await LSP.touchFile(file, true)
const result = await LSP.hover({ const result = await LSP.hover({
...args, ...args,
@ -21,7 +21,7 @@ export const LspHoverTool = Tool.define("lsp_hover", {
}) })
return { return {
title: path.relative(Paths.worktree, file) + ":" + args.line + ":" + args.character, title: path.relative(Instance.worktree, file) + ":" + args.line + ":" + args.character,
metadata: { metadata: {
result, result,
}, },

View file

@ -3,7 +3,7 @@ import { Tool } from "./tool"
import { EditTool } from "./edit" import { EditTool } from "./edit"
import DESCRIPTION from "./multiedit.txt" import DESCRIPTION from "./multiedit.txt"
import path from "path" import path from "path"
import { Paths } from "../project/path" import { Instance } from "../project/instance"
export const MultiEditTool = Tool.define("multiedit", { export const MultiEditTool = Tool.define("multiedit", {
description: DESCRIPTION, description: DESCRIPTION,
@ -36,7 +36,7 @@ export const MultiEditTool = Tool.define("multiedit", {
results.push(result) results.push(result)
} }
return { return {
title: path.relative(Paths.worktree, params.filePath), title: path.relative(Instance.worktree, params.filePath),
metadata: { metadata: {
results: results.map((r) => r.metadata), results: results.map((r) => r.metadata),
}, },

View file

@ -6,7 +6,7 @@ import { LSP } from "../lsp"
import { FileTime } from "../file/time" import { FileTime } from "../file/time"
import DESCRIPTION from "./read.txt" import DESCRIPTION from "./read.txt"
import { Filesystem } from "../util/filesystem" import { Filesystem } from "../util/filesystem"
import { Paths } from "../project/path" import { Instance } from "../project/instance"
const DEFAULT_READ_LIMIT = 2000 const DEFAULT_READ_LIMIT = 2000
const MAX_LINE_LENGTH = 2000 const MAX_LINE_LENGTH = 2000
@ -23,7 +23,7 @@ export const ReadTool = Tool.define("read", {
if (!path.isAbsolute(filepath)) { if (!path.isAbsolute(filepath)) {
filepath = path.join(process.cwd(), filepath) filepath = path.join(process.cwd(), filepath)
} }
if (!Filesystem.contains(Paths.directory, filepath)) { if (!Filesystem.contains(Instance.directory, filepath)) {
throw new Error(`File ${filepath} is not in the current working directory`) throw new Error(`File ${filepath} is not in the current working directory`)
} }
@ -76,7 +76,7 @@ export const ReadTool = Tool.define("read", {
FileTime.read(ctx.sessionID, filepath) FileTime.read(ctx.sessionID, filepath)
return { return {
title: path.relative(Paths.worktree, filepath), title: path.relative(Instance.worktree, filepath),
output, output,
metadata: { metadata: {
preview, preview,

View file

@ -1,8 +1,7 @@
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 { State } from "../project/state" import { Instance } from "../project/instance"
import { Paths } from "../project/path"
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"),
@ -12,8 +11,7 @@ const TodoInfo = z.object({
}) })
type TodoInfo = z.infer<typeof TodoInfo> type TodoInfo = z.infer<typeof TodoInfo>
const state = State.create( const state = Instance.state(
() => Paths.directory,
() => { () => {
const todos: { const todos: {
[sessionId: string]: TodoInfo[] [sessionId: string]: TodoInfo[]

View file

@ -9,7 +9,7 @@ import { File } from "../file"
import { FileTime } from "../file/time" import { FileTime } from "../file/time"
import { Config } from "../config/config" import { Config } from "../config/config"
import { Filesystem } from "../util/filesystem" import { Filesystem } from "../util/filesystem"
import { Paths } from "../project/path" import { Instance } from "../project/instance"
export const WriteTool = Tool.define("write", { export const WriteTool = Tool.define("write", {
description: DESCRIPTION, description: DESCRIPTION,
@ -18,8 +18,8 @@ export const WriteTool = Tool.define("write", {
content: z.string().describe("The content to write to the file"), content: z.string().describe("The content to write to the file"),
}), }),
async execute(params, ctx) { async execute(params, ctx) {
const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Paths.directory, params.filePath) const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath)
if (!Filesystem.contains(Paths.directory, filepath)) { if (!Filesystem.contains(Instance.directory, filepath)) {
throw new Error(`File ${filepath} is not in the current working directory`) throw new Error(`File ${filepath} is not in the current working directory`)
} }
@ -61,7 +61,7 @@ export const WriteTool = Tool.define("write", {
} }
return { return {
title: path.relative(Paths.worktree, filepath), title: path.relative(Instance.worktree, filepath),
metadata: { metadata: {
diagnostics, diagnostics,
filepath, filepath,

View file

@ -4,6 +4,9 @@ export type Event =
| ({ | ({
type: "installation.updated" type: "installation.updated"
} & EventInstallationUpdated) } & EventInstallationUpdated)
| ({
type: "storage.write"
} & EventStorageWrite)
| ({ | ({
type: "lsp.client.diagnostics" type: "lsp.client.diagnostics"
} & EventLspClientDiagnostics) } & EventLspClientDiagnostics)
@ -19,9 +22,6 @@ export type Event =
| ({ | ({
type: "message.part.removed" type: "message.part.removed"
} & EventMessagePartRemoved) } & EventMessagePartRemoved)
| ({
type: "storage.write"
} & EventStorageWrite)
| ({ | ({
type: "file.edited" type: "file.edited"
} & EventFileEdited) } & EventFileEdited)
@ -60,6 +60,14 @@ export type EventInstallationUpdated = {
} }
} }
export type EventStorageWrite = {
type: string
properties: {
key: Array<string>
content?: unknown
}
}
export type EventLspClientDiagnostics = { export type EventLspClientDiagnostics = {
type: string type: string
properties: { properties: {
@ -383,14 +391,6 @@ export type EventMessagePartRemoved = {
} }
} }
export type EventStorageWrite = {
type: string
properties: {
key: string
content?: unknown
}
}
export type EventFileEdited = { export type EventFileEdited = {
type: string type: string
properties: { properties: {
@ -444,6 +444,8 @@ export type EventSessionUpdated = {
export type Session = { export type Session = {
id: string id: string
projectID: string
directory: string
parentID?: string parentID?: string
share?: { share?: {
url: string url: string
@ -675,22 +677,7 @@ export type Config = {
} }
} }
experimental?: { experimental?: {
hook?: { [key: string]: unknown
file_edited?: {
[key: string]: Array<{
command: Array<string>
environment?: {
[key: string]: string
}
}>
}
session_completed?: Array<{
command: Array<string>
environment?: {
[key: string]: string
}
}>
}
} }
} }

View file

@ -58,6 +58,8 @@ POST /log
GET /provider?directory=<resolve path> -> Provider GET /provider?directory=<resolve path> -> Provider
GET /config?directory=<resolve path> -> Config // think only tui uses this? GET /config?directory=<resolve path> -> Config // think only tui uses this?
GET /agent?directory=<resolve path> -> Mode
GET /project/:projectID/agent?directory=<resolve path> -> Agent
GET /project/:projectID/find/file?directory=<resolve path> -> File
``` ```