Pārlūkot izejas kodu

resolve merge conflicts

Imanol Maiztegui 1 dienu atpakaļ
vecāks
revīzija
f4eaacfa55
34 mainītis faili ar 5272 papildinājumiem un 4606 dzēšanām
  1. 1 0
      .github/workflows/test.yml
  2. 12 2
      packages/app/e2e/fixtures.ts
  3. 2 0
      packages/app/e2e/session/session-review.spec.ts
  4. 1 1
      packages/app/script/e2e-local.ts
  5. 6 6
      packages/extensions/zed/extension.toml
  6. 2 2
      packages/opencode/package.json
  7. 15 0
      packages/opencode/script/seed-e2e.ts
  8. 22 9
      packages/opencode/specs/tui-plugins.md
  9. 2 1
      packages/opencode/src/bun/index.ts
  10. 2 2
      packages/opencode/src/cli/cmd/plug.ts
  11. 88 92
      packages/opencode/src/cli/cmd/run.ts
  12. 1 1
      packages/opencode/src/cli/cmd/tui/app.tsx
  13. 15 2
      packages/opencode/src/cli/cmd/tui/plugin/runtime.ts
  14. 5 2
      packages/opencode/src/config/config.ts
  15. 0 32
      packages/opencode/src/plugin/codex.ts
  16. 8 0
      packages/opencode/src/plugin/index.ts
  17. 52 19
      packages/opencode/src/plugin/install.ts
  18. 5 3
      packages/opencode/src/plugin/loader.ts
  19. 5 10
      packages/opencode/src/plugin/shared.ts
  20. 2 1
      packages/opencode/src/project/project.ts
  21. 5 15
      packages/opencode/src/session/llm.ts
  22. 114 0
      packages/opencode/src/session/prompt/kimi.txt
  23. 2 0
      packages/opencode/src/session/system.ts
  24. 1 2
      packages/opencode/src/tool/registry.ts
  25. 85 1
      packages/opencode/test/bun.test.ts
  26. 7 14
      packages/opencode/test/cli/tui/plugin-install.test.ts
  27. 6 0
      packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts
  28. 1 0
      packages/opencode/test/config/config.test.ts
  29. 7 1
      packages/opencode/test/plugin/install-concurrency.test.ts
  30. 34 7
      packages/opencode/test/plugin/install.test.ts
  31. 3 3
      packages/opencode/test/plugin/loader-shared.test.ts
  32. 4756 4373
      packages/opencode/test/tool/fixtures/models-api.json
  33. 4 4
      packages/plugin/package.json
  34. 1 1
      packages/script/src/index.ts

+ 1 - 0
.github/workflows/test.yml

@@ -107,6 +107,7 @@ jobs:
           KILO_ORG_ID: ${{ secrets.KILO_ORG_ID }}
           KILO_DISABLE_SHARE: "true"
           KILO_DISABLE_SESSION_INGEST: "true"
+          KILO_E2E_REQUIRE_PAID: "true"
           # kilocode_change end
         timeout-minutes: 30
 

+ 12 - 2
packages/app/e2e/fixtures.ts

@@ -15,6 +15,16 @@ import { createSdk, dirSlug, getWorktree, sessionPath } from "./utils"
 
 export const settingsKey = "settings.v3"
 
+const seedModel = (() => {
+  const [providerID = "opencode", modelID = "big-pickle"] = (
+    process.env.OPENCODE_E2E_MODEL ?? "opencode/big-pickle"
+  ).split("/")
+  return {
+    providerID: providerID || "opencode",
+    modelID: modelID || "big-pickle",
+  }
+})()
+
 type TestFixtures = {
   sdk: ReturnType<typeof createSdk>
   gotoSession: (sessionID?: string) => Promise<void>
@@ -125,7 +135,7 @@ export const test = base.extend<TestFixtures, WorkerFixtures>({
 
 async function seedStorage(page: Page, input: { directory: string; extra?: string[] }) {
   await seedProjects(page, input)
-  await page.addInitScript(() => {
+  await page.addInitScript((model: { providerID: string; modelID: string }) => {
     const win = window as E2EWindow
     win.__opencode_e2e = {
       ...win.__opencode_e2e,
@@ -148,7 +158,7 @@ async function seedStorage(page: Page, input: { directory: string; extra?: strin
         variant: {},
       }),
     )
-  })
+  }, seedModel)
 }
 
 export { expect }

+ 2 - 0
packages/app/e2e/session/session-review.spec.ts

@@ -234,6 +234,7 @@ async function fileOverflow(page: Parameters<typeof test>[0]["page"]) {
 }
 
 test("review applies inline comment clicks without horizontal overflow", async ({ page, withProject }) => {
+  test.skip(true, "Flaky in CI for now.")
   test.setTimeout(180_000)
 
   const tag = `review-comment-${Date.now()}`
@@ -283,6 +284,7 @@ test("review applies inline comment clicks without horizontal overflow", async (
 })
 
 test("review file comments submit on click without clipping actions", async ({ page, withProject }) => {
+  test.skip(true, "Flaky in CI for now.")
   test.setTimeout(180_000)
 
   const tag = `review-file-comment-${Date.now()}`

+ 1 - 1
packages/app/script/e2e-local.ts

@@ -59,8 +59,8 @@ const keepSandbox = process.env.KILO_E2E_KEEP_SANDBOX === "1"
 
 const serverEnv = {
   ...process.env,
-  KILO_DISABLE_SHARE: process.env.KILO_DISABLE_SHARE ?? "true", // kilocode_change
   KILO_DISABLE_SESSION_INGEST: "true", // kilocode_change
+  KILO_DISABLE_SHARE: process.env.KILO_DISABLE_SHARE ?? "true",
   KILO_DISABLE_LSP_DOWNLOAD: "true",
   KILO_DISABLE_DEFAULT_PLUGINS: "true",
   KILO_EXPERIMENTAL_DISABLE_FILEWATCHER: "true",

+ 6 - 6
packages/extensions/zed/extension.toml

@@ -1,7 +1,7 @@
 id = "kilo"
 name = "Kilo"
 description = "The open source coding agent."
-version = "1.3.10"
+version = "1.3.11"
 schema_version = 1
 authors = ["Anomaly"]
 repository = "https://github.com/Kilo-Org/kilocode"
@@ -11,26 +11,26 @@ name = "Kilo"
 icon = "./icons/opencode.svg"
 
 [agent_servers.opencode.targets.darwin-aarch64]
-archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.3.10/opencode-darwin-arm64.zip"
+archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.3.11/opencode-darwin-arm64.zip"
 cmd = "./opencode"
 args = ["acp"]
 
 [agent_servers.opencode.targets.darwin-x86_64]
-archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.3.10/opencode-darwin-x64.zip"
+archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.3.11/opencode-darwin-x64.zip"
 cmd = "./opencode"
 args = ["acp"]
 
 [agent_servers.opencode.targets.linux-aarch64]
-archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.3.10/opencode-linux-arm64.tar.gz"
+archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.3.11/opencode-linux-arm64.tar.gz"
 cmd = "./opencode"
 args = ["acp"]
 
 [agent_servers.opencode.targets.linux-x86_64]
-archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.3.10/opencode-linux-x64.tar.gz"
+archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.3.11/opencode-linux-x64.tar.gz"
 cmd = "./opencode"
 args = ["acp"]
 
 [agent_servers.opencode.targets.windows-x86_64]
-archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.3.10/opencode-windows-x64.zip"
+archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.3.11/opencode-windows-x64.zip"
 cmd = "./opencode.exe"
 args = ["acp"]

+ 2 - 2
packages/opencode/package.json

@@ -103,8 +103,8 @@
     "@kilocode/sdk": "workspace:*",
     "@opencode-ai/util": "workspace:*",
     "@openrouter/ai-sdk-provider": "2.3.3",
-    "@opentui/core": "0.1.92",
-    "@opentui/solid": "0.1.92",
+    "@opentui/core": "0.1.93",
+    "@opentui/solid": "0.1.93",
     "@parcel/watcher": "2.5.1",
     "@pierre/diffs": "catalog:",
     "@solid-primitives/event-bus": "1.1.2",

+ 15 - 0
packages/opencode/script/seed-e2e.ts

@@ -2,6 +2,7 @@ const dir = process.env.KILO_E2E_PROJECT_DIR ?? process.cwd()
 const title = process.env.KILO_E2E_SESSION_TITLE ?? "E2E Session"
 const text = process.env.KILO_E2E_MESSAGE ?? "Seeded for UI e2e"
 const model = process.env.KILO_E2E_MODEL ?? "kilo/kilo-auto/frontier"
+const requirePaid = process.env.KILO_E2E_REQUIRE_PAID === "true"
 const parts = model.split("/")
 const providerID = parts[0] ?? "kilo" // kilocode_change
 const modelID = parts.slice(1).join("/") || "kilo-auto/frontier" // kilocode_change
@@ -11,6 +12,7 @@ const seed = async () => {
   const { Instance } = await import("../src/project/instance")
   const { InstanceBootstrap } = await import("../src/project/bootstrap")
   const { Config } = await import("../src/config/config")
+  const { Provider } = await import("../src/provider/provider")
   const { Session } = await import("../src/session")
   const { MessageID, PartID } = await import("../src/session/schema")
   const { Project } = await import("../src/project/project")
@@ -25,6 +27,19 @@ const seed = async () => {
         await Config.waitForDependencies()
         await ToolRegistry.ids()
 
+        if (requirePaid && providerID === "kilo" && !process.env.KILO_API_KEY) {
+          throw new Error("KILO_API_KEY is required when KILO_E2E_REQUIRE_PAID=true")
+        }
+
+        const info = await Provider.getModel(ProviderID.make(providerID), ModelID.make(modelID))
+        if (requirePaid) {
+          const paid =
+            info.cost.input > 0 || info.cost.output > 0 || info.cost.cache.read > 0 || info.cost.cache.write > 0
+          if (!paid) {
+            throw new Error(`KILO_E2E_MODEL must resolve to a paid model: ${providerID}/${modelID}`)
+          }
+        }
+
         const session = await Session.create({ title })
         const messageID = MessageID.ascending()
         const partID = PartID.ascending()

+ 22 - 9
packages/opencode/specs/tui-plugins.md

@@ -88,6 +88,7 @@ export default plugin
 - If package `exports` exists, loader only resolves `./tui` or `./server`; it never falls back to `exports["."]`.
 - For npm package specs, TUI does not use `package.json` `main` as a fallback entry.
 - `package.json` `main` is only used for server plugin entrypoint resolution.
+- If a configured plugin has no target-specific entrypoint, it is skipped with a warning (not a load failure).
 - If a package supports both server and TUI, use separate files and package `exports` (`./server` and `./tui`) so each target resolves to a target-only module.
 - File/path plugins must export a non-empty `id`.
 - npm plugins may omit `id`; package `name` is used.
@@ -100,7 +101,10 @@ export default plugin
 
 ## Package manifest and install
 
-Package manifest is read from `package.json` field `oc-plugin`.
+Install target detection is inferred from `package.json` entrypoints:
+
+- `server` target when `exports["./server"]` exists or `main` is set.
+- `tui` target when `exports["./tui"]` exists.
 
 Example:
 
@@ -108,14 +112,20 @@ Example:
 {
   "name": "@acme/opencode-plugin",
   "type": "module",
-  "main": "./dist/index.js",
+  "main": "./dist/server.js",
+  "exports": {
+    "./server": {
+      "import": "./dist/server.js",
+      "config": { "custom": true }
+    },
+    "./tui": {
+      "import": "./dist/tui.js",
+      "config": { "compact": true }
+    }
+  },
   "engines": {
     "opencode": "^1.0.0"
-  },
-  "oc-plugin": [
-    ["server", { "custom": true }],
-    ["tui", { "compact": true }]
-  ]
+  }
 }
 ```
 
@@ -144,12 +154,16 @@ npm plugins can declare a version compatibility range in `package.json` using th
 - Local installs resolve target dir inside `patchPluginConfig`.
 - For local scope, path is `<worktree>/.opencode` only when VCS is git and `worktree !== "/"`; otherwise `<directory>/.opencode`.
 - Root-worktree fallback (`worktree === "/"` uses `<directory>/.opencode`) is covered by regression tests.
-- `patchPluginConfig` applies all declared manifest targets (`server` and/or `tui`) in one call.
+- `patchPluginConfig` applies all detected targets (`server` and/or `tui`) in one call.
 - `patchPluginConfig` returns structured result unions (`ok`, `code`, fields by error kind) instead of custom thrown errors.
 - `patchPluginConfig` serializes per-target config writes with `Flock.acquire(...)`.
 - `patchPluginConfig` uses targeted `jsonc-parser` edits, so existing JSONC comments are preserved when plugin entries are added or replaced.
+- npm plugin package installs are executed with `--ignore-scripts`, so package `install` / `postinstall` lifecycle scripts are not run.
+- `exports["./server"].config` and `exports["./tui"].config` can provide default plugin options written on first install.
 - Without `--force`, an already-configured npm package name is a no-op.
 - With `--force`, replacement matches by package name. If the existing row is `[spec, options]`, those tuple options are kept.
+- Explicit npm specs with a version suffix (for example `[email protected]`) are pinned. Runtime install requests that exact version and does not run stale/latest checks for newer registry versions.
+- Bare npm specs (`pkg`) are treated as `latest` and can refresh when the cached version is stale.
 - Tuple targets in `oc-plugin` provide default options written into config.
 - A package can target `server`, `tui`, or both.
 - If a package targets both, each target must still resolve to a separate target-only module. Do not export `{ server, tui }` from one module.
@@ -317,7 +331,6 @@ Slot notes:
 - `api.plugins.install(spec, { global? })` runs install -> manifest read -> config patch using the same helper flow as CLI install.
 - `api.plugins.install(...)` returns either `{ ok: false, message, missing? }` or `{ ok: true, dir, tui }`.
 - `api.plugins.install(...)` does not load plugins into the current session. Call `api.plugins.add(spec)` to load after install.
-- For packages that declare a tuple `tui` target in `oc-plugin`, `api.plugins.install(...)` stages those tuple options so a following `api.plugins.add(spec)` uses them.
 - If activation fails, the plugin can remain `enabled=true` and `active=false`.
 - `api.lifecycle.signal` is aborted before cleanup runs.
 - `api.lifecycle.onDispose(fn)` registers cleanup and returns an unregister function.

+ 2 - 1
packages/opencode/src/bun/index.ts

@@ -50,7 +50,7 @@ export namespace BunProc {
     }),
   )
 
-  export async function install(pkg: string, version = "latest") {
+  export async function install(pkg: string, version = "latest", opts?: { ignoreScripts?: boolean }) {
     // Use lock to ensure only one install at a time
     using _ = await Lock.write("bun-install")
 
@@ -82,6 +82,7 @@ export namespace BunProc {
       "add",
       "--force",
       "--exact",
+      ...(opts?.ignoreScripts ? ["--ignore-scripts"] : []),
       // TODO: get rid of this case (see: https://github.com/oven-sh/bun/issues/19936)
       ...(proxied() || process.env.CI ? ["--no-cache"] : []),
       "--cwd",

+ 2 - 2
packages/opencode/src/cli/cmd/plug.ts

@@ -114,8 +114,8 @@ export function createPlugTask(input: PlugInput, dep: PlugDeps = defaultPlugDeps
 
       if (manifest.code === "manifest_no_targets") {
         inspect.stop("No plugin targets found", 1)
-        dep.log.error(`"${mod}" does not declare supported targets in package.json`)
-        dep.log.info('Expected: "oc-plugin": ["server", "tui"] or tuples like [["tui", { ... }]].')
+        dep.log.error(`"${mod}" does not expose plugin entrypoints in package.json`)
+        dep.log.info('Expected one of: exports["./tui"], exports["./server"], or package.json main for server.')
         return false
       }
 

+ 88 - 92
packages/opencode/src/cli/cmd/run.ts

@@ -224,95 +224,93 @@ export const RunCommand = cmd({
   command: "run [message..]",
   describe: "run kilo with a message", // kilocode_change
   builder: (yargs: Argv) => {
-    return (
-      yargs
-        .positional("message", {
-          describe: "message to send",
-          type: "string",
-          array: true,
-          default: [],
-        })
-        .option("command", {
-          describe: "the command to run, use message for args",
-          type: "string",
-        })
-        .option("continue", {
-          alias: ["c"],
-          describe: "continue the last session",
-          type: "boolean",
-        })
-        .option("session", {
-          alias: ["s"],
-          describe: "session id to continue",
-          type: "string",
-        })
-        .option("fork", {
-          describe: "fork the session before continuing (requires --continue or --session)",
-          type: "boolean",
-        })
-        .option("share", {
-          type: "boolean",
-          describe: "share the session",
-        })
-        .option("model", {
-          type: "string",
-          alias: ["m"],
-          describe: "model to use in the format of provider/model",
-        })
-        .option("agent", {
-          type: "string",
-          describe: "agent to use",
-        })
-        .option("format", {
-          type: "string",
-          choices: ["default", "json"],
-          default: "default",
-          describe: "format: default (formatted) or json (raw JSON events)",
-        })
-        .option("file", {
-          alias: ["f"],
-          type: "string",
-          array: true,
-          describe: "file(s) to attach to message",
-        })
-        .option("title", {
-          type: "string",
-          describe: "title for the session (uses truncated prompt if no value provided)",
-        })
-        .option("attach", {
-          type: "string",
-          describe: "attach to a running opencode server (e.g., http://localhost:4096)",
-        })
-        .option("password", {
-          alias: ["p"],
-          type: "string",
-          describe: "basic auth password (defaults to KILO_SERVER_PASSWORD)",
-        })
-        .option("dir", {
-          type: "string",
-          describe: "directory to run in, path on remote server if attaching",
-        })
-        .option("port", {
-          type: "number",
-          describe: "port for the local server (defaults to random port if no value provided)",
-        })
-        .option("variant", {
-          type: "string",
-          describe: "model variant (provider-specific reasoning effort, e.g., high, max, minimal)",
-        })
-        // kilocode_change start - auto approve all permissions
-        .option("auto", {
-          type: "boolean",
-          describe: "auto-approve all permissions (for autonomous/pipeline usage)",
-          default: false,
-        })
-        // kilocode_change end
-        .option("thinking", {
-          type: "boolean",
-          describe: "show thinking blocks",
-          default: false,
-        })
-    )
+    return yargs
+      .positional("message", {
+        describe: "message to send",
+        type: "string",
+        array: true,
+        default: [],
+      })
+      .option("command", {
+        describe: "the command to run, use message for args",
+        type: "string",
+      })
+      .option("continue", {
+        alias: ["c"],
+        describe: "continue the last session",
+        type: "boolean",
+      })
+      .option("session", {
+        alias: ["s"],
+        describe: "session id to continue",
+        type: "string",
+      })
+      .option("fork", {
+        describe: "fork the session before continuing (requires --continue or --session)",
+        type: "boolean",
+      })
+      .option("share", {
+        type: "boolean",
+        describe: "share the session",
+      })
+      .option("model", {
+        type: "string",
+        alias: ["m"],
+        describe: "model to use in the format of provider/model",
+      })
+      .option("agent", {
+        type: "string",
+        describe: "agent to use",
+      })
+      .option("format", {
+        type: "string",
+        choices: ["default", "json"],
+        default: "default",
+        describe: "format: default (formatted) or json (raw JSON events)",
+      })
+      .option("file", {
+        alias: ["f"],
+        type: "string",
+        array: true,
+        describe: "file(s) to attach to message",
+      })
+      .option("title", {
+        type: "string",
+        describe: "title for the session (uses truncated prompt if no value provided)",
+      })
+      .option("attach", {
+        type: "string",
+        describe: "attach to a running opencode server (e.g., http://localhost:4096)",
+      })
+      .option("password", {
+        alias: ["p"],
+        type: "string",
+        describe: "basic auth password (defaults to KILO_SERVER_PASSWORD)",
+      })
+      .option("dir", {
+        type: "string",
+        describe: "directory to run in, path on remote server if attaching",
+      })
+      .option("port", {
+        type: "number",
+        describe: "port for the local server (defaults to random port if no value provided)",
+      })
+      .option("variant", {
+        type: "string",
+        describe: "model variant (provider-specific reasoning effort, e.g., high, max, minimal)",
+      })
+      // kilocode_change start - auto approve all permissions
+      .option("auto", {
+        type: "boolean",
+        describe: "auto-approve all permissions (for autonomous/pipeline usage)",
+        default: false,
+      })
+      // kilocode_change end
+      .option("thinking", {
+        type: "boolean",
+        describe: "show thinking blocks",
+        default: false,
+      })
   },
   handler: async (args) => {
     let message = [...args.message, ...(args["--"] || [])]
@@ -727,11 +725,9 @@ export const RunCommand = cmd({
 
     if (args.attach) {
       const headers = (() => {
-        // kilocode_change start
         const password = args.password ?? process.env.KILO_SERVER_PASSWORD
         if (!password) return undefined
-        const username = process.env.KILO_SERVER_USERNAME ?? "kilo"
-        // kilocode_change end
+        const username = process.env.KILO_SERVER_USERNAME ?? "kilo" // kilocode_change
         const auth = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`
         return { Authorization: auth }
       })()

+ 1 - 1
packages/opencode/src/cli/cmd/tui/app.tsx

@@ -128,6 +128,7 @@ import { DialogVariant } from "./component/dialog-variant"
 
 function rendererConfig(_config: TuiConfig.Info): CliRendererConfig {
   return {
+    externalOutputMode: "passthrough",
     targetFps: 60,
     gatherStats: false,
     exitOnCtrlC: false,
@@ -253,7 +254,6 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
   const route = useRoute()
   const dimensions = useTerminalDimensions()
   const renderer = useRenderer()
-  renderer.disableStdoutInterception()
   const dialog = useDialog()
   const local = useLocal()
   const kv = useKV()

+ 15 - 2
packages/opencode/src/cli/cmd/tui/plugin/runtime.ts

@@ -87,6 +87,11 @@ function fail(message: string, data: Record<string, unknown>) {
   console.error(`[tui.plugin] ${text}`, next)
 }
 
+function warn(message: string, data: Record<string, unknown>) {
+  log.warn(message, data)
+  console.warn(`[tui.plugin] ${message}`, data)
+}
+
 type CleanupResult = { type: "ok" } | { type: "error"; error: unknown } | { type: "timeout" }
 
 function runCleanup(fn: () => unknown, ms: number): Promise<CleanupResult> {
@@ -229,6 +234,15 @@ async function loadExternalPlugin(cfg: TuiConfig.PluginRecord, retry = false): P
   log.info("loading tui plugin", { path: plan.spec, retry })
   const resolved = await PluginLoader.resolve(plan, "tui")
   if (!resolved.ok) {
+    if (resolved.stage === "missing") {
+      warn("tui plugin has no entrypoint", {
+        path: plan.spec,
+        retry,
+        message: resolved.message,
+      })
+      return
+    }
+
     if (resolved.stage === "install") {
       fail("failed to resolve tui plugin", { path: plan.spec, retry, error: resolved.error })
       return
@@ -753,7 +767,6 @@ async function addPluginBySpec(state: RuntimeState | undefined, raw: string) {
     return [] as PluginLoad[]
   })
   if (!ready.length) {
-    fail("failed to add tui plugin", { path: next })
     return false
   }
 
@@ -824,7 +837,7 @@ async function installPluginBySpec(
     if (manifest.code === "manifest_no_targets") {
       return {
         ok: false,
-        message: `"${spec}" does not declare supported targets in package.json`,
+        message: `"${spec}" does not expose plugin entrypoints in package.json`,
       }
     }
 

+ 5 - 2
packages/opencode/src/config/config.ts

@@ -137,7 +137,10 @@ export namespace Config {
     const gitignore = path.join(dir, ".gitignore")
     const ignore = await Filesystem.exists(gitignore)
     if (!ignore) {
-      await Filesystem.write(gitignore, ["node_modules", "package.json", "bun.lock", ".gitignore"].join("\n"))
+      await Filesystem.write(
+        gitignore,
+        ["node_modules", "package.json", "package-lock.json", "bun.lock", ".gitignore"].join("\n"),
+      )
     }
 
     // Bun can race cache writes on Windows when installs run in parallel across dirs.
@@ -1585,12 +1588,12 @@ export namespace Config {
           }
 
           if (process.env.KILO_CONFIG_CONTENT) {
-            // kilocode_change start
             result = mergeConfigConcatArrays(
               result,
               yield* loadConfig(process.env.KILO_CONFIG_CONTENT, {
                 dir: ctx.directory,
                 source: "KILO_CONFIG_CONTENT",
+              // kilocode_change start
               }).pipe(
                 Effect.tap(() => Effect.sync(() => log.debug("loaded custom config from KILO_CONFIG_CONTENT"))),
                 Effect.catchDefect((err: unknown) => {

+ 0 - 32
packages/opencode/src/plugin/codex.ts

@@ -381,38 +381,6 @@ export async function CodexAuthPlugin(input: PluginInput): Promise<Hooks> {
           delete provider.models[modelId]
         }
 
-        if (!provider.models["gpt-5.3-codex"]) {
-          const model = {
-            id: ModelID.make("gpt-5.3-codex"),
-            providerID: ProviderID.openai,
-            api: {
-              id: "gpt-5.3-codex",
-              url: "https://chatgpt.com/backend-api/codex",
-              npm: "@ai-sdk/openai",
-            },
-            name: "GPT-5.3 Codex",
-            capabilities: {
-              temperature: false,
-              reasoning: true,
-              attachment: true,
-              toolcall: true,
-              input: { text: true, audio: false, image: true, video: false, pdf: false },
-              output: { text: true, audio: false, image: false, video: false, pdf: false },
-              interleaved: false,
-            },
-            cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
-            limit: { context: 400_000, input: 272_000, output: 128_000 },
-            status: "active" as const,
-            options: {},
-            headers: {},
-            release_date: "2026-02-05",
-            variants: {} as Record<string, Record<string, any>>,
-            family: "gpt-codex",
-          }
-          model.variants = ProviderTransform.variants(model)
-          provider.models["gpt-5.3-codex"] = model
-        }
-
         // Zero out costs for Codex (included with ChatGPT subscription)
         for (const model of Object.values(provider.models)) {
           model.cost = {

+ 8 - 0
packages/opencode/src/plugin/index.ts

@@ -165,6 +165,14 @@ export namespace Plugin {
 
                 const resolved = await PluginLoader.resolve(plan, "server")
                 if (!resolved.ok) {
+                  if (resolved.stage === "missing") {
+                    log.warn("plugin has no server entrypoint", {
+                      path: plan.spec,
+                      message: resolved.message,
+                    })
+                    return
+                  }
+
                   const cause =
                     resolved.error instanceof Error ? (resolved.error.cause ?? resolved.error) : resolved.error
                   const message = errorMessage(cause)

+ 52 - 19
packages/opencode/src/plugin/install.ts

@@ -11,6 +11,7 @@ import { ConfigPaths } from "@/config/paths"
 import { Global } from "@/global"
 import { Filesystem } from "@/util/filesystem"
 import { Flock } from "@/util/flock"
+import { isRecord } from "@/util/record"
 
 import { parsePluginSpecifier, readPluginPackage, resolvePluginTarget } from "./shared"
 
@@ -101,28 +102,60 @@ function pluginList(data: unknown) {
   return item.plugin
 }
 
-function parseTarget(item: unknown): Target | undefined {
-  if (item === "server" || item === "tui") return { kind: item }
-  if (!Array.isArray(item)) return
-  if (item[0] !== "server" && item[0] !== "tui") return
-  if (item.length < 2) return { kind: item[0] }
-  const opt = item[1]
-  if (!opt || typeof opt !== "object" || Array.isArray(opt)) return { kind: item[0] }
+function exportValue(value: unknown): string | undefined {
+  if (typeof value === "string") {
+    const next = value.trim()
+    if (next) return next
+    return
+  }
+  if (!isRecord(value)) return
+  for (const key of ["import", "default"]) {
+    const next = value[key]
+    if (typeof next !== "string") continue
+    const hit = next.trim()
+    if (!hit) continue
+    return hit
+  }
+}
+
+function exportOptions(value: unknown): Record<string, unknown> | undefined {
+  if (!isRecord(value)) return
+  const config = value.config
+  if (!isRecord(config)) return
+  return config
+}
+
+function exportTarget(pkg: Record<string, unknown>, kind: Kind) {
+  const exports = pkg.exports
+  if (!isRecord(exports)) return
+  const value = exports[`./${kind}`]
+  const entry = exportValue(value)
+  if (!entry) return
   return {
-    kind: item[0],
-    opts: opt,
+    opts: exportOptions(value),
   }
 }
 
-function parseTargets(raw: unknown) {
-  if (!Array.isArray(raw)) return []
-  const map = new Map<Kind, Target>()
-  for (const item of raw) {
-    const hit = parseTarget(item)
-    if (!hit) continue
-    map.set(hit.kind, hit)
+function hasMainTarget(pkg: Record<string, unknown>) {
+  const main = pkg.main
+  if (typeof main !== "string") return false
+  return Boolean(main.trim())
+}
+
+function packageTargets(pkg: Record<string, unknown>) {
+  const targets: Target[] = []
+  const server = exportTarget(pkg, "server")
+  if (server) {
+    targets.push({ kind: "server", opts: server.opts })
+  } else if (hasMainTarget(pkg)) {
+    targets.push({ kind: "server" })
+  }
+
+  const tui = exportTarget(pkg, "tui")
+  if (tui) {
+    targets.push({ kind: "tui", opts: tui.opts })
   }
-  return [...map.values()]
+  return targets
 }
 
 function patch(text: string, path: Array<string | number>, value: unknown, insert = false) {
@@ -260,7 +293,7 @@ export async function readPluginManifest(target: string): Promise<ManifestResult
     }
   }
 
-  const targets = parseTargets(pkg.item.json["oc-plugin"])
+  const targets = packageTargets(pkg.item.json)
   if (!targets.length) {
     return {
       ok: false,
@@ -330,7 +363,7 @@ async function patchOne(dir: string, target: Target, spec: string, force: boolea
   }
 
   const list = pluginList(data)
-  const item = target.opts ? [spec, target.opts] : spec
+  const item = target.opts ? ([spec, target.opts] as const) : spec
   const out = patchPluginList(text, list, spec, item, force)
   if (out.mode === "noop") {
     return {

+ 5 - 3
packages/opencode/src/plugin/loader.ts

@@ -43,7 +43,9 @@ export namespace PluginLoader {
     plan: Plan,
     kind: PluginKind,
   ): Promise<
-    { ok: true; value: Resolved } | { ok: false; stage: "install" | "entry" | "compatibility"; error: unknown }
+    | { ok: true; value: Resolved }
+    | { ok: false; stage: "missing"; message: string }
+    | { ok: false; stage: "install" | "entry" | "compatibility"; error: unknown }
   > {
     let target = ""
     try {
@@ -77,8 +79,8 @@ export namespace PluginLoader {
     if (!base.entry) {
       return {
         ok: false,
-        stage: "entry",
-        error: new Error(`Plugin ${plan.spec} entry is empty`),
+        stage: "missing",
+        message: `Plugin ${plan.spec} does not expose a ${kind} entrypoint`,
       }
     }
 

+ 5 - 10
packages/opencode/src/plugin/shared.ts

@@ -34,7 +34,7 @@ export type PluginEntry = {
   source: PluginSource
   target: string
   pkg?: PluginPackage
-  entry: string
+  entry?: string
 }
 
 const INDEX_FILES = ["index.ts", "index.tsx", "index.js", "index.mjs", "index.cjs"]
@@ -128,13 +128,8 @@ async function resolvePluginEntrypoint(spec: string, target: string, kind: Plugi
       if (index) return pathToFileURL(index).href
     }
 
-    if (source === "npm") {
-      throw new TypeError(`Plugin ${spec} must define package.json exports["./tui"]`)
-    }
-
-    if (dir) {
-      throw new TypeError(`Plugin ${spec} must define package.json exports["./tui"] or include index file`)
-    }
+    if (source === "npm") return
+    if (dir) return
 
     return target
   }
@@ -145,7 +140,7 @@ async function resolvePluginEntrypoint(spec: string, target: string, kind: Plugi
       if (index) return pathToFileURL(index).href
     }
 
-    throw new TypeError(`Plugin ${spec} must define package.json exports["./server"] or package.json main`)
+    return
   }
 
   return target
@@ -189,7 +184,7 @@ export async function checkPluginCompatibility(target: string, opencodeVersion:
 
 export async function resolvePluginTarget(spec: string, parsed = parsePluginSpecifier(spec)) {
   if (isPathPluginSpec(spec)) return resolvePathPluginTarget(spec)
-  return BunProc.install(parsed.pkg, parsed.version)
+  return BunProc.install(parsed.pkg, parsed.version, { ignoreScripts: true })
 }
 
 export async function readPluginPackage(target: string): Promise<PluginPackage> {

+ 2 - 1
packages/opencode/src/project/project.ts

@@ -260,7 +260,8 @@ export namespace Project {
               time: { created: Date.now(), updated: Date.now() },
             }
 
-        if (Flag.KILO_EXPERIMENTAL_ICON_DISCOVERY) yield* discover(existing).pipe(Effect.ignore, Effect.forkIn(scope))
+        if (Flag.KILO_EXPERIMENTAL_ICON_DISCOVERY)
+          yield* discover(existing).pipe(Effect.ignore, Effect.forkIn(scope))
 
         const result: Info = {
           ...existing,

+ 5 - 15
packages/opencode/src/session/llm.ts

@@ -60,32 +60,22 @@ export namespace LLM {
     Effect.gen(function* () {
       return Service.of({
         stream(input) {
-          const stream: Stream.Stream<Event, unknown> = Stream.scoped(
+          return Stream.scoped(
             Stream.unwrap(
               Effect.gen(function* () {
                 const ctrl = yield* Effect.acquireRelease(
                   Effect.sync(() => new AbortController()),
                   (ctrl) => Effect.sync(() => ctrl.abort()),
                 )
-                const queue = yield* Queue.unbounded<Event, unknown | Cause.Done>()
 
-                yield* Effect.promise(async () => {
-                  const result = await LLM.stream({ ...input, abort: ctrl.signal })
-                  for await (const event of result.fullStream) {
-                    if (!Queue.offerUnsafe(queue, event)) break
-                  }
-                  Queue.endUnsafe(queue)
-                }).pipe(
-                  Effect.catchCause((cause) => Effect.sync(() => void Queue.failCauseUnsafe(queue, cause))),
-                  Effect.onInterrupt(() => Effect.sync(() => ctrl.abort())),
-                  Effect.forkScoped,
-                )
+                const result = yield* Effect.promise(() => LLM.stream({ ...input, abort: ctrl.signal }))
 
-                return Stream.fromQueue(queue)
+                return Stream.fromAsyncIterable(result.fullStream, (e) =>
+                  e instanceof Error ? e : new Error(String(e)),
+                )
               }),
             ),
           )
-          return stream
         },
       })
     }),

+ 114 - 0
packages/opencode/src/session/prompt/kimi.txt

@@ -0,0 +1,114 @@
+You are OpenCode, an interactive general AI agent running on a user's computer.
+
+Your primary goal is to help users with software engineering tasks by taking action — use the tools available to you to make real changes on the user's system. You should also answer questions when asked. Always adhere strictly to the following system instructions and the user's requirements.
+
+# Prompt and Tool Use
+
+The user's messages may contain questions and/or task descriptions in natural language, code snippets, logs, file paths, or other forms of information. Read them, understand them and do what the user requested. For simple questions/greetings that do not involve any information in the working directory or on the internet, you may simply reply directly. For anything else, default to taking action with tools. When the request could be interpreted as either a question to answer or a task to complete, treat it as a task.
+
+When handling the user's request, if it involves creating, modifying, or running code or files, you MUST use the appropriate tools to make actual changes — do not just describe the solution in text. For questions that only need an explanation, you may reply in text directly. When calling tools, do not provide explanations because the tool calls themselves should be self-explanatory. You MUST follow the description of each tool and its parameters when calling tools.
+
+If the `task` tool is available, you can use it to delegate a focused subtask to a subagent instance. When delegating, provide a complete prompt with all necessary context because a newly created subagent does not automatically see your current context.
+
+You have the capability to output any number of tool calls in a single response. If you anticipate making multiple non-interfering tool calls, you are HIGHLY RECOMMENDED to make them in parallel to significantly improve efficiency. This is very important to your performance.
+
+The results of the tool calls will be returned to you in a tool message. You must determine your next action based on the tool call results, which could be one of the following: 1. Continue working on the task, 2. Inform the user that the task is completed or has failed, or 3. Ask the user for more information.
+
+Tool results and user messages may include `<system-reminder>` tags. These are authoritative system directives that you MUST follow. They bear no direct relation to the specific tool results or user messages in which they appear. Always read them carefully and comply with their instructions — they may override or constrain your normal behavior (e.g., restricting you to read-only actions during plan mode).
+
+When responding to the user, you MUST use the SAME language as the user, unless explicitly instructed to do otherwise.
+
+# General Guidelines for Coding
+
+When building something from scratch, you should:
+
+- Understand the user's requirements.
+- Ask the user for clarification if there is anything unclear.
+- Design the architecture and make a plan for the implementation.
+- Write the code in a modular and maintainable way.
+
+Always use tools to implement your code changes:
+
+- Use `write`/`edit` to create or modify source files. Code that only appears in your text response is NOT saved to the file system and will not take effect.
+- Use `bash` to run and test your code after writing it.
+- Iterate: if tests fail, read the error, fix the code with `write`/`edit`, and re-test with `bash`.
+
+When working on an existing codebase, you should:
+
+- Understand the codebase by reading it with tools (`read`, `glob`, `grep`) before making changes. Identify the ultimate goal and the most important criteria to achieve the goal.
+- For a bug fix, you typically need to check error logs or failed tests, scan over the codebase to find the root cause, and figure out a fix. If user mentioned any failed tests, you should make sure they pass after the changes.
+- For a feature, you typically need to design the architecture, and write the code in a modular and maintainable way, with minimal intrusions to existing code. Add new tests if the project already has tests.
+- For a code refactoring, you typically need to update all the places that call the code you are refactoring if the interface changes. DO NOT change any existing logic especially in tests, focus only on fixing any errors caused by the interface changes.
+- Make MINIMAL changes to achieve the goal. This is very important to your performance.
+- Follow the coding style of existing code in the project.
+
+DO NOT run `git commit`, `git push`, `git reset`, `git rebase` and/or do any other git mutations unless explicitly asked to do so. Ask for confirmation each time when you need to do git mutations, even if the user has confirmed in earlier conversations.
+
+# General Guidelines for Research and Data Processing
+
+The user may ask you to research on certain topics, process or generate certain multimedia files. When doing such tasks, you must:
+
+- Understand the user's requirements thoroughly, ask for clarification before you start if needed.
+- Make plans before doing deep or wide research, to ensure you are always on track.
+- Search on the Internet if possible, with carefully-designed search queries to improve efficiency and accuracy.
+- Use proper tools or shell commands or Python packages to process or generate images, videos, PDFs, docs, spreadsheets, presentations, or other multimedia files. Detect if there are already such tools in the environment. If you have to install third-party tools/packages, you MUST ensure that they are installed in a virtual/isolated environment.
+- Once you generate or edit any images, videos or other media files, try to read it again before proceed, to ensure that the content is as expected.
+- Avoid installing or deleting anything to/from outside of the current working directory. If you have to do so, ask the user for confirmation.
+
+# Working Environment
+
+## Operating System
+
+The operating environment is not in a sandbox. Any actions you do will immediately affect the user's system. So you MUST be extremely cautious. Unless being explicitly instructed to do so, you should never access (read/write/execute) files outside of the working directory.
+
+## Working Directory
+
+The working directory should be considered as the project root if you are instructed to perform tasks on the project. Every file system operation will be relative to the working directory if you do not explicitly specify the absolute path. Tools may require absolute paths for some parameters, IF SO, YOU MUST use absolute paths for these parameters.
+
+# Project Information
+
+Markdown files named `AGENTS.md` usually contain the background, structure, coding styles, user preferences and other relevant information about the project. You should use this information to understand the project and the user's preferences. `AGENTS.md` files may exist at different locations in the project, but typically there is one in the project root.
+
+> Why `AGENTS.md`?
+>
+> `README.md` files are for humans: quick starts, project descriptions, and contribution guidelines. `AGENTS.md` complements this by containing the extra, sometimes detailed context coding agents need: build steps, tests, and conventions that might clutter a README or aren’t relevant to human contributors.
+>
+> We intentionally kept it separate to:
+>
+> - Give agents a clear, predictable place for instructions.
+> - Keep `README`s concise and focused on human contributors.
+> - Provide precise, agent-focused guidance that complements existing `README` and docs.
+If the `AGENTS.md` is empty or insufficient, you may check `README`/`README.md` files or `AGENTS.md` files in subdirectories for more information about specific parts of the project.
+
+If you modified any files/styles/structures/configurations/workflows/... mentioned in `AGENTS.md` files, you MUST update the corresponding `AGENTS.md` files to keep them up-to-date.
+
+# Skills
+
+Skills are reusable, composable capabilities that enhance your abilities. Each skill is a self-contained directory with a `SKILL.md` file that contains instructions, examples, and/or reference material.
+
+## What are skills?
+
+Skills are modular extensions that provide:
+
+- Specialized knowledge: Domain-specific expertise (e.g., PDF processing, data analysis)
+- Workflow patterns: Best practices for common tasks
+- Tool integrations: Pre-configured tool chains for specific operations
+- Reference material: Documentation, templates, and examples
+
+## How to use skills
+
+Identify the skills that are likely to be useful for the tasks you are currently working on, use the `skill` tool to load a skill for detailed instructions, guidelines, scripts and more.
+
+Only load skill details when needed to conserve the context window.
+
+# Ultimate Reminders
+
+At any time, you should be HELPFUL, CONCISE, and ACCURATE. Be thorough in your actions — test what you build, verify what you change — not in your explanations.
+
+- Never diverge from the requirements and the goals of the task you work on. Stay on track.
+- Never give the user more than what they want.
+- Try your best to avoid any hallucination. Do fact checking before providing any factual information.
+- Think about the best approach, then take action decisively.
+- Do not give up too early.
+- ALWAYS, keep it stupidly simple. Do not overcomplicate things.
+- When the task requires creating or modifying files, always use tools to do so. Never treat displaying code in your response as a substitute for actually writing it to the file system.

+ 2 - 0
packages/opencode/src/session/system.ts

@@ -8,6 +8,7 @@ import PROMPT_DEFAULT from "./prompt/default.txt"
 import PROMPT_BEAST from "./prompt/beast.txt"
 import PROMPT_GEMINI from "./prompt/gemini.txt"
 import PROMPT_GPT from "./prompt/gpt.txt"
+import PROMPT_KIMI from "./prompt/kimi.txt"
 
 import PROMPT_CODEX from "./prompt/codex.txt"
 import PROMPT_TRINITY from "./prompt/trinity.txt"
@@ -44,6 +45,7 @@ export namespace SystemPrompt {
     if (model.api.id.includes("gemini-")) return [PROMPT_GEMINI]
     if (model.api.id.includes("claude")) return [PROMPT_ANTHROPIC]
     if (model.api.id.toLowerCase().includes("trinity")) return [PROMPT_TRINITY]
+    if (model.api.id.toLowerCase().includes("kimi")) return [PROMPT_KIMI]
     return [PROMPT_DEFAULT]
   }
 

+ 1 - 2
packages/opencode/src/tool/registry.ts

@@ -115,8 +115,7 @@ export namespace ToolRegistry {
 
       const all = Effect.fn("ToolRegistry.all")(function* (custom: Tool.Info[]) {
         const cfg = yield* config.get()
-        const question =
-          ["app", "cli", "desktop", "vscode"].includes(Flag.KILO_CLIENT) || Flag.KILO_ENABLE_QUESTION_TOOL
+        const question = ["app", "cli", "desktop", "vscode"].includes(Flag.KILO_CLIENT) || Flag.KILO_ENABLE_QUESTION_TOOL
 
         return [
           InvalidTool,

+ 85 - 1
packages/opencode/test/bun.test.ts

@@ -1,6 +1,10 @@
-import { describe, expect, test } from "bun:test"
+import { describe, expect, spyOn, test } from "bun:test"
 import fs from "fs/promises"
 import path from "path"
+import { BunProc } from "../src/bun"
+import { PackageRegistry } from "../src/bun/registry"
+import { Global } from "../src/global"
+import { Process } from "../src/util/process"
 
 describe("BunProc registry configuration", () => {
   test("should not contain hardcoded registry parameters", async () => {
@@ -51,3 +55,83 @@ describe("BunProc registry configuration", () => {
     }
   })
 })
+
+describe("BunProc install pinning", () => {
+  test("uses pinned cache without touching registry", async () => {
+    const pkg = `pin-test-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`
+    const ver = "1.2.3"
+    const mod = path.join(Global.Path.cache, "node_modules", pkg)
+    const data = path.join(Global.Path.cache, "package.json")
+
+    await fs.mkdir(mod, { recursive: true })
+    await Bun.write(path.join(mod, "package.json"), JSON.stringify({ name: pkg, version: ver }, null, 2))
+
+    const src = await fs.readFile(data, "utf8").catch(() => "")
+    const json = src ? ((JSON.parse(src) as { dependencies?: Record<string, string> }) ?? {}) : {}
+    const deps = json.dependencies ?? {}
+    deps[pkg] = ver
+    await Bun.write(data, JSON.stringify({ ...json, dependencies: deps }, null, 2))
+
+    const stale = spyOn(PackageRegistry, "isOutdated").mockImplementation(async () => {
+      throw new Error("unexpected registry check")
+    })
+    const run = spyOn(Process, "run").mockImplementation(async () => {
+      throw new Error("unexpected process.run")
+    })
+
+    try {
+      const out = await BunProc.install(pkg, ver)
+      expect(out).toBe(mod)
+      expect(stale).not.toHaveBeenCalled()
+      expect(run).not.toHaveBeenCalled()
+    } finally {
+      stale.mockRestore()
+      run.mockRestore()
+
+      await fs.rm(mod, { recursive: true, force: true })
+      const end = await fs
+        .readFile(data, "utf8")
+        .then((item) => JSON.parse(item) as { dependencies?: Record<string, string> })
+        .catch(() => undefined)
+      if (end?.dependencies) {
+        delete end.dependencies[pkg]
+        await Bun.write(data, JSON.stringify(end, null, 2))
+      }
+    }
+  })
+
+  test("passes --ignore-scripts when requested", async () => {
+    const pkg = `ignore-test-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`
+    const ver = "4.5.6"
+    const mod = path.join(Global.Path.cache, "node_modules", pkg)
+    const data = path.join(Global.Path.cache, "package.json")
+
+    const run = spyOn(Process, "run").mockImplementation(async () => ({
+      code: 0,
+      stdout: Buffer.alloc(0),
+      stderr: Buffer.alloc(0),
+    }))
+
+    try {
+      await fs.rm(mod, { recursive: true, force: true })
+      await BunProc.install(pkg, ver, { ignoreScripts: true })
+
+      expect(run).toHaveBeenCalled()
+      const call = run.mock.calls[0]?.[0]
+      expect(call).toContain("--ignore-scripts")
+      expect(call).toContain(`${pkg}@${ver}`)
+    } finally {
+      run.mockRestore()
+      await fs.rm(mod, { recursive: true, force: true })
+
+      const end = await fs
+        .readFile(data, "utf8")
+        .then((item) => JSON.parse(item) as { dependencies?: Record<string, string> })
+        .catch(() => undefined)
+      if (end?.dependencies) {
+        delete end.dependencies[pkg]
+        await Bun.write(data, JSON.stringify(end, null, 2))
+      }
+    }
+  })
+})

+ 7 - 14
packages/opencode/test/cli/tui/plugin-install.test.ts

@@ -21,8 +21,12 @@ test("installs plugin without loading it", async () => {
           {
             name: "demo-install-plugin",
             type: "module",
-            main: "./install-plugin.ts",
-            "oc-plugin": [["tui", { marker }]],
+            exports: {
+              "./tui": {
+                import: "./install-plugin.ts",
+                config: { marker },
+              },
+            },
           },
           null,
           2,
@@ -46,7 +50,7 @@ test("installs plugin without loading it", async () => {
   })
 
   process.env.KILO_PLUGIN_META_FILE = path.join(tmp.path, "plugin-meta.json")
-  let cfg: Awaited<ReturnType<typeof TuiConfig.get>> = {
+  const cfg: Awaited<ReturnType<typeof TuiConfig.get>> = {
     plugin: [],
     plugin_records: undefined,
   }
@@ -66,17 +70,6 @@ test("installs plugin without loading it", async () => {
 
   try {
     await TuiPluginRuntime.init(api)
-    cfg = {
-      plugin: [[tmp.extra.spec, { marker: tmp.extra.marker }]],
-      plugin_records: [
-        {
-          item: [tmp.extra.spec, { marker: tmp.extra.marker }],
-          scope: "local",
-          source: path.join(tmp.path, "tui.json"),
-        },
-      ],
-    }
-
     const out = await TuiPluginRuntime.installPlugin(tmp.extra.spec)
     expect(out).toMatchObject({
       ok: true,

+ 6 - 0
packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts

@@ -304,17 +304,23 @@ test("does not use npm package main for tui entry", async () => {
   const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
   const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
   const install = spyOn(BunProc, "install").mockResolvedValue(tmp.extra.mod)
+  const warn = spyOn(console, "warn").mockImplementation(() => {})
+  const error = spyOn(console, "error").mockImplementation(() => {})
 
   try {
     await TuiPluginRuntime.init(createTuiPluginApi())
     await expect(fs.readFile(tmp.extra.marker, "utf8")).rejects.toThrow()
     expect(TuiPluginRuntime.list().some((item) => item.spec === tmp.extra.spec)).toBe(false)
+    expect(error).not.toHaveBeenCalled()
+    expect(warn.mock.calls.some((call) => String(call[0]).includes("tui plugin has no entrypoint"))).toBe(true)
   } finally {
     await TuiPluginRuntime.dispose()
     install.mockRestore()
     cwd.mockRestore()
     get.mockRestore()
     wait.mockRestore()
+    warn.mockRestore()
+    error.mockRestore()
     delete process.env.KILO_PLUGIN_META_FILE
   }
 })

+ 1 - 0
packages/opencode/test/config/config.test.ts

@@ -861,6 +861,7 @@ test("installs dependencies in writable KILO_CONFIG_DIR", async () => {
 
     expect(await Filesystem.exists(path.join(tmp.extra, "package.json"))).toBe(true)
     expect(await Filesystem.exists(path.join(tmp.extra, ".gitignore"))).toBe(true)
+    expect(await Filesystem.readText(path.join(tmp.extra, ".gitignore"))).toContain("package-lock.json")
   } finally {
     online.mockRestore()
     run.mockRestore()

+ 7 - 1
packages/opencode/test/plugin/install-concurrency.test.ts

@@ -25,6 +25,11 @@ function run(msg: Msg) {
 
 async function plugin(dir: string, kinds: Array<"server" | "tui">) {
   const p = path.join(dir, "plugin")
+  const server = kinds.includes("server")
+  const tui = kinds.includes("tui")
+  const exports: Record<string, string> = {}
+  if (server) exports["./server"] = "./server.js"
+  if (tui) exports["./tui"] = "./tui.js"
   await fs.mkdir(p, { recursive: true })
   await Bun.write(
     path.join(p, "package.json"),
@@ -32,7 +37,8 @@ async function plugin(dir: string, kinds: Array<"server" | "tui">) {
       {
         name: "acme",
         version: "1.0.0",
-        "oc-plugin": kinds,
+        ...(server ? { main: "./server.js" } : {}),
+        ...(Object.keys(exports).length ? { exports } : {}),
       },
       null,
       2,

+ 34 - 7
packages/opencode/test/plugin/install.test.ts

@@ -55,8 +55,34 @@ function ctxRoot(dir: string): PlugCtx {
   }
 }
 
-async function plugin(dir: string, kinds?: unknown) {
+async function plugin(
+  dir: string,
+  kinds?: Array<"server" | "tui">,
+  opts?: {
+    server?: Record<string, unknown>
+    tui?: Record<string, unknown>
+  },
+) {
   const p = path.join(dir, "plugin")
+  const server = kinds?.includes("server") ?? false
+  const tui = kinds?.includes("tui") ?? false
+  const exports: Record<string, unknown> = {}
+  if (server) {
+    exports["./server"] = opts?.server
+      ? {
+          import: "./server.js",
+          config: opts.server,
+        }
+      : "./server.js"
+  }
+  if (tui) {
+    exports["./tui"] = opts?.tui
+      ? {
+          import: "./tui.js",
+          config: opts.tui,
+        }
+      : "./tui.js"
+  }
   await fs.mkdir(p, { recursive: true })
   await Bun.write(
     path.join(p, "package.json"),
@@ -64,7 +90,8 @@ async function plugin(dir: string, kinds?: unknown) {
       {
         name: "acme",
         version: "1.0.0",
-        ...(kinds === undefined ? {} : { "oc-plugin": kinds }),
+        ...(server ? { main: "./server.js" } : {}),
+        ...(Object.keys(exports).length ? { exports } : {}),
       },
       null,
       2,
@@ -99,12 +126,12 @@ describe("plugin.install.task", () => {
     expect(tui.plugin).toEqual(["[email protected]"])
   })
 
-  test("writes default options from tuple manifest targets", async () => {
+  test("writes default options from exports config metadata", async () => {
     await using tmp = await tmpdir()
-    const target = await plugin(tmp.path, [
-      ["server", { custom: true, other: false }],
-      ["tui", { compact: true }],
-    ])
+    const target = await plugin(tmp.path, ["server", "tui"], {
+      server: { custom: true, other: false },
+      tui: { compact: true },
+    })
     const run = createPlugTask(
       {
         mod: "[email protected]",

+ 3 - 3
packages/opencode/test/plugin/loader-shared.test.ts

@@ -266,8 +266,8 @@ describe("plugin.loader.shared", () => {
     try {
       await load(tmp.path)
 
-      expect(install.mock.calls).toContainEqual(["acme-plugin", "latest"])
-      expect(install.mock.calls).toContainEqual(["scope-plugin", "2.3.4"])
+      expect(install.mock.calls).toContainEqual(["acme-plugin", "latest", { ignoreScripts: true }])
+      expect(install.mock.calls).toContainEqual(["scope-plugin", "2.3.4", { ignoreScripts: true }])
     } finally {
       install.mockRestore()
     }
@@ -487,7 +487,7 @@ describe("plugin.loader.shared", () => {
         .catch(() => false)
 
       expect(called).toBe(false)
-      expect(errors.some((x) => x.includes('exports["./server"]') && x.includes("package.json main"))).toBe(true)
+      expect(errors).toHaveLength(0)
     } finally {
       install.mockRestore()
     }

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 4756 - 4373
packages/opencode/test/tool/fixtures/models-api.json


+ 4 - 4
packages/plugin/package.json

@@ -21,8 +21,8 @@
     "zod": "catalog:"
   },
   "peerDependencies": {
-    "@opentui/core": ">=0.1.92",
-    "@opentui/solid": ">=0.1.92"
+    "@opentui/core": ">=0.1.93",
+    "@opentui/solid": ">=0.1.93"
   },
   "peerDependenciesMeta": {
     "@opentui/core": {
@@ -33,8 +33,8 @@
     }
   },
   "devDependencies": {
-    "@opentui/core": "0.1.92",
-    "@opentui/solid": "0.1.92",
+    "@opentui/core": "0.1.93",
+    "@opentui/solid": "0.1.93",
     "@tsconfig/node22": "catalog:",
     "@types/node": "catalog:",
     "typescript": "catalog:",

+ 1 - 1
packages/script/src/index.ts

@@ -90,7 +90,7 @@ function bumpVersion(current: string, type: string) {
 // kilocode_change end
 
 const VERSION = await (async () => {
-  if (env.KILO_VERSION) return env.KILO_VERSION // kilocode_change
+  if (env.KILO_VERSION) return env.KILO_VERSION
   if (IS_PREVIEW) {
     // kilocode_change start - rc releases use plain semver required by VS Code Marketplace
     if (env.KILO_BUMP && env.KILO_PRE_RELEASE === "true") {

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels