Просмотр исходного кода

Refactor app context system to use Zod schemas and sync access pattern

🤖 Generated with opencode
Co-Authored-By: opencode <[email protected]>
Dax Raad 8 месяцев назад
Родитель
Сommit
bfb36a8566

+ 42 - 13
packages/opencode/src/app/app.ts

@@ -3,21 +3,44 @@ import { Context } from "../util/context"
 import { Filesystem } from "../util/filesystem"
 import { Filesystem } from "../util/filesystem"
 import { Global } from "../global"
 import { Global } from "../global"
 import path from "path"
 import path from "path"
+import { z } from "zod"
 
 
 export namespace App {
 export namespace App {
   const log = Log.create({ service: "app" })
   const log = Log.create({ service: "app" })
 
 
-  export type Info = Awaited<ReturnType<typeof create>>
+  export const Info = z
+    .object({
+      time: z.object({
+        initialized: z.number().optional(),
+      }),
+      path: z.object({
+        data: z.string(),
+        root: z.string(),
+        cwd: z.string(),
+      }),
+    })
+    .openapi({
+      ref: "App.Info",
+    })
+  export type Info = z.infer<typeof Info>
 
 
-  const ctx = Context.create<Info>("app")
+  const ctx = Context.create<Awaited<ReturnType<typeof create>>>("app")
 
 
   async function create(input: { cwd: string; version: string }) {
   async function create(input: { cwd: string; version: string }) {
-    let root = await Filesystem.findUp(".git", input.cwd).then((x) =>
-      x ? path.dirname(x) : input.cwd,
+    const git = await Filesystem.findUp(".git", input.cwd).then((x) =>
+      x ? path.dirname(x) : undefined,
     )
     )
 
 
-    const data = path.join(Global.data(), root)
+    const data = path.join(Global.data(), git ?? "global")
     await Bun.write(path.join(data, "version"), input.version)
     await Bun.write(path.join(data, "version"), input.version)
+    const stateFile = Bun.file(path.join(data, "state"))
+    const state = ((await stateFile.exists()) ? stateFile.json() : {}) as {
+      initialized: number
+      version: string
+    }
+    state.version = input.version
+    if (!git) state.initialized = Date.now()
+    await stateFile.write(JSON.stringify(state))
 
 
     const services = new Map<
     const services = new Map<
       any,
       any,
@@ -29,14 +52,20 @@ export namespace App {
 
 
     await Log.file(path.join(data, "log"))
     await Log.file(path.join(data, "log"))
 
 
-    const result = Object.freeze({
-      services,
+    const info: Info = {
+      time: {
+        initialized: state.initialized,
+      },
       path: {
       path: {
         data,
         data,
-        root,
+        root: git ?? input.cwd,
         cwd: input.cwd,
         cwd: input.cwd,
       },
       },
-    })
+    }
+    const result = {
+      services,
+      info,
+    }
 
 
     return result
     return result
   }
   }
@@ -52,7 +81,7 @@ export namespace App {
       if (!services.has(key)) {
       if (!services.has(key)) {
         log.info("registering service", { name: key })
         log.info("registering service", { name: key })
         services.set(key, {
         services.set(key, {
-          state: init(app),
+          state: init(app.info),
           shutdown: shutdown,
           shutdown: shutdown,
         })
         })
       }
       }
@@ -60,8 +89,8 @@ export namespace App {
     }
     }
   }
   }
 
 
-  export async function use() {
-    return ctx.use()
+  export function info() {
+    return ctx.use().info
   }
   }
 
 
   export async function provide<T extends (app: Info) => any>(
   export async function provide<T extends (app: Info) => any>(
@@ -71,7 +100,7 @@ export namespace App {
     const app = await create(input)
     const app = await create(input)
 
 
     return ctx.provide(app, async () => {
     return ctx.provide(app, async () => {
-      const result = await cb(app)
+      const result = await cb(app.info)
       for (const [key, entry] of app.services.entries()) {
       for (const [key, entry] of app.services.entries()) {
         log.info("shutdown", { name: key })
         log.info("shutdown", { name: key })
         await entry.shutdown?.(await entry.state)
         await entry.shutdown?.(await entry.state)

+ 1 - 1
packages/opencode/src/lsp/client.ts

@@ -32,7 +32,7 @@ export namespace LSPClient {
   export async function create(input: { cmd: string[]; serverID: string }) {
   export async function create(input: { cmd: string[]; serverID: string }) {
     log.info("starting client", input)
     log.info("starting client", input)
 
 
-    const app = await App.use()
+    const app = App.info()
     const [command, ...args] = input.cmd
     const [command, ...args] = input.cmd
     const server = spawn(command, args, {
     const server = spawn(command, args, {
       stdio: ["pipe", "pipe", "pipe"],
       stdio: ["pipe", "pipe", "pipe"],

+ 21 - 2
packages/opencode/src/server/server.ts

@@ -15,7 +15,7 @@ export namespace Server {
   const log = Log.create({ service: "server" })
   const log = Log.create({ service: "server" })
   const PORT = 16713
   const PORT = 16713
 
 
-  export type App = ReturnType<typeof app>
+  export type Routes = ReturnType<typeof app>
 
 
   function app() {
   function app() {
     const app = new Hono()
     const app = new Hono()
@@ -74,6 +74,25 @@ export namespace Server {
           })
           })
         },
         },
       )
       )
+      .post(
+        "/app_info",
+        describeRoute({
+          description: "Get app info",
+          responses: {
+            200: {
+              description: "200",
+              content: {
+                "application/json": {
+                  schema: resolver(App.Info),
+                },
+              },
+            },
+          },
+        }),
+        async (c) => {
+          return c.json(App.info())
+        },
+      )
       .post(
       .post(
         "/path_get",
         "/path_get",
         describeRoute({
         describeRoute({
@@ -97,7 +116,7 @@ export namespace Server {
           },
           },
         }),
         }),
         async (c) => {
         async (c) => {
-          const app = await App.use()
+          const app = App.info()
           return c.json({
           return c.json({
             root: app.path.root,
             root: app.path.root,
             data: app.path.data,
             data: app.path.data,

+ 29 - 0
packages/opencode/src/session/context.ts

@@ -0,0 +1,29 @@
+import { App } from "../app/app"
+import path from "path"
+
+export namespace SessionContext {
+  const FILES = [
+    "AGENTS.md",
+    "CLAUDE.md",
+    "CONTEXT.md", // deprecated
+  ]
+  export async function find() {
+    const { cwd, root } = App.info().path
+    let current = cwd
+    const found = []
+    while (true) {
+      for (const item of FILES) {
+        const file = Bun.file(path.join(current, item))
+        if (await file.exists()) {
+          found.push(file.text())
+        }
+      }
+
+      if (current === root) break
+      const parent = path.dirname(current)
+      if (parent === current) break
+      current = parent
+    }
+    return Promise.all(found).then((parts) => parts.join("\n\n"))
+  }
+}

+ 3 - 4
packages/opencode/src/session/session.ts

@@ -23,6 +23,7 @@ import { Share } from "../share/share"
 import { Message } from "./message"
 import { Message } from "./message"
 import { Bus } from "../bus"
 import { Bus } from "../bus"
 import { Provider } from "../provider/provider"
 import { Provider } from "../provider/provider"
+import { SessionContext } from "./context"
 
 
 export namespace Session {
 export namespace Session {
   const log = Log.create({ service: "session" })
   const log = Log.create({ service: "session" })
@@ -201,7 +202,6 @@ export namespace Session {
         (msg) => msg.role === "system" || msg.id >= lastSummary.id,
         (msg) => msg.role === "system" || msg.id >= lastSummary.id,
       )
       )
 
 
-    const app = await App.use()
     if (msgs.length === 0) {
     if (msgs.length === 0) {
       const system: Message.Info = {
       const system: Message.Info = {
         id: Identifier.ascending("message"),
         id: Identifier.ascending("message"),
@@ -220,9 +220,8 @@ export namespace Session {
           tool: {},
           tool: {},
         },
         },
       }
       }
-      const contextFile = Bun.file(path.join(app.path.root, "CONTEXT.md"))
-      if (await contextFile.exists()) {
-        const context = await contextFile.text()
+      const context = await SessionContext.find()
+      if (context) {
         system.parts.push({
         system.parts.push({
           type: "text",
           type: "text",
           text: context,
           text: context,

+ 1 - 1
packages/opencode/src/storage/storage.ts

@@ -18,7 +18,7 @@ export namespace Storage {
   }
   }
 
 
   const state = App.state("storage", async () => {
   const state = App.state("storage", async () => {
-    const app = await App.use()
+    const app = App.info()
     const storageDir = path.join(app.path.data, "storage")
     const storageDir = path.join(app.path.data, "storage")
     await fs.mkdir(storageDir, { recursive: true })
     await fs.mkdir(storageDir, { recursive: true })
     const storage = new FileStorage(new LocalStorageAdapter(storageDir))
     const storage = new FileStorage(new LocalStorageAdapter(storageDir))

+ 1 - 1
packages/opencode/src/tool/glob.ts

@@ -50,7 +50,7 @@ export const GlobTool = Tool.define({
       .optional(),
       .optional(),
   }),
   }),
   async execute(params) {
   async execute(params) {
-    const app = await App.use()
+    const app = App.info()
     const search = params.path || app.path.cwd
     const search = params.path || app.path.cwd
     const limit = 100
     const limit = 100
     const glob = new Bun.Glob(params.pattern)
     const glob = new Bun.Glob(params.pattern)

+ 1 - 1
packages/opencode/src/tool/grep.ts

@@ -286,7 +286,7 @@ export const GrepTool = Tool.define({
       throw new Error("pattern is required")
       throw new Error("pattern is required")
     }
     }
 
 
-    const app = await App.use()
+    const app = App.info()
     const searchPath = params.path || app.path.cwd
     const searchPath = params.path || app.path.cwd
 
 
     // If literalText is true, escape the pattern
     // If literalText is true, escape the pattern

+ 1 - 1
packages/opencode/src/tool/ls.ts

@@ -25,7 +25,7 @@ export const ListTool = Tool.define({
     ignore: z.array(z.string()).optional(),
     ignore: z.array(z.string()).optional(),
   }),
   }),
   async execute(params) {
   async execute(params) {
-    const app = await App.use()
+    const app = App.info()
     const searchPath = path.resolve(app.path.cwd, params.path || ".")
     const searchPath = path.resolve(app.path.cwd, params.path || ".")
 
 
     const glob = new Bun.Glob("**/*")
     const glob = new Bun.Glob("**/*")

+ 1 - 1
packages/opencode/src/tool/lsp-diagnostics.ts

@@ -34,7 +34,7 @@ TIPS:
     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 app = await App.use()
+    const app = App.info()
     const normalized = path.isAbsolute(args.path)
     const normalized = path.isAbsolute(args.path)
       ? args.path
       ? args.path
       : path.join(app.path.cwd, args.path)
       : path.join(app.path.cwd, args.path)

+ 1 - 2
packages/opencode/src/tool/lsp-hover.ts

@@ -17,8 +17,7 @@ export const LspHoverTool = Tool.define({
     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) => {
-    console.log(args)
-    const app = await App.use()
+    const app = App.info()
     const file = path.isAbsolute(args.file)
     const file = path.isAbsolute(args.file)
       ? args.file
       ? args.file
       : path.join(app.path.cwd, args.file)
       : path.join(app.path.cwd, args.file)

+ 12 - 9
packages/opencode/test/tool/tool.test.ts

@@ -6,17 +6,19 @@ import { ListTool } from "../../src/tool/ls"
 describe("tool.glob", () => {
 describe("tool.glob", () => {
   test("truncate", async () => {
   test("truncate", async () => {
     await App.provide({ cwd: process.cwd(), version: "test" }, async () => {
     await App.provide({ cwd: process.cwd(), version: "test" }, async () => {
-      let result = await GlobTool.execute({
-        pattern: "./node_modules/**/*",
-      })
+      let result = await GlobTool.execute(
+        { pattern: "./node_modules/**/*" },
+        { sessionID: "test" },
+      )
       expect(result.metadata.truncated).toBe(true)
       expect(result.metadata.truncated).toBe(true)
     })
     })
   })
   })
   test("basic", async () => {
   test("basic", async () => {
     await App.provide({ cwd: process.cwd(), version: "test" }, async () => {
     await App.provide({ cwd: process.cwd(), version: "test" }, async () => {
-      let result = await GlobTool.execute({
-        pattern: "*.json",
-      })
+      let result = await GlobTool.execute(
+        { pattern: "*.json" },
+        { sessionID: "test" },
+      )
       expect(result.metadata).toMatchObject({
       expect(result.metadata).toMatchObject({
         truncated: false,
         truncated: false,
         count: 2,
         count: 2,
@@ -30,9 +32,10 @@ describe("tool.ls", () => {
     const result = await App.provide(
     const result = await App.provide(
       { cwd: process.cwd(), version: "test" },
       { cwd: process.cwd(), version: "test" },
       async () => {
       async () => {
-        return await ListTool.execute({
-          path: "./example",
-        })
+        return await ListTool.execute(
+          { path: "./example" },
+          { sessionID: "test" },
+        )
       },
       },
     )
     )
     expect(result.output).toMatchSnapshot()
     expect(result.output).toMatchSnapshot()