mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 11:05:31 +08:00
refactor: kilo compat for v1.15.13
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
.git
|
||||
.opencode
|
||||
.sst
|
||||
.turbo
|
||||
.wrangler
|
||||
node_modules
|
||||
**/node_modules
|
||||
**/.output
|
||||
**/dist
|
||||
**/.turbo
|
||||
**/.vite
|
||||
**/coverage
|
||||
@@ -1,5 +1,5 @@
|
||||
name: Bug report
|
||||
description: Report an issue that should be fixed
|
||||
description: Report an issue that should be fixed (avoid pasting giant AI generated summaries or your issue may be closed/ignored)
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
v1.15.9
|
||||
v1.15.13
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
# Deepening
|
||||
|
||||
How to deepen a cluster of shallow modules safely, given its dependencies. Assumes the vocabulary in [LANGUAGE.md](LANGUAGE.md) — **module**, **interface**, **seam**, **adapter**.
|
||||
|
||||
## Dependency categories
|
||||
|
||||
When assessing a candidate for deepening, classify its dependencies. The category determines how the deepened module is tested across its seam.
|
||||
|
||||
### 1. In-process
|
||||
|
||||
Pure computation, in-memory state, no I/O. Always deepenable — merge the modules and test through the new interface directly. No adapter needed.
|
||||
|
||||
### 2. Local-substitutable
|
||||
|
||||
Dependencies that have local test stand-ins (PGLite for Postgres, in-memory filesystem). Deepenable if the stand-in exists. The deepened module is tested with the stand-in running in the test suite. The seam is internal; no port at the module's external interface.
|
||||
|
||||
### 3. Remote but owned (Ports & Adapters)
|
||||
|
||||
Your own services across a network boundary (microservices, internal APIs). Define a **port** (interface) at the seam. The deep module owns the logic; the transport is injected as an **adapter**. Tests use an in-memory adapter. Production uses an HTTP/gRPC/queue adapter.
|
||||
|
||||
Recommendation shape: _"Define a port at the seam, implement an HTTP adapter for production and an in-memory adapter for testing, so the logic sits in one deep module even though it's deployed across a network."_
|
||||
|
||||
### 4. True external (Mock)
|
||||
|
||||
Third-party services (Stripe, Twilio, etc.) you don't control. The deepened module takes the external dependency as an injected port; tests provide a mock adapter.
|
||||
|
||||
## Seam discipline
|
||||
|
||||
- **One adapter means a hypothetical seam. Two adapters means a real one.** Don't introduce a port unless at least two adapters are justified (typically production + test). A single-adapter seam is just indirection.
|
||||
- **Internal seams vs external seams.** A deep module can have internal seams (private to its implementation, used by its own tests) as well as the external seam at its interface. Don't expose internal seams through the interface just because tests use them.
|
||||
|
||||
## Testing strategy: replace, don't layer
|
||||
|
||||
- Old unit tests on shallow modules become waste once tests at the deepened module's interface exist — delete them.
|
||||
- Write new tests at the deepened module's interface. The **interface is the test surface**.
|
||||
- Tests assert on observable outcomes through the interface, not internal state.
|
||||
- Tests should survive internal refactors — they describe behaviour, not implementation. If a test has to change when the implementation changes, it's testing past the interface.
|
||||
@@ -1,44 +0,0 @@
|
||||
# Interface Design
|
||||
|
||||
When the user wants to explore alternative interfaces for a chosen deepening candidate, use this parallel sub-agent pattern. Based on "Design It Twice" (Ousterhout) — your first idea is unlikely to be the best.
|
||||
|
||||
Uses the vocabulary in [LANGUAGE.md](LANGUAGE.md) — **module**, **interface**, **seam**, **adapter**, **leverage**.
|
||||
|
||||
## Process
|
||||
|
||||
### 1. Frame the problem space
|
||||
|
||||
Before spawning sub-agents, write a user-facing explanation of the problem space for the chosen candidate:
|
||||
|
||||
- The constraints any new interface would need to satisfy
|
||||
- The dependencies it would rely on, and which category they fall into (see [DEEPENING.md](DEEPENING.md))
|
||||
- A rough illustrative code sketch to ground the constraints — not a proposal, just a way to make the constraints concrete
|
||||
|
||||
Show this to the user, then immediately proceed to Step 2. The user reads and thinks while the sub-agents work in parallel.
|
||||
|
||||
### 2. Spawn sub-agents
|
||||
|
||||
Spawn 3+ sub-agents in parallel using the Agent tool. Each must produce a **radically different** interface for the deepened module.
|
||||
|
||||
Prompt each sub-agent with a separate technical brief (file paths, coupling details, dependency category from [DEEPENING.md](DEEPENING.md), what sits behind the seam). The brief is independent of the user-facing problem-space explanation in Step 1. Give each agent a different design constraint:
|
||||
|
||||
- Agent 1: "Minimize the interface — aim for 1–3 entry points max. Maximise leverage per entry point."
|
||||
- Agent 2: "Maximise flexibility — support many use cases and extension."
|
||||
- Agent 3: "Optimise for the most common caller — make the default case trivial."
|
||||
- Agent 4 (if applicable): "Design around ports & adapters for cross-seam dependencies."
|
||||
|
||||
Include both [LANGUAGE.md](LANGUAGE.md) vocabulary and CONTEXT.md vocabulary in the brief so each sub-agent names things consistently with the architecture language and the project's domain language.
|
||||
|
||||
Each sub-agent outputs:
|
||||
|
||||
1. Interface (types, methods, params — plus invariants, ordering, error modes)
|
||||
2. Usage example showing how callers use it
|
||||
3. What the implementation hides behind the seam
|
||||
4. Dependency strategy and adapters (see [DEEPENING.md](DEEPENING.md))
|
||||
5. Trade-offs — where leverage is high, where it's thin
|
||||
|
||||
### 3. Present and compare
|
||||
|
||||
Present designs sequentially so the user can absorb each one, then compare them in prose. Contrast by **depth** (leverage at the interface), **locality** (where change concentrates), and **seam placement**.
|
||||
|
||||
After comparing, give your own recommendation: which design you think is strongest and why. If elements from different designs would combine well, propose a hybrid. Be opinionated — the user wants a strong read, not a menu.
|
||||
@@ -1,53 +0,0 @@
|
||||
# Language
|
||||
|
||||
Shared vocabulary for every suggestion this skill makes. Use these terms exactly — don't substitute "component," "service," "API," or "boundary." Consistent language is the whole point.
|
||||
|
||||
## Terms
|
||||
|
||||
**Module**
|
||||
Anything with an interface and an implementation. Deliberately scale-agnostic — applies equally to a function, class, package, or tier-spanning slice.
|
||||
_Avoid_: unit, component, service.
|
||||
|
||||
**Interface**
|
||||
Everything a caller must know to use the module correctly. Includes the type signature, but also invariants, ordering constraints, error modes, required configuration, and performance characteristics.
|
||||
_Avoid_: API, signature (too narrow — those refer only to the type-level surface).
|
||||
|
||||
**Implementation**
|
||||
What's inside a module — its body of code. Distinct from **Adapter**: a thing can be a small adapter with a large implementation (a Postgres repo) or a large adapter with a small implementation (an in-memory fake). Reach for "adapter" when the seam is the topic; "implementation" otherwise.
|
||||
|
||||
**Depth**
|
||||
Leverage at the interface — the amount of behaviour a caller (or test) can exercise per unit of interface they have to learn. A module is **deep** when a large amount of behaviour sits behind a small interface. A module is **shallow** when the interface is nearly as complex as the implementation.
|
||||
|
||||
**Seam** _(from Michael Feathers)_
|
||||
A place where you can alter behaviour without editing in that place. The _location_ at which a module's interface lives. Choosing where to put the seam is its own design decision, distinct from what goes behind it.
|
||||
_Avoid_: boundary (overloaded with DDD's bounded context).
|
||||
|
||||
**Adapter**
|
||||
A concrete thing that satisfies an interface at a seam. Describes _role_ (what slot it fills), not substance (what's inside).
|
||||
|
||||
**Leverage**
|
||||
What callers get from depth. More capability per unit of interface they have to learn. One implementation pays back across N call sites and M tests.
|
||||
|
||||
**Locality**
|
||||
What maintainers get from depth. Change, bugs, knowledge, and verification concentrate at one place rather than spreading across callers. Fix once, fixed everywhere.
|
||||
|
||||
## Principles
|
||||
|
||||
- **Depth is a property of the interface, not the implementation.** A deep module can be internally composed of small, mockable, swappable parts — they just aren't part of the interface. A module can have **internal seams** (private to its implementation, used by its own tests) as well as the **external seam** at its interface.
|
||||
- **The deletion test.** Imagine deleting the module. If complexity vanishes, the module wasn't hiding anything (it was a pass-through). If complexity reappears across N callers, the module was earning its keep.
|
||||
- **The interface is the test surface.** Callers and tests cross the same seam. If you want to test _past_ the interface, the module is probably the wrong shape.
|
||||
- **One adapter means a hypothetical seam. Two adapters means a real one.** Don't introduce a seam unless something actually varies across it.
|
||||
|
||||
## Relationships
|
||||
|
||||
- A **Module** has exactly one **Interface** (the surface it presents to callers and tests).
|
||||
- **Depth** is a property of a **Module**, measured against its **Interface**.
|
||||
- A **Seam** is where a **Module**'s **Interface** lives.
|
||||
- An **Adapter** sits at a **Seam** and satisfies the **Interface**.
|
||||
- **Depth** produces **Leverage** for callers and **Locality** for maintainers.
|
||||
|
||||
## Rejected framings
|
||||
|
||||
- **Depth as ratio of implementation-lines to interface-lines** (Ousterhout): rewards padding the implementation. We use depth-as-leverage instead.
|
||||
- **"Interface" as the TypeScript `interface` keyword or a class's public methods**: too narrow — interface here includes every fact a caller must know.
|
||||
- **"Boundary"**: overloaded with DDD's bounded context. Say **seam** or **interface**.
|
||||
@@ -1,71 +0,0 @@
|
||||
---
|
||||
name: improve-codebase-architecture
|
||||
description: Find deepening opportunities in a codebase, informed by the domain language in CONTEXT.md and the decisions in docs/adr/. Use when the user wants to improve architecture, find refactoring opportunities, consolidate tightly-coupled modules, or make a codebase more testable and AI-navigable.
|
||||
---
|
||||
|
||||
# Improve Codebase Architecture
|
||||
|
||||
Surface architectural friction and propose **deepening opportunities** — refactors that turn shallow modules into deep ones. The aim is testability and AI-navigability.
|
||||
|
||||
## Glossary
|
||||
|
||||
Use these terms exactly in every suggestion. Consistent language is the point — don't drift into "component," "service," "API," or "boundary." Full definitions in [LANGUAGE.md](LANGUAGE.md).
|
||||
|
||||
- **Module** — anything with an interface and an implementation (function, class, package, slice).
|
||||
- **Interface** — everything a caller must know to use the module: types, invariants, error modes, ordering, config. Not just the type signature.
|
||||
- **Implementation** — the code inside.
|
||||
- **Depth** — leverage at the interface: a lot of behaviour behind a small interface. **Deep** = high leverage. **Shallow** = interface nearly as complex as the implementation.
|
||||
- **Seam** — where an interface lives; a place behaviour can be altered without editing in place. (Use this, not "boundary.")
|
||||
- **Adapter** — a concrete thing satisfying an interface at a seam.
|
||||
- **Leverage** — what callers get from depth.
|
||||
- **Locality** — what maintainers get from depth: change, bugs, knowledge concentrated in one place.
|
||||
|
||||
Key principles (see [LANGUAGE.md](LANGUAGE.md) for the full list):
|
||||
|
||||
- **Deletion test**: imagine deleting the module. If complexity vanishes, it was a pass-through. If complexity reappears across N callers, it was earning its keep.
|
||||
- **The interface is the test surface.**
|
||||
- **One adapter = hypothetical seam. Two adapters = real seam.**
|
||||
|
||||
This skill is _informed_ by the project's domain model. The domain language gives names to good seams; ADRs record decisions the skill should not re-litigate.
|
||||
|
||||
## Process
|
||||
|
||||
### 1. Explore
|
||||
|
||||
Read the project's domain glossary and any ADRs in the area you're touching first.
|
||||
|
||||
Then use the Agent tool with `subagent_type=Explore` to walk the codebase. Don't follow rigid heuristics — explore organically and note where you experience friction:
|
||||
|
||||
- Where does understanding one concept require bouncing between many small modules?
|
||||
- Where are modules **shallow** — interface nearly as complex as the implementation?
|
||||
- Where have pure functions been extracted just for testability, but the real bugs hide in how they're called (no **locality**)?
|
||||
- Where do tightly-coupled modules leak across their seams?
|
||||
- Which parts of the codebase are untested, or hard to test through their current interface?
|
||||
|
||||
Apply the **deletion test** to anything you suspect is shallow: would deleting it concentrate complexity, or just move it? A "yes, concentrates" is the signal you want.
|
||||
|
||||
### 2. Present candidates
|
||||
|
||||
Present a numbered list of deepening opportunities. For each candidate:
|
||||
|
||||
- **Files** — which files/modules are involved
|
||||
- **Problem** — why the current architecture is causing friction
|
||||
- **Solution** — plain English description of what would change
|
||||
- **Benefits** — explained in terms of locality and leverage, and also in how tests would improve
|
||||
|
||||
**Use CONTEXT.md vocabulary for the domain, and [LANGUAGE.md](LANGUAGE.md) vocabulary for the architecture.** If `CONTEXT.md` defines "Order," talk about "the Order intake module" — not "the FooBarHandler," and not "the Order service."
|
||||
|
||||
**ADR conflicts**: if a candidate contradicts an existing ADR, only surface it when the friction is real enough to warrant revisiting the ADR. Mark it clearly (e.g. _"contradicts ADR-0007 — but worth reopening because…"_). Don't list every theoretical refactor an ADR forbids.
|
||||
|
||||
Do NOT propose interfaces yet. Ask the user: "Which of these would you like to explore?"
|
||||
|
||||
### 3. Grilling loop
|
||||
|
||||
Once the user picks a candidate, drop into a grilling conversation. Walk the design tree with them — constraints, dependencies, the shape of the deepened module, what sits behind the seam, what tests survive.
|
||||
|
||||
Side effects happen inline as decisions crystallize:
|
||||
|
||||
- **Naming a deepened module after a concept not in `CONTEXT.md`?** Add the term to `CONTEXT.md` — same discipline as `/grill-with-docs` (see [CONTEXT-FORMAT.md](../grill-with-docs/CONTEXT-FORMAT.md)). Create the file lazily if it doesn't exist.
|
||||
- **Sharpening a fuzzy term during the conversation?** Update `CONTEXT.md` right there.
|
||||
- **User rejects the candidate with a load-bearing reason?** Offer an ADR, framed as: _"Want me to record this as an ADR so future architecture reviews don't re-suggest it?"_ Only offer when the reason would actually be needed by a future explorer to avoid re-suggesting the same thing — skip ephemeral reasons ("not worth it right now") and self-evident ones. See [ADR-FORMAT.md](../grill-with-docs/ADR-FORMAT.md).
|
||||
- **Want to explore alternative interfaces for the deepened module?** See [INTERFACE-DESIGN.md](INTERFACE-DESIGN.md).
|
||||
@@ -1,8 +1,6 @@
|
||||
- To regenerate the JavaScript SDK, run `./packages/sdk/js/script/build.ts`.
|
||||
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE.
|
||||
- The default branch in this repo is `dev`.
|
||||
- Local `main` ref may not exist; use `dev` or `origin/dev` for diffs.
|
||||
- Prefer automation: execute requested actions without confirmation unless blocked by missing info or safety/irreversibility.
|
||||
|
||||
## Commits and PR Titles
|
||||
|
||||
|
||||
@@ -23,13 +23,13 @@
|
||||
"oxlint-tsgolint": "0.21.0",
|
||||
"prettier": "3.6.2",
|
||||
"semver": "^7.6.0",
|
||||
"sst": "4.13.1",
|
||||
"sst": "catalog:",
|
||||
"turbo": "2.8.13",
|
||||
},
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.15.9",
|
||||
"version": "1.15.13",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -83,9 +83,26 @@
|
||||
"vite-plugin-solid": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@opencode-ai/cli",
|
||||
"version": "1.15.13",
|
||||
"bin": {
|
||||
"opencode": "./src/index.ts",
|
||||
},
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.15.9",
|
||||
"version": "1.15.13",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -102,6 +119,7 @@
|
||||
"@solidjs/router": "catalog:",
|
||||
"@solidjs/start": "catalog:",
|
||||
"@stripe/stripe-js": "8.6.1",
|
||||
"@upstash/redis": "1.38.0",
|
||||
"chart.js": "4.5.1",
|
||||
"nitro": "3.0.1-alpha.1",
|
||||
"solid-js": "catalog:",
|
||||
@@ -120,7 +138,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.15.9",
|
||||
"version": "1.15.13",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -147,7 +165,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.15.9",
|
||||
"version": "1.15.13",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "3.0.64",
|
||||
"@ai-sdk/openai": "3.0.48",
|
||||
@@ -169,7 +187,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.15.9",
|
||||
"version": "1.15.13",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -191,23 +209,43 @@
|
||||
"cloudflare": "5.2.0",
|
||||
},
|
||||
},
|
||||
"packages/console/support": {
|
||||
"name": "@opencode-ai/console-support",
|
||||
"version": "1.15.13",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@opencode-ai/console-core": "workspace:*",
|
||||
"@solidjs/meta": "catalog:",
|
||||
"@solidjs/router": "catalog:",
|
||||
"@solidjs/start": "catalog:",
|
||||
"nitro": "3.0.1-alpha.1",
|
||||
"solid-js": "catalog:",
|
||||
"vite": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"wrangler": "4.50.0",
|
||||
},
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode-ai/core",
|
||||
"version": "1.15.9",
|
||||
"version": "1.15.13",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/alibaba": "1.0.17",
|
||||
"@ai-sdk/amazon-bedrock": "4.0.96",
|
||||
"@ai-sdk/amazon-bedrock": "4.0.107",
|
||||
"@ai-sdk/anthropic": "3.0.71",
|
||||
"@ai-sdk/azure": "3.0.49",
|
||||
"@ai-sdk/cerebras": "2.0.41",
|
||||
"@ai-sdk/cohere": "3.0.27",
|
||||
"@ai-sdk/deepinfra": "2.0.41",
|
||||
"@ai-sdk/gateway": "3.0.104",
|
||||
"@ai-sdk/google": "3.0.63",
|
||||
"@ai-sdk/google-vertex": "4.0.112",
|
||||
"@ai-sdk/google": "3.0.73",
|
||||
"@ai-sdk/google-vertex": "4.0.128",
|
||||
"@ai-sdk/groq": "3.0.31",
|
||||
"@ai-sdk/mistral": "3.0.27",
|
||||
"@ai-sdk/openai": "3.0.53",
|
||||
@@ -231,15 +269,16 @@
|
||||
"ai-gateway-provider": "3.1.2",
|
||||
"cross-spawn": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"gitlab-ai-provider": "6.7.0",
|
||||
"gitlab-ai-provider": "6.8.0",
|
||||
"glob": "13.0.5",
|
||||
"google-auth-library": "10.5.0",
|
||||
"immer": "11.1.4",
|
||||
"jsonc-parser": "3.3.1",
|
||||
"mime-types": "3.0.2",
|
||||
"minimatch": "10.2.5",
|
||||
"npm-package-arg": "13.0.2",
|
||||
"semver": "^7.6.3",
|
||||
"venice-ai-sdk-provider": "2.0.1",
|
||||
"venice-ai-sdk-provider": "2.0.2",
|
||||
"xdg-basedir": "5.1.0",
|
||||
"zod": "catalog:",
|
||||
},
|
||||
@@ -254,7 +293,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.15.9",
|
||||
"version": "1.15.13",
|
||||
"dependencies": {
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
"drizzle-orm": "catalog:",
|
||||
@@ -291,12 +330,12 @@
|
||||
"zod-openapi": "5.4.6",
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@lydell/node-pty-darwin-arm64": "1.2.0-beta.10",
|
||||
"@lydell/node-pty-darwin-x64": "1.2.0-beta.10",
|
||||
"@lydell/node-pty-linux-arm64": "1.2.0-beta.10",
|
||||
"@lydell/node-pty-linux-x64": "1.2.0-beta.10",
|
||||
"@lydell/node-pty-win32-arm64": "1.2.0-beta.10",
|
||||
"@lydell/node-pty-win32-x64": "1.2.0-beta.10",
|
||||
"@lydell/node-pty-darwin-arm64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-darwin-x64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-linux-arm64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-linux-x64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-win32-arm64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-win32-x64": "1.2.0-beta.12",
|
||||
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||
"@parcel/watcher-darwin-x64": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
|
||||
@@ -309,7 +348,7 @@
|
||||
},
|
||||
"packages/effect-drizzle-sqlite": {
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"version": "1.15.9",
|
||||
"version": "1.15.13",
|
||||
"dependencies": {
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -323,7 +362,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.15.9",
|
||||
"version": "1.15.13",
|
||||
"dependencies": {
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -353,7 +392,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.15.9",
|
||||
"version": "1.15.13",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -369,7 +408,7 @@
|
||||
},
|
||||
"packages/http-recorder": {
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"version": "1.15.9",
|
||||
"version": "1.15.13",
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -382,7 +421,7 @@
|
||||
},
|
||||
"packages/llm": {
|
||||
"name": "@opencode-ai/llm",
|
||||
"version": "1.15.9",
|
||||
"version": "1.15.13",
|
||||
"dependencies": {
|
||||
"@smithy/eventstream-codec": "4.2.14",
|
||||
"@smithy/util-utf8": "4.2.2",
|
||||
@@ -400,7 +439,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.15.9",
|
||||
"version": "1.15.13",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -409,15 +448,15 @@
|
||||
"@actions/github": "6.0.1",
|
||||
"@agentclientprotocol/sdk": "0.21.0",
|
||||
"@ai-sdk/alibaba": "1.0.17",
|
||||
"@ai-sdk/amazon-bedrock": "4.0.96",
|
||||
"@ai-sdk/amazon-bedrock": "4.0.107",
|
||||
"@ai-sdk/anthropic": "3.0.71",
|
||||
"@ai-sdk/azure": "3.0.49",
|
||||
"@ai-sdk/cerebras": "2.0.41",
|
||||
"@ai-sdk/cohere": "3.0.27",
|
||||
"@ai-sdk/deepinfra": "2.0.41",
|
||||
"@ai-sdk/gateway": "3.0.104",
|
||||
"@ai-sdk/google": "3.0.63",
|
||||
"@ai-sdk/google-vertex": "4.0.112",
|
||||
"@ai-sdk/google": "3.0.73",
|
||||
"@ai-sdk/google-vertex": "4.0.128",
|
||||
"@ai-sdk/groq": "3.0.31",
|
||||
"@ai-sdk/mistral": "3.0.27",
|
||||
"@ai-sdk/openai": "3.0.53",
|
||||
@@ -457,6 +496,7 @@
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
"@solid-primitives/scheduled": "1.5.2",
|
||||
"@standard-schema/spec": "1.0.0",
|
||||
"@types/ws": "8.18.1",
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
"ai": "catalog:",
|
||||
"ai-gateway-provider": "3.1.2",
|
||||
@@ -470,7 +510,7 @@
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"fuzzysort": "3.1.0",
|
||||
"gitlab-ai-provider": "6.7.0",
|
||||
"gitlab-ai-provider": "6.8.0",
|
||||
"glob": "13.0.5",
|
||||
"google-auth-library": "10.5.0",
|
||||
"gray-matter": "4.0.3",
|
||||
@@ -494,10 +534,11 @@
|
||||
"tree-sitter-powershell": "0.25.10",
|
||||
"turndown": "7.2.0",
|
||||
"ulid": "catalog:",
|
||||
"venice-ai-sdk-provider": "2.0.1",
|
||||
"venice-ai-sdk-provider": "2.0.2",
|
||||
"vscode-jsonrpc": "8.2.1",
|
||||
"web-tree-sitter": "0.25.10",
|
||||
"which": "6.0.1",
|
||||
"ws": "8.21.0",
|
||||
"xdg-basedir": "5.1.0",
|
||||
"yargs": "18.0.0",
|
||||
"zod": "catalog:",
|
||||
@@ -538,7 +579,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.15.9",
|
||||
"version": "1.15.13",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
@@ -554,9 +595,9 @@
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentui/core": ">=0.2.15",
|
||||
"@opentui/keymap": ">=0.2.15",
|
||||
"@opentui/solid": ">=0.2.15",
|
||||
"@opentui/core": ">=0.2.16",
|
||||
"@opentui/keymap": ">=0.2.16",
|
||||
"@opentui/solid": ">=0.2.16",
|
||||
},
|
||||
"optionalPeers": [
|
||||
"@opentui/core",
|
||||
@@ -576,7 +617,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.15.9",
|
||||
"version": "1.15.13",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -591,7 +632,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.15.9",
|
||||
"version": "1.15.13",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -602,6 +643,68 @@
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/stats/app": {
|
||||
"name": "@opencode-ai/stats-app",
|
||||
"version": "1.15.13",
|
||||
"dependencies": {
|
||||
"@ibm/plex": "6.4.1",
|
||||
"@opencode-ai/stats-core": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@solidjs/meta": "catalog:",
|
||||
"@solidjs/router": "catalog:",
|
||||
"@solidjs/start": "catalog:",
|
||||
"d3-scale": "4.0.2",
|
||||
"effect": "catalog:",
|
||||
"nitro": "3.0.1-alpha.1",
|
||||
"solid-js": "catalog:",
|
||||
"sst": "catalog:",
|
||||
"vite": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/d3-scale": "4.0.9",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/stats/core": {
|
||||
"name": "@opencode-ai/stats-core",
|
||||
"version": "1.15.13",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-athena": "3.933.0",
|
||||
"@planetscale/database": "1.19.0",
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"sst": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"drizzle-kit": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/stats/server": {
|
||||
"name": "@opencode-ai/stats-server",
|
||||
"version": "1.15.13",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-firehose": "3.933.0",
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@opencode-ai/stats-core": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
"sst": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/storybook": {
|
||||
"name": "@opencode-ai/storybook",
|
||||
"devDependencies": {
|
||||
@@ -626,7 +729,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.15.9",
|
||||
"version": "1.15.13",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -675,7 +778,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.15.9",
|
||||
"version": "1.15.13",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -718,6 +821,8 @@
|
||||
],
|
||||
"patchedDependencies": {
|
||||
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
||||
"virtua@0.49.1": "patches/virtua@0.49.1.patch",
|
||||
"gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch",
|
||||
"@ai-sdk/xai@3.0.82": "patches/@ai-sdk%2Fxai@3.0.82.patch",
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
"@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.patch",
|
||||
@@ -737,13 +842,13 @@
|
||||
"@effect/sql-sqlite-bun": "4.0.0-beta.66",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"@kobalte/core": "0.13.11",
|
||||
"@lydell/node-pty": "1.2.0-beta.10",
|
||||
"@lydell/node-pty": "1.2.0-beta.12",
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@opentui/core": "0.2.15",
|
||||
"@opentui/keymap": "0.2.15",
|
||||
"@opentui/solid": "0.2.15",
|
||||
"@opentui/core": "0.2.16",
|
||||
"@opentui/keymap": "0.2.16",
|
||||
"@opentui/solid": "0.2.16",
|
||||
"@pierre/diffs": "1.1.0-beta.18",
|
||||
"@playwright/test": "1.59.1",
|
||||
"@sentry/solid": "10.36.0",
|
||||
@@ -781,6 +886,7 @@
|
||||
"shiki": "3.20.0",
|
||||
"solid-js": "1.9.10",
|
||||
"solid-list": "0.3.0",
|
||||
"sst": "4.13.1",
|
||||
"tailwindcss": "4.1.11",
|
||||
"typescript": "5.8.2",
|
||||
"ulid": "3.0.1",
|
||||
@@ -810,7 +916,7 @@
|
||||
|
||||
"@ai-sdk/alibaba": ["@ai-sdk/alibaba@1.0.17", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZbE+U5bWz2JBc5DERLowx5+TKbjGBE93LqKZAWvuEn7HOSQMraxFMZuc0ST335QZJAyfBOzh7m1mPQ+y7EaaoA=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.96", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.71", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Mc4Ias2jRMD1jOB6xWtKNPdhECeuCZyIlbr9EAGfBnyBt++sS13ziZh9qv9TdyMCAZJ7xoQcpbchoRJcKwPdpA=="],
|
||||
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.107", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.78", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8nT08pGPy25rleJNk56ep00UHK6kCtCmu+ZNqVVSSPDieADlIZqcaN1iRXAFBoCH0Fb9F6C2EjFDaySdsargfQ=="],
|
||||
|
||||
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rwLi/Rsuj2pYniQXIrvClHvXDzgM4UQHHnvHTWEF14efnlKclG/1ghpNC+adsRujAbCTr6gRsSbDE2vEqriV7g=="],
|
||||
|
||||
@@ -832,9 +938,9 @@
|
||||
|
||||
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.104", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZKX5n74io8VIRlhIMSLWVlvT3sXC8Z7cZ9GHuWBWZDVi96+62AIsWuLGvMfcBA1STYuSoDrp6rIziZmvrTq0TA=="],
|
||||
|
||||
"@ai-sdk/google": ["@ai-sdk/google@3.0.63", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-RfOZWVMYSPu2sPRfGajrauWAZ9BSaRopSn+AszkKWQ1MFj8nhaXvCqRHB5pBQUaHTfZKagvOmMpNfa/s3gPLgQ=="],
|
||||
"@ai-sdk/google": ["@ai-sdk/google@3.0.73", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-o2MuIeyvZrFIeIbnbA8Thrr63irdyUBh0uWBZ2lY6yFeXuE/tcwyXF74bDKS4KvTu84uFpQfpbS/LXHGKKXz+g=="],
|
||||
|
||||
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.112", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.71", "@ai-sdk/google": "3.0.64", "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cSfHCkM+9ZrFtQWIN1WlV93JPD+isGSdFxKj7u1L9m2aLVZajlXdcE41GL9hMt7ld7bZYE4NnZ+4VLxBAHE+Eg=="],
|
||||
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.128", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.77", "@ai-sdk/google": "3.0.73", "@ai-sdk/openai-compatible": "2.0.47", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-jK8fixb4km2yfgvb9DUFQRpV/jiDB0v9gyxHoHfPydaQvz+CpAz8DTt1quyaM+Wg9G2R8Zo68CYmHbIkUqW2AA=="],
|
||||
|
||||
"@ai-sdk/groq": ["@ai-sdk/groq@3.0.31", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XbbugpnFmXGu2TlXiq8KUJskP6/VVbuFcnFIGDzDIB/Chg6XHsNnqrTF80Zxkh0Pd3+NvbM+2Uqrtsndk6bDAg=="],
|
||||
|
||||
@@ -906,8 +1012,12 @@
|
||||
|
||||
"@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="],
|
||||
|
||||
"@aws-sdk/client-athena": ["@aws-sdk/client-athena@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/credential-provider-node": "3.933.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-9eMUCu1Ay3C9ojo+dJcynSdpbxuwDVtZUt/Xhce+c2+mgDsmvRzjww+wfLpZwRNWxBWmeauQQAZk52tCwQgXsQ=="],
|
||||
|
||||
"@aws-sdk/client-cognito-identity": ["@aws-sdk/client-cognito-identity@3.993.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.11", "@aws-sdk/credential-provider-node": "^3.972.10", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.11", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.993.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.9", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.2", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.16", "@smithy/middleware-retry": "^4.4.33", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.10", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.32", "@smithy/util-defaults-mode-node": "^4.2.35", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7Ne3Yk/bgQPVebAkv7W+RfhiwTRSbfER9BtbhOa2w/+dIr902LrJf6vrZlxiqaJbGj2ALx8M+ZK1YIHVxSwu9A=="],
|
||||
|
||||
"@aws-sdk/client-firehose": ["@aws-sdk/client-firehose@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/credential-provider-node": "3.933.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-tDrtgczN2lQsflLDPYu/wdOoyCZLVYtgzmWnYzSEOBWd/cp2AbuQ7D+FemSwUTzyoMTuhhIevyEJKzqsF+QYxA=="],
|
||||
|
||||
"@aws-sdk/client-lambda": ["@aws-sdk/client-lambda@3.1048.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.11", "@aws-sdk/credential-provider-node": "^3.972.42", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/fetch-http-handler": "^5.4.2", "@smithy/node-http-handler": "^4.7.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-ryEYNVdilyWkKsOs/7Xy/l7+qjtSz4sll8NpcWD6AtONxjG/5OMaAhxxDkQb4iBoNMKnISxsARzQAp/Wa8pXIg=="],
|
||||
|
||||
"@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.933.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/credential-provider-node": "3.933.0", "@aws-sdk/middleware-bucket-endpoint": "3.930.0", "@aws-sdk/middleware-expect-continue": "3.930.0", "@aws-sdk/middleware-flexible-checksums": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-location-constraint": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-sdk-s3": "3.932.0", "@aws-sdk/middleware-ssec": "3.930.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/signature-v4-multi-region": "3.932.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/eventstream-serde-browser": "^4.2.5", "@smithy/eventstream-serde-config-resolver": "^4.3.5", "@smithy/eventstream-serde-node": "^4.2.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-blob-browser": "^4.2.6", "@smithy/hash-node": "^4.2.5", "@smithy/hash-stream-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/md5-js": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-stream": "^4.5.6", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-KxwZvdxdCeWK6o8mpnb+kk7Kgb8V+8AjTwSXUWH1UAD85B0tjdo1cSfE5zoR5fWGol4Ml5RLez12a6LPhsoTqA=="],
|
||||
@@ -1416,19 +1526,19 @@
|
||||
|
||||
"@lukeed/ms": ["@lukeed/ms@2.0.2", "", {}, "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA=="],
|
||||
|
||||
"@lydell/node-pty": ["@lydell/node-pty@1.2.0-beta.10", "", { "optionalDependencies": { "@lydell/node-pty-darwin-arm64": "1.2.0-beta.10", "@lydell/node-pty-darwin-x64": "1.2.0-beta.10", "@lydell/node-pty-linux-arm64": "1.2.0-beta.10", "@lydell/node-pty-linux-x64": "1.2.0-beta.10", "@lydell/node-pty-win32-arm64": "1.2.0-beta.10", "@lydell/node-pty-win32-x64": "1.2.0-beta.10" } }, "sha512-Fv+A3+MZVA8qhkBIZsM1E6dCdHNMyXXz22mAYiMWd03LlyK///F3OH6CKPX9mj4id7LUlxpr45yPzyBVy9aDPw=="],
|
||||
"@lydell/node-pty": ["@lydell/node-pty@1.2.0-beta.12", "", { "optionalDependencies": { "@lydell/node-pty-darwin-arm64": "1.2.0-beta.12", "@lydell/node-pty-darwin-x64": "1.2.0-beta.12", "@lydell/node-pty-linux-arm64": "1.2.0-beta.12", "@lydell/node-pty-linux-x64": "1.2.0-beta.12", "@lydell/node-pty-win32-arm64": "1.2.0-beta.12", "@lydell/node-pty-win32-x64": "1.2.0-beta.12" } }, "sha512-qIK890UwPupoj07osVvgOIa++1mxeHbcGry4PKRHhNVNs81V2SCG34eJr46GybiOmBtc8Sj5PB1/GGM5PL549g=="],
|
||||
|
||||
"@lydell/node-pty-darwin-arm64": ["@lydell/node-pty-darwin-arm64@1.2.0-beta.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-C+eqDyRNHRYvx7RaHj6VVCx6nCpRBPuuxhTcc3JH3GuBMoxTsYeY4GkWH2XOktrgbAq1BG8e/Y8bu/wNQreCEw=="],
|
||||
"@lydell/node-pty-darwin-arm64": ["@lydell/node-pty-darwin-arm64@1.2.0-beta.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tqaifcY9Cr41SblO1+FLzh8oxxtkNhuW9Dhl22lKme9BreYvKvxEZcdPIXTuqkJc5tagOEC4QHShKmJjLyLXLQ=="],
|
||||
|
||||
"@lydell/node-pty-darwin-x64": ["@lydell/node-pty-darwin-x64@1.2.0-beta.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-aZoIK6HtJO5BiT4ELm683U4dyHtt8b7wNgq3NJqYAQwSXrcPv576Z8vY3BIulVxfcFkht/SPLKou9TtdFXdNpg=="],
|
||||
"@lydell/node-pty-darwin-x64": ["@lydell/node-pty-darwin-x64@1.2.0-beta.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-4LrS5pCJwqHKDVf1zS2gyNV0m4hKAXch+XZNhbZ6LY8uwVL8BhchzQBO40Os5anuRxRCWzHpw4Sp64Ie8q7E4Q=="],
|
||||
|
||||
"@lydell/node-pty-linux-arm64": ["@lydell/node-pty-linux-arm64@1.2.0-beta.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-0cKX2iMyXFNBE4fGtGK6B7IkdXcDMZajyEDoGMOgQQs/DDtoI5tSPcBcqNY9VitVrsRQA8+gFt6eKYU9Ye/lUA=="],
|
||||
"@lydell/node-pty-linux-arm64": ["@lydell/node-pty-linux-arm64@1.2.0-beta.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-Sx+A71x5BDGHt9ansfrtGxwq2VFVDWvJUAdlUL0Hv0qeiJUfts+hgopx+CgT4PSwahKjdEgtu0+FAfY9rICKRw=="],
|
||||
|
||||
"@lydell/node-pty-linux-x64": ["@lydell/node-pty-linux-x64@1.2.0-beta.10", "", { "os": "linux", "cpu": "x64" }, "sha512-J9HnxvSzEeMH748+Ul1VrmCLWMo7iCVJy9EGijRR62+YO/Yk5GaCydUTZ+KzlH0/X5aTrgt5cfiof4vx45tRRg=="],
|
||||
"@lydell/node-pty-linux-x64": ["@lydell/node-pty-linux-x64@1.2.0-beta.12", "", { "os": "linux", "cpu": "x64" }, "sha512-bJzs94njofYhGg/UDqW1nj0dtvvu+2OvxMY+RlLS1T17VgcktKoIR6PuenTwE5HJ/D6StCPADmXcT0nNsCKmIQ=="],
|
||||
|
||||
"@lydell/node-pty-win32-arm64": ["@lydell/node-pty-win32-arm64@1.2.0-beta.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-PlDJpJX/pnKyy6OmADKzhf+INZDDnzTBGaI0LT4laVNc6NblZNqUSkCMjLFWbeakeuQp0VG37M49WQSN9FDfeA=="],
|
||||
"@lydell/node-pty-win32-arm64": ["@lydell/node-pty-win32-arm64@1.2.0-beta.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-p7POgjVEiFaBC3/y+AKuV1FzePCsJ6HmZDv2XK+jBZSfwP8+uBAw181ZiKYN1YuRa/XpmBGaWezcI8hZkbW++g=="],
|
||||
|
||||
"@lydell/node-pty-win32-x64": ["@lydell/node-pty-win32-x64@1.2.0-beta.10", "", { "os": "win32", "cpu": "x64" }, "sha512-ExFgWrzyldNAMi45U9PLIOu+g/RatP+f0c/dZxaooifME6yLW32BoHveH26/TtoAjZyJrc2iL0u48pgnR1fzmg=="],
|
||||
"@lydell/node-pty-win32-x64": ["@lydell/node-pty-win32-x64@1.2.0-beta.12", "", { "os": "win32", "cpu": "x64" }, "sha512-IDFa00g7qUDGUYgByrUBJtC+mOjYVt/8KYyWivCg5JjGOHbBUACUQZLl0jTWmnr+tld/UyTpX90a2PY6oTVtRw=="],
|
||||
|
||||
"@malept/cross-spawn-promise": ["@malept/cross-spawn-promise@2.0.0", "", { "dependencies": { "cross-spawn": "^7.0.1" } }, "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg=="],
|
||||
|
||||
@@ -1554,6 +1664,8 @@
|
||||
|
||||
"@opencode-ai/app": ["@opencode-ai/app@workspace:packages/app"],
|
||||
|
||||
"@opencode-ai/cli": ["@opencode-ai/cli@workspace:packages/cli"],
|
||||
|
||||
"@opencode-ai/console-app": ["@opencode-ai/console-app@workspace:packages/console/app"],
|
||||
|
||||
"@opencode-ai/console-core": ["@opencode-ai/console-core@workspace:packages/console/core"],
|
||||
@@ -1564,6 +1676,8 @@
|
||||
|
||||
"@opencode-ai/console-resource": ["@opencode-ai/console-resource@workspace:packages/console/resource"],
|
||||
|
||||
"@opencode-ai/console-support": ["@opencode-ai/console-support@workspace:packages/console/support"],
|
||||
|
||||
"@opencode-ai/core": ["@opencode-ai/core@workspace:packages/core"],
|
||||
|
||||
"@opencode-ai/desktop": ["@opencode-ai/desktop@workspace:packages/desktop"],
|
||||
@@ -1586,6 +1700,12 @@
|
||||
|
||||
"@opencode-ai/slack": ["@opencode-ai/slack@workspace:packages/slack"],
|
||||
|
||||
"@opencode-ai/stats-app": ["@opencode-ai/stats-app@workspace:packages/stats/app"],
|
||||
|
||||
"@opencode-ai/stats-core": ["@opencode-ai/stats-core@workspace:packages/stats/core"],
|
||||
|
||||
"@opencode-ai/stats-server": ["@opencode-ai/stats-server@workspace:packages/stats/server"],
|
||||
|
||||
"@opencode-ai/storybook": ["@opencode-ai/storybook@workspace:packages/storybook"],
|
||||
|
||||
"@opencode-ai/ui": ["@opencode-ai/ui@workspace:packages/ui"],
|
||||
@@ -1620,23 +1740,23 @@
|
||||
|
||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="],
|
||||
|
||||
"@opentui/core": ["@opentui/core@0.2.15", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.2.15", "@opentui/core-darwin-x64": "0.2.15", "@opentui/core-linux-arm64": "0.2.15", "@opentui/core-linux-x64": "0.2.15", "@opentui/core-win32-arm64": "0.2.15", "@opentui/core-win32-x64": "0.2.15" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-YGHttdZWScMcSvtYgZkLR6VhUO1OoUiQzwYjZgIusf5eCkPLD8PapH+PTMVqAiX16CHO6JxfMlkHv5qDiHAccQ=="],
|
||||
"@opentui/core": ["@opentui/core@0.2.16", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.2.16", "@opentui/core-darwin-x64": "0.2.16", "@opentui/core-linux-arm64": "0.2.16", "@opentui/core-linux-x64": "0.2.16", "@opentui/core-win32-arm64": "0.2.16", "@opentui/core-win32-x64": "0.2.16" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-4vWN15Zc3nsXJlOiHhhpqkBXD+wrNFKxCPtiTiillZYDRre+XsZogVTOOGUDwaBIC23OSxq7imezLmmtShVBEA=="],
|
||||
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-s25f9GmZd6wxNM5ExRmwwnLT+NLCKxnTWuO9aObOlqsXfLMGHQZrb6YwgAn/PSTua98KmH7GJCVWdPgZ/P+0RQ=="],
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aFb2Yp+oqDu3h6VCWi7xpQ9yjpKSQcROzGGfHgqC6Nd3U+uiLfPJBkmiI87iK0opCggCFj5TkKI004050DmGjg=="],
|
||||
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-GyaipN+nOcEr8rcTO2mqKTGmOBk0C300I69fLtubD3BadHcMI1DVNlQrcf/J1mkQEuMYbmBTi/1hT1ybWGr2Mw=="],
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-KimiHE0j7EsTB5P8doW0lr1eH5iZKLPKWQO+tmy1VcdYr/TzqhdHSvGuJXrZvfTFi9/rV57Eq0d7964Ri9O0vQ=="],
|
||||
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-h+uyufselGT4afKMP8Lg4yUl5Kp+DJBlhu3XpWXhphE5Pnq5+f0uGBr4P+34CNcWxMsDnvagSQLFRCS4rGrOWA=="],
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-4fCwRCfTtUgS/5QcSEkSuBjgQymSOUWXgrXG2ycrf3Swi0QhKDA/pVjwLrUJ6eF+/8mQyQSEV72T8MxMO3M2qg=="],
|
||||
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.15", "", { "os": "linux", "cpu": "x64" }, "sha512-jx+NImPq4wSp3Apfe7tlixiEJNnRyECTRJRWhGF6ZJz4PwFfgK2UHZKYR0DZHbV8nYawoDNQPJDXEWcoZShnMg=="],
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.16", "", { "os": "linux", "cpu": "x64" }, "sha512-KgQBGjiucw4e7gM+R8qOzHWBFhjCY1IfCrGjW3Wzxv2hKUlL+mPhelaeJwnEqtNxMUdVTYjlwlu3IHxslXMJWQ=="],
|
||||
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-2SQQLvf3sgmToxrNika9AdcccKrjPJEn5jW6sSv0oEixNBzUzW41vSZZG4LM/V3lL8eg0LoYDnRZeKLB4gwSqQ=="],
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.16", "", { "os": "win32", "cpu": "arm64" }, "sha512-C6WqEI3VkXatXraMgSFXZjEXq0pzURGjRpFAJZYmuVDmpqE57o7E80Np2UkdZ6m5kpJDt4mRyu3krc/P825iNQ=="],
|
||||
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.2.15", "", { "os": "win32", "cpu": "x64" }, "sha512-SVMVgnC7LVEm+yVZKdmmhRBj/xAT94PanT+UCcHxaCWK+OLmv/AX+ohHq2m0odup6iXcEqj+7mAltO9fgJLFIg=="],
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.2.16", "", { "os": "win32", "cpu": "x64" }, "sha512-kCX3CMTns6DMCFDNTDV4sjmBKyA/iEvzaVhl/jYi4JRIVT2zcy1lo+lhXT5mPgYHmJZu8Uye6j3Zi3c7Z2Me5A=="],
|
||||
|
||||
"@opentui/keymap": ["@opentui/keymap@0.2.15", "", { "dependencies": { "@opentui/core": "0.2.15" }, "peerDependencies": { "@opentui/react": "0.2.15", "@opentui/solid": "0.2.15", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-wqQp6y7P2jZZJiOMwupxjGryuSWCs+njjglwW/xny9J17gomBmUvTIcIIWNG0Jv+EGO9ScBzCScGlwBHFhHyYw=="],
|
||||
"@opentui/keymap": ["@opentui/keymap@0.2.16", "", { "dependencies": { "@opentui/core": "0.2.16" }, "peerDependencies": { "@opentui/react": "0.2.16", "@opentui/solid": "0.2.16", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-YBLQfNLbU2kx49bjEY9rrFoNlvIoi5qNJfRcOt6frvnR3C6MLl0/8hZY+vMQ2PEQWeEiNejFnl1lQw+z4Nk2FQ=="],
|
||||
|
||||
"@opentui/solid": ["@opentui/solid@0.2.15", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.2.15", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-CViepAjsCWXwrLndMt+qlLo7cooVX7DXwSJHNizw7mfrRJtOPzSYJZCIk1vF4IJTWffCHygoYMe3uSeKvzAcbw=="],
|
||||
"@opentui/solid": ["@opentui/solid@0.2.16", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.2.16", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-2Q+v1PPpXXr+sALi9Aj6I5Jvo7xDfbmstYjRLL7lW3Hghh9i7ONQKpt/gyDDRbhSsYrhxKYTNenF9OxgoXkTHg=="],
|
||||
|
||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||
|
||||
@@ -2324,6 +2444,10 @@
|
||||
|
||||
"@types/cross-spawn": ["@types/cross-spawn@6.0.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA=="],
|
||||
|
||||
"@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="],
|
||||
|
||||
"@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="],
|
||||
|
||||
"@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
|
||||
|
||||
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
|
||||
@@ -2468,6 +2592,8 @@
|
||||
|
||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||
|
||||
"@upstash/redis": ["@upstash/redis@1.38.0", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-wu+dZBptlLy0+MCUEoHmzrY/TnmgDey3+c7EbIGwrLqAvkP8yi5MWZHYGIFtAygmL4Bkz2TdFu+eU0vFPncIcg=="],
|
||||
|
||||
"@valibot/to-json-schema": ["@valibot/to-json-schema@1.6.0", "", { "peerDependencies": { "valibot": "^1.3.0" } }, "sha512-d6rYyK5KVa2XdqamWgZ4/Nr+cXhxjy7lmpe6Iajw15J/jmU+gyxl2IEd1Otg1d7Rl3gOQL5reulnSypzBtYy1A=="],
|
||||
|
||||
"@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="],
|
||||
@@ -2894,6 +3020,20 @@
|
||||
|
||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
|
||||
"d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
|
||||
|
||||
"d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
|
||||
|
||||
"d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="],
|
||||
|
||||
"d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
|
||||
|
||||
"d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="],
|
||||
|
||||
"d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="],
|
||||
|
||||
"d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="],
|
||||
|
||||
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
|
||||
|
||||
"data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
|
||||
@@ -3300,7 +3440,7 @@
|
||||
|
||||
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
|
||||
|
||||
"gitlab-ai-provider": ["gitlab-ai-provider@6.7.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-J7apROAmDwDJcLPcbQ9YQiPhyFN1Vvt/f3ugsePaUnzh3sedfHCHGETgG0Mm0dJCB2rC7DYwmV/sgxLQcQ/sjw=="],
|
||||
"gitlab-ai-provider": ["gitlab-ai-provider@6.8.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-KwHASXkHtDcgrzTXZVp9Dyx6t8m9nK0R2fCm47MWcxxQ1kOBt3f2LZugtu1kOby8i4Sbd+kvBSYM66PGkDclng=="],
|
||||
|
||||
"glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="],
|
||||
|
||||
@@ -3468,6 +3608,8 @@
|
||||
|
||||
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
|
||||
|
||||
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
|
||||
|
||||
"ioredis": ["ioredis@5.10.1", "", { "dependencies": { "@ioredis/commands": "1.5.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA=="],
|
||||
|
||||
"ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
|
||||
@@ -4834,7 +4976,7 @@
|
||||
|
||||
"uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
|
||||
|
||||
"undici": ["undici@7.25.0", "", {}, "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ=="],
|
||||
"undici": ["undici@8.1.0", "", {}, "sha512-E9MkTS4xXLnRPYqxH2e6Hr2/49e7WFDKczKcCaFH4VaZs2iNvHMqeIkyUAD9vM8kujy9TjVrRlQ5KkdEJxB2pw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
@@ -4912,7 +5054,7 @@
|
||||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"venice-ai-sdk-provider": ["venice-ai-sdk-provider@2.0.1", "", { "dependencies": { "@ai-sdk/openai-compatible": "^2.0.37", "@ai-sdk/provider": "^3.0.8", "@ai-sdk/provider-utils": "^4.0.21" }, "peerDependencies": { "ai": "^6.0.90" } }, "sha512-6SxA8a4MoA6Q/c+D3q7My0Hfog76enN3n0MXhwosM+tso66rXBEGeBRD/0lravRDVzL2Q1w5QJPc86rAVJtfXg=="],
|
||||
"venice-ai-sdk-provider": ["venice-ai-sdk-provider@2.0.2", "", { "dependencies": { "@ai-sdk/openai-compatible": "^2.0.47", "@ai-sdk/provider": "^3.0.10", "@ai-sdk/provider-utils": "^4.0.27" }, "peerDependencies": { "ai": "^6.0.90" } }, "sha512-aoa05nI3BTK5aGbjBflq+Gfln2AHAkwNbWuGGvCzUIsOfp5Y3iPD4O4PUGDAEiWVJWbjpPn0KfDa0H/HebwsaA=="],
|
||||
|
||||
"verror": ["verror@1.10.1", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="],
|
||||
|
||||
@@ -5020,7 +5162,7 @@
|
||||
|
||||
"write-file-atomic": ["write-file-atomic@7.0.1", "", { "dependencies": { "signal-exit": "^4.0.1" } }, "sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg=="],
|
||||
|
||||
"ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
|
||||
"ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
|
||||
|
||||
"wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="],
|
||||
|
||||
@@ -5090,9 +5232,13 @@
|
||||
|
||||
"@ai-sdk/alibaba/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.71", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bUWOzrzR0gJKJO/PLGMR4uH2dqEgqGhrsCV+sSpk4KtOEnUQlfjZI/F7BFlqSvVpFbjdgYRRLysAeEZpJ6S1lg=="],
|
||||
"@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.78", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-0OY12G20cUt6iU6htpEA1491Oz++NVxZxlmWGX4B7rSbeZ5pnDmOu6YtW9BKzdZlNx5Gn23i6WMxyZFoMKNcgA=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.13", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.0", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-vYahwBAtRaAcFbOmE9aLr12z7RiHYDSLcnogSdxfm7kKfsNa3wH+NU5r7vTeB5rKvLsWyPjVX8iH94brP7umiQ=="],
|
||||
"@ai-sdk/amazon-bedrock/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.14", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
||||
|
||||
@@ -5108,11 +5254,17 @@
|
||||
|
||||
"@ai-sdk/fireworks/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
||||
|
||||
"@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.71", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bUWOzrzR0gJKJO/PLGMR4uH2dqEgqGhrsCV+sSpk4KtOEnUQlfjZI/F7BFlqSvVpFbjdgYRRLysAeEZpJ6S1lg=="],
|
||||
"@ai-sdk/google/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
|
||||
|
||||
"@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CbR82EgGPNrj/6q0HtclwuCqe0/pDShyv3nWDP/A9DroujzWXnLMlUJVrgPOsg4b40zQCwwVs2XSKCxvt/4QaA=="],
|
||||
"@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
|
||||
|
||||
"@ai-sdk/google-vertex/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
||||
"@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.77", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ML8C2M1YvPA1ulEx4TiyF0k1xvC2ikEiPBIC1PPQ0a5xELUGrO2lAaEzsTEoJ+eCeDd8PSBuFJjs+r+9yIwQXA=="],
|
||||
|
||||
"@ai-sdk/google-vertex/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.47", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Enm5UlL0zUCrW3792opk5h7hRWxZOZzDe6eQYVFqX9LUOGGCe1h8MZWAGim765nwzgnjlpeYOsuzZmLtRsTPlg=="],
|
||||
|
||||
"@ai-sdk/google-vertex/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
|
||||
|
||||
"@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
|
||||
|
||||
"@ai-sdk/groq/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||
|
||||
@@ -5166,6 +5318,16 @@
|
||||
|
||||
"@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
|
||||
|
||||
"@aws-sdk/client-athena/@smithy/core": ["@smithy/core@3.24.3", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg=="],
|
||||
|
||||
"@aws-sdk/client-athena/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.4.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A=="],
|
||||
|
||||
"@aws-sdk/client-athena/@smithy/node-http-handler": ["@smithy/node-http-handler@4.7.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA=="],
|
||||
|
||||
"@aws-sdk/client-athena/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="],
|
||||
|
||||
"@aws-sdk/client-athena/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
||||
|
||||
"@aws-sdk/client-cognito-identity/@aws-sdk/core": ["@aws-sdk/core@3.973.27", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@aws-sdk/xml-builder": "^3.972.17", "@smithy/core": "^3.23.14", "@smithy/node-config-provider": "^4.3.13", "@smithy/property-provider": "^4.2.13", "@smithy/protocol-http": "^5.3.13", "@smithy/signature-v4": "^5.3.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.13", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-CUZ5m8hwMCH6OYI4Li/WgMfIEx10Q2PLI9Y3XOUTPGZJ53aZ0007jCv+X/ywsaERyKPdw5MRZWk877roQksQ4A=="],
|
||||
|
||||
"@aws-sdk/client-cognito-identity/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.30", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.25", "@aws-sdk/credential-provider-http": "^3.972.27", "@aws-sdk/credential-provider-ini": "^3.972.29", "@aws-sdk/credential-provider-process": "^3.972.25", "@aws-sdk/credential-provider-sso": "^3.972.29", "@aws-sdk/credential-provider-web-identity": "^3.972.29", "@aws-sdk/types": "^3.973.7", "@smithy/credential-provider-imds": "^4.2.13", "@smithy/property-provider": "^4.2.13", "@smithy/shared-ini-file-loader": "^4.4.8", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-FMnAnWxc8PG+ZrZ2OBKzY4luCUJhe9CG0B9YwYr4pzrYGLXBS2rl+UoUvjGbAwiptxRL6hyA3lFn03Bv1TLqTw=="],
|
||||
@@ -5190,6 +5352,16 @@
|
||||
|
||||
"@aws-sdk/client-cognito-identity/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
||||
|
||||
"@aws-sdk/client-firehose/@smithy/core": ["@smithy/core@3.24.3", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg=="],
|
||||
|
||||
"@aws-sdk/client-firehose/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.4.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A=="],
|
||||
|
||||
"@aws-sdk/client-firehose/@smithy/node-http-handler": ["@smithy/node-http-handler@4.7.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA=="],
|
||||
|
||||
"@aws-sdk/client-firehose/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="],
|
||||
|
||||
"@aws-sdk/client-firehose/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
||||
|
||||
"@aws-sdk/client-lambda/@aws-sdk/core": ["@aws-sdk/core@3.974.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@aws-sdk/xml-builder": "^3.972.24", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.2", "@smithy/signature-v4": "^5.4.2", "@smithy/types": "^4.14.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-QpnINq5FZH6EOaDEkmHdT7eUunbvD27pDNQypaWjFyYz7Zl1q3UCMQErBZxpmfGfI7MvI2TlK8KTkgNpv8b1ug=="],
|
||||
|
||||
"@aws-sdk/client-lambda/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.42", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.37", "@aws-sdk/credential-provider-http": "^3.972.39", "@aws-sdk/credential-provider-ini": "^3.972.41", "@aws-sdk/credential-provider-process": "^3.972.37", "@aws-sdk/credential-provider-sso": "^3.972.41", "@aws-sdk/credential-provider-web-identity": "^3.972.41", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/credential-provider-imds": "^4.3.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-D4oon2zbqqsWOJUM99Gm3/ZyJ0IJvTXVN3PyloGb3kQEyI36fjCZheZj422lAgTWWd6TSHgiImLt3RIaLdv3dQ=="],
|
||||
@@ -5348,14 +5520,14 @@
|
||||
|
||||
"@cloudflare/kv-asset-handler/mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="],
|
||||
|
||||
"@cloudflare/vite-plugin/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
|
||||
|
||||
"@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
|
||||
|
||||
"@develar/schema-utils/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
|
||||
|
||||
"@dot/log/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"@effect/platform-node/undici": ["undici@8.1.0", "", {}, "sha512-E9MkTS4xXLnRPYqxH2e6Hr2/49e7WFDKczKcCaFH4VaZs2iNvHMqeIkyUAD9vM8kujy9TjVrRlQ5KkdEJxB2pw=="],
|
||||
|
||||
"@effect/platform-node-shared/ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="],
|
||||
|
||||
"@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="],
|
||||
@@ -5490,6 +5662,8 @@
|
||||
|
||||
"@opencode-ai/core/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
||||
|
||||
"@opencode-ai/core/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="],
|
||||
|
||||
"@opencode-ai/desktop/@actions/artifact": ["@actions/artifact@4.0.0", "", { "dependencies": { "@actions/core": "^1.10.0", "@actions/github": "^6.0.1", "@actions/http-client": "^2.1.0", "@azure/core-http": "^3.0.5", "@azure/storage-blob": "^12.15.0", "@octokit/core": "^5.2.1", "@octokit/plugin-request-log": "^1.0.4", "@octokit/plugin-retry": "^3.0.9", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "@protobuf-ts/plugin": "^2.2.3-alpha.1", "archiver": "^7.0.1", "jwt-decode": "^3.1.2", "unzip-stream": "^0.3.1" } }, "sha512-HCc2jMJRAfviGFAh0FsOR/jNfWhirxl7W6z8zDtttt0GltwxBLdEIjLiweOPFl9WbyJRW1VWnPUSAixJqcWUMQ=="],
|
||||
|
||||
"@opencode-ai/desktop/marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="],
|
||||
@@ -5500,6 +5674,8 @@
|
||||
|
||||
"@opencode-ai/llm/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
||||
|
||||
"@opencode-ai/script/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="],
|
||||
|
||||
"@opencode-ai/ui/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="],
|
||||
|
||||
"@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="],
|
||||
@@ -5810,6 +5986,8 @@
|
||||
|
||||
"miniflare/undici": ["undici@7.14.0", "", {}, "sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ=="],
|
||||
|
||||
"miniflare/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
|
||||
|
||||
"miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="],
|
||||
|
||||
"minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||
@@ -5824,6 +6002,8 @@
|
||||
|
||||
"nitro/h3": ["h3@2.0.1-rc.5", "", { "dependencies": { "rou3": "^0.7.9", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-qkohAzCab0nLzXNm78tBjZDvtKMTmtygS8BJLT3VPczAQofdqlFXDPkXdLMJN4r05+xqneG8snZJ0HgkERCZTg=="],
|
||||
|
||||
"nitro/undici": ["undici@7.25.0", "", {}, "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ=="],
|
||||
|
||||
"node-gyp-build-optional-packages/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
|
||||
@@ -5842,6 +6022,8 @@
|
||||
|
||||
"opencode/minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="],
|
||||
|
||||
"opencode/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="],
|
||||
|
||||
"opencode-gitlab-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
|
||||
|
||||
"opencode-poe-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
|
||||
@@ -5926,6 +6108,8 @@
|
||||
|
||||
"storybook/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
|
||||
|
||||
"storybook/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
|
||||
|
||||
"storybook-solidjs-vite/vite-plugin-solid": ["vite-plugin-solid@2.11.12", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA=="],
|
||||
|
||||
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
@@ -5956,7 +6140,11 @@
|
||||
|
||||
"unused-filename/path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="],
|
||||
|
||||
"venice-ai-sdk-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
||||
"venice-ai-sdk-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.47", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Enm5UlL0zUCrW3792opk5h7hRWxZOZzDe6eQYVFqX9LUOGGCe1h8MZWAGim765nwzgnjlpeYOsuzZmLtRsTPlg=="],
|
||||
|
||||
"venice-ai-sdk-provider/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
|
||||
|
||||
"venice-ai-sdk-provider/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
|
||||
|
||||
"vite-plugin-icons-spritesheet/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="],
|
||||
|
||||
@@ -6002,6 +6190,12 @@
|
||||
|
||||
"@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@smithy/eventstream-codec/@smithy/types": ["@smithy/types@4.14.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg=="],
|
||||
|
||||
"@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
@@ -6012,6 +6206,14 @@
|
||||
|
||||
"@ai-sdk/deepinfra/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@ai-sdk/google-vertex/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@ai-sdk/google-vertex/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="],
|
||||
|
||||
"@ai-sdk/google/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@ai-sdk/google/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="],
|
||||
|
||||
"@ai-sdk/groq/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@ai-sdk/mistral/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
@@ -6658,6 +6860,10 @@
|
||||
|
||||
"unplugin/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||
|
||||
"venice-ai-sdk-provider/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"venice-ai-sdk-provider/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="],
|
||||
|
||||
"vitest/@vitest/expect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
exact = true
|
||||
# Only install newly resolved package versions published at least 3 days ago.
|
||||
minimumReleaseAge = 259200
|
||||
minimumReleaseAgeExcludes = ["@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-x64", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid"]
|
||||
minimumReleaseAgeExcludes = ["@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-x64", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "gitlab-ai-provider"]
|
||||
|
||||
[test]
|
||||
root = "./do-not-run-tests-from-root"
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-1RiaZQHzIhdtcOJUMsLagpP+nBBL/Qu6zQgrAXMHDCI=",
|
||||
"aarch64-linux": "sha256-5QZhtkWuNpY/qUxlKRHcGbILOAVnuyzu+h8VDIuMQcU=",
|
||||
"aarch64-darwin": "sha256-9w3QA22XNc1itGLyhikYU90xGH/iLUUM+SSGXla7lhw=",
|
||||
"x86_64-darwin": "sha256-cSYiyhhSqIYiTeK1uWHDkHbYstQYD5jEB7JaYjWjgi4="
|
||||
"x86_64-linux": "sha256-aTweGlyK8w+A9X7FpZnbVh+czXAVlttnn7Efhzm+8kY=",
|
||||
"aarch64-linux": "sha256-Gl+fwiGv/BC9u8xV4h0NIYbEac+LJSODLSTipWwdWII=",
|
||||
"aarch64-darwin": "sha256-Vt6eVf9gb+A5nULpMgtrg2YNIQy3L//wNVThVkVbgyc=",
|
||||
"x86_64-darwin": "sha256-0uHk7YnGTeIOLxajdj+CcAokfAIdDLFeS0VwN0mXRrc="
|
||||
}
|
||||
}
|
||||
|
||||
+16
-13
@@ -7,6 +7,7 @@
|
||||
"packageManager": "bun@1.3.14",
|
||||
"scripts": {
|
||||
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
||||
"dev:stats": "bun sst shell --stage=production -- bun run --cwd packages/stats/app dev",
|
||||
"dev:storybook": "bun --cwd packages/storybook storybook",
|
||||
"lint": "oxlint",
|
||||
"typecheck": "bun turbo typecheck",
|
||||
@@ -14,7 +15,7 @@
|
||||
"postinstall": "bun run --cwd packages/opencode fix-node-pty && bun run script/setup-git.ts",
|
||||
"prepare": "husky",
|
||||
"random": "echo 'Random script'",
|
||||
"hello": "echo 'Hello World!'",
|
||||
"sso": "aws sso login --sso-session=opencode --no-browser",
|
||||
"test": "echo 'do not run tests from root' && exit 1",
|
||||
"extension": "bun --cwd packages/kilo-vscode script/launch.ts",
|
||||
"dev-setup": "bun run --cwd packages/opencode --conditions=browser src/index.ts dev-setup"
|
||||
@@ -31,9 +32,8 @@
|
||||
"@types/bun": "1.3.14",
|
||||
"@types/cross-spawn": "6.0.6",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"@opentui/core": "0.2.15",
|
||||
"@opentui/solid": "0.2.15",
|
||||
"@opentui/core": "0.2.16",
|
||||
"@opentui/solid": "0.2.16",
|
||||
"ulid": "3.0.1",
|
||||
"@kobalte/core": "0.13.11",
|
||||
"@types/luxon": "3.7.1",
|
||||
@@ -77,13 +77,14 @@
|
||||
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
|
||||
"solid-js": "1.9.12",
|
||||
"vite-plugin-solid": "2.11.10",
|
||||
"@lydell/node-pty": "1.2.0-beta.10",
|
||||
"@opentui/keymap": "0.2.15",
|
||||
"@effect/sql-sqlite-bun": "4.0.0-beta.66"
|
||||
"@lydell/node-pty": "1.2.0-beta.12",
|
||||
"@opentui/keymap": "0.2.16",
|
||||
"@effect/sql-sqlite-bun": "4.0.0-beta.66",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"sst": "4.13.1"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/artifact": "5.0.1",
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/mime-types": "3.0.1",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
@@ -93,20 +94,20 @@
|
||||
"oxlint-tsgolint": "0.21.0",
|
||||
"prettier": "3.6.2",
|
||||
"semver": "^7.6.0",
|
||||
"sst": "4.13.1",
|
||||
"turbo": "2.9.14",
|
||||
"@types/bun": "catalog:",
|
||||
"@changesets/changelog-github": "^0.5.1",
|
||||
"@changesets/cli": "^2.27.10"
|
||||
"@changesets/cli": "^2.27.10",
|
||||
"@actions/artifact": "5.0.1",
|
||||
"sst": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.1025.0",
|
||||
"@kilocode/plugin": "workspace:*",
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"heap-snapshot-toolkit": "1.1.3",
|
||||
"typescript": "catalog:",
|
||||
"@morphllm/morphsdk": "0.2.166"
|
||||
"@aws-sdk/client-s3": "3.933.0",
|
||||
"heap-snapshot-toolkit": "1.1.3"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -152,7 +153,9 @@
|
||||
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
||||
"virtua@0.49.1": "patches/virtua@0.49.1.patch",
|
||||
"@ai-sdk/xai@3.0.82": "patches/@ai-sdk%2Fxai@3.0.82.patch",
|
||||
"gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch",
|
||||
"mammoth@1.12.0": "patches/mammoth@1.12.0.patch"
|
||||
},
|
||||
"version": "7.3.46",
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/cli",
|
||||
"version": "7.3.46",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"opencode": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "bun run src/index.ts",
|
||||
"typecheck": "tsgo --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"effect": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { EOL } from "os"
|
||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||
import { PluginBoot } from "@opencode-ai/core/plugin/boot"
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as Command from "effect/unstable/cli/Command"
|
||||
|
||||
export const AgentsCommand = Command.make("agents", {}, () =>
|
||||
Effect.gen(function* () {
|
||||
yield* PluginBoot.Service.use((service) => service.wait())
|
||||
const agents = yield* AgentV2.Service.use((service) => service.all())
|
||||
process.stdout.write(
|
||||
JSON.stringify(
|
||||
agents.sort((a, b) => a.id.localeCompare(b.id)),
|
||||
null,
|
||||
2,
|
||||
) + EOL,
|
||||
)
|
||||
}),
|
||||
).pipe(Command.withDescription("List all agents"))
|
||||
@@ -0,0 +1,7 @@
|
||||
import * as Command from "effect/unstable/cli/Command"
|
||||
import { AgentsCommand } from "./agents"
|
||||
|
||||
export const DebugCommand = Command.make("debug").pipe(
|
||||
Command.withDescription("Debugging and troubleshooting tools"),
|
||||
Command.withSubcommands([AgentsCommand]),
|
||||
)
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import * as NodeRuntime from "@effect/platform-node/NodeRuntime"
|
||||
import * as NodeServices from "@effect/platform-node/NodeServices"
|
||||
import { AccountV2 } from "@opencode-ai/core/account"
|
||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
import { Config } from "@opencode-ai/core/config"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { Npm } from "@opencode-ai/core/npm"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { PluginBoot } from "@opencode-ai/core/plugin/boot"
|
||||
import { Policy } from "@opencode-ai/core/policy"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as Layer from "effect/Layer"
|
||||
import * as Command from "effect/unstable/cli/Command"
|
||||
import { DebugCommand } from "./debug"
|
||||
|
||||
const cli = Command.make("opencode", {}, () => Effect.void).pipe(
|
||||
Command.withDescription("OpenCode command line interface"),
|
||||
Command.withSubcommands([DebugCommand]),
|
||||
)
|
||||
|
||||
const locationLayer = Location.defaultLayer({
|
||||
directory: AbsolutePath.make(process.cwd()),
|
||||
})
|
||||
|
||||
const policyLayer = Policy.defaultLayer.pipe(Layer.provideMerge(locationLayer))
|
||||
const pluginLayer = PluginV2.defaultLayer
|
||||
const eventLayer = EventV2.defaultLayer
|
||||
|
||||
const layer = PluginBoot.layer.pipe(
|
||||
Layer.provideMerge(
|
||||
Layer.mergeAll(
|
||||
NodeServices.layer,
|
||||
Catalog.layer.pipe(Layer.provideMerge(Layer.mergeAll(eventLayer, pluginLayer, policyLayer))),
|
||||
eventLayer,
|
||||
pluginLayer,
|
||||
AccountV2.defaultLayer,
|
||||
AgentV2.defaultLayer,
|
||||
Config.defaultLayer.pipe(Layer.provideMerge(policyLayer)),
|
||||
Npm.defaultLayer,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
Command.run(cli, { version: "local" }).pipe(Effect.provide(layer), Effect.scoped, NodeRuntime.runMain)
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@tsconfig/bun/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noUncheckedIndexedAccess": false
|
||||
}
|
||||
}
|
||||
@@ -46,15 +46,15 @@
|
||||
"xdg-basedir": "5.1.0",
|
||||
"zod": "catalog:",
|
||||
"@ai-sdk/alibaba": "1.0.17",
|
||||
"@ai-sdk/amazon-bedrock": "4.0.96",
|
||||
"@ai-sdk/amazon-bedrock": "4.0.107",
|
||||
"@ai-sdk/anthropic": "3.0.71",
|
||||
"@ai-sdk/azure": "3.0.49",
|
||||
"@ai-sdk/cerebras": "2.0.54",
|
||||
"@ai-sdk/cohere": "3.0.27",
|
||||
"@ai-sdk/deepinfra": "2.0.41",
|
||||
"@ai-sdk/gateway": "3.0.104",
|
||||
"@ai-sdk/google": "3.0.63",
|
||||
"@ai-sdk/google-vertex": "4.0.112",
|
||||
"@ai-sdk/google": "3.0.73",
|
||||
"@ai-sdk/google-vertex": "4.0.128",
|
||||
"@ai-sdk/groq": "3.0.31",
|
||||
"@ai-sdk/mistral": "3.0.27",
|
||||
"@ai-sdk/openai": "3.0.53",
|
||||
@@ -68,10 +68,11 @@
|
||||
"@aws-sdk/credential-providers": "3.993.0",
|
||||
"@openrouter/ai-sdk-provider": "2.9.0",
|
||||
"ai-gateway-provider": "3.1.2",
|
||||
"gitlab-ai-provider": "6.7.0",
|
||||
"gitlab-ai-provider": "6.8.0",
|
||||
"google-auth-library": "10.5.0",
|
||||
"immer": "11.1.4",
|
||||
"venice-ai-sdk-provider": "2.0.1"
|
||||
"venice-ai-sdk-provider": "2.0.2",
|
||||
"jsonc-parser": "3.3.1"
|
||||
},
|
||||
"overrides": {
|
||||
"drizzle-orm": "catalog:"
|
||||
|
||||
+76
-116
@@ -1,147 +1,107 @@
|
||||
export * as AgentV2 from "./agent"
|
||||
|
||||
import { Context, Effect, HashMap, Layer, Option, Order, pipe, Schema, Array } from "effect"
|
||||
import { produce, type Draft } from "immer"
|
||||
import { Array, Context, Effect, Layer, Schema, Scope } from "effect"
|
||||
import { castDraft, enableMapSet, type Draft } from "immer"
|
||||
import { ModelV2 } from "./model"
|
||||
import { PermissionV2 } from "./permission"
|
||||
import { PluginV2 } from "./plugin"
|
||||
import { ProviderV2 } from "./provider"
|
||||
import { PositiveInt } from "./schema"
|
||||
import { State } from "./state"
|
||||
|
||||
export const ID = Schema.String.pipe(Schema.brand("AgentV2.ID"))
|
||||
export type ID = typeof ID.Type
|
||||
|
||||
export const Mode = Schema.Literals(["subagent", "primary", "all"]).annotate({ identifier: "AgentV2.Mode" })
|
||||
export type Mode = typeof Mode.Type
|
||||
export const Color = Schema.Union([
|
||||
Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)),
|
||||
Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]),
|
||||
])
|
||||
|
||||
export const Info = Schema.Struct({
|
||||
name: ID,
|
||||
description: Schema.optional(Schema.String),
|
||||
mode: Mode,
|
||||
hidden: Schema.Boolean.pipe(Schema.optional),
|
||||
color: Schema.String.pipe(Schema.optional),
|
||||
permission: PermissionV2.Ruleset,
|
||||
export class Info extends Schema.Class<Info>("AgentV2.Info")({
|
||||
id: ID,
|
||||
model: ModelV2.Ref.pipe(Schema.optional),
|
||||
options: ProviderV2.Options,
|
||||
system: Schema.String.pipe(Schema.optional),
|
||||
options: ProviderV2.Options.pipe(Schema.optional),
|
||||
steps: Schema.Int.pipe(Schema.optional),
|
||||
}).annotate({ identifier: "AgentV2.Info" })
|
||||
export type Info = typeof Info.Type
|
||||
description: Schema.String.pipe(Schema.optional),
|
||||
mode: Schema.Literals(["subagent", "primary", "all"]),
|
||||
hidden: Schema.Boolean,
|
||||
color: Color.pipe(Schema.optional),
|
||||
steps: PositiveInt.pipe(Schema.optional),
|
||||
permissions: PermissionV2.Ruleset,
|
||||
}) {
|
||||
static empty(id: ID) {
|
||||
return new Info({
|
||||
id,
|
||||
options: {
|
||||
headers: {},
|
||||
body: {},
|
||||
aisdk: {
|
||||
provider: {},
|
||||
request: {},
|
||||
},
|
||||
},
|
||||
mode: "all",
|
||||
hidden: false,
|
||||
permissions: [],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("AgentV2.NotFound", {
|
||||
agent: ID,
|
||||
}) {}
|
||||
type Data = {
|
||||
agents: Map<ID, Info>
|
||||
}
|
||||
|
||||
export class InvalidDefaultError extends Schema.TaggedErrorClass<InvalidDefaultError>()("AgentV2.InvalidDefault", {
|
||||
agent: ID,
|
||||
reason: Schema.Literals(["missing", "subagent", "hidden"]),
|
||||
}) {}
|
||||
|
||||
export class NoDefaultError extends Schema.TaggedErrorClass<NoDefaultError>()("AgentV2.NoDefault", {}) {}
|
||||
export type Editor = {
|
||||
list: () => readonly Info[]
|
||||
get: (id: ID) => Info | undefined
|
||||
update: (id: ID, fn: (agent: Draft<Info>) => void) => void
|
||||
remove: (id: ID) => void
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly get: (agent: ID) => Effect.Effect<Info, NotFoundError>
|
||||
readonly list: () => Effect.Effect<Info[]>
|
||||
readonly update: (agent: ID, fn: (agent: Draft<Info>) => void) => Effect.Effect<void>
|
||||
readonly remove: (agent: ID) => Effect.Effect<void>
|
||||
readonly defaultInfo: () => Effect.Effect<Info, InvalidDefaultError | NoDefaultError>
|
||||
readonly defaultAgent: () => Effect.Effect<ID, InvalidDefaultError | NoDefaultError>
|
||||
readonly setDefault: (agent: ID) => Effect.Effect<void, NotFoundError>
|
||||
readonly transform: State.Interface<Data, Editor>["transform"]
|
||||
readonly update: (update: State.Transform<Editor>) => Effect.Effect<void, never, Scope.Scope>
|
||||
readonly get: (id: ID) => Effect.Effect<Info | undefined>
|
||||
readonly all: () => Effect.Effect<Info[]>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Agent") {}
|
||||
|
||||
enableMapSet()
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
let agents = HashMap.empty<ID, Info>()
|
||||
let defaultAgent: ID | undefined
|
||||
|
||||
const result: Interface = {
|
||||
get: Effect.fn("AgentV2.get")(function* (agent) {
|
||||
const match = HashMap.get(agents, agent)
|
||||
if (!match.valueOrUndefined) return yield* new NotFoundError({ agent })
|
||||
return match.value
|
||||
const state = State.create<Data, Editor>({
|
||||
initial: () => ({ agents: new Map() }),
|
||||
editor: (draft) => ({
|
||||
list: () => Array.fromIterable(draft.agents.values()) as Info[],
|
||||
get: (id) => draft.agents.get(id),
|
||||
update: (id, fn) => {
|
||||
const current = draft.agents.get(id) ?? castDraft(Info.empty(id))
|
||||
if (!draft.agents.has(id)) draft.agents.set(id, current)
|
||||
fn(current)
|
||||
current.id = id
|
||||
},
|
||||
remove: (id) => {
|
||||
draft.agents.delete(id)
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
list: Effect.fn("AgentV2.list")(function* () {
|
||||
return pipe(
|
||||
HashMap.toValues(agents),
|
||||
Array.sortWith((agent) => agent.name, Order.String),
|
||||
)
|
||||
return Service.of({
|
||||
transform: state.transform,
|
||||
update: Effect.fn("AgentV2.update")(function* (update) {
|
||||
const transform = yield* state.transform()
|
||||
yield* transform(update)
|
||||
}),
|
||||
|
||||
update: Effect.fnUntraced(function* (agent, fn) {
|
||||
const next = produce(
|
||||
HashMap.get(agents, agent).pipe(
|
||||
Option.getOrElse(
|
||||
() =>
|
||||
({
|
||||
name: agent,
|
||||
mode: "all",
|
||||
permission: [],
|
||||
options: {
|
||||
headers: {},
|
||||
body: {},
|
||||
aisdk: {
|
||||
provider: {},
|
||||
request: {},
|
||||
},
|
||||
},
|
||||
}) satisfies Info,
|
||||
),
|
||||
),
|
||||
fn,
|
||||
)
|
||||
const updated = yield* plugin.trigger("agent.update", {}, { agent: next, cancel: false })
|
||||
if (updated.cancel) return
|
||||
agents = HashMap.set(agents, agent, { ...updated.agent, name: agent })
|
||||
get: Effect.fn("AgentV2.get")(function* (id) {
|
||||
return state.get().agents.get(id)
|
||||
}),
|
||||
|
||||
remove: Effect.fn("AgentV2.remove")(function* (agent) {
|
||||
const existing = Option.getOrUndefined(HashMap.get(agents, agent))
|
||||
if (!existing) return
|
||||
if ((yield* plugin.trigger("agent.remove", { agent: existing }, { cancel: false })).cancel) return
|
||||
agents = HashMap.remove(agents, agent)
|
||||
if (defaultAgent === agent) defaultAgent = undefined
|
||||
all: Effect.fn("AgentV2.all")(function* () {
|
||||
return Array.fromIterable(state.get().agents.values())
|
||||
}),
|
||||
|
||||
defaultInfo: Effect.fn("AgentV2.defaultInfo")(function* () {
|
||||
const updated = yield* plugin.trigger("agent.default", {}, { agent: defaultAgent })
|
||||
const selected = updated.agent
|
||||
if (selected) {
|
||||
const agent = yield* result
|
||||
.get(selected)
|
||||
.pipe(
|
||||
Effect.catchTag("AgentV2.NotFound", () =>
|
||||
Effect.fail(new InvalidDefaultError({ agent: selected, reason: "missing" })),
|
||||
),
|
||||
)
|
||||
if (agent.mode === "subagent") return yield* new InvalidDefaultError({ agent: selected, reason: "subagent" })
|
||||
if (agent.hidden === true) return yield* new InvalidDefaultError({ agent: selected, reason: "hidden" })
|
||||
return agent
|
||||
}
|
||||
|
||||
const visible = pipe(
|
||||
yield* result.list(),
|
||||
Array.findFirst((agent) => agent.mode !== "subagent" && agent.hidden !== true),
|
||||
)
|
||||
if (Option.isSome(visible)) return visible.value
|
||||
return yield* new NoDefaultError()
|
||||
}),
|
||||
|
||||
defaultAgent: Effect.fn("AgentV2.defaultAgent")(function* () {
|
||||
return (yield* result.defaultInfo()).name
|
||||
}),
|
||||
|
||||
setDefault: Effect.fn("AgentV2.setDefault")(function* (agent) {
|
||||
yield* result.get(agent)
|
||||
defaultAgent = agent
|
||||
}),
|
||||
}
|
||||
|
||||
return Service.of(result)
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(PluginV2.defaultLayer))
|
||||
export const defaultLayer = layer
|
||||
|
||||
+95
-132
@@ -1,18 +1,22 @@
|
||||
export * as Catalog from "./catalog"
|
||||
|
||||
import { Context, Effect, HashMap, Layer, Option, Order, pipe, Schema, Array, Scope, Stream } from "effect"
|
||||
import { produce, type Draft } from "immer"
|
||||
import { Context, Effect, Layer, Option, Order, pipe, Schema, Array, Scope, Stream } from "effect"
|
||||
import { castDraft, enableMapSet, type Draft } from "immer"
|
||||
import { ModelV2 } from "./model"
|
||||
import { PluginV2 } from "./plugin"
|
||||
import { ProviderV2 } from "./provider"
|
||||
import { Location } from "./location"
|
||||
import { EventV2 } from "./event"
|
||||
import { Policy } from "./policy"
|
||||
import { State } from "./state"
|
||||
|
||||
export type ProviderRecord = {
|
||||
provider: ProviderV2.Info
|
||||
models: Map<ModelV2.ID, ModelV2.Info>
|
||||
}
|
||||
|
||||
export type DefaultModel = { providerID: ProviderV2.ID; modelID: ModelV2.ID }
|
||||
|
||||
export class ProviderNotFoundError extends Schema.TaggedErrorClass<ProviderNotFoundError>()(
|
||||
"CatalogV2.ProviderNotFound",
|
||||
{
|
||||
@@ -25,6 +29,8 @@ export class ModelNotFoundError extends Schema.TaggedErrorClass<ModelNotFoundErr
|
||||
modelID: ModelV2.ID,
|
||||
}) {}
|
||||
|
||||
export const PolicyActions = Schema.Literals(["provider.use"])
|
||||
|
||||
export const Event = {
|
||||
ModelUpdated: EventV2.define({
|
||||
type: "catalog.model.updated",
|
||||
@@ -34,24 +40,31 @@ export const Event = {
|
||||
}),
|
||||
}
|
||||
|
||||
export type Context = {
|
||||
data: readonly ProviderRecord[]
|
||||
updateProvider: (providerID: ProviderV2.ID, fn: (provider: Draft<ProviderV2.Info>) => void) => void
|
||||
updateModel: (providerID: ProviderV2.ID, modelID: ModelV2.ID, fn: (model: Draft<ModelV2.Info>) => void) => void
|
||||
type Data = {
|
||||
providers: Map<ProviderV2.ID, ProviderRecord>
|
||||
defaultModel?: DefaultModel
|
||||
}
|
||||
|
||||
export type Editor = {
|
||||
provider: {
|
||||
list: () => readonly ProviderRecord[]
|
||||
get: (providerID: ProviderV2.ID) => ProviderRecord | undefined
|
||||
update: (providerID: ProviderV2.ID, fn: (provider: Draft<ProviderV2.Info>) => void) => void
|
||||
remove: (providerID: ProviderV2.ID) => void
|
||||
}
|
||||
model: {
|
||||
get: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => ModelV2.Info | undefined
|
||||
update: (providerID: ProviderV2.ID, modelID: ModelV2.ID, fn: (model: Draft<ModelV2.Info>) => void) => void
|
||||
remove: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => void
|
||||
default: {
|
||||
get: () => DefaultModel | undefined
|
||||
set: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type Loader = (update: (ctx: Context) => void) => Effect.Effect<void>
|
||||
|
||||
export interface Interface {
|
||||
readonly loader: () => Effect.Effect<Loader, never, Scope.Scope>
|
||||
readonly transform: State.Interface<Data, Editor>["transform"]
|
||||
readonly provider: {
|
||||
readonly get: (providerID: ProviderV2.ID) => Effect.Effect<ProviderV2.Info, ProviderNotFoundError>
|
||||
readonly all: () => Effect.Effect<ProviderV2.Info[]>
|
||||
@@ -65,29 +78,25 @@ export interface Interface {
|
||||
readonly all: () => Effect.Effect<ModelV2.Info[]>
|
||||
readonly available: () => Effect.Effect<ModelV2.Info[]>
|
||||
readonly default: () => Effect.Effect<Option.Option<ModelV2.Info>>
|
||||
readonly setDefault: (
|
||||
providerID: ProviderV2.ID,
|
||||
modelID: ModelV2.ID,
|
||||
) => Effect.Effect<void, ProviderNotFoundError | ModelNotFoundError>
|
||||
readonly small: (providerID: ProviderV2.ID) => Effect.Effect<Option.Option<ModelV2.Info>>
|
||||
}
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Catalog") {}
|
||||
|
||||
enableMapSet()
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
yield* Location.Service
|
||||
let records = HashMap.empty<ProviderV2.ID, ProviderRecord>()
|
||||
let loaders: { update: (ctx: Context) => void }[] = []
|
||||
let defaultModel: { providerID: ProviderV2.ID; modelID: ModelV2.ID } | undefined
|
||||
const plugin = yield* PluginV2.Service
|
||||
const events = yield* EventV2.Service
|
||||
const policy = yield* Policy.Service
|
||||
const scope = yield* Scope.Scope
|
||||
|
||||
const resolve = (model: ModelV2.Info) => {
|
||||
const provider = Option.getOrThrow(HashMap.get(records, model.providerID)).provider
|
||||
const provider = state.get().providers.get(model.providerID)!.provider
|
||||
const endpoint =
|
||||
model.endpoint.type === "unknown"
|
||||
? provider.endpoint
|
||||
@@ -120,9 +129,9 @@ export const layer = Layer.effect(
|
||||
}
|
||||
|
||||
function* getRecord(providerID: ProviderV2.ID) {
|
||||
const match = HashMap.get(records, providerID)
|
||||
if (!match.valueOrUndefined) return yield* new ProviderNotFoundError({ providerID })
|
||||
return match.value
|
||||
const match = state.get().providers.get(providerID)
|
||||
if (!match) return yield* new ProviderNotFoundError({ providerID })
|
||||
return match
|
||||
}
|
||||
|
||||
const normalizeEndpoint = (item: Draft<ProviderV2.Info> | Draft<ModelV2.Info>) => {
|
||||
@@ -131,114 +140,73 @@ export const layer = Layer.effect(
|
||||
delete item.options.aisdk.provider.baseURL
|
||||
}
|
||||
|
||||
const clone = (input: HashMap.HashMap<ProviderV2.ID, ProviderRecord>) =>
|
||||
HashMap.fromIterable(
|
||||
HashMap.toEntries(input).map(([key, value]) => [key, { ...value, models: new Map(value.models) }] as const),
|
||||
)
|
||||
|
||||
const context = (draft: {
|
||||
records: HashMap.HashMap<ProviderV2.ID, ProviderRecord>
|
||||
data: ProviderRecord[]
|
||||
}): Context => {
|
||||
const result: Context = {
|
||||
data: draft.data,
|
||||
updateProvider: (providerID, fn) => result.provider.update(providerID, fn),
|
||||
updateModel: (providerID, modelID, fn) => result.model.update(providerID, modelID, fn),
|
||||
provider: {
|
||||
update: (providerID, fn) => {
|
||||
const current = Option.getOrUndefined(HashMap.get(draft.records, providerID))
|
||||
const provider = produce(current?.provider ?? ProviderV2.Info.empty(providerID), (draft) => {
|
||||
fn(draft)
|
||||
normalizeEndpoint(draft)
|
||||
})
|
||||
const next = {
|
||||
provider,
|
||||
models: current?.models ?? new Map<ModelV2.ID, ModelV2.Info>(),
|
||||
}
|
||||
draft.records = HashMap.set(draft.records, providerID, next)
|
||||
const index = draft.data.findIndex((item) => item.provider.id === providerID)
|
||||
if (index === -1) draft.data.push(next)
|
||||
else draft.data[index] = next
|
||||
const state = State.create<Data, Editor>({
|
||||
initial: () => ({ providers: new Map() }),
|
||||
editor: (draft) => {
|
||||
const result: Editor = {
|
||||
provider: {
|
||||
list: () => Array.fromIterable(draft.providers.values()) as ProviderRecord[],
|
||||
get: (providerID) => draft.providers.get(providerID),
|
||||
update: (providerID, fn) => {
|
||||
let current = draft.providers.get(providerID)
|
||||
if (!current) {
|
||||
current = castDraft({
|
||||
provider: ProviderV2.Info.empty(providerID),
|
||||
models: new Map<ModelV2.ID, ModelV2.Info>(),
|
||||
})
|
||||
draft.providers.set(providerID, current)
|
||||
}
|
||||
fn(current.provider)
|
||||
normalizeEndpoint(current.provider)
|
||||
},
|
||||
remove: (providerID) => {
|
||||
draft.providers.delete(providerID)
|
||||
},
|
||||
},
|
||||
remove: (providerID) => {
|
||||
draft.records = HashMap.remove(draft.records, providerID)
|
||||
const index = draft.data.findIndex((item) => item.provider.id === providerID)
|
||||
if (index !== -1) draft.data.splice(index, 1)
|
||||
model: {
|
||||
get: (providerID, modelID) => draft.providers.get(providerID)?.models.get(modelID),
|
||||
update: (providerID, modelID, fn) => {
|
||||
result.provider.update(providerID, () => {})
|
||||
const record = draft.providers.get(providerID)!
|
||||
const model = record.models.get(modelID) ?? castDraft(ModelV2.Info.empty(providerID, modelID))
|
||||
if (!record.models.has(modelID)) record.models.set(modelID, model)
|
||||
fn(model)
|
||||
model.id = modelID
|
||||
model.providerID = providerID
|
||||
normalizeEndpoint(model)
|
||||
},
|
||||
remove: (providerID, modelID) => {
|
||||
draft.providers.get(providerID)?.models.delete(modelID)
|
||||
},
|
||||
default: {
|
||||
get: () => draft.defaultModel,
|
||||
set: (providerID, modelID) => {
|
||||
draft.defaultModel = { providerID, modelID }
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
model: {
|
||||
update: (providerID, modelID, fn) => {
|
||||
const current = Option.getOrThrow(HashMap.get(draft.records, providerID))
|
||||
const model = produce(current.models.get(modelID) ?? ModelV2.Info.empty(providerID, modelID), (draft) => {
|
||||
fn(draft)
|
||||
normalizeEndpoint(draft)
|
||||
})
|
||||
const next = {
|
||||
provider: current.provider,
|
||||
models: new Map(current.models).set(modelID, new ModelV2.Info({ ...model, id: modelID, providerID })),
|
||||
}
|
||||
draft.records = HashMap.set(draft.records, providerID, next)
|
||||
const index = draft.data.findIndex((item) => item.provider.id === providerID)
|
||||
if (index === -1) draft.data.push(next)
|
||||
else draft.data[index] = next
|
||||
},
|
||||
remove: (providerID, modelID) => {
|
||||
const current = Option.getOrUndefined(HashMap.get(draft.records, providerID))
|
||||
if (!current) return
|
||||
const next = {
|
||||
provider: current.provider,
|
||||
models: new Map(current.models),
|
||||
}
|
||||
next.models.delete(modelID)
|
||||
draft.records = HashMap.set(draft.records, providerID, next)
|
||||
const index = draft.data.findIndex((item) => item.provider.id === providerID)
|
||||
if (index !== -1) draft.data[index] = next
|
||||
},
|
||||
},
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const transform = Effect.fn("CatalogV2.transform")(function* () {
|
||||
const draft = { records: clone(records), data: HashMap.toValues(records) }
|
||||
yield* plugin.trigger("catalog.transform", context(draft), {})
|
||||
records = draft.records
|
||||
}
|
||||
return result
|
||||
},
|
||||
finalize: Effect.fn("CatalogV2.finalize")(function* (catalog, reason) {
|
||||
if (reason !== "plugin.added") yield* plugin.trigger("catalog.transform", catalog, {}).pipe(Effect.asVoid)
|
||||
for (const record of [...catalog.provider.list()]) {
|
||||
if ((yield* policy.evaluate("provider.use", record.provider.id, "allow")) === "deny") {
|
||||
catalog.provider.remove(record.provider.id)
|
||||
}
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
const rebuild = Effect.fn("CatalogV2.rebuild")(function* () {
|
||||
const draft = { records: HashMap.empty<ProviderV2.ID, ProviderRecord>(), data: [] as ProviderRecord[] }
|
||||
for (const loader of loaders) loader.update(context(draft))
|
||||
yield* plugin.trigger("catalog.transform", context(draft), {})
|
||||
records = draft.records
|
||||
})
|
||||
|
||||
yield* plugin.added().pipe(
|
||||
Stream.runForEach((id) =>
|
||||
Effect.gen(function* () {
|
||||
const draft = { records: clone(records), data: HashMap.toValues(records) }
|
||||
yield* plugin.triggerFor(id, "catalog.transform", context(draft), {})
|
||||
records = draft.records
|
||||
}),
|
||||
yield* events.subscribe(PluginV2.Event.Added).pipe(
|
||||
Stream.runForEach((event) =>
|
||||
state.update((catalog) => plugin.triggerFor(event.data.id, "catalog.transform", catalog, {}), "plugin.added"),
|
||||
),
|
||||
Effect.forkIn(scope, { startImmediately: true }),
|
||||
)
|
||||
|
||||
const result: Interface = {
|
||||
loader: Effect.fn("CatalogV2.loader")(function* () {
|
||||
const loader = { update: (_ctx: Context) => {} }
|
||||
loaders = [...loaders, loader]
|
||||
const scope = yield* Scope.Scope
|
||||
yield* Scope.addFinalizer(
|
||||
scope,
|
||||
Effect.sync(() => {
|
||||
loaders = loaders.filter((item) => item !== loader)
|
||||
}).pipe(Effect.andThen(rebuild())),
|
||||
)
|
||||
return Effect.fnUntraced(function* (update) {
|
||||
loader.update = update
|
||||
yield* rebuild()
|
||||
})
|
||||
}),
|
||||
transform: state.transform,
|
||||
|
||||
provider: {
|
||||
get: Effect.fn("CatalogV2.provider.get")(function* (providerID) {
|
||||
@@ -247,11 +215,11 @@ export const layer = Layer.effect(
|
||||
}),
|
||||
|
||||
all: Effect.fn("CatalogV2.provider.all")(function* () {
|
||||
return globalThis.Array.from(HashMap.values(records)).map((record) => record.provider)
|
||||
return Array.fromIterable(state.get().providers.values()).map((record) => record.provider)
|
||||
}),
|
||||
|
||||
available: Effect.fn("CatalogV2.provider.available")(function* () {
|
||||
return globalThis.Array.from(HashMap.values(records))
|
||||
return Array.fromIterable(state.get().providers.values())
|
||||
.map((record) => record.provider)
|
||||
.filter((provider) => provider.enabled)
|
||||
}),
|
||||
@@ -267,9 +235,8 @@ export const layer = Layer.effect(
|
||||
|
||||
all: Effect.fn("CatalogV2.model.all")(function* () {
|
||||
return pipe(
|
||||
records,
|
||||
HashMap.toValues,
|
||||
Array.flatMap((record) => globalThis.Array.from(record.models.values())),
|
||||
Array.fromIterable(state.get().providers.values()),
|
||||
Array.flatMap((record) => Array.fromIterable(record.models.values())),
|
||||
Array.map(resolve),
|
||||
Array.sortWith((item) => item.time.released.epochMilliseconds, Order.flip(Order.Number)),
|
||||
)
|
||||
@@ -277,12 +244,13 @@ export const layer = Layer.effect(
|
||||
|
||||
available: Effect.fn("CatalogV2.model.available")(function* () {
|
||||
return (yield* result.model.all()).filter((model) => {
|
||||
const record = Option.getOrUndefined(HashMap.get(records, model.providerID))
|
||||
const record = state.get().providers.get(model.providerID)
|
||||
return record?.provider.enabled !== false && model.enabled
|
||||
})
|
||||
}),
|
||||
|
||||
default: Effect.fn("CatalogV2.model.default")(function* () {
|
||||
const defaultModel = state.get().defaultModel
|
||||
if (defaultModel) {
|
||||
const model = yield* result.model.get(defaultModel.providerID, defaultModel.modelID).pipe(Effect.option)
|
||||
if (Option.isSome(model) && model.value.enabled) return model
|
||||
@@ -295,13 +263,8 @@ export const layer = Layer.effect(
|
||||
)
|
||||
}),
|
||||
|
||||
setDefault: Effect.fn("CatalogV2.model.setDefault")(function* (providerID, modelID) {
|
||||
yield* result.model.get(providerID, modelID)
|
||||
defaultModel = { providerID, modelID }
|
||||
}),
|
||||
|
||||
small: Effect.fn("CatalogV2.model.small")(function* (providerID) {
|
||||
const record = Option.getOrUndefined(HashMap.get(records, providerID))
|
||||
const record = state.get().providers.get(providerID)
|
||||
if (!record) return Option.none<ModelV2.Info>()
|
||||
|
||||
if (providerID === ProviderV2.ID.opencode) {
|
||||
@@ -310,7 +273,7 @@ export const layer = Layer.effect(
|
||||
}
|
||||
|
||||
const candidates = pipe(
|
||||
globalThis.Array.from(record.models.values()),
|
||||
Array.fromIterable(record.models.values()),
|
||||
Array.filter(
|
||||
(model) =>
|
||||
model.providerID === providerID &&
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
export * as Config from "./config"
|
||||
|
||||
import path from "path"
|
||||
import { type ParseError, parse } from "jsonc-parser"
|
||||
import { Context, Effect, Layer, Option, Schema } from "effect"
|
||||
import { AppFileSystem } from "./filesystem"
|
||||
import { Global } from "./global"
|
||||
import { Location } from "./location"
|
||||
import { PermissionV2 } from "./permission"
|
||||
import { Policy } from "./policy"
|
||||
import { AbsolutePath } from "./schema"
|
||||
import { ConfigAgent } from "./config/agent"
|
||||
import { ConfigAttachments } from "./config/attachments"
|
||||
import { ConfigCompaction } from "./config/compaction"
|
||||
import { ConfigExperimental } from "./config/experimental"
|
||||
import { ConfigFormatter } from "./config/formatter"
|
||||
import { ConfigLSP } from "./config/lsp"
|
||||
import { ConfigMCP } from "./config/mcp"
|
||||
import { ConfigPlugin } from "./config/plugin"
|
||||
import { ConfigProvider } from "./config/provider"
|
||||
import { ConfigReference } from "./config/reference"
|
||||
import { ConfigToolOutput } from "./config/tool-output"
|
||||
import { ConfigWatcher } from "./config/watcher"
|
||||
|
||||
export class Info extends Schema.Class<Info>("Config.Info")({
|
||||
$schema: Schema.optional(Schema.String).annotate({
|
||||
description: "JSON schema reference for configuration validation",
|
||||
}),
|
||||
shell: Schema.String.pipe(Schema.optional).annotate({
|
||||
description: "Default shell to use for terminal and shell tool execution",
|
||||
}),
|
||||
model: Schema.String.pipe(Schema.optional).annotate({
|
||||
description: "Default model to use when no session or agent model is selected",
|
||||
}),
|
||||
autoupdate: Schema.Union([Schema.Boolean, Schema.Literal("notify")])
|
||||
.pipe(Schema.optional)
|
||||
.annotate({
|
||||
description: "Automatically update or notify when a new version is available",
|
||||
}),
|
||||
share: Schema.Literals(["manual", "auto", "disabled"]).pipe(Schema.optional).annotate({
|
||||
description: "Control whether sessions may be shared manually, automatically, or not at all",
|
||||
}),
|
||||
enterprise: Schema.Struct({
|
||||
url: Schema.String.pipe(Schema.optional),
|
||||
})
|
||||
.pipe(Schema.optional)
|
||||
.annotate({
|
||||
description: "Enterprise sharing service configuration",
|
||||
}),
|
||||
username: Schema.String.pipe(Schema.optional).annotate({
|
||||
description: "Username displayed in conversations and used for telemetry identity",
|
||||
}),
|
||||
permissions: PermissionV2.Ruleset.pipe(Schema.optional).annotate({
|
||||
description: "Ordered tool permission rules applied to agent tool use",
|
||||
}),
|
||||
agents: Schema.Record(Schema.String, ConfigAgent.Info).pipe(Schema.optional).annotate({
|
||||
description: "Named built-in agent overrides and custom agent definitions",
|
||||
}),
|
||||
snapshots: Schema.Boolean.pipe(Schema.optional).annotate({
|
||||
description: "Enable snapshots used for undo and revert behavior",
|
||||
}),
|
||||
watcher: ConfigWatcher.Info.pipe(Schema.optional).annotate({
|
||||
description: "Filesystem watcher configuration",
|
||||
}),
|
||||
formatter: ConfigFormatter.Info.pipe(Schema.optional).annotate({
|
||||
description: "Enable built-in formatters or configure formatter overrides",
|
||||
}),
|
||||
lsp: ConfigLSP.Info.pipe(Schema.optional).annotate({
|
||||
description: "Enable built-in language servers or configure server overrides",
|
||||
}),
|
||||
attachments: ConfigAttachments.Info.pipe(Schema.optional).annotate({
|
||||
description: "Attachment processing configuration",
|
||||
}),
|
||||
tool_output: ConfigToolOutput.Info.pipe(Schema.optional).annotate({
|
||||
description: "Tool output truncation thresholds",
|
||||
}),
|
||||
mcp: ConfigMCP.Info.pipe(Schema.optional).annotate({
|
||||
description: "MCP server configuration",
|
||||
}),
|
||||
compaction: ConfigCompaction.Info.pipe(Schema.optional).annotate({
|
||||
description: "Conversation compaction behavior",
|
||||
}),
|
||||
skills: Schema.String.pipe(Schema.Array, Schema.optional).annotate({
|
||||
description: "Additional paths or URLs to discover skills from",
|
||||
}),
|
||||
instructions: Schema.String.pipe(Schema.Array, Schema.optional).annotate({
|
||||
description: "Additional paths or URLs supplying ambient instructions",
|
||||
}),
|
||||
references: ConfigReference.Info.pipe(Schema.optional).annotate({
|
||||
description: "Named local directories or Git repositories available as external context",
|
||||
}),
|
||||
plugins: ConfigPlugin.Plugins.pipe(Schema.optional).annotate({
|
||||
description: "Ordered external plugin packages to load",
|
||||
}),
|
||||
experimental: ConfigExperimental.Experimental.pipe(Schema.optional),
|
||||
providers: Schema.Record(Schema.String, ConfigProvider.Info).pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export const FileSource = Schema.Struct({
|
||||
type: Schema.Literal("file"),
|
||||
path: Schema.String,
|
||||
}).annotate({ identifier: "Config.FileSource" })
|
||||
export type FileSource = typeof FileSource.Type
|
||||
|
||||
export const MemorySource = Schema.Struct({
|
||||
type: Schema.Literal("memory"),
|
||||
}).annotate({ identifier: "Config.MemorySource" })
|
||||
export type MemorySource = typeof MemorySource.Type
|
||||
|
||||
export const Source = Schema.Union([FileSource, MemorySource]).pipe(Schema.toTaggedUnion("type"))
|
||||
export type Source = typeof Source.Type
|
||||
|
||||
export class Loaded extends Schema.Class<Loaded>("Config.Loaded")({
|
||||
source: Source,
|
||||
info: Info,
|
||||
}) {}
|
||||
|
||||
export interface Interface {
|
||||
/** Returns supplemental config directories from lowest to highest priority. */
|
||||
readonly directories: () => Effect.Effect<AbsolutePath[]>
|
||||
/** Loads location config files from lowest to highest priority. */
|
||||
readonly get: () => Effect.Effect<Loaded[]>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Config") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const global = yield* Global.Service
|
||||
const location = yield* Location.Service
|
||||
const policy = yield* Policy.Service
|
||||
const names = ["config.json", "opencode.json", "opencode.jsonc"]
|
||||
|
||||
const loadFile = Effect.fnUntraced(function* (filepath: string) {
|
||||
const text = yield* fs.readFileStringSafe(filepath)
|
||||
if (!text) return
|
||||
|
||||
const errors: ParseError[] = []
|
||||
const input: unknown = parse(text, errors, { allowTrailingComma: true })
|
||||
if (errors.length) return
|
||||
|
||||
// Accept legacy fields while v2 is migrated incrementally; recognized
|
||||
// fields still have to satisfy the v2 schema.
|
||||
const info = Option.getOrUndefined(
|
||||
Schema.decodeUnknownOption(Info)(input, { errors: "all", onExcessProperty: "ignore" }),
|
||||
)
|
||||
if (!info) return
|
||||
return new Loaded({ source: { type: "file", path: filepath }, info })
|
||||
})
|
||||
|
||||
const loadDirectory = Effect.fnUntraced(function* (directory: AbsolutePath) {
|
||||
return yield* Effect.forEach(names, (file) => loadFile(path.join(directory, file))).pipe(
|
||||
Effect.map((configs) => configs.filter((config): config is Loaded => config !== undefined)),
|
||||
)
|
||||
})
|
||||
|
||||
const globalDirectory = AbsolutePath.make(global.config)
|
||||
const locationIsGlobal = path.resolve(location.directory) === path.resolve(global.config)
|
||||
// Read configuration once when this location opens. Later calls reuse these
|
||||
// values until the location is reopened.
|
||||
const directories = locationIsGlobal
|
||||
? [globalDirectory]
|
||||
: [
|
||||
globalDirectory,
|
||||
...(yield* fs
|
||||
.up({ targets: [".opencode"], start: location.directory, stop: location.project.directory })
|
||||
.pipe(Effect.orDie))
|
||||
.toReversed()
|
||||
.map((directory) => AbsolutePath.make(directory)),
|
||||
]
|
||||
// A config closer to the opened directory should win over one higher up.
|
||||
// Search starts nearby, so reverse the results before applying them.
|
||||
const directPaths = locationIsGlobal
|
||||
? []
|
||||
: (yield* fs
|
||||
.up({ targets: names.toReversed(), start: location.directory, stop: location.project.directory })
|
||||
.pipe(Effect.orDie)).toReversed()
|
||||
const direct = yield* Effect.forEach(directPaths, loadFile).pipe(
|
||||
Effect.orDie,
|
||||
Effect.map((configs) => configs.filter((config): config is Loaded => config !== undefined)),
|
||||
)
|
||||
const supplementary = yield* Effect.forEach(directories, loadDirectory).pipe(Effect.orDie)
|
||||
// Apply general settings first and more specific settings last:
|
||||
// global config, project files, then `.opencode` files.
|
||||
const configs = [...(supplementary[0] ?? []), ...direct, ...supplementary.slice(1).flat()]
|
||||
// Rules use the opposite order so a user-global rule can override a
|
||||
// repository rule. Statement order inside each file stays unchanged.
|
||||
yield* policy.load(configs.toReversed().flatMap((config) => config.info.experimental?.policies ?? []))
|
||||
|
||||
return Service.of({
|
||||
directories: Effect.fn("Config.directories")(function* () {
|
||||
return directories
|
||||
}),
|
||||
get: Effect.fn("Config.get")(function* () {
|
||||
return configs
|
||||
}),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer), Layer.provide(Global.defaultLayer))
|
||||
@@ -0,0 +1,25 @@
|
||||
export * as ConfigAgent from "./agent"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { PermissionV2 } from "../permission"
|
||||
import { ConfigProvider } from "./provider"
|
||||
import { PositiveInt } from "../schema"
|
||||
|
||||
export const Color = Schema.Union([
|
||||
Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)),
|
||||
Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]),
|
||||
])
|
||||
|
||||
export class Info extends Schema.Class<Info>("ConfigV2.Agent")({
|
||||
model: Schema.String.pipe(Schema.optional),
|
||||
variant: Schema.String.pipe(Schema.optional),
|
||||
options: ConfigProvider.Options.pipe(Schema.optional),
|
||||
system: Schema.String.pipe(Schema.optional),
|
||||
description: Schema.String.pipe(Schema.optional),
|
||||
mode: Schema.Literals(["subagent", "primary", "all"]).pipe(Schema.optional),
|
||||
hidden: Schema.Boolean.pipe(Schema.optional),
|
||||
color: Color.pipe(Schema.optional),
|
||||
steps: PositiveInt.pipe(Schema.optional),
|
||||
disabled: Schema.Boolean.pipe(Schema.optional),
|
||||
permissions: PermissionV2.Ruleset.pipe(Schema.optional),
|
||||
}) {}
|
||||
@@ -0,0 +1,15 @@
|
||||
export * as ConfigAttachments from "./attachments"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { PositiveInt } from "../schema"
|
||||
|
||||
export class Image extends Schema.Class<Image>("ConfigV2.Attachments.Image")({
|
||||
auto_resize: Schema.Boolean.pipe(Schema.optional),
|
||||
max_width: PositiveInt.pipe(Schema.optional),
|
||||
max_height: PositiveInt.pipe(Schema.optional),
|
||||
max_base64_bytes: PositiveInt.pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export class Info extends Schema.Class<Info>("ConfigV2.Attachments")({
|
||||
image: Image.pipe(Schema.optional),
|
||||
}) {}
|
||||
@@ -0,0 +1,16 @@
|
||||
export * as ConfigCompaction from "./compaction"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { NonNegativeInt } from "../schema"
|
||||
|
||||
export class Keep extends Schema.Class<Keep>("ConfigV2.Compaction.Keep")({
|
||||
turns: NonNegativeInt.pipe(Schema.optional),
|
||||
tokens: NonNegativeInt.pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export class Info extends Schema.Class<Info>("ConfigV2.Compaction")({
|
||||
auto: Schema.Boolean.pipe(Schema.optional),
|
||||
prune: Schema.Boolean.pipe(Schema.optional),
|
||||
keep: Keep.pipe(Schema.optional),
|
||||
buffer: NonNegativeInt.pipe(Schema.optional),
|
||||
}) {}
|
||||
@@ -0,0 +1,18 @@
|
||||
export * as ConfigExperimental from "./experimental"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { Catalog } from "../catalog"
|
||||
import { Policy as PolicyV2 } from "../policy"
|
||||
|
||||
// Each core domain exports the policy actions it supports. Adding an action to
|
||||
// this union makes it valid in authored config while keeping Policy generic.
|
||||
export const PolicyAction = Schema.Union([Catalog.PolicyActions])
|
||||
|
||||
export class Policy extends Schema.Class<Policy>("ConfigV2.Experimental.Policy")({
|
||||
...PolicyV2.Info.fields,
|
||||
action: PolicyAction,
|
||||
}) {}
|
||||
|
||||
export class Experimental extends Schema.Class<Experimental>("ConfigV2.Experimental")({
|
||||
policies: Policy.pipe(Schema.Array, Schema.optional),
|
||||
}) {}
|
||||
@@ -0,0 +1,12 @@
|
||||
export * as ConfigFormatter from "./formatter"
|
||||
|
||||
import { Schema } from "effect"
|
||||
|
||||
export class Entry extends Schema.Class<Entry>("ConfigV2.Formatter.Entry")({
|
||||
disabled: Schema.Boolean.pipe(Schema.optional),
|
||||
command: Schema.String.pipe(Schema.Array, Schema.optional),
|
||||
environment: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
|
||||
extensions: Schema.String.pipe(Schema.Array, Schema.optional),
|
||||
}) {}
|
||||
|
||||
export const Info = Schema.Union([Schema.Boolean, Schema.Record(Schema.String, Entry)])
|
||||
@@ -0,0 +1,18 @@
|
||||
export * as ConfigLSP from "./lsp"
|
||||
|
||||
import { Schema } from "effect"
|
||||
|
||||
export const Disabled = Schema.Struct({
|
||||
disabled: Schema.Literal(true),
|
||||
})
|
||||
|
||||
export class Server extends Schema.Class<Server>("ConfigV2.LSP.Server")({
|
||||
command: Schema.String.pipe(Schema.Array),
|
||||
extensions: Schema.String.pipe(Schema.Array, Schema.optional),
|
||||
disabled: Schema.Boolean.pipe(Schema.optional),
|
||||
env: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
|
||||
initialization: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export const Entry = Schema.Union([Disabled, Server])
|
||||
export const Info = Schema.Union([Schema.Boolean, Schema.Record(Schema.String, Entry)])
|
||||
@@ -0,0 +1,36 @@
|
||||
export * as ConfigMCP from "./mcp"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { PositiveInt } from "../schema"
|
||||
|
||||
export class Local extends Schema.Class<Local>("ConfigV2.MCP.Local")({
|
||||
type: Schema.Literal("local"),
|
||||
command: Schema.String.pipe(Schema.Array),
|
||||
environment: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
|
||||
disabled: Schema.Boolean.pipe(Schema.optional),
|
||||
timeout: PositiveInt.pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export class OAuth extends Schema.Class<OAuth>("ConfigV2.MCP.OAuth")({
|
||||
client_id: Schema.String.pipe(Schema.optional),
|
||||
client_secret: Schema.String.pipe(Schema.optional),
|
||||
scope: Schema.String.pipe(Schema.optional),
|
||||
callback_port: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65535 })).pipe(Schema.optional),
|
||||
redirect_uri: Schema.String.pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export class Remote extends Schema.Class<Remote>("ConfigV2.MCP.Remote")({
|
||||
type: Schema.Literal("remote"),
|
||||
url: Schema.String,
|
||||
headers: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
|
||||
oauth: Schema.Union([OAuth, Schema.Literal(false)]).pipe(Schema.optional),
|
||||
disabled: Schema.Boolean.pipe(Schema.optional),
|
||||
timeout: PositiveInt.pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export const Server = Schema.Union([Local, Remote]).pipe(Schema.toTaggedUnion("type"))
|
||||
|
||||
export class Info extends Schema.Class<Info>("ConfigV2.MCP")({
|
||||
timeout: PositiveInt.pipe(Schema.optional),
|
||||
servers: Schema.Record(Schema.String, Server).pipe(Schema.optional),
|
||||
}) {}
|
||||
@@ -0,0 +1,13 @@
|
||||
export * as ConfigPlugin from "./plugin"
|
||||
|
||||
import { Schema } from "effect"
|
||||
|
||||
export class Entry extends Schema.Class<Entry>("ConfigV2.Plugin.Entry")({
|
||||
package: Schema.String,
|
||||
options: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export const Plugin = Schema.Union([Schema.String, Entry])
|
||||
export type Plugin = typeof Plugin.Type
|
||||
|
||||
export const Plugins = Plugin.pipe(Schema.Array)
|
||||
@@ -0,0 +1,65 @@
|
||||
export * as ConfigAgentPlugin from "./agent"
|
||||
|
||||
import { Effect } from "effect"
|
||||
import { AgentV2 } from "../../agent"
|
||||
import { Config } from "../../config"
|
||||
import { ModelV2 } from "../../model"
|
||||
import { PermissionV2 } from "../../permission"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
|
||||
export const Plugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("config-agent"),
|
||||
effect: Effect.gen(function* () {
|
||||
const agent = yield* AgentV2.Service
|
||||
const config = yield* Config.Service
|
||||
const files = yield* config.get()
|
||||
|
||||
yield* agent.update((editor) => {
|
||||
const permissions = new Map<AgentV2.ID, PermissionV2.Ruleset>()
|
||||
|
||||
for (const file of files) {
|
||||
for (const [id, item] of Object.entries(file.info.agents ?? {})) {
|
||||
const agentID = AgentV2.ID.make(id)
|
||||
if (item.disabled) {
|
||||
editor.remove(agentID)
|
||||
permissions.delete(agentID)
|
||||
continue
|
||||
}
|
||||
|
||||
editor.update(agentID, (agent) => {
|
||||
if (item.model !== undefined) {
|
||||
const model = ModelV2.parse(item.model)
|
||||
agent.model = { id: model.modelID, providerID: model.providerID, variant: agent.model?.variant }
|
||||
}
|
||||
if (item.variant !== undefined && agent.model !== undefined) {
|
||||
agent.model.variant = ModelV2.VariantID.make(item.variant)
|
||||
}
|
||||
if (item.options !== undefined) {
|
||||
Object.assign(agent.options.headers, item.options.headers ?? {})
|
||||
Object.assign(agent.options.body, item.options.body ?? {})
|
||||
Object.assign(agent.options.aisdk.provider, item.options.aisdk?.provider ?? {})
|
||||
Object.assign(agent.options.aisdk.request, item.options.aisdk?.request ?? {})
|
||||
}
|
||||
if (item.system !== undefined) agent.system = item.system
|
||||
if (item.description !== undefined) agent.description = item.description
|
||||
if (item.mode !== undefined) agent.mode = item.mode
|
||||
if (item.hidden !== undefined) agent.hidden = item.hidden
|
||||
if (item.color !== undefined) agent.color = item.color
|
||||
if (item.steps !== undefined) agent.steps = item.steps
|
||||
})
|
||||
|
||||
if (item.permissions !== undefined) {
|
||||
permissions.set(agentID, [...(permissions.get(agentID) ?? []), ...item.permissions])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const global = files.flatMap((file) => file.info.permissions ?? [])
|
||||
for (const current of editor.list()) {
|
||||
editor.update(current.id, (agent) => {
|
||||
agent.permissions.push(...global, ...(permissions.get(current.id) ?? []))
|
||||
})
|
||||
}
|
||||
})
|
||||
}),
|
||||
})
|
||||
@@ -0,0 +1,95 @@
|
||||
export * as ConfigProviderPlugin from "./provider"
|
||||
|
||||
import { Effect } from "effect"
|
||||
import { Catalog } from "../../catalog"
|
||||
import { Config } from "../../config"
|
||||
import { ModelV2 } from "../../model"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const Plugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("config-provider"),
|
||||
effect: Effect.gen(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
const config = yield* Config.Service
|
||||
const transform = yield* catalog.transform()
|
||||
const files = yield* config.get()
|
||||
|
||||
yield* transform((catalog) => {
|
||||
for (const file of files) {
|
||||
for (const [id, item] of Object.entries(file.info.providers ?? {})) {
|
||||
const providerID = ProviderV2.ID.make(id)
|
||||
catalog.provider.update(providerID, (provider) => {
|
||||
if (item.name !== undefined) provider.name = item.name
|
||||
if (item.env !== undefined) provider.env = [...item.env]
|
||||
provider.enabled = { via: "custom", data: {} }
|
||||
if (item.endpoint !== undefined) provider.endpoint = { ...item.endpoint }
|
||||
if (item.options !== undefined) {
|
||||
Object.assign(provider.options.headers, item.options.headers ?? {})
|
||||
Object.assign(provider.options.body, item.options.body ?? {})
|
||||
Object.assign(provider.options.aisdk.provider, item.options.aisdk?.provider ?? {})
|
||||
Object.assign(provider.options.aisdk.request, item.options.aisdk?.request ?? {})
|
||||
}
|
||||
})
|
||||
|
||||
for (const [id, config] of Object.entries(item.models ?? {})) {
|
||||
catalog.model.update(providerID, ModelV2.ID.make(id), (model) => {
|
||||
if (config.api_id !== undefined) model.apiID = config.api_id
|
||||
if (config.family !== undefined) model.family = config.family
|
||||
if (config.name !== undefined) model.name = config.name
|
||||
if (config.endpoint !== undefined) model.endpoint = { ...config.endpoint }
|
||||
if (config.capabilities !== undefined) {
|
||||
model.capabilities = {
|
||||
tools: config.capabilities.tools,
|
||||
input: [...config.capabilities.input],
|
||||
output: [...config.capabilities.output],
|
||||
}
|
||||
}
|
||||
if (config.options !== undefined) {
|
||||
Object.assign(model.options.headers, config.options.headers ?? {})
|
||||
Object.assign(model.options.body, config.options.body ?? {})
|
||||
Object.assign(model.options.aisdk.provider, config.options.aisdk?.provider ?? {})
|
||||
Object.assign(model.options.aisdk.request, config.options.aisdk?.request ?? {})
|
||||
if (config.options.variant !== undefined) model.options.variant = config.options.variant
|
||||
}
|
||||
if (config.variants !== undefined) {
|
||||
for (const variant of config.variants) {
|
||||
let existing = model.variants.find((item) => item.id === variant.id)
|
||||
if (!existing) {
|
||||
existing = {
|
||||
id: variant.id,
|
||||
headers: {},
|
||||
body: {},
|
||||
aisdk: {
|
||||
provider: {},
|
||||
request: {},
|
||||
},
|
||||
}
|
||||
model.variants.push(existing)
|
||||
}
|
||||
Object.assign(existing.headers, variant.headers ?? {})
|
||||
Object.assign(existing.body, variant.body ?? {})
|
||||
Object.assign(existing.aisdk.provider, variant.aisdk?.provider ?? {})
|
||||
Object.assign(existing.aisdk.request, variant.aisdk?.request ?? {})
|
||||
}
|
||||
}
|
||||
if (config.cost !== undefined) {
|
||||
model.cost = (Array.isArray(config.cost) ? config.cost : [config.cost]).map((cost) => ({
|
||||
tier: cost.tier && { ...cost.tier },
|
||||
input: cost.input,
|
||||
output: cost.output,
|
||||
cache: {
|
||||
read: cost.cache?.read ?? 0,
|
||||
write: cost.cache?.write ?? 0,
|
||||
},
|
||||
}))
|
||||
}
|
||||
if (config.disabled !== undefined) model.enabled = !config.disabled
|
||||
if (config.limit !== undefined) model.limit = { ...model.limit, ...config.limit }
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}),
|
||||
})
|
||||
@@ -0,0 +1,62 @@
|
||||
export * as ConfigProvider from "./provider"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { ProviderV2 } from "../provider"
|
||||
import { ModelV2 } from "../model"
|
||||
|
||||
export class Options extends Schema.Class<Options>("ConfigV2.Provider.Options")({
|
||||
headers: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
|
||||
body: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
||||
aisdk: Schema.Struct({
|
||||
provider: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
||||
request: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
||||
}).pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
class Cache extends Schema.Class<Cache>("ConfigV2.Model.Cost.Cache")({
|
||||
read: Schema.Finite.pipe(Schema.optional),
|
||||
write: Schema.Finite.pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
class Cost extends Schema.Class<Cost>("ConfigV2.Model.Cost")({
|
||||
tier: Schema.Struct({
|
||||
type: Schema.Literal("context"),
|
||||
size: Schema.Int,
|
||||
}).pipe(Schema.optional),
|
||||
input: Schema.Finite,
|
||||
output: Schema.Finite,
|
||||
cache: Cache.pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
class Limit extends Schema.Class<Limit>("ConfigV2.Model.Limit")({
|
||||
context: Schema.Int.pipe(Schema.optional),
|
||||
input: Schema.Int.pipe(Schema.optional),
|
||||
output: Schema.Int.pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
class Model extends Schema.Class<Model>("ConfigV2.Model")({
|
||||
api_id: ModelV2.ID.pipe(Schema.optional),
|
||||
family: ModelV2.Family.pipe(Schema.optional),
|
||||
name: Schema.String.pipe(Schema.optional),
|
||||
endpoint: ProviderV2.Endpoint.pipe(Schema.optional),
|
||||
capabilities: ModelV2.Capabilities.pipe(Schema.optional),
|
||||
options: Schema.Struct({
|
||||
...Options.fields,
|
||||
variant: Schema.String.pipe(Schema.optional),
|
||||
}).pipe(Schema.optional),
|
||||
variants: Schema.Struct({
|
||||
id: ModelV2.VariantID,
|
||||
...Options.fields,
|
||||
}).pipe(Schema.Array, Schema.optional),
|
||||
cost: Schema.Union([Cost, Cost.pipe(Schema.Array)]).pipe(Schema.optional),
|
||||
disabled: Schema.Boolean.pipe(Schema.optional),
|
||||
limit: Limit.pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export class Info extends Schema.Class<Info>("ConfigV2.Provider")({
|
||||
name: Schema.String.pipe(Schema.optional),
|
||||
env: Schema.String.pipe(Schema.Array, Schema.optional),
|
||||
endpoint: ProviderV2.Endpoint.pipe(Schema.optional),
|
||||
options: Options.pipe(Schema.optional),
|
||||
models: Schema.Record(Schema.String, Model).pipe(Schema.optional),
|
||||
}) {}
|
||||
@@ -0,0 +1,17 @@
|
||||
export * as ConfigReference from "./reference"
|
||||
|
||||
import { Schema } from "effect"
|
||||
|
||||
export class Git extends Schema.Class<Git>("ConfigV2.Reference.Git")({
|
||||
repository: Schema.String,
|
||||
branch: Schema.String.pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export class Local extends Schema.Class<Local>("ConfigV2.Reference.Local")({
|
||||
path: Schema.String,
|
||||
}) {}
|
||||
|
||||
export const Entry = Schema.Union([Schema.String, Git, Local])
|
||||
export type Entry = typeof Entry.Type
|
||||
|
||||
export const Info = Schema.Record(Schema.String, Entry)
|
||||
@@ -0,0 +1,9 @@
|
||||
export * as ConfigToolOutput from "./tool-output"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { PositiveInt } from "../schema"
|
||||
|
||||
export class Info extends Schema.Class<Info>("ConfigV2.ToolOutput")({
|
||||
max_lines: PositiveInt.pipe(Schema.optional),
|
||||
max_bytes: PositiveInt.pipe(Schema.optional),
|
||||
}) {}
|
||||
@@ -0,0 +1,7 @@
|
||||
export * as ConfigWatcher from "./watcher"
|
||||
|
||||
import { Schema } from "effect"
|
||||
|
||||
export class Info extends Schema.Class<Info>("ConfigV2.Watcher")({
|
||||
ignore: Schema.String.pipe(Schema.Array, Schema.optional),
|
||||
}) {}
|
||||
+6
-1
@@ -15,6 +15,7 @@ type ServiceUse<Identifier, Shape> = {
|
||||
}
|
||||
|
||||
export const serviceUse = <Identifier, Shape>(tag: Context.Service<Identifier, Shape>) => {
|
||||
const cache = new Map<string, (...args: unknown[]) => Effect.Effect<unknown, unknown, unknown>>()
|
||||
// This is the only dynamic boundary: TypeScript knows the accessor shape,
|
||||
// but Proxy property names are runtime values.
|
||||
const access = new Proxy(
|
||||
@@ -22,7 +23,9 @@ export const serviceUse = <Identifier, Shape>(tag: Context.Service<Identifier, S
|
||||
{
|
||||
get: (_, key) => {
|
||||
if (typeof key !== "string") return undefined
|
||||
return (...args: unknown[]) =>
|
||||
const cached = cache.get(key)
|
||||
if (cached) return cached
|
||||
const accessor = (...args: unknown[]) =>
|
||||
tag.use((service) => {
|
||||
// oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- Proxy keys are checked at runtime.
|
||||
const method = service[key as keyof Shape]
|
||||
@@ -30,6 +33,8 @@ export const serviceUse = <Identifier, Shape>(tag: Context.Service<Identifier, S
|
||||
// oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- ServiceUse exposes only Effect-returning methods.
|
||||
return (method as (...args: unknown[]) => Effect.Effect<unknown, unknown, unknown>)(...args)
|
||||
})
|
||||
cache.set(key, accessor)
|
||||
return accessor
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -128,7 +128,7 @@ export const layer = Layer.effect(
|
||||
...(options?.metadata ? { metadata: options.metadata } : {}),
|
||||
type: definition.type,
|
||||
...(definition.version === undefined ? {} : { version: definition.version }),
|
||||
...(location ? { location } : {}),
|
||||
...(location ? { location: { directory: location.directory, workspaceID: location.workspaceID } } : {}),
|
||||
data,
|
||||
} as Payload<D>
|
||||
return yield* publishEvent(event)
|
||||
|
||||
@@ -3,9 +3,10 @@ import { dirname, join, relative, resolve as pathResolve } from "path"
|
||||
import { realpathSync } from "fs"
|
||||
import * as NFS from "fs/promises"
|
||||
import { lookup } from "mime-types"
|
||||
import { Effect, FileSystem, Layer, Schema, Context } from "effect"
|
||||
import { Context, Effect, FileSystem, Layer, Schema } from "effect"
|
||||
import type { PlatformError } from "effect/PlatformError"
|
||||
import { Glob } from "./util/glob"
|
||||
import { serviceUse } from "./effect/service-use"
|
||||
|
||||
export namespace AppFileSystem {
|
||||
export class FileSystemError extends Schema.TaggedErrorClass<FileSystemError>()("FileSystemError", {
|
||||
@@ -39,6 +40,8 @@ export namespace AppFileSystem {
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/FileSystem") {}
|
||||
|
||||
export const use = serviceUse(Service)
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
|
||||
@@ -8,6 +8,10 @@ function truthy(key: string) {
|
||||
const KILO_EXPERIMENTAL = truthy("KILO_EXPERIMENTAL")
|
||||
const copy = process.env["KILO_EXPERIMENTAL_DISABLE_COPY_ON_SELECT"]
|
||||
|
||||
function enabledByExperimental(key: string) {
|
||||
return process.env[key] === undefined ? KILO_EXPERIMENTAL : truthy(key)
|
||||
}
|
||||
|
||||
export const Flag = {
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: process.env["OTEL_EXPORTER_OTLP_ENDPOINT"],
|
||||
OTEL_EXPORTER_OTLP_HEADERS: process.env["OTEL_EXPORTER_OTLP_HEADERS"],
|
||||
@@ -42,7 +46,8 @@ export const Flag = {
|
||||
KILO_DB: process.env["KILO_DB"],
|
||||
|
||||
KILO_WORKSPACE_ID: process.env["KILO_WORKSPACE_ID"],
|
||||
KILO_EXPERIMENTAL_WORKSPACES: KILO_EXPERIMENTAL || truthy("KILO_EXPERIMENTAL_WORKSPACES"),
|
||||
KILO_EXPERIMENTAL_WORKSPACES: enabledByExperimental("KILO_EXPERIMENTAL_WORKSPACES"),
|
||||
KILO_EXPERIMENTAL_SESSION_SWITCHER: enabledByExperimental("KILO_EXPERIMENTAL_SESSION_SWITCHER"),
|
||||
|
||||
// Evaluated at access time (not module load) because tests, the CLI, and
|
||||
// external tooling set these env vars at runtime.
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
export * as Git from "./git"
|
||||
|
||||
import path from "path"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { ChildProcess } from "effect/unstable/process"
|
||||
import { AbsolutePath } from "./schema"
|
||||
import { AppFileSystem } from "./filesystem"
|
||||
import { AppProcess } from "./process"
|
||||
|
||||
export interface Repo {
|
||||
/**
|
||||
* The root directory of the working tree that contains the input path.
|
||||
*
|
||||
* For `/home/me/app/src/file.ts` in a normal clone, this is `/home/me/app`.
|
||||
* For `/home/me/app-feature/src/file.ts` in a linked worktree, this is
|
||||
* `/home/me/app-feature`.
|
||||
*/
|
||||
readonly directory: AbsolutePath
|
||||
/**
|
||||
* The shared Git storage directory used by this repo and any linked worktrees.
|
||||
*
|
||||
* For a normal clone at `/home/me/app`, this is usually `/home/me/app/.git`.
|
||||
* For a linked worktree at `/home/me/app-feature` whose main checkout is
|
||||
* `/home/me/app`, this is usually `/home/me/app/.git`.
|
||||
*/
|
||||
readonly store: AbsolutePath
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly find: (input: AbsolutePath) => Effect.Effect<Repo | undefined>
|
||||
readonly remote: (repo: Repo, name?: string) => Effect.Effect<string | undefined>
|
||||
readonly roots: (repo: Repo) => Effect.Effect<string[]>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/GitV2") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const proc = yield* AppProcess.Service
|
||||
|
||||
const find = Effect.fn("Git.find")(function* (input: AbsolutePath) {
|
||||
const dotgit = yield* fs.up({ targets: [".git"], start: input }).pipe(
|
||||
Effect.map((matches) => matches[0]),
|
||||
Effect.catch(() => Effect.succeed(undefined)),
|
||||
)
|
||||
if (!dotgit) return undefined
|
||||
|
||||
const cwd = path.dirname(dotgit)
|
||||
const git = run(cwd, proc)
|
||||
const topLevel = yield* git(["rev-parse", "--show-toplevel"])
|
||||
const commonDir = yield* git(["rev-parse", "--git-common-dir"])
|
||||
if (commonDir.exitCode !== 0) return undefined
|
||||
|
||||
return {
|
||||
directory: AbsolutePath.make(topLevel.exitCode === 0 ? resolvePath(cwd, topLevel.text) : cwd),
|
||||
store: AbsolutePath.make(resolvePath(cwd, commonDir.text)),
|
||||
} satisfies Repo
|
||||
})
|
||||
|
||||
const remote = Effect.fn("Git.remote")(function* (repo: Repo, name = "origin") {
|
||||
const result = yield* run(repo.directory, proc)(["remote", "get-url", name])
|
||||
if (result.exitCode !== 0) return undefined
|
||||
return result.text.trim() || undefined
|
||||
})
|
||||
|
||||
const roots = Effect.fn("Git.roots")(function* (repo: Repo) {
|
||||
const result = yield* run(repo.directory, proc)(["rev-list", "--max-parents=0", "HEAD"])
|
||||
if (result.exitCode !== 0) return []
|
||||
return result.text
|
||||
.split("\n")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
.toSorted()
|
||||
})
|
||||
|
||||
return Service.of({ find, remote, roots })
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(AppProcess.defaultLayer),
|
||||
)
|
||||
|
||||
interface Result {
|
||||
readonly exitCode: number
|
||||
readonly text: string
|
||||
}
|
||||
|
||||
function run(cwd: string, proc: AppProcess.Interface) {
|
||||
return (args: string[]) =>
|
||||
proc
|
||||
.run(
|
||||
ChildProcess.make("git", args, {
|
||||
cwd,
|
||||
extendEnv: true,
|
||||
stdin: "ignore",
|
||||
}),
|
||||
)
|
||||
.pipe(
|
||||
Effect.map((result) => ({ exitCode: result.exitCode, text: result.stdout.toString("utf8") }) satisfies Result),
|
||||
Effect.catch(() => Effect.succeed({ exitCode: 1, text: "" } satisfies Result)),
|
||||
)
|
||||
}
|
||||
|
||||
function resolvePath(cwd: string, value: string) {
|
||||
const trimmed = value.replace(/[\r\n]+$/, "")
|
||||
if (!trimmed) return cwd
|
||||
const normalized = AppFileSystem.windowsPath(trimmed)
|
||||
if (path.isAbsolute(normalized)) return path.normalize(normalized)
|
||||
return path.resolve(cwd, normalized)
|
||||
}
|
||||
@@ -2,12 +2,17 @@ import { Layer, LayerMap } from "effect"
|
||||
import { Location } from "./location"
|
||||
import { Catalog } from "./catalog"
|
||||
import { PluginBoot } from "./plugin/boot"
|
||||
import { Policy } from "./policy"
|
||||
import { Config } from "./config"
|
||||
|
||||
export class LocationServiceMap extends LayerMap.Service<LocationServiceMap>()("@opencode/example/LocationServiceMap", {
|
||||
lookup: (ref: Location.Ref) =>
|
||||
Layer.mergeAll(Catalog.defaultLayer, PluginBoot.defaultLayer).pipe(
|
||||
Layer.provide([Layer.succeed(Location.Service, Location.Service.of(ref))]),
|
||||
),
|
||||
idleTimeToLive: "5 minutes",
|
||||
lookup: (ref: Location.Ref) => {
|
||||
const result = Layer.mergeAll(Catalog.defaultLayer, PluginBoot.defaultLayer, Config.defaultLayer).pipe(
|
||||
Layer.provideMerge(Policy.defaultLayer),
|
||||
Layer.provideMerge(Location.defaultLayer(ref)),
|
||||
)
|
||||
return result
|
||||
},
|
||||
idleTimeToLive: "60 minutes",
|
||||
dependencies: [],
|
||||
}) {}
|
||||
|
||||
@@ -1,11 +1,40 @@
|
||||
import { Context, Schema } from "effect"
|
||||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
import { Project } from "./project"
|
||||
import { AbsolutePath } from "./schema"
|
||||
|
||||
export * as Location from "./location"
|
||||
|
||||
export const Ref = Schema.Struct({
|
||||
directory: Schema.String,
|
||||
directory: AbsolutePath,
|
||||
workspaceID: Schema.optional(Schema.String),
|
||||
}).annotate({ identifier: "Location.Ref" })
|
||||
export type Ref = typeof Ref.Type
|
||||
|
||||
export class Service extends Context.Service<Service, Ref>()("@opencode/Location") {}
|
||||
export interface Interface {
|
||||
readonly directory: AbsolutePath
|
||||
readonly workspaceID?: string
|
||||
readonly project: {
|
||||
readonly id: Project.ID
|
||||
readonly directory: AbsolutePath
|
||||
}
|
||||
readonly vcs?: Project.Vcs
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Location") {}
|
||||
|
||||
export const layer = (ref: Ref) =>
|
||||
Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const project = yield* Project.Service
|
||||
const resolved = yield* project.resolve(ref.directory)
|
||||
return Service.of({
|
||||
directory: ref.directory,
|
||||
workspaceID: ref.workspaceID,
|
||||
project: { id: resolved.id, directory: resolved.directory },
|
||||
vcs: resolved.vcs,
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = (ref: Ref) => layer(ref).pipe(Layer.provide(Project.defaultLayer))
|
||||
|
||||
@@ -36,7 +36,7 @@ export const Cost = Schema.Struct({
|
||||
export const Ref = Schema.Struct({
|
||||
id: ID,
|
||||
providerID: ProviderV2.ID,
|
||||
variant: VariantID,
|
||||
variant: VariantID.pipe(Schema.optional),
|
||||
})
|
||||
export type Ref = typeof Ref.Type
|
||||
|
||||
|
||||
+15
-31
@@ -2,17 +2,26 @@ export * as PluginV2 from "./plugin"
|
||||
|
||||
import { createDraft, finishDraft, type Draft } from "immer"
|
||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import { Context, Effect, Exit, Layer, PubSub, Schema, Scope, Stream } from "effect"
|
||||
import { Context, Effect, Exit, Layer, Schema, Scope } from "effect"
|
||||
import type { ModelV2 } from "./model"
|
||||
import type { AgentV2 } from "./agent"
|
||||
import type { Catalog } from "./catalog"
|
||||
import { EventV2 } from "./event"
|
||||
|
||||
export const ID = Schema.String.pipe(Schema.brand("Plugin.ID"))
|
||||
export type ID = typeof ID.Type
|
||||
|
||||
export const Event = {
|
||||
Added: EventV2.define({
|
||||
type: "plugin.added",
|
||||
schema: {
|
||||
id: ID,
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
type HookSpec = {
|
||||
"catalog.transform": {
|
||||
input: Catalog.Context
|
||||
input: Catalog.Editor
|
||||
output: {}
|
||||
}
|
||||
"account.switched": {
|
||||
@@ -43,27 +52,6 @@ type HookSpec = {
|
||||
sdk?: any
|
||||
}
|
||||
}
|
||||
"agent.update": {
|
||||
input: {}
|
||||
output: {
|
||||
agent: AgentV2.Info
|
||||
cancel: boolean
|
||||
}
|
||||
}
|
||||
"agent.remove": {
|
||||
input: {
|
||||
agent: AgentV2.Info
|
||||
}
|
||||
output: {
|
||||
cancel: boolean
|
||||
}
|
||||
}
|
||||
"agent.default": {
|
||||
input: {}
|
||||
output: {
|
||||
agent?: AgentV2.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type Hooks = {
|
||||
@@ -93,7 +81,6 @@ export interface Interface {
|
||||
effect: Effect.Effect<void | HookFunctions, never, Scope.Scope>
|
||||
}) => Effect.Effect<void, never, never>
|
||||
readonly remove: (id: ID) => Effect.Effect<void>
|
||||
readonly added: () => Stream.Stream<ID>
|
||||
readonly triggerFor: <Name extends keyof Hooks>(
|
||||
id: ID,
|
||||
name: Name,
|
||||
@@ -117,9 +104,7 @@ export const layer = Layer.effect(
|
||||
hooks: HookFunctions
|
||||
scope: Scope.Closeable
|
||||
}[] = []
|
||||
const added = yield* PubSub.unbounded<ID>()
|
||||
|
||||
yield* Effect.addFinalizer(() => PubSub.shutdown(added))
|
||||
const events = yield* EventV2.Service
|
||||
|
||||
const svc = Service.of({
|
||||
add: Effect.fn("Plugin.add")(function* (input) {
|
||||
@@ -135,9 +120,8 @@ export const layer = Layer.effect(
|
||||
scope,
|
||||
},
|
||||
]
|
||||
yield* PubSub.publish(added, input.id)
|
||||
yield* events.publish(Event.Added, { id: input.id })
|
||||
}),
|
||||
added: () => Stream.fromPubSub(added),
|
||||
trigger: Effect.fn("Plugin.trigger")(function* (name, input, output) {
|
||||
return yield* svc.triggerFor(ID.make("*"), name, input, output)
|
||||
}),
|
||||
@@ -185,7 +169,7 @@ export const layer = Layer.effect(
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer
|
||||
export const defaultLayer = layer.pipe(Layer.provide(EventV2.defaultLayer))
|
||||
|
||||
// opencode
|
||||
// sdcok
|
||||
|
||||
@@ -19,7 +19,7 @@ export const AccountPlugin = PluginV2.define({
|
||||
|
||||
return {
|
||||
"catalog.transform": Effect.fn(function* (evt) {
|
||||
for (const item of evt.data) {
|
||||
for (const item of evt.provider.list()) {
|
||||
const account = yield* accounts.active(AccountV2.ServiceID.make(item.provider.id)).pipe(Effect.orDie)
|
||||
if (!account) continue
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
export * as AgentPlugin from "./agent"
|
||||
|
||||
import path from "path"
|
||||
import { Effect } from "effect"
|
||||
import { AgentV2 } from "../agent"
|
||||
import { Global } from "../global"
|
||||
import { Location } from "../location"
|
||||
import { PermissionV2 } from "../permission"
|
||||
import { PluginV2 } from "../plugin"
|
||||
|
||||
const TRUNCATION_GLOB = path.join(Global.Path.data, "tool-output", "*")
|
||||
|
||||
const PROMPT_EXPLORE = `You are a file search specialist. You excel at thoroughly navigating and exploring codebases.
|
||||
|
||||
Your strengths:
|
||||
- Rapidly finding files using glob patterns
|
||||
- Searching code and text with powerful regex patterns
|
||||
- Reading and analyzing file contents
|
||||
|
||||
Guidelines:
|
||||
- Use Glob for broad file pattern matching
|
||||
- Use Grep for searching file contents with regex
|
||||
- Use Read when you know the specific file path you need to read
|
||||
- Use Bash for file operations like copying, moving, or listing directory contents
|
||||
- Adapt your search approach based on the thoroughness level specified by the caller
|
||||
- Return file paths as absolute paths in your final response
|
||||
- For clear communication, avoid using emojis
|
||||
- Do not create any files, or run bash commands that modify the user's system state in any way
|
||||
|
||||
Complete the user's search request efficiently and report your findings clearly.`
|
||||
|
||||
const PROMPT_COMPACTION = `You are an anchored context summarization assistant for coding sessions.
|
||||
|
||||
Summarize only the conversation history you are given. The newest turns may be kept verbatim outside your summary, so focus on the older context that still matters for continuing the work.
|
||||
|
||||
If the prompt includes a <previous-summary> block, treat it as the current anchored summary. Update it with the new history by preserving still-true details, removing stale details, and merging in new facts.
|
||||
|
||||
Always follow the exact output structure requested by the user prompt. Keep every section, preserve exact file paths and identifiers when known, and prefer terse bullets over paragraphs.
|
||||
|
||||
Do not answer the conversation itself. Do not mention that you are summarizing, compacting, or merging context. Respond in the same language as the conversation.`
|
||||
|
||||
const PROMPT_TITLE = `You are a title generator. You output ONLY a thread title. Nothing else.
|
||||
|
||||
<task>
|
||||
Generate a brief title that would help the user find this conversation later.
|
||||
|
||||
Follow all rules in <rules>
|
||||
Use the <examples> so you know what a good title looks like.
|
||||
Your output must be:
|
||||
- A single line
|
||||
- <=50 characters
|
||||
- No explanations
|
||||
</task>
|
||||
|
||||
<rules>
|
||||
- you MUST use the same language as the user message you are summarizing
|
||||
- Title must be grammatically correct and read naturally - no word salad
|
||||
- Never include tool names in the title (e.g. "read tool", "bash tool", "edit tool")
|
||||
- Focus on the main topic or question the user needs to retrieve
|
||||
- Vary your phrasing - avoid repetitive patterns like always starting with "Analyzing"
|
||||
- When a file is mentioned, focus on WHAT the user wants to do WITH the file, not just that they shared it
|
||||
- Keep exact: technical terms, numbers, filenames, HTTP codes
|
||||
- Remove: the, this, my, a, an
|
||||
- Never assume tech stack
|
||||
- Never use tools
|
||||
- NEVER respond to questions, just generate a title for the conversation
|
||||
- The title should NEVER include "summarizing" or "generating" when generating a title
|
||||
- DO NOT SAY YOU CANNOT GENERATE A TITLE OR COMPLAIN ABOUT THE INPUT
|
||||
- Always output something meaningful, even if the input is minimal.
|
||||
- If the user message is short or conversational (e.g. "hello", "lol", "what's up", "hey"):
|
||||
-> create a title that reflects the user's tone or intent (such as Greeting, Quick check-in, Light chat, Intro message, etc.)
|
||||
</rules>
|
||||
|
||||
<examples>
|
||||
"debug 500 errors in production" -> Debugging production 500 errors
|
||||
"refactor user service" -> Refactoring user service
|
||||
"why is app.js failing" -> app.js failure investigation
|
||||
"implement rate limiting" -> Rate limiting implementation
|
||||
"how do I connect postgres to my API" -> Postgres API connection
|
||||
"best practices for React hooks" -> React hooks best practices
|
||||
"@src/auth.ts can you add refresh token support" -> Auth refresh token support
|
||||
"@utils/parser.ts this is broken" -> Parser bug fix
|
||||
"look at @config.json" -> Config review
|
||||
"@App.tsx add dark mode toggle" -> Dark mode toggle in App
|
||||
</examples>`
|
||||
|
||||
const PROMPT_SUMMARY = `Summarize what was done in this conversation. Write like a pull request description.
|
||||
|
||||
Rules:
|
||||
- 2-3 sentences max
|
||||
- Describe the changes made, not the process
|
||||
- Do not mention running tests, builds, or other validation steps
|
||||
- Do not explain what the user asked for
|
||||
- Write in first person (I added..., I fixed...)
|
||||
- Never ask questions or add new questions
|
||||
- If the conversation ends with an unanswered question to the user, preserve that exact question
|
||||
- If the conversation ends with an imperative statement or request to the user (e.g. "Now please run the command and paste the console output"), always include that exact request in the summary`
|
||||
|
||||
export const Plugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("agent"),
|
||||
effect: Effect.gen(function* () {
|
||||
const agent = yield* AgentV2.Service
|
||||
const location = yield* Location.Service
|
||||
const worktree = location.directory
|
||||
const whitelistedDirs = [TRUNCATION_GLOB, path.join(Global.Path.tmp, "*")]
|
||||
const readonlyExternalDirectory: PermissionV2.Ruleset = [
|
||||
{ permission: "external_directory", pattern: "*", action: "ask" },
|
||||
...whitelistedDirs.map(
|
||||
(pattern): PermissionV2.Rule => ({ permission: "external_directory", pattern, action: "allow" }),
|
||||
),
|
||||
]
|
||||
const defaults: PermissionV2.Ruleset = [
|
||||
{ permission: "*", pattern: "*", action: "allow" },
|
||||
...readonlyExternalDirectory,
|
||||
{ permission: "question", pattern: "*", action: "deny" },
|
||||
{ permission: "plan_enter", pattern: "*", action: "deny" },
|
||||
{ permission: "plan_exit", pattern: "*", action: "deny" },
|
||||
{ permission: "repo_clone", pattern: "*", action: "deny" },
|
||||
{ permission: "repo_overview", pattern: "*", action: "deny" },
|
||||
{ permission: "read", pattern: "*", action: "allow" },
|
||||
{ permission: "read", pattern: "*.env", action: "ask" },
|
||||
{ permission: "read", pattern: "*.env.*", action: "ask" },
|
||||
{ permission: "read", pattern: "*.env.example", action: "allow" },
|
||||
]
|
||||
|
||||
yield* agent.update((editor) => {
|
||||
editor.update(AgentV2.ID.make("build"), (item) => {
|
||||
item.description = "The default agent. Executes tools based on configured permissions."
|
||||
item.mode = "primary"
|
||||
item.permissions.push(
|
||||
...PermissionV2.merge(defaults, [
|
||||
{ permission: "question", pattern: "*", action: "allow" },
|
||||
{ permission: "plan_enter", pattern: "*", action: "allow" },
|
||||
]),
|
||||
)
|
||||
})
|
||||
|
||||
editor.update(AgentV2.ID.make("plan"), (item) => {
|
||||
item.description = "Plan mode. Disallows all edit tools."
|
||||
item.mode = "primary"
|
||||
item.permissions.push(
|
||||
...PermissionV2.merge(defaults, [
|
||||
{ permission: "question", pattern: "*", action: "allow" },
|
||||
{ permission: "plan_exit", pattern: "*", action: "allow" },
|
||||
{ permission: "external_directory", pattern: path.join(Global.Path.data, "plans", "*"), action: "allow" },
|
||||
{ permission: "edit", pattern: "*", action: "deny" },
|
||||
{ permission: "edit", pattern: path.join(".opencode", "plans", "*.md"), action: "allow" },
|
||||
{
|
||||
permission: "edit",
|
||||
pattern: path.relative(worktree, path.join(Global.Path.data, "plans", "*.md")),
|
||||
action: "allow",
|
||||
},
|
||||
]),
|
||||
)
|
||||
})
|
||||
|
||||
editor.update(AgentV2.ID.make("general"), (item) => {
|
||||
item.description =
|
||||
"General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel."
|
||||
item.mode = "subagent"
|
||||
item.permissions.push(
|
||||
...PermissionV2.merge(defaults, [{ permission: "todowrite", pattern: "*", action: "deny" }]),
|
||||
)
|
||||
})
|
||||
|
||||
editor.update(AgentV2.ID.make("explore"), (item) => {
|
||||
item.description =
|
||||
'Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.'
|
||||
item.system = PROMPT_EXPLORE
|
||||
item.mode = "subagent"
|
||||
item.permissions.push(
|
||||
...PermissionV2.merge(
|
||||
defaults,
|
||||
[
|
||||
{ permission: "*", pattern: "*", action: "deny" },
|
||||
{ permission: "grep", pattern: "*", action: "allow" },
|
||||
{ permission: "glob", pattern: "*", action: "allow" },
|
||||
{ permission: "list", pattern: "*", action: "allow" },
|
||||
{ permission: "bash", pattern: "*", action: "allow" },
|
||||
{ permission: "webfetch", pattern: "*", action: "allow" },
|
||||
{ permission: "websearch", pattern: "*", action: "allow" },
|
||||
{ permission: "read", pattern: "*", action: "allow" },
|
||||
],
|
||||
readonlyExternalDirectory,
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
editor.update(AgentV2.ID.make("compaction"), (item) => {
|
||||
item.mode = "primary"
|
||||
item.hidden = true
|
||||
item.system = PROMPT_COMPACTION
|
||||
item.permissions.push(...PermissionV2.merge(defaults, [{ permission: "*", pattern: "*", action: "deny" }]))
|
||||
})
|
||||
|
||||
editor.update(AgentV2.ID.make("title"), (item) => {
|
||||
item.mode = "primary"
|
||||
item.hidden = true
|
||||
item.system = PROMPT_TITLE
|
||||
item.permissions.push(...PermissionV2.merge(defaults, [{ permission: "*", pattern: "*", action: "deny" }]))
|
||||
})
|
||||
|
||||
editor.update(AgentV2.ID.make("summary"), (item) => {
|
||||
item.mode = "primary"
|
||||
item.hidden = true
|
||||
item.system = PROMPT_SUMMARY
|
||||
item.permissions.push(...PermissionV2.merge(defaults, [{ permission: "*", pattern: "*", action: "deny" }]))
|
||||
})
|
||||
})
|
||||
}),
|
||||
})
|
||||
@@ -4,10 +4,15 @@ import { Context, Deferred, Effect, Layer } from "effect"
|
||||
import { AccountV2 } from "../account"
|
||||
import { AgentV2 } from "../agent"
|
||||
import { Catalog } from "../catalog"
|
||||
import { Config } from "../config"
|
||||
import { ConfigAgentPlugin } from "../config/plugin/agent"
|
||||
import { EventV2 } from "../event"
|
||||
import { Location } from "../location"
|
||||
import { Npm } from "../npm"
|
||||
import { PluginV2 } from "../plugin"
|
||||
import { AccountPlugin } from "./account"
|
||||
import { AgentPlugin } from "./agent"
|
||||
import { ConfigProviderPlugin } from "../config/plugin/provider"
|
||||
import { EnvPlugin } from "./env"
|
||||
import { ModelsDevPlugin } from "./models-dev"
|
||||
import { ProviderPlugins } from "./provider"
|
||||
@@ -15,7 +20,14 @@ import { ProviderPlugins } from "./provider"
|
||||
type Plugin = {
|
||||
id: PluginV2.ID
|
||||
effect: PluginV2.Effect<
|
||||
Catalog.Service | AgentV2.Service | AccountV2.Service | Npm.Service | EventV2.Service | PluginV2.Service
|
||||
| Catalog.Service
|
||||
| AccountV2.Service
|
||||
| AgentV2.Service
|
||||
| Npm.Service
|
||||
| EventV2.Service
|
||||
| Location.Service
|
||||
| PluginV2.Service
|
||||
| Config.Service
|
||||
>
|
||||
}
|
||||
|
||||
@@ -28,10 +40,12 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const agent = yield* AgentV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
const plugin = yield* PluginV2.Service
|
||||
const accounts = yield* AccountV2.Service
|
||||
const agents = yield* AgentV2.Service
|
||||
const config = yield* Config.Service
|
||||
const location = yield* Location.Service
|
||||
const npm = yield* Npm.Service
|
||||
const events = yield* EventV2.Service
|
||||
const done = yield* Deferred.make<void>()
|
||||
@@ -41,8 +55,10 @@ export const layer = Layer.effect(
|
||||
id: input.id,
|
||||
effect: input.effect.pipe(
|
||||
Effect.provideService(Catalog.Service, catalog),
|
||||
Effect.provideService(AgentV2.Service, agent),
|
||||
Effect.provideService(AccountV2.Service, accounts),
|
||||
Effect.provideService(AgentV2.Service, agents),
|
||||
Effect.provideService(Config.Service, config),
|
||||
Effect.provideService(Location.Service, location),
|
||||
Effect.provideService(Npm.Service, npm),
|
||||
Effect.provideService(EventV2.Service, events),
|
||||
Effect.provideService(PluginV2.Service, plugin),
|
||||
@@ -53,10 +69,13 @@ export const layer = Layer.effect(
|
||||
const boot = Effect.gen(function* () {
|
||||
yield* add(EnvPlugin)
|
||||
yield* add(AccountPlugin)
|
||||
yield* add(AgentPlugin.Plugin)
|
||||
for (const item of ProviderPlugins) {
|
||||
yield* add(item)
|
||||
}
|
||||
yield* add(ModelsDevPlugin)
|
||||
yield* add(ConfigProviderPlugin.Plugin)
|
||||
yield* add(ConfigAgentPlugin.Plugin)
|
||||
}).pipe(Effect.withSpan("PluginBoot.boot"))
|
||||
|
||||
yield* boot.pipe(
|
||||
@@ -72,10 +91,11 @@ export const layer = Layer.effect(
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(
|
||||
Layer.provide(AgentV2.defaultLayer),
|
||||
Layer.provide(Catalog.defaultLayer),
|
||||
Layer.provide(EventV2.defaultLayer),
|
||||
Layer.provide(PluginV2.defaultLayer),
|
||||
Layer.provide(AccountV2.defaultLayer),
|
||||
Layer.provide(AgentV2.defaultLayer),
|
||||
Layer.provide(Config.defaultLayer),
|
||||
Layer.provide(Npm.defaultLayer),
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ export const EnvPlugin = PluginV2.define({
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"catalog.transform": Effect.fn(function* (evt) {
|
||||
for (const item of evt.data) {
|
||||
for (const item of evt.provider.list()) {
|
||||
const key = item.provider.env.find((env) => process.env[env])
|
||||
if (!key) continue
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
|
||||
@@ -57,10 +57,10 @@ export const ModelsDevPlugin = PluginV2.define({
|
||||
const modelsDev = yield* ModelsDev.Service
|
||||
const events = yield* EventV2.Service
|
||||
const scope = yield* Scope.Scope
|
||||
const load = yield* catalog.loader()
|
||||
const transform = yield* catalog.transform()
|
||||
const refresh = Effect.fn("ModelsDevPlugin.refresh")(function* () {
|
||||
const data = yield* modelsDev.get()
|
||||
yield* load((catalog) => {
|
||||
yield* transform((catalog) => {
|
||||
for (const item of Object.values(data)) {
|
||||
const providerID = ProviderV2.ID.make(item.id)
|
||||
catalog.provider.update(providerID, (provider) => {
|
||||
|
||||
@@ -51,7 +51,7 @@ export const AmazonBedrockPlugin = PluginV2.define({
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"catalog.transform": Effect.fn(function* (evt) {
|
||||
for (const item of evt.data) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (item.provider.endpoint.type !== "aisdk") continue
|
||||
if (item.provider.endpoint.package !== "@ai-sdk/amazon-bedrock") continue
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
|
||||
@@ -6,7 +6,7 @@ export const AnthropicPlugin = PluginV2.define({
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"catalog.transform": Effect.fn(function* (evt) {
|
||||
for (const item of evt.data) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (item.provider.endpoint.type !== "aisdk") continue
|
||||
if (item.provider.endpoint.package !== "@ai-sdk/anthropic") continue
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
|
||||
@@ -15,7 +15,7 @@ export const AzurePlugin = PluginV2.define({
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"catalog.transform": Effect.fn(function* (evt) {
|
||||
for (const item of evt.data) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (item.provider.endpoint.type !== "aisdk") continue
|
||||
if (item.provider.endpoint.package !== "@ai-sdk/azure") continue
|
||||
const configured = item.provider.options.aisdk.provider.resourceName
|
||||
@@ -58,7 +58,7 @@ export const AzureCognitiveServicesPlugin = PluginV2.define({
|
||||
"catalog.transform": Effect.fn(function* (evt) {
|
||||
const resourceName = process.env.AZURE_COGNITIVE_SERVICES_RESOURCE_NAME
|
||||
if (!resourceName) return
|
||||
for (const item of evt.data) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (item.provider.endpoint.type !== "aisdk") continue
|
||||
if (item.provider.endpoint.package !== "@ai-sdk/openai-compatible") continue
|
||||
if (!item.provider.id.includes("azure-cognitive-services")) continue
|
||||
|
||||
@@ -6,7 +6,7 @@ export const CerebrasPlugin = PluginV2.define({
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"catalog.transform": Effect.fn(function* (ctx) {
|
||||
for (const item of ctx.data) {
|
||||
for (const item of ctx.provider.list()) {
|
||||
if (item.provider.endpoint.type !== "aisdk") continue
|
||||
if (item.provider.endpoint.package !== "@ai-sdk/cerebras") continue
|
||||
ctx.provider.update(item.provider.id, (provider) => {
|
||||
|
||||
@@ -11,7 +11,7 @@ export const CloudflareWorkersAIPlugin = PluginV2.define({
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"catalog.transform": Effect.fn(function* (evt) {
|
||||
const item = evt.data.find((record) => record.provider.id === providerID)
|
||||
const item = evt.provider.get(providerID)
|
||||
if (!item) return
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
if (provider.endpoint.type !== "aisdk") return
|
||||
|
||||
@@ -31,7 +31,7 @@ export const GithubCopilotPlugin = PluginV2.define({
|
||||
: evt.sdk.chat(evt.model.apiID)
|
||||
}),
|
||||
"catalog.transform": Effect.fn(function* (evt) {
|
||||
const item = evt.data.find((record) => record.provider.id === ProviderV2.ID.githubCopilot)
|
||||
const item = evt.provider.get(ProviderV2.ID.githubCopilot)
|
||||
if (!item || !item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) return
|
||||
evt.model.update(item.provider.id, ModelV2.ID.make("gpt-5-chat-latest"), (model) => {
|
||||
// This chat-only alias conflicts with the Copilot GPT-5 Responses route,
|
||||
|
||||
@@ -43,9 +43,9 @@ function authFetch(fetchWithRuntimeOptions?: unknown) {
|
||||
// do not, so inject a Google access token into their fetch path.
|
||||
return async (input: Parameters<typeof fetch>[0], init?: RequestInit) => {
|
||||
const { GoogleAuth } = await import("google-auth-library")
|
||||
const auth = new GoogleAuth()
|
||||
const client = await auth.getApplicationDefault()
|
||||
const token = await client.credential.getAccessToken()
|
||||
const auth = new GoogleAuth({ scopes: ["https://www.googleapis.com/auth/cloud-platform"] })
|
||||
const client = await auth.getClient()
|
||||
const token = await client.getAccessToken()
|
||||
const headers = new Headers(init?.headers)
|
||||
headers.set("Authorization", `Bearer ${token.token}`)
|
||||
return typeof fetchWithRuntimeOptions === "function"
|
||||
@@ -59,7 +59,7 @@ export const GoogleVertexPlugin = PluginV2.define({
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"catalog.transform": Effect.fn(function* (evt) {
|
||||
for (const item of evt.data) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (item.provider.endpoint.type !== "aisdk") continue
|
||||
if (
|
||||
item.provider.endpoint.package !== "@ai-sdk/google-vertex" &&
|
||||
@@ -110,7 +110,7 @@ export const GoogleVertexAnthropicPlugin = PluginV2.define({
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"catalog.transform": Effect.fn(function* (evt) {
|
||||
for (const item of evt.data) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (item.provider.endpoint.type !== "aisdk") continue
|
||||
if (item.provider.endpoint.package !== "@ai-sdk/google-vertex/anthropic") continue
|
||||
const project =
|
||||
|
||||
@@ -6,7 +6,7 @@ export const KiloPlugin = PluginV2.define({
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"catalog.transform": Effect.fn(function* (evt) {
|
||||
for (const item of evt.data) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (item.provider.endpoint.type !== "aisdk") continue
|
||||
if (item.provider.endpoint.package !== "@ai-sdk/openai-compatible") continue
|
||||
if (item.provider.endpoint.url !== "https://api.kilo.ai/api/gateway") continue
|
||||
|
||||
@@ -6,7 +6,7 @@ export const LLMGatewayPlugin = PluginV2.define({
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"catalog.transform": Effect.fn(function* (evt) {
|
||||
for (const item of evt.data) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (item.provider.enabled === false) continue
|
||||
if (item.provider.endpoint.type !== "aisdk") continue
|
||||
if (item.provider.endpoint.package !== "@ai-sdk/openai-compatible") continue
|
||||
|
||||
@@ -6,7 +6,7 @@ export const NvidiaPlugin = PluginV2.define({
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"catalog.transform": Effect.fn(function* (evt) {
|
||||
for (const item of evt.data) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (item.provider.endpoint.type !== "aisdk") continue
|
||||
if (item.provider.endpoint.package !== "@ai-sdk/openai-compatible") continue
|
||||
if (item.provider.endpoint.url !== "https://integrate.api.nvidia.com/v1") continue
|
||||
|
||||
@@ -17,7 +17,7 @@ export const OpenAIPlugin = PluginV2.define({
|
||||
evt.language = evt.sdk.responses(evt.model.apiID)
|
||||
}),
|
||||
"catalog.transform": Effect.fn(function* (evt) {
|
||||
for (const item of evt.data) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (item.provider.endpoint.type !== "aisdk") continue
|
||||
if (item.provider.endpoint.package !== "@ai-sdk/openai") continue
|
||||
if (!item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) continue
|
||||
|
||||
@@ -8,7 +8,7 @@ export const OpencodePlugin = PluginV2.define({
|
||||
let hasKey = false
|
||||
return {
|
||||
"catalog.transform": Effect.fn(function* (evt) {
|
||||
const item = evt.data.find((record) => record.provider.id === ProviderV2.ID.opencode)
|
||||
const item = evt.provider.get(ProviderV2.ID.opencode)
|
||||
if (!item) return
|
||||
hasKey = Boolean(
|
||||
process.env.OPENCODE_API_KEY ||
|
||||
|
||||
@@ -7,7 +7,7 @@ export const OpenRouterPlugin = PluginV2.define({
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"catalog.transform": Effect.fn(function* (evt) {
|
||||
for (const item of evt.data) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (item.provider.endpoint.type !== "aisdk") continue
|
||||
if (item.provider.endpoint.package !== "@openrouter/ai-sdk-provider") continue
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
|
||||
@@ -6,7 +6,7 @@ export const VercelPlugin = PluginV2.define({
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"catalog.transform": Effect.fn(function* (evt) {
|
||||
for (const item of evt.data) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (item.provider.endpoint.type !== "aisdk") continue
|
||||
if (item.provider.endpoint.package !== "@ai-sdk/vercel") continue
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
|
||||
@@ -6,7 +6,7 @@ export const ZenmuxPlugin = PluginV2.define({
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"catalog.transform": Effect.fn(function* (evt) {
|
||||
for (const item of evt.data) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (item.provider.endpoint.type !== "aisdk") continue
|
||||
if (item.provider.endpoint.package !== "@ai-sdk/openai-compatible") continue
|
||||
if (item.provider.endpoint.url !== "https://zenmux.ai/api/v1") continue
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
export * as Policy from "./policy"
|
||||
|
||||
import { Context, Effect as EffectRuntime, Layer, Schema } from "effect"
|
||||
import { Wildcard } from "./util/wildcard"
|
||||
import { Location } from "./location"
|
||||
|
||||
export const Effect = Schema.Literals(["allow", "deny"]).annotate({ identifier: "Policy.Effect" })
|
||||
export type Effect = typeof Effect.Type
|
||||
|
||||
export class Info extends Schema.Class<Info>("Policy.Info")({
|
||||
action: Schema.String,
|
||||
effect: Effect,
|
||||
resource: Schema.String,
|
||||
}) {}
|
||||
|
||||
export interface Interface {
|
||||
readonly load: (statements: Info[]) => EffectRuntime.Effect<void>
|
||||
readonly evaluate: (action: string, resource: string, fallback: Effect) => EffectRuntime.Effect<Effect>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Policy") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
EffectRuntime.gen(function* () {
|
||||
let statements: Info[] = []
|
||||
yield* Location.Service
|
||||
|
||||
return Service.of({
|
||||
load: EffectRuntime.fn("Policy.load")(function* (input) {
|
||||
statements = input
|
||||
}),
|
||||
evaluate: EffectRuntime.fn("Policy.evaluate")(function* (action, resource, fallback) {
|
||||
return (
|
||||
statements.findLast(
|
||||
(statement) => Wildcard.match(action, statement.action) && Wildcard.match(resource, statement.resource),
|
||||
)?.effect ?? fallback
|
||||
)
|
||||
}),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer
|
||||
@@ -0,0 +1,128 @@
|
||||
export * as Project from "./project"
|
||||
|
||||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
import path from "path"
|
||||
import { AbsolutePath, withStatics } from "./schema"
|
||||
import { AppFileSystem } from "./filesystem"
|
||||
import { Git } from "./git"
|
||||
import { Hash } from "./util/hash"
|
||||
|
||||
export const ID = Schema.String.pipe(
|
||||
Schema.brand("Project.ID"),
|
||||
withStatics((schema) => ({
|
||||
global: schema.make("global"),
|
||||
})),
|
||||
)
|
||||
export type ID = typeof ID.Type
|
||||
|
||||
export const Vcs = Schema.Union([
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("git"),
|
||||
store: AbsolutePath,
|
||||
}),
|
||||
])
|
||||
export type Vcs = typeof Vcs.Type
|
||||
|
||||
export class Info extends Schema.Class<Info>("Project.Info")({
|
||||
id: ID,
|
||||
}) {}
|
||||
|
||||
export interface Interface {
|
||||
readonly resolve: (input: AbsolutePath) => Effect.Effect<
|
||||
{
|
||||
previous?: ID
|
||||
id: ID
|
||||
directory: AbsolutePath
|
||||
vcs?: Vcs
|
||||
},
|
||||
never
|
||||
>
|
||||
/**
|
||||
* Temporary bridge method for writing the resolved project ID to the repo-local cache.
|
||||
*
|
||||
* This exists while the old opencode project service and this core project
|
||||
* service work together: core resolves the ID, while the old service still owns
|
||||
* database migration and persistence. The old service should call this after it
|
||||
* finishes migrating from `resolve().previous` to `resolve().id`; once project
|
||||
* persistence moves into core, this separate bridge method can go away.
|
||||
*/
|
||||
readonly commit: (input: { store: AbsolutePath; id: ID }) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/ProjectV2") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const git = yield* Git.Service
|
||||
|
||||
const cached = Effect.fnUntraced(function* (dir: string) {
|
||||
return yield* fs.readFileString(path.join(dir, "opencode")).pipe(
|
||||
Effect.map((value) => value.trim()),
|
||||
Effect.map((value) => (value ? ID.make(value) : undefined)),
|
||||
Effect.catch(() => Effect.succeed(undefined)),
|
||||
)
|
||||
})
|
||||
|
||||
const remote = Effect.fnUntraced(function* (repo: Git.Repo) {
|
||||
const origin = yield* git.remote(repo)
|
||||
if (!origin) return undefined
|
||||
const normalized = url(origin)
|
||||
if (!normalized) return undefined
|
||||
return ID.make(Hash.fast(`git-remote:${normalized}`))
|
||||
})
|
||||
|
||||
function url(input: string) {
|
||||
const value = input.trim()
|
||||
if (!value) return undefined
|
||||
|
||||
try {
|
||||
const parsed = new URL(value)
|
||||
if (parsed.protocol === "file:") return undefined
|
||||
return parts(parsed.hostname, parsed.pathname)
|
||||
} catch {
|
||||
const scp = value.match(/^([^@/:]+@)?([^/:]+):(.+)$/)
|
||||
if (scp) return parts(scp[2], scp[3])
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
function parts(host: string, name: string) {
|
||||
const pathname = name
|
||||
.replace(/^\/+/, "")
|
||||
.replace(/\.git\/?$/, "")
|
||||
.replace(/\/+$/, "")
|
||||
if (!host || !pathname) return undefined
|
||||
return `${host.toLowerCase()}/${pathname}`
|
||||
}
|
||||
|
||||
const root = Effect.fnUntraced(function* (repo: Git.Repo) {
|
||||
const root = (yield* git.roots(repo))[0]
|
||||
return root ? ID.make(root) : undefined
|
||||
})
|
||||
|
||||
const resolve = Effect.fn("Project.resolve")(function* (input: AbsolutePath) {
|
||||
const repo = yield* git.find(input)
|
||||
if (!repo) return { id: ID.global, directory: AbsolutePath.make(path.parse(input).root), vcs: undefined }
|
||||
|
||||
const previous = yield* cached(repo.store)
|
||||
const id = (yield* remote(repo)) ?? previous ?? (yield* root(repo))
|
||||
|
||||
return {
|
||||
previous,
|
||||
id: id ?? ID.global,
|
||||
directory: repo.directory,
|
||||
vcs: { type: "git" as const, store: repo.store },
|
||||
}
|
||||
})
|
||||
|
||||
const commit = Effect.fn("Project.commit")(function* (input: { store: AbsolutePath; id: ID }) {
|
||||
yield* fs.writeFileString(path.join(input.store, "opencode"), input.id).pipe(Effect.ignore)
|
||||
})
|
||||
|
||||
return Service.of({ resolve, commit })
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer), Layer.provide(Git.defaultLayer))
|
||||
@@ -1,5 +1,11 @@
|
||||
import { Option, Schema, SchemaGetter } from "effect"
|
||||
|
||||
export const AbsolutePath = Schema.String.pipe(Schema.brand("AbsolutePath"))
|
||||
export type AbsolutePath = typeof AbsolutePath.Type
|
||||
|
||||
export const RelativePath = Schema.String.pipe(Schema.brand("RelativePath"))
|
||||
export type RelativePath = typeof RelativePath.Type
|
||||
|
||||
/**
|
||||
* Integer greater than zero.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
export * as State from "./state"
|
||||
|
||||
import { Effect, Scope, Semaphore } from "effect"
|
||||
import { createDraft, finishDraft, type Draft, type Objectish } from "immer"
|
||||
|
||||
export type Transform<Editor> = (editor: Editor) => void
|
||||
export type MakeEditor<State extends Objectish, Editor> = (draft: Draft<State>) => Editor
|
||||
|
||||
export interface Options<State extends Objectish, Editor> {
|
||||
readonly initial: () => State
|
||||
readonly editor: MakeEditor<State, Editor>
|
||||
/** Completes every committed edit; reason identifies exceptional update origins. */
|
||||
readonly finalize?: (editor: Editor, reason?: string) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export interface Interface<State extends Objectish, Editor> {
|
||||
readonly get: () => State
|
||||
readonly transform: () => Effect.Effect<(transform: Transform<Editor>) => Effect.Effect<void>, never, Scope.Scope>
|
||||
readonly update: (update: (editor: Editor) => Effect.Effect<void>, reason?: string) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export function create<State extends Objectish, Editor>(options: Options<State, Editor>): Interface<State, Editor> {
|
||||
let state = options.initial()
|
||||
let transforms: { update: Transform<Editor> }[] = []
|
||||
const semaphore = Semaphore.makeUnsafe(1)
|
||||
|
||||
const commit = Effect.fn("State.commit")(function* (draft: Draft<State>, reason?: string) {
|
||||
const api = options.editor(draft)
|
||||
if (options.finalize) yield* options.finalize(api, reason)
|
||||
state = finishDraft(draft) as State
|
||||
})
|
||||
|
||||
const rebuild = Effect.fn("State.rebuild")(function* () {
|
||||
const draft = createDraft(options.initial())
|
||||
const api = options.editor(draft)
|
||||
for (const transform of transforms) transform.update(api)
|
||||
yield* commit(draft)
|
||||
}, semaphore.withPermit)
|
||||
|
||||
return {
|
||||
get: () => state,
|
||||
transform: Effect.fn("State.transform")(function* () {
|
||||
const transform = { update: (_editor: Editor) => {} }
|
||||
transforms = [...transforms, transform]
|
||||
const scope = yield* Scope.Scope
|
||||
yield* Scope.addFinalizer(
|
||||
scope,
|
||||
Effect.sync(() => {
|
||||
transforms = transforms.filter((item) => item !== transform)
|
||||
}).pipe(Effect.andThen(rebuild())),
|
||||
)
|
||||
return Effect.fnUntraced(function* (update: Transform<Editor>) {
|
||||
transform.update = update
|
||||
yield* rebuild()
|
||||
})
|
||||
}),
|
||||
update: Effect.fn("State.update")(function* (update, reason) {
|
||||
const draft = createDraft(state)
|
||||
yield* update(options.editor(draft))
|
||||
yield* commit(draft, reason)
|
||||
}, semaphore.withPermit),
|
||||
}
|
||||
}
|
||||
@@ -19,12 +19,11 @@ const it = testEffect(PluginV2.defaultLayer)
|
||||
function context(
|
||||
records: { provider: ProviderV2.Info; models: Map<ModelV2.ID, ModelV2.Info> }[],
|
||||
updates: Array<{ id: ProviderV2.ID; enabled: ProviderV2.Info["enabled"]; apiKey?: string }>,
|
||||
): Catalog.Context {
|
||||
): Catalog.Editor {
|
||||
return {
|
||||
data: records,
|
||||
updateProvider: (providerID, fn) => context(records, updates).provider.update(providerID, fn),
|
||||
updateModel: (providerID, modelID, fn) => context(records, updates).model.update(providerID, modelID, fn),
|
||||
provider: {
|
||||
list: () => records,
|
||||
get: (providerID) => records.find((item) => item.provider.id === providerID),
|
||||
update: (providerID, fn) => {
|
||||
const record = records.find((item) => item.provider.id === providerID)
|
||||
const provider = produce(record?.provider ?? ProviderV2.Info.empty(providerID), fn)
|
||||
@@ -45,8 +44,13 @@ function context(
|
||||
},
|
||||
},
|
||||
model: {
|
||||
get: () => undefined,
|
||||
update: () => {},
|
||||
remove: () => {},
|
||||
default: {
|
||||
get: () => undefined,
|
||||
set: () => {},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -192,7 +196,7 @@ describe("AccountV2", () => {
|
||||
]
|
||||
const updates: Array<{ id: ProviderV2.ID; enabled: ProviderV2.Info["enabled"]; apiKey?: string }> = []
|
||||
const catalog = Catalog.Service.of({
|
||||
loader: () => Effect.die("unexpected catalog.loader"),
|
||||
transform: () => Effect.die("unexpected catalog.transform"),
|
||||
provider: {
|
||||
get: () => Effect.die("unexpected provider.get"),
|
||||
all: () => Effect.succeed([]),
|
||||
@@ -203,7 +207,6 @@ describe("AccountV2", () => {
|
||||
all: () => Effect.succeed([]),
|
||||
available: () => Effect.succeed([]),
|
||||
default: () => Effect.succeed(Option.none<ModelV2.Info>()),
|
||||
setDefault: () => Effect.die("unexpected model.setDefault"),
|
||||
small: () => Effect.succeed(Option.none<ModelV2.Info>()),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Exit, Scope } from "effect"
|
||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||
import { testEffect } from "./lib/effect"
|
||||
|
||||
const it = testEffect(AgentV2.defaultLayer)
|
||||
|
||||
describe("AgentV2", () => {
|
||||
it.effect("starts without agents", () =>
|
||||
Effect.gen(function* () {
|
||||
const agent = yield* AgentV2.Service
|
||||
|
||||
expect(yield* agent.all()).toEqual([])
|
||||
expect(yield* agent.get(AgentV2.ID.make("build"))).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("materializes replayable agent transforms", () =>
|
||||
Effect.gen(function* () {
|
||||
const agent = yield* AgentV2.Service
|
||||
const id = AgentV2.ID.make("reviewer")
|
||||
const transform = yield* agent.transform()
|
||||
|
||||
yield* transform((editor) =>
|
||||
editor.update(id, (info) => {
|
||||
info.description = "Reviews code"
|
||||
info.mode = "subagent"
|
||||
}),
|
||||
)
|
||||
|
||||
expect(yield* agent.get(id)).toMatchObject({ id, description: "Reviews code", mode: "subagent" })
|
||||
expect((yield* agent.all()).map((info) => info.id)).toEqual([id])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rebuilds state when a transform is replaced", () =>
|
||||
Effect.gen(function* () {
|
||||
const agent = yield* AgentV2.Service
|
||||
const id = AgentV2.ID.make("reviewer")
|
||||
const transform = yield* agent.transform()
|
||||
|
||||
yield* transform((editor) =>
|
||||
editor.update(id, (info) => {
|
||||
info.description = "Old description"
|
||||
info.hidden = true
|
||||
}),
|
||||
)
|
||||
yield* transform((editor) =>
|
||||
editor.update(id, (info) => {
|
||||
info.description = "New description"
|
||||
}),
|
||||
)
|
||||
|
||||
expect(yield* agent.get(id)).toMatchObject({ description: "New description", hidden: false })
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("removes a transform contribution when its scope closes", () =>
|
||||
Effect.gen(function* () {
|
||||
const agent = yield* AgentV2.Service
|
||||
const id = AgentV2.ID.make("scoped")
|
||||
const scope = yield* Scope.make()
|
||||
const transform = yield* agent.transform().pipe(Scope.provide(scope))
|
||||
|
||||
yield* transform((editor) => editor.update(id, () => {}))
|
||||
expect(yield* agent.get(id)).toBeDefined()
|
||||
|
||||
yield* Scope.close(scope, Exit.void)
|
||||
expect(yield* agent.get(id)).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("applies direct agent updates", () =>
|
||||
Effect.gen(function* () {
|
||||
const agent = yield* AgentV2.Service
|
||||
const id = AgentV2.ID.make("build")
|
||||
|
||||
yield* agent.update((editor) =>
|
||||
editor.update(id, (info) => {
|
||||
info.mode = "primary"
|
||||
info.hidden = true
|
||||
}),
|
||||
)
|
||||
|
||||
expect(yield* agent.get(id)).toMatchObject({ id, mode: "primary", hidden: true })
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("creates agents with runtime defaults and supports direct removal", () =>
|
||||
Effect.gen(function* () {
|
||||
const agent = yield* AgentV2.Service
|
||||
const id = AgentV2.ID.make("custom")
|
||||
|
||||
yield* agent.update((editor) => editor.update(id, () => {}))
|
||||
expect(yield* agent.get(id)).toEqual(AgentV2.Info.empty(id))
|
||||
|
||||
yield* agent.update((editor) => editor.remove(id))
|
||||
expect(yield* agent.get(id)).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -5,14 +5,21 @@ import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { Policy } from "@opencode-ai/core/policy"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { location } from "./fixture/location"
|
||||
import { testEffect } from "./lib/effect"
|
||||
|
||||
const locationLayer = Layer.succeed(Location.Service, Location.Service.of({ directory: "test" }))
|
||||
const locationLayer = Layer.succeed(
|
||||
Location.Service,
|
||||
Location.Service.of(location({ directory: AbsolutePath.make("test") })),
|
||||
)
|
||||
const it = testEffect(
|
||||
Catalog.layer.pipe(
|
||||
Layer.provideMerge(EventV2.defaultLayer),
|
||||
Layer.provideMerge(PluginV2.defaultLayer),
|
||||
Layer.provideMerge(Policy.defaultLayer),
|
||||
Layer.provideMerge(locationLayer),
|
||||
),
|
||||
)
|
||||
@@ -22,9 +29,9 @@ describe("CatalogV2", () => {
|
||||
Effect.gen(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
const providerID = ProviderV2.ID.make("test")
|
||||
const load = yield* catalog.loader()
|
||||
const transform = yield* catalog.transform()
|
||||
|
||||
yield* load((catalog) =>
|
||||
yield* transform((catalog) =>
|
||||
catalog.provider.update(providerID, (provider) => {
|
||||
provider.endpoint = {
|
||||
type: "aisdk",
|
||||
@@ -48,9 +55,9 @@ describe("CatalogV2", () => {
|
||||
const catalog = yield* Catalog.Service
|
||||
const providerID = ProviderV2.ID.make("test")
|
||||
const modelID = ModelV2.ID.make("model")
|
||||
const load = yield* catalog.loader()
|
||||
const transform = yield* catalog.transform()
|
||||
|
||||
yield* load((catalog) => {
|
||||
yield* transform((catalog) => {
|
||||
catalog.provider.update(providerID, (provider) => {
|
||||
provider.endpoint = {
|
||||
type: "aisdk",
|
||||
@@ -77,9 +84,9 @@ describe("CatalogV2", () => {
|
||||
const catalog = yield* Catalog.Service
|
||||
const providerID = ProviderV2.ID.make("test")
|
||||
const modelID = ModelV2.ID.make("model")
|
||||
const load = yield* catalog.loader()
|
||||
const transform = yield* catalog.transform()
|
||||
|
||||
yield* load((catalog) => {
|
||||
yield* transform((catalog) => {
|
||||
catalog.provider.update(providerID, (provider) => {
|
||||
provider.endpoint = {
|
||||
type: "aisdk",
|
||||
@@ -104,14 +111,14 @@ describe("CatalogV2", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const providerID = ProviderV2.ID.make("test")
|
||||
const seen: unknown[] = []
|
||||
const load = yield* catalog.loader()
|
||||
const transform = yield* catalog.transform()
|
||||
|
||||
yield* plugin.add({
|
||||
id: PluginV2.ID.make("test"),
|
||||
effect: Effect.succeed({
|
||||
"catalog.transform": (evt) =>
|
||||
Effect.sync(() => {
|
||||
const item = evt.data.find((record) => record.provider.id === providerID)
|
||||
const item = evt.provider.get(providerID)
|
||||
if (!item) return
|
||||
seen.push(item.provider.endpoint.type)
|
||||
if (item?.provider.endpoint.type === "aisdk") seen.push(item.provider.endpoint.url)
|
||||
@@ -119,7 +126,7 @@ describe("CatalogV2", () => {
|
||||
}),
|
||||
}),
|
||||
})
|
||||
yield* load((catalog) =>
|
||||
yield* transform((catalog) =>
|
||||
catalog.provider.update(providerID, (provider) => {
|
||||
provider.endpoint = { type: "aisdk", package: "@ai-sdk/openai-compatible" }
|
||||
provider.options.aisdk.provider.baseURL = "https://provider.example.com"
|
||||
@@ -135,9 +142,9 @@ describe("CatalogV2", () => {
|
||||
const catalog = yield* Catalog.Service
|
||||
const plugin = yield* PluginV2.Service
|
||||
const providerID = ProviderV2.ID.make("test")
|
||||
const load = yield* catalog.loader()
|
||||
const transform = yield* catalog.transform()
|
||||
|
||||
yield* load((catalog) =>
|
||||
yield* transform((catalog) =>
|
||||
catalog.provider.update(providerID, (provider) => {
|
||||
provider.name = "Before"
|
||||
}),
|
||||
@@ -164,9 +171,9 @@ describe("CatalogV2", () => {
|
||||
const catalog = yield* Catalog.Service
|
||||
const providerID = ProviderV2.ID.make("test")
|
||||
const modelID = ModelV2.ID.make("model")
|
||||
const load = yield* catalog.loader()
|
||||
const transform = yield* catalog.transform()
|
||||
|
||||
yield* load((catalog) => {
|
||||
yield* transform((catalog) => {
|
||||
catalog.provider.update(providerID, (provider) => {
|
||||
provider.options.headers.provider = "provider"
|
||||
provider.options.headers.shared = "provider"
|
||||
@@ -194,9 +201,9 @@ describe("CatalogV2", () => {
|
||||
Effect.gen(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
const providerID = ProviderV2.ID.make("test")
|
||||
const load = yield* catalog.loader()
|
||||
const transform = yield* catalog.transform()
|
||||
|
||||
yield* load((catalog) => {
|
||||
yield* transform((catalog) => {
|
||||
catalog.provider.update(providerID, (provider) => {
|
||||
provider.enabled = { via: "custom", data: {} }
|
||||
})
|
||||
@@ -212,13 +219,44 @@ describe("CatalogV2", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("uses a transform-provided default model until that transform is replaced", () =>
|
||||
Effect.gen(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
const providerID = ProviderV2.ID.make("test")
|
||||
const old = ModelV2.ID.make("old")
|
||||
const newest = ModelV2.ID.make("new")
|
||||
const transform = yield* catalog.transform()
|
||||
|
||||
const models = (catalog: Catalog.Editor) => {
|
||||
catalog.provider.update(providerID, (provider) => {
|
||||
provider.enabled = { via: "custom", data: {} }
|
||||
})
|
||||
catalog.model.update(providerID, old, (model) => {
|
||||
model.time.released = DateTime.makeUnsafe(1000)
|
||||
})
|
||||
catalog.model.update(providerID, newest, (model) => {
|
||||
model.time.released = DateTime.makeUnsafe(2000)
|
||||
})
|
||||
}
|
||||
|
||||
yield* transform((catalog) => {
|
||||
models(catalog)
|
||||
catalog.model.default.set(providerID, old)
|
||||
})
|
||||
expect(Option.getOrUndefined(yield* catalog.model.default())?.id).toBe(old)
|
||||
|
||||
yield* transform(models)
|
||||
expect(Option.getOrUndefined(yield* catalog.model.default())?.id).toBe(newest)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("small model prefers small keyword candidates before cost scoring", () =>
|
||||
Effect.gen(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
const providerID = ProviderV2.ID.make("test")
|
||||
const load = yield* catalog.loader()
|
||||
const transform = yield* catalog.transform()
|
||||
|
||||
yield* load((catalog) => {
|
||||
yield* transform((catalog) => {
|
||||
catalog.provider.update(providerID, () => {})
|
||||
catalog.model.update(providerID, ModelV2.ID.make("cheap-large"), (model) => {
|
||||
model.capabilities.input = ["text"]
|
||||
@@ -237,4 +275,23 @@ describe("CatalogV2", () => {
|
||||
expect(Option.getOrUndefined(yield* catalog.model.small(providerID))?.id).toMatch("expensive-mini")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("removes providers denied by policy after loading", () =>
|
||||
Effect.gen(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
const policy = yield* Policy.Service
|
||||
const providerID = ProviderV2.ID.make("blocked")
|
||||
const transform = yield* catalog.transform()
|
||||
|
||||
yield* policy.load([new Policy.Info({ effect: "deny", action: "provider.use", resource: "blocked" })])
|
||||
yield* transform((catalog) => {
|
||||
catalog.provider.update(providerID, () => {})
|
||||
catalog.model.update(providerID, ModelV2.ID.make("model"), () => {})
|
||||
})
|
||||
|
||||
expect(yield* catalog.provider.all()).toEqual([])
|
||||
expect(yield* catalog.model.all()).toEqual([])
|
||||
expect(yield* catalog.provider.get(providerID).pipe(Effect.option)).toEqual(Option.none())
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||
import { Config } from "@opencode-ai/core/config"
|
||||
import { ConfigAgentPlugin } from "@opencode-ai/core/config/plugin/agent"
|
||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const it = testEffect(AgentV2.defaultLayer)
|
||||
const decode = Schema.decodeUnknownSync(Config.Info)
|
||||
|
||||
describe("ConfigAgentPlugin.Plugin", () => {
|
||||
it.effect("applies global permissions between built-in and agent-specific permissions", () =>
|
||||
Effect.gen(function* () {
|
||||
const agents = yield* AgentV2.Service
|
||||
const build = AgentV2.ID.make("build")
|
||||
const defaults = yield* agents.transform()
|
||||
|
||||
yield* defaults((editor) =>
|
||||
editor.update(build, (agent) => {
|
||||
agent.mode = "primary"
|
||||
agent.permissions.push({ permission: "bash", pattern: "*", action: "allow" })
|
||||
}),
|
||||
)
|
||||
|
||||
const config = Config.Service.of({
|
||||
directories: () => Effect.succeed([]),
|
||||
get: () =>
|
||||
Effect.succeed([
|
||||
new Config.Loaded({
|
||||
source: { type: "memory" },
|
||||
info: decode({
|
||||
permissions: [{ permission: "bash", pattern: "*", action: "ask" }],
|
||||
agents: {
|
||||
build: {
|
||||
permissions: [{ permission: "bash", pattern: "git *", action: "allow" }],
|
||||
},
|
||||
reviewer: {
|
||||
model: "openrouter/openai/gpt-5",
|
||||
description: "Review changes",
|
||||
mode: "subagent",
|
||||
permissions: [{ permission: "edit", pattern: "*", action: "deny" }],
|
||||
},
|
||||
removed: { description: "Removed later" },
|
||||
},
|
||||
}),
|
||||
}),
|
||||
new Config.Loaded({
|
||||
source: { type: "memory" },
|
||||
info: decode({
|
||||
agents: {
|
||||
reviewer: { variant: "high", hidden: true },
|
||||
removed: { disabled: true },
|
||||
},
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
|
||||
yield* ConfigAgentPlugin.Plugin.effect.pipe(
|
||||
Effect.provideService(Config.Service, config),
|
||||
Effect.provideService(AgentV2.Service, agents),
|
||||
)
|
||||
|
||||
const buildAgent = yield* agents.get(build)
|
||||
if (!buildAgent) throw new Error("expected configured build agent")
|
||||
expect(buildAgent.permissions).toEqual([
|
||||
{ permission: "bash", pattern: "*", action: "allow" },
|
||||
{ permission: "bash", pattern: "*", action: "ask" },
|
||||
{ permission: "bash", pattern: "git *", action: "allow" },
|
||||
])
|
||||
expect(PermissionV2.evaluate("bash", "git status", buildAgent.permissions).action).toBe("allow")
|
||||
expect(PermissionV2.evaluate("bash", "bun test", buildAgent.permissions).action).toBe("ask")
|
||||
|
||||
const reviewer = yield* agents.get(AgentV2.ID.make("reviewer"))
|
||||
if (!reviewer) throw new Error("expected configured reviewer agent")
|
||||
expect(reviewer).toMatchObject({
|
||||
description: "Review changes",
|
||||
mode: "subagent",
|
||||
hidden: true,
|
||||
model: { providerID: "openrouter", id: "openai/gpt-5", variant: "high" },
|
||||
})
|
||||
expect(reviewer.permissions).toEqual([
|
||||
{ permission: "bash", pattern: "*", action: "ask" },
|
||||
{ permission: "edit", pattern: "*", action: "deny" },
|
||||
])
|
||||
expect(yield* agents.get(AgentV2.ID.make("removed"))).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("maps configured agent fields and preserves an unspecified model variant", () =>
|
||||
Effect.gen(function* () {
|
||||
const agents = yield* AgentV2.Service
|
||||
const config = Config.Service.of({
|
||||
directories: () => Effect.succeed([]),
|
||||
get: () =>
|
||||
Effect.succeed([
|
||||
new Config.Loaded({
|
||||
source: { type: "memory" },
|
||||
info: decode({
|
||||
agents: {
|
||||
reviewer: {
|
||||
model: "anthropic/claude-sonnet",
|
||||
system: "Review carefully.",
|
||||
description: "Reviews changes",
|
||||
mode: "subagent",
|
||||
hidden: true,
|
||||
color: "warning",
|
||||
steps: 12,
|
||||
options: {
|
||||
headers: { first: "one", shared: "first" },
|
||||
body: { enabled: true },
|
||||
aisdk: { provider: { profile: "review" }, request: { effort: "medium" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
new Config.Loaded({
|
||||
source: { type: "memory" },
|
||||
info: decode({
|
||||
agents: {
|
||||
reviewer: {
|
||||
options: {
|
||||
headers: { shared: "last", second: "two" },
|
||||
body: { retries: 2 },
|
||||
aisdk: { request: { effort: "high" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
|
||||
yield* ConfigAgentPlugin.Plugin.effect.pipe(
|
||||
Effect.provideService(Config.Service, config),
|
||||
Effect.provideService(AgentV2.Service, agents),
|
||||
)
|
||||
|
||||
const reviewer = yield* agents.get(AgentV2.ID.make("reviewer"))
|
||||
if (!reviewer) throw new Error("expected configured reviewer agent")
|
||||
expect(reviewer).toMatchObject({
|
||||
system: "Review carefully.",
|
||||
description: "Reviews changes",
|
||||
mode: "subagent",
|
||||
hidden: true,
|
||||
color: "warning",
|
||||
steps: 12,
|
||||
model: { providerID: "anthropic", id: "claude-sonnet", variant: undefined },
|
||||
})
|
||||
expect(reviewer.options).toEqual({
|
||||
headers: { first: "one", shared: "last", second: "two" },
|
||||
body: { enabled: true, retries: 2 },
|
||||
aisdk: { provider: { profile: "review" }, request: { effort: "high" } },
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("removes a built-in agent disabled by configuration", () =>
|
||||
Effect.gen(function* () {
|
||||
const agents = yield* AgentV2.Service
|
||||
const build = AgentV2.ID.make("build")
|
||||
const defaults = yield* agents.transform()
|
||||
yield* defaults((editor) => editor.update(build, () => {}))
|
||||
|
||||
const config = Config.Service.of({
|
||||
directories: () => Effect.succeed([]),
|
||||
get: () =>
|
||||
Effect.succeed([
|
||||
new Config.Loaded({
|
||||
source: { type: "memory" },
|
||||
info: decode({ agents: { build: { disabled: true } } }),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
|
||||
yield* ConfigAgentPlugin.Plugin.effect.pipe(
|
||||
Effect.provideService(Config.Service, config),
|
||||
Effect.provideService(AgentV2.Service, agents),
|
||||
)
|
||||
|
||||
expect(yield* agents.get(build)).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,460 @@
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Config } from "@opencode-ai/core/config"
|
||||
import { ConfigProvider } from "@opencode-ai/core/config/provider"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { Policy } from "@opencode-ai/core/policy"
|
||||
import { Project } from "@opencode-ai/core/project"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { location } from "../fixture/location"
|
||||
import { tmpdir } from "../fixture/tmpdir"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const it = testEffect(Layer.empty)
|
||||
|
||||
function testLayer(
|
||||
directory: string,
|
||||
globalDirectory = path.join(directory, "global"),
|
||||
projectDirectory = directory,
|
||||
vcs?: Project.Vcs,
|
||||
) {
|
||||
return Config.layer.pipe(
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(Global.layerWith({ config: globalDirectory })),
|
||||
Layer.provideMerge(Policy.defaultLayer),
|
||||
Layer.provide(
|
||||
Layer.succeed(
|
||||
Location.Service,
|
||||
Location.Service.of(
|
||||
location(
|
||||
{ directory: AbsolutePath.make(directory) },
|
||||
{ projectDirectory: AbsolutePath.make(projectDirectory), vcs },
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const provider = {
|
||||
endpoint: { type: "unknown" },
|
||||
options: {
|
||||
headers: {},
|
||||
body: {},
|
||||
aisdk: {
|
||||
provider: {},
|
||||
request: {},
|
||||
},
|
||||
},
|
||||
models: {},
|
||||
}
|
||||
|
||||
describe("Config", () => {
|
||||
it.live("returns an empty configuration when directory files do not exist", () =>
|
||||
Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
).pipe(
|
||||
Effect.flatMap((tmp) =>
|
||||
Effect.gen(function* () {
|
||||
const config = yield* Config.Service
|
||||
const documents = yield* config.get()
|
||||
|
||||
expect(documents).toEqual([])
|
||||
}).pipe(Effect.provide(testLayer(tmp.path))),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("loads JSON and JSONC files from lowest to highest priority", () =>
|
||||
Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
).pipe(
|
||||
Effect.flatMap((tmp) =>
|
||||
Effect.gen(function* () {
|
||||
yield* Effect.promise(() =>
|
||||
Promise.all([
|
||||
fs.writeFile(
|
||||
path.join(tmp.path, "config.json"),
|
||||
JSON.stringify({ $schema: "base", providers: { base: provider } }),
|
||||
),
|
||||
fs.writeFile(
|
||||
path.join(tmp.path, "opencode.json"),
|
||||
JSON.stringify({ $schema: "middle", providers: { middle: provider } }),
|
||||
),
|
||||
fs.writeFile(
|
||||
path.join(tmp.path, "opencode.jsonc"),
|
||||
`{
|
||||
// Later global files override scalar fields while retaining providers.
|
||||
"$schema": "last",
|
||||
"providers": { "last": ${JSON.stringify(provider)} },
|
||||
}`,
|
||||
),
|
||||
]),
|
||||
)
|
||||
return yield* Effect.gen(function* () {
|
||||
const config = yield* Config.Service
|
||||
const documents = yield* config.get()
|
||||
|
||||
expect(documents).toHaveLength(3)
|
||||
expect(documents.map((document) => document.source.type)).toEqual(["file", "file", "file"])
|
||||
expect(documents.map((document) => document.info.$schema)).toEqual(["base", "middle", "last"])
|
||||
expect(documents[0]).toBeInstanceOf(Config.Loaded)
|
||||
expect(documents[0]?.source.type === "file" ? documents[0].source.path : undefined).toBe(
|
||||
path.join(tmp.path, "config.json"),
|
||||
)
|
||||
expect(documents[2]?.info.providers?.last).toBeInstanceOf(ConfigProvider.Info)
|
||||
|
||||
yield* Effect.promise(() =>
|
||||
fs.writeFile(path.join(tmp.path, "opencode.jsonc"), JSON.stringify({ $schema: "changed" })),
|
||||
)
|
||||
expect((yield* config.get()).map((document) => document.info.$schema)).toEqual(["base", "middle", "last"])
|
||||
}).pipe(Effect.provide(testLayer(tmp.path)))
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("accepts $schema metadata without writing it into config files", () =>
|
||||
Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
).pipe(
|
||||
Effect.flatMap((tmp) =>
|
||||
Effect.gen(function* () {
|
||||
const file = path.join(tmp.path, "opencode.json")
|
||||
const contents = JSON.stringify({
|
||||
shell: "/bin/zsh",
|
||||
experimental: { policies: [{ effect: "deny", action: "provider.use", resource: "openai" }] },
|
||||
providers: { local: provider },
|
||||
})
|
||||
yield* Effect.promise(() => fs.writeFile(file, contents))
|
||||
|
||||
return yield* Effect.gen(function* () {
|
||||
const config = yield* Config.Service
|
||||
const documents = yield* config.get()
|
||||
|
||||
expect(documents[0]?.info.$schema).toBeUndefined()
|
||||
expect(documents[0]?.info.shell).toBe("/bin/zsh")
|
||||
expect(documents[0]?.info.experimental?.policies?.[0]).toEqual({
|
||||
effect: "deny",
|
||||
action: "provider.use",
|
||||
resource: "openai",
|
||||
})
|
||||
expect(yield* Effect.promise(() => fs.readFile(file, "utf8"))).toBe(contents)
|
||||
}).pipe(Effect.provide(testLayer(tmp.path)))
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("loads supported scalar and resource configuration", () =>
|
||||
Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
).pipe(
|
||||
Effect.flatMap((tmp) =>
|
||||
Effect.gen(function* () {
|
||||
yield* Effect.promise(() =>
|
||||
fs.writeFile(
|
||||
path.join(tmp.path, "opencode.json"),
|
||||
JSON.stringify({
|
||||
shell: "/bin/bash",
|
||||
model: "anthropic/claude",
|
||||
autoupdate: "notify",
|
||||
share: "disabled",
|
||||
enterprise: { url: "https://share.example.com" },
|
||||
username: "test-user",
|
||||
permissions: [
|
||||
{ permission: "bash", pattern: "*", action: "ask" },
|
||||
{ permission: "bash", pattern: "git status", action: "allow" },
|
||||
],
|
||||
agents: {
|
||||
reviewer: {
|
||||
model: "openrouter/openai/gpt-5",
|
||||
variant: "high",
|
||||
options: {
|
||||
headers: { "x-agent": "reviewer" },
|
||||
aisdk: { request: { reasoningEffort: "high" } },
|
||||
},
|
||||
description: "Review changes for correctness",
|
||||
system: "Find regressions.",
|
||||
mode: "subagent",
|
||||
hidden: false,
|
||||
color: "warning",
|
||||
steps: 12,
|
||||
disabled: false,
|
||||
permissions: [{ permission: "edit", pattern: "*", action: "deny" }],
|
||||
},
|
||||
},
|
||||
snapshots: false,
|
||||
watcher: { ignore: ["node_modules/**", "dist/**", ".git"] },
|
||||
formatter: {
|
||||
prettier: { disabled: true },
|
||||
custom: { command: ["custom-fmt", "$FILE"], extensions: [".foo"] },
|
||||
},
|
||||
lsp: { typescript: { disabled: true }, custom: { command: ["custom-lsp"], extensions: [".foo"] } },
|
||||
attachments: {
|
||||
image: { auto_resize: false, max_width: 1200, max_height: 900, max_base64_bytes: 1048576 },
|
||||
},
|
||||
tool_output: { max_lines: 1000, max_bytes: 32768 },
|
||||
mcp: {
|
||||
timeout: 5000,
|
||||
servers: {
|
||||
local: {
|
||||
type: "local",
|
||||
command: ["node", "./mcp/server.js"],
|
||||
environment: { API_KEY: "secret" },
|
||||
disabled: false,
|
||||
timeout: 10000,
|
||||
},
|
||||
remote: {
|
||||
type: "remote",
|
||||
url: "https://mcp.example.com/mcp",
|
||||
headers: { Authorization: "Bearer token" },
|
||||
oauth: { client_id: "client", scope: "read write", callback_port: 19876 },
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
compaction: {
|
||||
auto: true,
|
||||
prune: false,
|
||||
keep: { turns: 3, tokens: 2000 },
|
||||
buffer: 10000,
|
||||
},
|
||||
skills: ["./skills", "~/shared-skills", "https://example.com/.well-known/skills/"],
|
||||
instructions: ["CONTRIBUTING.md", ".cursor/rules/*.md", "https://example.com/shared-rules.md"],
|
||||
references: {
|
||||
local: { path: "../library" },
|
||||
sdk: { repository: "github.com/example/sdk", branch: "main" },
|
||||
shorthand: "github.com/example/docs",
|
||||
},
|
||||
plugins: [
|
||||
"opencode-helicone-session",
|
||||
{ package: "@my-org/audit-plugin", options: { endpoint: "https://audit.example.com" } },
|
||||
],
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
return yield* Effect.gen(function* () {
|
||||
const config = yield* Config.Service
|
||||
const documents = yield* config.get()
|
||||
|
||||
expect(documents).toHaveLength(1)
|
||||
expect(documents[0]?.info.shell).toBe("/bin/bash")
|
||||
expect(documents[0]?.info.model).toBe("anthropic/claude")
|
||||
expect(documents[0]?.info.autoupdate).toBe("notify")
|
||||
expect(documents[0]?.info.share).toBe("disabled")
|
||||
expect(documents[0]?.info.enterprise).toEqual({ url: "https://share.example.com" })
|
||||
expect(documents[0]?.info.username).toBe("test-user")
|
||||
expect(documents[0]?.info.permissions).toEqual([
|
||||
{ permission: "bash", pattern: "*", action: "ask" },
|
||||
{ permission: "bash", pattern: "git status", action: "allow" },
|
||||
])
|
||||
expect(documents[0]?.info.agents?.reviewer).toEqual({
|
||||
model: "openrouter/openai/gpt-5",
|
||||
variant: "high",
|
||||
options: {
|
||||
headers: { "x-agent": "reviewer" },
|
||||
aisdk: { request: { reasoningEffort: "high" } },
|
||||
},
|
||||
description: "Review changes for correctness",
|
||||
system: "Find regressions.",
|
||||
mode: "subagent",
|
||||
hidden: false,
|
||||
color: "warning",
|
||||
steps: 12,
|
||||
disabled: false,
|
||||
permissions: [{ permission: "edit", pattern: "*", action: "deny" }],
|
||||
})
|
||||
expect(documents[0]?.info.snapshots).toBe(false)
|
||||
expect(documents[0]?.info.watcher).toEqual({ ignore: ["node_modules/**", "dist/**", ".git"] })
|
||||
expect(documents[0]?.info.formatter).toEqual({
|
||||
prettier: { disabled: true },
|
||||
custom: { command: ["custom-fmt", "$FILE"], extensions: [".foo"] },
|
||||
})
|
||||
expect(documents[0]?.info.lsp).toEqual({
|
||||
typescript: { disabled: true },
|
||||
custom: { command: ["custom-lsp"], extensions: [".foo"] },
|
||||
})
|
||||
expect(documents[0]?.info.attachments).toEqual({
|
||||
image: { auto_resize: false, max_width: 1200, max_height: 900, max_base64_bytes: 1048576 },
|
||||
})
|
||||
expect(documents[0]?.info.tool_output).toEqual({ max_lines: 1000, max_bytes: 32768 })
|
||||
expect(documents[0]?.info.mcp).toEqual({
|
||||
timeout: 5000,
|
||||
servers: {
|
||||
local: {
|
||||
type: "local",
|
||||
command: ["node", "./mcp/server.js"],
|
||||
environment: { API_KEY: "secret" },
|
||||
disabled: false,
|
||||
timeout: 10000,
|
||||
},
|
||||
remote: {
|
||||
type: "remote",
|
||||
url: "https://mcp.example.com/mcp",
|
||||
headers: { Authorization: "Bearer token" },
|
||||
oauth: { client_id: "client", scope: "read write", callback_port: 19876 },
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(documents[0]?.info.compaction).toEqual({
|
||||
auto: true,
|
||||
prune: false,
|
||||
keep: { turns: 3, tokens: 2000 },
|
||||
buffer: 10000,
|
||||
})
|
||||
expect(documents[0]?.info.skills).toEqual([
|
||||
"./skills",
|
||||
"~/shared-skills",
|
||||
"https://example.com/.well-known/skills/",
|
||||
])
|
||||
expect(documents[0]?.info.instructions).toEqual([
|
||||
"CONTRIBUTING.md",
|
||||
".cursor/rules/*.md",
|
||||
"https://example.com/shared-rules.md",
|
||||
])
|
||||
expect(documents[0]?.info.references).toEqual({
|
||||
local: { path: "../library" },
|
||||
sdk: { repository: "github.com/example/sdk", branch: "main" },
|
||||
shorthand: "github.com/example/docs",
|
||||
})
|
||||
expect(documents[0]?.info.plugins).toEqual([
|
||||
"opencode-helicone-session",
|
||||
{ package: "@my-org/audit-plugin", options: { endpoint: "https://audit.example.com" } },
|
||||
])
|
||||
}).pipe(Effect.provide(testLayer(tmp.path)))
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("ignores invalid files while loading valid config values", () =>
|
||||
Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
).pipe(
|
||||
Effect.flatMap((tmp) =>
|
||||
Effect.gen(function* () {
|
||||
yield* Effect.promise(() =>
|
||||
Promise.all([
|
||||
fs.writeFile(path.join(tmp.path, "config.json"), JSON.stringify({ $schema: "base" })),
|
||||
fs.writeFile(path.join(tmp.path, "opencode.json"), "{ invalid"),
|
||||
fs.writeFile(path.join(tmp.path, "opencode.jsonc"), JSON.stringify({ providers: { invalid: true } })),
|
||||
]),
|
||||
)
|
||||
return yield* Effect.gen(function* () {
|
||||
const config = yield* Config.Service
|
||||
const documents = yield* config.get()
|
||||
|
||||
expect(documents.map((document) => document.info.$schema)).toEqual(["base"])
|
||||
}).pipe(Effect.provide(testLayer(tmp.path)))
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("loads policy statements in reverse config order", () =>
|
||||
Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
).pipe(
|
||||
Effect.flatMap((tmp) => {
|
||||
const global = path.join(tmp.path, "global")
|
||||
return Effect.gen(function* () {
|
||||
yield* Effect.promise(async () => {
|
||||
await fs.mkdir(global, { recursive: true })
|
||||
await fs.writeFile(
|
||||
path.join(global, "opencode.json"),
|
||||
JSON.stringify({
|
||||
experimental: { policies: [{ effect: "deny", action: "provider.use", resource: "openai" }] },
|
||||
}),
|
||||
)
|
||||
await fs.writeFile(
|
||||
path.join(tmp.path, "opencode.json"),
|
||||
JSON.stringify({
|
||||
experimental: { policies: [{ effect: "allow", action: "provider.use", resource: "openai" }] },
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
return yield* Effect.gen(function* () {
|
||||
const policy = yield* Policy.Service
|
||||
|
||||
expect(yield* policy.evaluate("provider.use", "openai", "allow")).toBe("deny")
|
||||
}).pipe(Effect.provide(testLayer(tmp.path, global)))
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("loads global, ancestor, and .opencode configuration up to the project boundary", () =>
|
||||
Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
).pipe(
|
||||
Effect.flatMap((tmp) => {
|
||||
const global = path.join(tmp.path, "global")
|
||||
const root = path.join(tmp.path, "repo")
|
||||
const parent = path.join(root, "packages")
|
||||
const directory = path.join(parent, "app")
|
||||
return Effect.gen(function* () {
|
||||
yield* Effect.promise(async () => {
|
||||
await fs.mkdir(global, { recursive: true })
|
||||
await fs.mkdir(directory, { recursive: true })
|
||||
await fs.mkdir(path.join(root, ".opencode"), { recursive: true })
|
||||
await fs.mkdir(path.join(directory, ".opencode"), { recursive: true })
|
||||
await Promise.all([
|
||||
fs.writeFile(path.join(tmp.path, "opencode.json"), JSON.stringify({ $schema: "outside" })),
|
||||
fs.writeFile(path.join(global, "opencode.json"), JSON.stringify({ $schema: "global" })),
|
||||
fs.writeFile(path.join(root, "opencode.json"), JSON.stringify({ $schema: "root" })),
|
||||
fs.writeFile(path.join(parent, "opencode.jsonc"), JSON.stringify({ $schema: "parent" })),
|
||||
fs.writeFile(path.join(directory, "config.json"), JSON.stringify({ $schema: "directory" })),
|
||||
fs.writeFile(path.join(root, ".opencode", "opencode.json"), JSON.stringify({ $schema: "root-dot" })),
|
||||
fs.writeFile(
|
||||
path.join(directory, ".opencode", "opencode.jsonc"),
|
||||
JSON.stringify({ $schema: "directory-dot" }),
|
||||
),
|
||||
])
|
||||
})
|
||||
|
||||
return yield* Effect.gen(function* () {
|
||||
const config = yield* Config.Service
|
||||
const directories = yield* config.directories()
|
||||
const documents = yield* config.get()
|
||||
|
||||
expect(directories).toEqual([
|
||||
AbsolutePath.make(global),
|
||||
AbsolutePath.make(path.join(root, ".opencode")),
|
||||
AbsolutePath.make(path.join(directory, ".opencode")),
|
||||
])
|
||||
expect(documents.map((document) => document.info.$schema)).toEqual([
|
||||
"global",
|
||||
"root",
|
||||
"parent",
|
||||
"directory",
|
||||
"root-dot",
|
||||
"directory-dot",
|
||||
])
|
||||
}).pipe(
|
||||
Effect.provide(
|
||||
testLayer(directory, global, root, {
|
||||
type: "git",
|
||||
store: AbsolutePath.make(path.join(root, ".git")),
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,131 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
import { Config } from "@opencode-ai/core/config"
|
||||
import { ConfigProviderPlugin } from "@opencode-ai/core/config/plugin/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { it } from "../plugin/provider-helper"
|
||||
|
||||
function options(headers: Record<string, string>, variant?: string) {
|
||||
return {
|
||||
headers,
|
||||
variant,
|
||||
}
|
||||
}
|
||||
|
||||
const decode = Schema.decodeUnknownSync(Config.Info)
|
||||
|
||||
describe("ConfigProviderPlugin.Plugin", () => {
|
||||
it.effect("loads configured providers and applies later model overrides", () =>
|
||||
Effect.gen(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
const plugin = yield* PluginV2.Service
|
||||
const providerID = ProviderV2.ID.make("custom")
|
||||
const modelID = ModelV2.ID.make("chat")
|
||||
const config = Config.Service.of({
|
||||
directories: () => Effect.succeed([]),
|
||||
get: () =>
|
||||
Effect.succeed([
|
||||
new Config.Loaded({
|
||||
source: { type: "memory" },
|
||||
info: decode({
|
||||
providers: {
|
||||
custom: {
|
||||
name: "Configured",
|
||||
env: ["CUSTOM_API_KEY"],
|
||||
endpoint: { type: "unknown" },
|
||||
options: options({ first: "first", shared: "first" }),
|
||||
models: {
|
||||
chat: {
|
||||
name: "First",
|
||||
capabilities: { tools: true, input: ["text"], output: ["text"] },
|
||||
disabled: true,
|
||||
limit: { context: 100, output: 50 },
|
||||
cost: { input: 1, output: 2 },
|
||||
options: options({ first: "first", shared: "first" }, "retained"),
|
||||
variants: [
|
||||
{
|
||||
id: "fast",
|
||||
headers: { first: "first", shared: "first" },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
new Config.Loaded({
|
||||
source: { type: "memory" },
|
||||
info: decode({
|
||||
providers: {
|
||||
custom: {
|
||||
endpoint: { type: "aisdk", package: "custom-sdk", url: "https://example.test" },
|
||||
options: options({ last: "last", shared: "last" }),
|
||||
models: {
|
||||
chat: {
|
||||
api_id: "api-chat",
|
||||
name: "Last",
|
||||
limit: { output: 75 },
|
||||
options: options({ last: "last", shared: "last" }),
|
||||
variants: [
|
||||
{
|
||||
id: "fast",
|
||||
headers: { last: "last", shared: "last" },
|
||||
},
|
||||
{
|
||||
id: "slow",
|
||||
headers: { slow: "slow" },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
new Config.Loaded({
|
||||
source: { type: "memory" },
|
||||
info: decode({
|
||||
providers: {
|
||||
custom: { name: "Renamed" },
|
||||
},
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
|
||||
yield* plugin.add({
|
||||
...ConfigProviderPlugin.Plugin,
|
||||
effect: ConfigProviderPlugin.Plugin.effect.pipe(
|
||||
Effect.provideService(Config.Service, config),
|
||||
Effect.provideService(Catalog.Service, catalog),
|
||||
),
|
||||
})
|
||||
|
||||
const provider = yield* catalog.provider.get(providerID)
|
||||
const model = yield* catalog.model.get(providerID, modelID)
|
||||
expect(provider.name).toBe("Renamed")
|
||||
expect(provider.env).toEqual(["CUSTOM_API_KEY"])
|
||||
expect(provider.enabled).toEqual({ via: "custom", data: {} })
|
||||
expect(provider.endpoint).toEqual({ type: "aisdk", package: "custom-sdk", url: "https://example.test" })
|
||||
expect(provider.options.headers).toEqual({ first: "first", shared: "last", last: "last" })
|
||||
expect(model.apiID).toBe(ModelV2.ID.make("api-chat"))
|
||||
expect(model.name).toBe("Last")
|
||||
expect(model.capabilities).toEqual({ tools: true, input: ["text"], output: ["text"] })
|
||||
expect(model.enabled).toBe(false)
|
||||
expect(model.limit).toEqual({ context: 100, output: 75 })
|
||||
expect(model.cost).toEqual([{ input: 1, output: 2, cache: { read: 0, write: 0 }, tier: undefined }])
|
||||
expect(model.options.headers).toEqual({ first: "first", shared: "last", last: "last" })
|
||||
expect(model.options.variant).toBe("retained")
|
||||
expect(model.variants.map((variant) => variant.id)).toEqual([
|
||||
ModelV2.VariantID.make("fast"),
|
||||
ModelV2.VariantID.make("slow"),
|
||||
])
|
||||
expect(model.variants[0]?.headers).toEqual({ first: "first", shared: "last", last: "last" })
|
||||
expect(model.variants[1]?.headers).toEqual({ slow: "slow" })
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -2,11 +2,13 @@ import { describe, expect } from "bun:test"
|
||||
import { Effect, Fiber, Layer, Schema, Stream } from "effect"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { location } from "./fixture/location"
|
||||
import { testEffect } from "./lib/effect"
|
||||
|
||||
const locationLayer = Layer.succeed(
|
||||
Location.Service,
|
||||
Location.Service.of({ directory: "project", workspaceID: "workspace" }),
|
||||
Location.Service.of(location({ directory: AbsolutePath.make("project"), workspaceID: "workspace" })),
|
||||
)
|
||||
const it = testEffect(EventV2.layer.pipe(Layer.provideMerge(locationLayer)))
|
||||
const itWithoutLocation = testEffect(EventV2.layer)
|
||||
@@ -46,7 +48,7 @@ describe("EventV2", () => {
|
||||
expect(event.type).toBe("test.message")
|
||||
expect(event).not.toHaveProperty("version")
|
||||
expect(event.data).toEqual({ text: "hello" })
|
||||
expect(event.location).toEqual({ directory: "project", workspaceID: "workspace" })
|
||||
expect(event.location).toEqual({ directory: AbsolutePath.make("project"), workspaceID: "workspace" })
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { Project } from "@opencode-ai/core/project"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
|
||||
export function location(ref: Location.Ref, input: { projectDirectory?: AbsolutePath; vcs?: Project.Vcs } = {}) {
|
||||
return {
|
||||
directory: ref.directory,
|
||||
workspaceID: ref.workspaceID,
|
||||
project: { id: Project.ID.global, directory: input.projectDirectory ?? ref.directory },
|
||||
vcs: input.vcs,
|
||||
} satisfies Location.Interface
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { Project } from "@opencode-ai/core/project"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { testEffect } from "./lib/effect"
|
||||
|
||||
const ref = { directory: AbsolutePath.make("/repo/packages/app"), workspaceID: "workspace" }
|
||||
const projectLayer = Layer.succeed(
|
||||
Project.Service,
|
||||
Project.Service.of({
|
||||
resolve: () =>
|
||||
Effect.succeed({
|
||||
id: Project.ID.make("project"),
|
||||
directory: AbsolutePath.make("/repo"),
|
||||
vcs: { type: "git", store: AbsolutePath.make("/repo/.git") },
|
||||
}),
|
||||
commit: () => Effect.void,
|
||||
}),
|
||||
)
|
||||
const it = testEffect(Location.layer(ref).pipe(Layer.provide(projectLayer)))
|
||||
|
||||
describe("Location", () => {
|
||||
it.effect("resolves the current project and vcs information", () =>
|
||||
Effect.gen(function* () {
|
||||
const location = yield* Location.Service
|
||||
|
||||
expect(location.directory).toBe(AbsolutePath.make("/repo/packages/app"))
|
||||
expect(location.workspaceID).toBe("workspace")
|
||||
expect(location.project.id).toBe(Project.ID.make("project"))
|
||||
expect(location.project.directory).toBe(AbsolutePath.make("/repo"))
|
||||
expect(location.vcs).toEqual({
|
||||
type: "git",
|
||||
store: AbsolutePath.make("/repo/.git"),
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Schema } from "effect"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
|
||||
const decode = Schema.decodeUnknownSync(ModelV2.Ref)
|
||||
|
||||
describe("ModelV2.Ref", () => {
|
||||
test("accepts a model selection without a variant", () => {
|
||||
expect(decode({ id: "claude-sonnet", providerID: "anthropic" })).toEqual({
|
||||
id: ModelV2.ID.make("claude-sonnet"),
|
||||
providerID: ProviderV2.ID.make("anthropic"),
|
||||
})
|
||||
})
|
||||
|
||||
test("preserves an explicit model variant", () => {
|
||||
expect(decode({ id: "claude-sonnet", providerID: "anthropic", variant: "high" })).toEqual({
|
||||
id: ModelV2.ID.make("claude-sonnet"),
|
||||
providerID: ProviderV2.ID.make("anthropic"),
|
||||
variant: ModelV2.VariantID.make("high"),
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -24,8 +24,8 @@ describe("AmazonBedrockPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(AmazonBedrockPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
const bedrock = provider("amazon-bedrock", {
|
||||
endpoint: { type: "aisdk", package: "@ai-sdk/amazon-bedrock" },
|
||||
options: {
|
||||
|
||||
@@ -12,8 +12,8 @@ describe("AnthropicPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(AnthropicPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
const item = provider("anthropic", {
|
||||
endpoint: { type: "aisdk", package: "@ai-sdk/anthropic" },
|
||||
options: { headers: { Existing: "1" }, body: {}, aisdk: { provider: {}, request: {} } },
|
||||
@@ -35,8 +35,8 @@ describe("AnthropicPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(AnthropicPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => catalog.provider.update(provider("openai").id, () => {}))
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => catalog.provider.update(provider("openai").id, () => {}))
|
||||
expect((yield* catalog.provider.get(ProviderV2.ID.openai)).options.headers["anthropic-beta"]).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -13,8 +13,8 @@ describe("AzureCognitiveServicesPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(AzureCognitiveServicesPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
catalog.provider.update(ProviderV2.ID.make("azure-cognitive-services"), (item) => {
|
||||
item.endpoint = { type: "aisdk", package: "@ai-sdk/openai-compatible" }
|
||||
})
|
||||
@@ -37,8 +37,8 @@ describe("AzureCognitiveServicesPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(AzureCognitiveServicesPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
const azure = provider("azure-cognitive-services", {
|
||||
endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible" },
|
||||
})
|
||||
|
||||
@@ -5,9 +5,12 @@ import { Catalog } from "@opencode-ai/core/catalog"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { Policy } from "@opencode-ai/core/policy"
|
||||
import { AccountPlugin } from "@opencode-ai/core/plugin/account"
|
||||
import { AzurePlugin } from "@opencode-ai/core/plugin/provider/azure"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { location } from "../fixture/location"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { fakeSelectorSdk, it, model, npmLayer, provider, withEnv } from "./provider-helper"
|
||||
|
||||
@@ -16,7 +19,10 @@ const itWithAccount = testEffect(
|
||||
Layer.provideMerge(PluginV2.defaultLayer),
|
||||
Layer.provideMerge(AccountV2.defaultLayer),
|
||||
Layer.provideMerge(EventV2.defaultLayer),
|
||||
Layer.provideMerge(Layer.succeed(Location.Service, Location.Service.of({ directory: "test" }))),
|
||||
Layer.provide(Policy.defaultLayer),
|
||||
Layer.provideMerge(
|
||||
Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("test") }))),
|
||||
),
|
||||
Layer.provideMerge(npmLayer),
|
||||
),
|
||||
)
|
||||
@@ -28,8 +34,8 @@ describe("AzurePlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(AzurePlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
catalog.provider.update(ProviderV2.ID.azure, (item) => {
|
||||
item.endpoint = { type: "aisdk", package: "@ai-sdk/azure" }
|
||||
})
|
||||
@@ -45,8 +51,8 @@ describe("AzurePlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(AzurePlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
const azure = provider("azure", {
|
||||
endpoint: { type: "aisdk", package: "@ai-sdk/azure" },
|
||||
options: { headers: {}, body: {}, aisdk: { provider: { resourceName: "from-config" }, request: {} } },
|
||||
@@ -94,8 +100,8 @@ describe("AzurePlugin", () => {
|
||||
),
|
||||
})
|
||||
yield* plugin.add(AzurePlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
catalog.provider.update(ProviderV2.ID.azure, (item) => {
|
||||
item.endpoint = { type: "aisdk", package: "@ai-sdk/azure" }
|
||||
})
|
||||
@@ -113,8 +119,8 @@ describe("AzurePlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(AzurePlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
const azure = provider("azure", {
|
||||
endpoint: { type: "aisdk", package: "@ai-sdk/azure" },
|
||||
options: { headers: {}, body: {}, aisdk: { provider: { resourceName: "" }, request: {} } },
|
||||
@@ -135,8 +141,8 @@ describe("AzurePlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(AzurePlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
const azure = provider("azure", {
|
||||
endpoint: { type: "aisdk", package: "@ai-sdk/azure" },
|
||||
options: { headers: {}, body: {}, aisdk: { provider: { resourceName: " " }, request: {} } },
|
||||
|
||||
@@ -24,8 +24,8 @@ describe("CerebrasPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(CerebrasPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
catalog.provider.update(ProviderV2.ID.make("cerebras"), (item) => {
|
||||
item.endpoint = { type: "aisdk", package: "@ai-sdk/cerebras" }
|
||||
item.options.headers.Existing = "1"
|
||||
@@ -43,8 +43,8 @@ describe("CerebrasPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(CerebrasPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => catalog.provider.update(ProviderV2.ID.make("groq"), () => {}))
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => catalog.provider.update(ProviderV2.ID.make("groq"), () => {}))
|
||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("groq"))).options.headers).toEqual({})
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -6,9 +6,12 @@ import { Location } from "@opencode-ai/core/location"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { Policy } from "@opencode-ai/core/policy"
|
||||
import { AccountPlugin } from "@opencode-ai/core/plugin/account"
|
||||
import { CloudflareWorkersAIPlugin } from "@opencode-ai/core/plugin/provider/cloudflare-workers-ai"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { location } from "../fixture/location"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { fakeSelectorSdk, it, model, npmLayer, withEnv } from "./provider-helper"
|
||||
|
||||
@@ -17,7 +20,10 @@ const itWithAccount = testEffect(
|
||||
Layer.provideMerge(PluginV2.defaultLayer),
|
||||
Layer.provideMerge(AccountV2.defaultLayer),
|
||||
Layer.provideMerge(EventV2.defaultLayer),
|
||||
Layer.provideMerge(Layer.succeed(Location.Service, Location.Service.of({ directory: "test" }))),
|
||||
Layer.provide(Policy.defaultLayer),
|
||||
Layer.provideMerge(
|
||||
Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("test") }))),
|
||||
),
|
||||
Layer.provideMerge(npmLayer),
|
||||
),
|
||||
)
|
||||
@@ -48,8 +54,8 @@ describe("CloudflareWorkersAIPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(CloudflareWorkersAIPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) =>
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) =>
|
||||
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
|
||||
provider.endpoint = { type: "aisdk", package: "test-provider" }
|
||||
}),
|
||||
@@ -80,8 +86,8 @@ describe("CloudflareWorkersAIPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(CloudflareWorkersAIPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) =>
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) =>
|
||||
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
|
||||
provider.endpoint = { type: "aisdk", package: "test-provider", url: "https://proxy.example/v1" }
|
||||
}),
|
||||
@@ -146,8 +152,8 @@ describe("CloudflareWorkersAIPlugin", () => {
|
||||
),
|
||||
})
|
||||
yield* plugin.add(CloudflareWorkersAIPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) =>
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) =>
|
||||
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
|
||||
provider.endpoint = { type: "aisdk", package: "test-provider" }
|
||||
}),
|
||||
@@ -167,8 +173,8 @@ describe("CloudflareWorkersAIPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(CloudflareWorkersAIPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) =>
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) =>
|
||||
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
|
||||
provider.endpoint = { type: "aisdk", package: "test-provider" }
|
||||
provider.options.aisdk.provider.accountId = "configured-acct"
|
||||
|
||||
@@ -152,8 +152,8 @@ describe("GithubCopilotPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(GithubCopilotPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
catalog.provider.update(ProviderV2.ID.make("github-copilot"), () => {})
|
||||
catalog.model.update(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-chat-latest"), () => {})
|
||||
})
|
||||
@@ -168,8 +168,8 @@ describe("GithubCopilotPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(GithubCopilotPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
catalog.provider.update(ProviderV2.ID.make("custom-copilot"), () => {})
|
||||
catalog.model.update(ProviderV2.ID.make("custom-copilot"), ModelV2.ID.make("gpt-5-chat-latest"), () => {})
|
||||
})
|
||||
|
||||
@@ -5,9 +5,11 @@ import { Catalog } from "@opencode-ai/core/catalog"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { Policy } from "@opencode-ai/core/policy"
|
||||
import { AccountPlugin } from "@opencode-ai/core/plugin/account"
|
||||
import { GitLabPlugin } from "@opencode-ai/core/plugin/provider/gitlab"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { it, model, npmLayer, withEnv } from "./provider-helper"
|
||||
|
||||
@@ -27,12 +29,15 @@ void mock.module("gitlab-ai-provider", () => ({
|
||||
}))
|
||||
|
||||
const itWithAccount = testEffect(
|
||||
Catalog.layer.pipe(
|
||||
Layer.provideMerge(PluginV2.defaultLayer),
|
||||
Layer.provideMerge(AccountV2.defaultLayer),
|
||||
Layer.provideMerge(EventV2.defaultLayer),
|
||||
Layer.provideMerge(Layer.succeed(Location.Service, Location.Service.of({ directory: "test" }))),
|
||||
Layer.provideMerge(npmLayer),
|
||||
Layer.mergeAll(
|
||||
Catalog.defaultLayer,
|
||||
PluginV2.defaultLayer,
|
||||
AccountV2.defaultLayer,
|
||||
EventV2.defaultLayer,
|
||||
npmLayer,
|
||||
).pipe(
|
||||
Layer.provide(Policy.defaultLayer),
|
||||
Layer.provide(Location.defaultLayer({ directory: AbsolutePath.make("/") })),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -179,8 +184,8 @@ describe("GitLabPlugin", () => {
|
||||
),
|
||||
})
|
||||
yield* plugin.add(GitLabPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => catalog.provider.update(ProviderV2.ID.make("gitlab"), () => {}))
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => catalog.provider.update(ProviderV2.ID.make("gitlab"), () => {}))
|
||||
const provider = yield* catalog.provider.get(ProviderV2.ID.make("gitlab"))
|
||||
yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
@@ -227,8 +232,8 @@ describe("GitLabPlugin", () => {
|
||||
),
|
||||
})
|
||||
yield* plugin.add(GitLabPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => catalog.provider.update(ProviderV2.ID.make("gitlab"), () => {}))
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => catalog.provider.update(ProviderV2.ID.make("gitlab"), () => {}))
|
||||
const provider = yield* catalog.provider.get(ProviderV2.ID.make("gitlab"))
|
||||
yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
|
||||
@@ -22,8 +22,8 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(GoogleVertexAnthropicPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) =>
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) =>
|
||||
catalog.provider.update(ProviderV2.ID.make("google-vertex-anthropic"), (provider) => {
|
||||
provider.endpoint = { type: "aisdk", package: "@ai-sdk/google-vertex/anthropic" }
|
||||
}),
|
||||
@@ -41,8 +41,8 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(GoogleVertexAnthropicPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) =>
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) =>
|
||||
catalog.provider.update(ProviderV2.ID.make("google-vertex-anthropic"), (provider) => {
|
||||
provider.endpoint = { type: "aisdk", package: "@ai-sdk/google-vertex/anthropic" }
|
||||
provider.options.aisdk.provider.project = "configured-project"
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { fakeSelectorSdk, it, model, withEnv } from "./provider-helper"
|
||||
|
||||
const vertexOptions: Record<string, any>[] = []
|
||||
const googleAuthOptions: Record<string, any>[] = []
|
||||
|
||||
void mock.module("@ai-sdk/google-vertex", () => ({
|
||||
createVertex: (options: Record<string, any>) => {
|
||||
@@ -19,12 +20,14 @@ void mock.module("@ai-sdk/google-vertex", () => ({
|
||||
|
||||
void mock.module("google-auth-library", () => ({
|
||||
GoogleAuth: class {
|
||||
async getApplicationDefault() {
|
||||
constructor(options: Record<string, any>) {
|
||||
googleAuthOptions.push(options)
|
||||
}
|
||||
|
||||
async getClient() {
|
||||
return {
|
||||
credential: {
|
||||
async getAccessToken() {
|
||||
return { token: "vertex-token" }
|
||||
},
|
||||
async getAccessToken() {
|
||||
return { token: "vertex-token" }
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -47,8 +50,8 @@ describe("GoogleVertexPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(GoogleVertexPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) =>
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) =>
|
||||
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
|
||||
provider.endpoint = {
|
||||
type: "aisdk",
|
||||
@@ -86,8 +89,8 @@ describe("GoogleVertexPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(GoogleVertexPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) =>
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) =>
|
||||
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
|
||||
provider.endpoint = {
|
||||
type: "aisdk",
|
||||
@@ -136,8 +139,8 @@ describe("GoogleVertexPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(GoogleVertexPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) =>
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) =>
|
||||
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
|
||||
provider.endpoint = {
|
||||
type: "aisdk",
|
||||
@@ -165,8 +168,8 @@ describe("GoogleVertexPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(GoogleVertexPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) =>
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) =>
|
||||
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
|
||||
provider.endpoint = {
|
||||
type: "aisdk",
|
||||
@@ -201,8 +204,8 @@ describe("GoogleVertexPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(GoogleVertexPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) =>
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) =>
|
||||
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
|
||||
provider.endpoint = { type: "aisdk", package: "@ai-sdk/google-vertex" }
|
||||
provider.options.aisdk.provider.project = "config-project"
|
||||
@@ -247,6 +250,7 @@ describe("GoogleVertexPlugin", () => {
|
||||
|
||||
it.effect("keeps Google auth fetch for OpenAI-compatible Vertex endpoints", () =>
|
||||
Effect.gen(function* () {
|
||||
googleAuthOptions.length = 0
|
||||
const fetchCalls: { input: Parameters<typeof fetch>[0]; init?: RequestInit }[] = []
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* plugin.add(GoogleVertexPlugin)
|
||||
@@ -292,6 +296,7 @@ describe("GoogleVertexPlugin", () => {
|
||||
}),
|
||||
)
|
||||
expect(fetchCalls).toHaveLength(1)
|
||||
expect(googleAuthOptions).toEqual([{ scopes: ["https://www.googleapis.com/auth/cloud-platform"] }])
|
||||
expect(fetchCalls[0].input).toBe("https://vertex.example")
|
||||
expect(new Headers(fetchCalls[0].init?.headers).get("authorization")).toBe("Bearer vertex-token")
|
||||
expect(new Headers(fetchCalls[0].init?.headers).get("x-test")).toBe("1")
|
||||
|
||||
@@ -7,11 +7,17 @@ import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { Policy } from "@opencode-ai/core/policy"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { location } from "../fixture/location"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
export const fixtureProvider = new URL("./fixtures/provider-factory.ts", import.meta.url).href
|
||||
const locationLayer = Layer.succeed(Location.Service, Location.Service.of({ directory: "test" }))
|
||||
const locationLayer = Layer.succeed(
|
||||
Location.Service,
|
||||
Location.Service.of(location({ directory: AbsolutePath.make("test") })),
|
||||
)
|
||||
|
||||
export const npmLayer = Layer.succeed(
|
||||
Npm.Service,
|
||||
@@ -25,7 +31,7 @@ export const npmLayer = Layer.succeed(
|
||||
export const catalogLayer = Layer.succeed(
|
||||
Catalog.Service,
|
||||
Catalog.Service.of({
|
||||
loader: () => Effect.die("unexpected catalog.loader"),
|
||||
transform: () => Effect.die("unexpected catalog.transform"),
|
||||
provider: {
|
||||
get: () => Effect.die("unexpected provider.get"),
|
||||
all: () => Effect.succeed([]),
|
||||
@@ -36,7 +42,6 @@ export const catalogLayer = Layer.succeed(
|
||||
all: () => Effect.succeed([]),
|
||||
available: () => Effect.succeed([]),
|
||||
default: () => Effect.succeed(Option.none<ModelV2.Info>()),
|
||||
setDefault: () => Effect.die("unexpected model.setDefault"),
|
||||
small: () => Effect.succeed(Option.none<ModelV2.Info>()),
|
||||
},
|
||||
}),
|
||||
@@ -46,6 +51,7 @@ export const it = testEffect(
|
||||
Catalog.layer.pipe(
|
||||
Layer.provideMerge(PluginV2.defaultLayer),
|
||||
Layer.provideMerge(EventV2.defaultLayer),
|
||||
Layer.provide(Policy.defaultLayer),
|
||||
Layer.provideMerge(locationLayer),
|
||||
Layer.provideMerge(npmLayer),
|
||||
),
|
||||
|
||||
@@ -22,8 +22,8 @@ describe("KiloPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(KiloPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
const kilo = provider("kilo", {
|
||||
endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.kilo.ai/api/gateway" },
|
||||
options: { headers: { Existing: "value" }, body: {}, aisdk: { provider: {}, request: {} } },
|
||||
@@ -48,8 +48,8 @@ describe("KiloPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(KiloPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
const item = provider("kilo", {
|
||||
endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.kilo.ai/api/gateway" },
|
||||
})
|
||||
@@ -74,8 +74,8 @@ describe("KiloPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(KiloPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
const kilo = provider("kilo", {
|
||||
endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.kilo.ai/api/gateway" },
|
||||
})
|
||||
|
||||
@@ -22,8 +22,8 @@ describe("LLMGatewayPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(LLMGatewayPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
const llmgateway = provider("llmgateway", {
|
||||
enabled: { via: "env", name: "LLMGATEWAY_API_KEY" },
|
||||
endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.llmgateway.io/v1" },
|
||||
@@ -56,8 +56,8 @@ describe("LLMGatewayPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(LLMGatewayPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
const item = provider("llmgateway", {
|
||||
endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.llmgateway.io/v1" },
|
||||
})
|
||||
|
||||
@@ -22,8 +22,8 @@ describe("NvidiaPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(NvidiaPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
const nvidia = provider("nvidia", {
|
||||
endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://integrate.api.nvidia.com/v1" },
|
||||
options: { headers: { Existing: "value" }, body: {}, aisdk: { provider: {}, request: {} } },
|
||||
@@ -49,8 +49,8 @@ describe("NvidiaPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(NvidiaPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
const item = provider("nvidia", {
|
||||
endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://integrate.api.nvidia.com/v1" },
|
||||
options: { headers: {}, body: {}, aisdk: { provider: {}, request: {} } },
|
||||
@@ -74,8 +74,8 @@ describe("NvidiaPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(NvidiaPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
const item = provider("nvidia", {
|
||||
endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://integrate.api.nvidia.com/v1" },
|
||||
options: {
|
||||
|
||||
@@ -77,8 +77,8 @@ describe("OpenAIPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(OpenAIPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
const item = provider("openai", { endpoint: { type: "aisdk", package: "@ai-sdk/openai" } })
|
||||
catalog.provider.update(item.id, (draft) => {
|
||||
draft.endpoint = item.endpoint
|
||||
@@ -96,8 +96,8 @@ describe("OpenAIPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(OpenAIPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
const item = provider("custom-openai")
|
||||
catalog.provider.update(item.id, () => {})
|
||||
catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {})
|
||||
|
||||
@@ -5,11 +5,17 @@ import { Location } from "@opencode-ai/core/location"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { OpencodePlugin } from "@opencode-ai/core/plugin/provider/opencode"
|
||||
import { Policy } from "@opencode-ai/core/policy"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { location } from "../fixture/location"
|
||||
import { it, model, provider, withEnv } from "./provider-helper"
|
||||
|
||||
const cost = (input: number, output = 0) => [{ input, output, cache: { read: 0, write: 0 } }]
|
||||
const locationLayer = Layer.succeed(Location.Service, Location.Service.of({ directory: "test" }))
|
||||
const locationLayer = Layer.succeed(
|
||||
Location.Service,
|
||||
Location.Service.of(location({ directory: AbsolutePath.make("test") })),
|
||||
)
|
||||
|
||||
describe("OpencodePlugin", () => {
|
||||
it.effect("uses a public key and disables paid models without credentials", () =>
|
||||
@@ -18,8 +24,8 @@ describe("OpencodePlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(OpencodePlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
const item = provider("opencode")
|
||||
catalog.provider.update(item.id, () => {})
|
||||
const paid = model("opencode", "paid", { cost: cost(1) })
|
||||
@@ -39,8 +45,8 @@ describe("OpencodePlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(OpencodePlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
const item = provider("opencode")
|
||||
catalog.provider.update(item.id, () => {})
|
||||
const free = model("opencode", "free", { cost: cost(0) })
|
||||
@@ -60,8 +66,8 @@ describe("OpencodePlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(OpencodePlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
const item = provider("opencode")
|
||||
catalog.provider.update(item.id, () => {})
|
||||
const outputOnly = model("opencode", "output-only", { cost: cost(0, 1) })
|
||||
@@ -81,8 +87,8 @@ describe("OpencodePlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(OpencodePlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
const item = provider("opencode")
|
||||
catalog.provider.update(item.id, () => {})
|
||||
const paid = model("opencode", "paid", { cost: cost(1) })
|
||||
@@ -102,8 +108,8 @@ describe("OpencodePlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(OpencodePlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
const item = provider("opencode", { env: ["CUSTOM_OPENCODE_API_KEY"] })
|
||||
catalog.provider.update(item.id, (draft) => {
|
||||
draft.env = [...item.env]
|
||||
@@ -125,8 +131,8 @@ describe("OpencodePlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(OpencodePlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
const item = provider("opencode", {
|
||||
options: {
|
||||
headers: {},
|
||||
@@ -157,8 +163,8 @@ describe("OpencodePlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(OpencodePlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
const item = provider("opencode", { enabled: { via: "account", service: "opencode" } })
|
||||
catalog.provider.update(item.id, (draft) => {
|
||||
draft.enabled = item.enabled
|
||||
@@ -180,8 +186,8 @@ describe("OpencodePlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(OpencodePlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
const item = provider("openai")
|
||||
catalog.provider.update(item.id, () => {})
|
||||
const paid = model("openai", "paid", { cost: cost(1) })
|
||||
@@ -200,8 +206,8 @@ describe("OpencodePlugin", () => {
|
||||
const catalog = yield* Catalog.Service
|
||||
const providerID = ProviderV2.ID.opencode
|
||||
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
catalog.provider.update(providerID, () => {})
|
||||
catalog.model.update(providerID, ModelV2.ID.make("cheap-mini"), (model) => {
|
||||
model.capabilities.input = ["text"]
|
||||
@@ -220,6 +226,8 @@ describe("OpencodePlugin", () => {
|
||||
const selected = yield* catalog.model.small(providerID)
|
||||
|
||||
expect(Option.getOrUndefined(selected)?.id).toBe(ModelV2.ID.make("gpt-5-nano"))
|
||||
}).pipe(Effect.provide(Catalog.defaultLayer.pipe(Layer.provide(locationLayer)))),
|
||||
}).pipe(
|
||||
Effect.provide(Catalog.defaultLayer.pipe(Layer.provide(Policy.defaultLayer), Layer.provide(locationLayer))),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -23,8 +23,8 @@ describe("OpenRouterPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(OpenRouterPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
const openrouter = provider("openrouter", {
|
||||
endpoint: { type: "aisdk", package: "@openrouter/ai-sdk-provider" },
|
||||
options: { headers: { Existing: "value" }, body: {}, aisdk: { provider: {}, request: {} } },
|
||||
@@ -75,8 +75,8 @@ describe("OpenRouterPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(OpenRouterPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
const openrouter = provider("openrouter", {
|
||||
endpoint: { type: "aisdk", package: "@openrouter/ai-sdk-provider" },
|
||||
})
|
||||
@@ -108,8 +108,8 @@ describe("OpenRouterPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(OpenRouterPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
catalog.provider.update(ProviderV2.ID.make("custom-openrouter"), () => {})
|
||||
catalog.model.update(ProviderV2.ID.make("custom-openrouter"), ModelV2.ID.make("gpt-5-chat-latest"), () => {})
|
||||
})
|
||||
|
||||
@@ -12,8 +12,8 @@ describe("VercelPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(VercelPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
const item = provider("vercel", {
|
||||
endpoint: { type: "aisdk", package: "@ai-sdk/vercel" },
|
||||
options: { headers: { Existing: "1" }, body: {}, aisdk: { provider: {}, request: {} } },
|
||||
@@ -36,8 +36,8 @@ describe("VercelPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(VercelPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
const item = provider("vercel", { endpoint: { type: "aisdk", package: "@ai-sdk/vercel" } })
|
||||
catalog.provider.update(item.id, (draft) => {
|
||||
draft.endpoint = item.endpoint
|
||||
@@ -69,8 +69,8 @@ describe("VercelPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(VercelPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => catalog.provider.update(provider("gateway").id, () => {}))
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => catalog.provider.update(provider("gateway").id, () => {}))
|
||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("gateway"))).options.headers).toEqual({})
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -22,8 +22,8 @@ describe("ZenmuxPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(ZenmuxPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
const item = provider("zenmux", {
|
||||
endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://zenmux.ai/api/v1" },
|
||||
})
|
||||
@@ -42,8 +42,8 @@ describe("ZenmuxPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(ZenmuxPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
const item = provider("zenmux", {
|
||||
endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://zenmux.ai/api/v1" },
|
||||
options: { headers: { Existing: "value" }, body: {}, aisdk: { provider: {}, request: {} } },
|
||||
@@ -67,8 +67,8 @@ describe("ZenmuxPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(ZenmuxPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
const item = provider("zenmux", {
|
||||
endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://zenmux.ai/api/v1" },
|
||||
options: {
|
||||
@@ -95,8 +95,8 @@ describe("ZenmuxPlugin", () => {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(ZenmuxPlugin)
|
||||
const load = yield* catalog.loader()
|
||||
yield* load((catalog) => {
|
||||
const transform = yield* catalog.transform()
|
||||
yield* transform((catalog) => {
|
||||
const item = provider("openrouter", {
|
||||
options: {
|
||||
headers: { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" },
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { Policy } from "@opencode-ai/core/policy"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { location } from "./fixture/location"
|
||||
import { testEffect } from "./lib/effect"
|
||||
|
||||
const it = testEffect(
|
||||
Policy.defaultLayer.pipe(
|
||||
Layer.provide(
|
||||
Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("test") }))),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
describe("Policy", () => {
|
||||
it.effect("returns the caller's fallback when no statement matches", () =>
|
||||
Effect.gen(function* () {
|
||||
const policy = yield* Policy.Service
|
||||
|
||||
expect(yield* policy.evaluate("provider.use", "anthropic", "allow")).toBe("allow")
|
||||
expect(yield* policy.evaluate("provider.use", "anthropic", "deny")).toBe("deny")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("evaluates wildcard provider rules in written order", () =>
|
||||
Effect.gen(function* () {
|
||||
const policy = yield* Policy.Service
|
||||
yield* policy.load([
|
||||
new Policy.Info({
|
||||
effect: "deny",
|
||||
action: "provider.*",
|
||||
resource: "*",
|
||||
}),
|
||||
new Policy.Info({
|
||||
effect: "allow",
|
||||
action: "provider.use",
|
||||
resource: "anthropic",
|
||||
}),
|
||||
])
|
||||
|
||||
expect(yield* policy.evaluate("provider.use", "anthropic", "allow")).toBe("allow")
|
||||
expect(yield* policy.evaluate("provider.use", "openai", "allow")).toBe("deny")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("matches action and resource independently", () =>
|
||||
Effect.gen(function* () {
|
||||
const policy = yield* Policy.Service
|
||||
yield* policy.load([
|
||||
new Policy.Info({
|
||||
effect: "deny",
|
||||
action: "provider.*",
|
||||
resource: "company-*",
|
||||
}),
|
||||
])
|
||||
|
||||
expect(yield* policy.evaluate("provider.use", "company-stable", "allow")).toBe("deny")
|
||||
expect(yield* policy.evaluate("plugin.load", "company-stable", "allow")).toBe("allow")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("uses the last matching loaded statement", () =>
|
||||
Effect.gen(function* () {
|
||||
const policy = yield* Policy.Service
|
||||
yield* policy.load([
|
||||
new Policy.Info({
|
||||
effect: "allow",
|
||||
action: "provider.use",
|
||||
resource: "openai",
|
||||
}),
|
||||
new Policy.Info({
|
||||
effect: "deny",
|
||||
action: "provider.use",
|
||||
resource: "openai",
|
||||
}),
|
||||
])
|
||||
|
||||
expect(yield* policy.evaluate("provider.use", "openai", "allow")).toBe("deny")
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,220 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { $ } from "bun"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { Effect } from "effect"
|
||||
import { Project } from "@opencode-ai/core/project"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { Hash } from "@opencode-ai/core/util/hash"
|
||||
import { tmpdir } from "./fixture/tmpdir"
|
||||
import { testEffect } from "./lib/effect"
|
||||
|
||||
const it = testEffect(Project.defaultLayer)
|
||||
|
||||
function remoteID(remote: string) {
|
||||
return Project.ID.make(Hash.fast(`git-remote:${remote}`))
|
||||
}
|
||||
|
||||
function abs(value: string) {
|
||||
return AbsolutePath.make(value)
|
||||
}
|
||||
|
||||
function real(value: string) {
|
||||
return Effect.promise(() => fs.realpath(value)).pipe(Effect.map((value) => AbsolutePath.make(value)))
|
||||
}
|
||||
|
||||
async function initRepo(dir: string, opts?: { commit?: boolean; remote?: string }) {
|
||||
await $`git init`.cwd(dir).quiet()
|
||||
await $`git config core.fsmonitor false`.cwd(dir).quiet()
|
||||
await $`git config commit.gpgsign false`.cwd(dir).quiet()
|
||||
await $`git config user.email test@opencode.test`.cwd(dir).quiet()
|
||||
await $`git config user.name Test`.cwd(dir).quiet()
|
||||
if (opts?.commit) await $`git commit --allow-empty -m root`.cwd(dir).quiet()
|
||||
if (opts?.remote) await $`git remote add origin ${opts.remote}`.cwd(dir).quiet()
|
||||
}
|
||||
|
||||
async function rootCommit(dir: string) {
|
||||
return (await $`git rev-list --max-parents=0 HEAD`.cwd(dir).text()).trim()
|
||||
}
|
||||
|
||||
describe("ProjectV2.resolve", () => {
|
||||
it.live("returns global for non-git directory", () =>
|
||||
Effect.gen(function* () {
|
||||
const tmp = yield* Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
)
|
||||
const project = yield* Project.Service
|
||||
|
||||
const result = yield* project.resolve(abs(tmp.path))
|
||||
|
||||
expect(result.id).toBe(Project.ID.make("global"))
|
||||
expect(path.resolve(result.directory)).toBe(path.parse(tmp.path).root)
|
||||
expect(result.previous).toBeUndefined()
|
||||
expect(result.vcs).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("returns git global for repo with no commits and no remote", () =>
|
||||
Effect.gen(function* () {
|
||||
const tmp = yield* Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
)
|
||||
yield* Effect.promise(() => initRepo(tmp.path))
|
||||
const project = yield* Project.Service
|
||||
|
||||
const result = yield* project.resolve(abs(tmp.path))
|
||||
|
||||
expect(result.id).toBe(Project.ID.make("global"))
|
||||
expect(result.directory).toBe(yield* real(tmp.path))
|
||||
expect(result.previous).toBeUndefined()
|
||||
expect(result.vcs?.type).toBe("git")
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("falls back to root commit when origin is missing", () =>
|
||||
Effect.gen(function* () {
|
||||
const tmp = yield* Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
)
|
||||
yield* Effect.promise(() => initRepo(tmp.path, { commit: true }))
|
||||
const project = yield* Project.Service
|
||||
|
||||
const result = yield* project.resolve(abs(tmp.path))
|
||||
|
||||
expect(result.id).toBe(Project.ID.make(yield* Effect.promise(() => rootCommit(tmp.path))))
|
||||
expect(result.directory).toBe(yield* real(tmp.path))
|
||||
expect(result.previous).toBeUndefined()
|
||||
expect(result.vcs?.type).toBe("git")
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("prefers normalized origin over root commit", () =>
|
||||
Effect.gen(function* () {
|
||||
const tmp = yield* Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
)
|
||||
yield* Effect.promise(() => initRepo(tmp.path, { commit: true, remote: "git@github.com:Acme/App.git" }))
|
||||
const project = yield* Project.Service
|
||||
|
||||
const result = yield* project.resolve(abs(tmp.path))
|
||||
|
||||
expect(result.id).toBe(remoteID("github.com/Acme/App"))
|
||||
expect(result.id).not.toBe(Project.ID.make(yield* Effect.promise(() => rootCommit(tmp.path))))
|
||||
expect(result.directory).toBe(yield* real(tmp.path))
|
||||
expect(result.vcs?.type).toBe("git")
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("normalizes ssh and https remotes to the same id", () =>
|
||||
Effect.gen(function* () {
|
||||
const ssh = yield* Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
)
|
||||
const https = yield* Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
)
|
||||
yield* Effect.promise(() => initRepo(ssh.path, { commit: true, remote: "git@github.com:owner/repo.git" }))
|
||||
yield* Effect.promise(() => initRepo(https.path, { commit: true, remote: "https://github.com/owner/repo.git" }))
|
||||
const project = yield* Project.Service
|
||||
|
||||
const a = yield* project.resolve(abs(ssh.path))
|
||||
const b = yield* project.resolve(abs(https.path))
|
||||
|
||||
expect(a.id).toBe(remoteID("github.com/owner/repo"))
|
||||
expect(b.id).toBe(a.id)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("ignores file remotes and falls back to root commit", () =>
|
||||
Effect.gen(function* () {
|
||||
const tmp = yield* Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
)
|
||||
yield* Effect.promise(() => initRepo(tmp.path, { commit: true, remote: `file://${tmp.path}` }))
|
||||
const project = yield* Project.Service
|
||||
|
||||
const result = yield* project.resolve(abs(tmp.path))
|
||||
|
||||
expect(result.id).toBe(Project.ID.make(yield* Effect.promise(() => rootCommit(tmp.path))))
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("returns previous cached id from common dir", () =>
|
||||
Effect.gen(function* () {
|
||||
const tmp = yield* Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
)
|
||||
yield* Effect.promise(() => initRepo(tmp.path, { commit: true, remote: "git@github.com:owner/repo.git" }))
|
||||
yield* Effect.promise(() => Bun.write(path.join(tmp.path, ".git", "opencode"), "old-id"))
|
||||
const project = yield* Project.Service
|
||||
|
||||
const result = yield* project.resolve(abs(tmp.path))
|
||||
|
||||
expect(result.previous).toBe(Project.ID.make("old-id"))
|
||||
expect(result.id).toBe(remoteID("github.com/owner/repo"))
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("does not write the cache while resolving", () =>
|
||||
Effect.gen(function* () {
|
||||
const tmp = yield* Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
)
|
||||
yield* Effect.promise(() => initRepo(tmp.path, { commit: true, remote: "git@github.com:owner/repo.git" }))
|
||||
const project = yield* Project.Service
|
||||
|
||||
yield* project.resolve(abs(tmp.path))
|
||||
|
||||
expect(yield* Effect.promise(() => Bun.file(path.join(tmp.path, ".git", "opencode")).exists())).toBe(false)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("resolves from nested directories to repo root", () =>
|
||||
Effect.gen(function* () {
|
||||
const tmp = yield* Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
)
|
||||
yield* Effect.promise(() => initRepo(tmp.path, { commit: true }))
|
||||
yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, "a", "b"), { recursive: true }))
|
||||
const project = yield* Project.Service
|
||||
|
||||
const result = yield* project.resolve(abs(path.join(tmp.path, "a", "b")))
|
||||
|
||||
expect(result.directory).toBe(yield* real(tmp.path))
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("linked worktree returns opened worktree directory and previous from common dir", () =>
|
||||
Effect.gen(function* () {
|
||||
const tmp = yield* Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
)
|
||||
const worktree = `${tmp.path}-worktree`
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.promise(() => $`rm -rf ${worktree}`.quiet().nothrow()).pipe(Effect.ignore),
|
||||
)
|
||||
yield* Effect.promise(() => initRepo(tmp.path, { commit: true, remote: "git@github.com:owner/repo.git" }))
|
||||
yield* Effect.promise(() => Bun.write(path.join(tmp.path, ".git", "opencode"), "old-id"))
|
||||
yield* Effect.promise(() => $`git worktree add ${worktree} -b test-${Date.now()}`.cwd(tmp.path).quiet())
|
||||
const project = yield* Project.Service
|
||||
|
||||
const result = yield* project.resolve(abs(worktree))
|
||||
|
||||
expect(result.directory).toBe(yield* real(worktree))
|
||||
expect(result.previous).toBe(Project.ID.make("old-id"))
|
||||
expect(result.id).toBe(remoteID("github.com/owner/repo"))
|
||||
expect(result.vcs?.type).toBe("git")
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -335,7 +335,9 @@ const lowerToolResultContent = Effect.fn("AnthropicMessages.lowerToolResultConte
|
||||
// Text / json / error results stay as a string for backward compatibility
|
||||
// with existing cassettes and provider expectations.
|
||||
if (part.result.type !== "content") return ProviderShared.toolResultText(part)
|
||||
return yield* Effect.forEach(part.result.value, lowerToolResultContentItem)
|
||||
// Preserve the narrowed array element type when compiled through a consumer package.
|
||||
const content: ReadonlyArray<ToolResultContentPart> = part.result.value
|
||||
return yield* Effect.forEach(content, lowerToolResultContentItem)
|
||||
})
|
||||
|
||||
const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user