mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-16 09:28:27 -04:00
Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c590e27639 | |||
| 8f4b62eb49 | |||
| 945d1c8cb2 | |||
| 610e618bc5 | |||
| 9daa4d85a4 | |||
| 62af66a74f | |||
| 5b44e5bf41 | |||
| a15afbe8f2 | |||
| 8e0856c43b | |||
| f66c829231 | |||
| 3baaabede8 | |||
| afe3ebbc35 | |||
| e2faeb84e5 | |||
| 35ed09ff37 | |||
| 9751615651 | |||
| c9b24ef027 | |||
| b04d8d53e6 | |||
| 7bd3f8ac83 | |||
| 6ae2fa5196 | |||
| 650d774372 | |||
| 64e4f6f91b | |||
| d097cc8065 | |||
| 438654768c | |||
| 37b26e495b | |||
| 5d14c7a185 | |||
| e66cbf36e9 | |||
| 50977dd4fe | |||
| a644e0e7a0 | |||
| ca861fdf43 | |||
| 34a08cbdb8 | |||
| bf3ae45439 | |||
| 394e0b9045 | |||
| 4790a2772c | |||
| 2ff19171dc | |||
| 1401901529 | |||
| 1b88ff8d53 | |||
| 08293e74a0 | |||
| 24d26365e6 | |||
| c22793d5f6 | |||
| c6a52a39b5 | |||
| aad8d90dd1 | |||
| 6f47459c68 | |||
| dd768e30e2 | |||
| de476aa51b | |||
| bd8d858bf7 | |||
| 5fc4e18b82 |
@@ -1,36 +0,0 @@
|
|||||||
export default {
|
|
||||||
id: "Orchestrator",
|
|
||||||
setup: async (ctx) => {
|
|
||||||
await ctx.agent.transform((agents) => {
|
|
||||||
agents.update("orchestrator", (agent) => {
|
|
||||||
agent.description = "Coordinates work by delegating implementation tasks to the minion subagent."
|
|
||||||
agent.mode = "primary"
|
|
||||||
agent.system = [
|
|
||||||
"You are Orchestrator, the primary coordinating agent for this repository. You do meta work only: you coordinate, brief, and synthesize — you do not perform the work itself.",
|
|
||||||
"Delegate ALL actual work to the minion subagent — implementation, exploration, discovery, searching the codebase, reading files to understand a problem, and even trivial one-line edits. Task size is never a reason to do it yourself, and there is no 'final integration' exception.",
|
|
||||||
"You are not hard-banned from tools, but direct tool use is reserved for coordination overhead: a quick peek to phrase a better brief, a fast read-only check to verify a minion's reported result, or answering a question about coordination state. If a tool call is producing the answer or the artifact the user asked for, that call belongs to a minion, not you.",
|
|
||||||
"Exploration is work. If the user asks how something works or where something lives, delegate the investigation to a minion rather than exploring yourself.",
|
|
||||||
"Always start minion subagents in the background. Even if you have nothing else to coordinate right now, the user may assign you new work while a Minion runs, and you must stay free to receive it. Never poll; you will be notified when they finish.",
|
|
||||||
"Give each minion a clear, self-contained brief: the goal, constraints, expected output, and any files or context already known from the user or previous minion reports.",
|
|
||||||
"Synthesize minion results, decide next steps, and report back concisely.",
|
|
||||||
].join("\n")
|
|
||||||
})
|
|
||||||
|
|
||||||
agents.update("minion", (agent) => {
|
|
||||||
agent.description = "Subagent that executes focused tasks delegated by Orchestrator."
|
|
||||||
agent.mode = "subagent"
|
|
||||||
agent.model = { providerID: "opencode", id: "glm-5.2" }
|
|
||||||
agent.system = [
|
|
||||||
"You are minion, a focused execution subagent for this repository.",
|
|
||||||
"Complete the specific task delegated to you by Orchestrator using the available tools.",
|
|
||||||
"Inspect the codebase before making assumptions, make targeted changes when requested, and verify your work when feasible.",
|
|
||||||
"Follow the repository's AGENTS.md conventions: respect the style guide, run `bun typecheck` from the affected package directory after code changes, never run tests from the repo root, and do not modify packages/opencode unless the task explicitly says V1 work.",
|
|
||||||
"If the task is ambiguous or you hit a blocker, stop and report your findings instead of guessing.",
|
|
||||||
"Keep your final response concise: summarize what you did, list important files changed or findings, and call out blockers or verification gaps.",
|
|
||||||
"Do not delegate to other subagents; execute the assigned work yourself.",
|
|
||||||
].join("\n")
|
|
||||||
agent.permissions.push({ action: "subagent", resource: "*", effect: "deny" })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -104,6 +104,25 @@ bun dev api <operationId> --param key=value
|
|||||||
- If no compatible background server is registered, `bun dev api` starts one through the daemon service. Use `bun dev service status`, `bun dev service restart`, and `bun dev service stop` when you need explicit lifecycle control.
|
- If no compatible background server is registered, `bun dev api` starts one through the daemon service. Use `bun dev service status`, `bun dev service restart`, and `bun dev service stop` when you need explicit lifecycle control.
|
||||||
- Prefer raw method/path calls for quick server debugging and operation IDs when exercising documented OpenAPI routes with path or query parameters.
|
- Prefer raw method/path calls for quick server debugging and operation IDs when exercising documented OpenAPI routes with path or query parameters.
|
||||||
|
|
||||||
|
## Auditing installed `opencode2` sessions
|
||||||
|
|
||||||
|
Installed next-channel sessions normally use `~/.local/share/opencode/opencode-next.db` and `~/.local/share/opencode/log/opencode.log`; `OPENCODE_DB` can override the database. Before calling `opencode2 api`, inspect `~/.local/state/opencode/service.json` because the command may start a daemon when none is healthy.
|
||||||
|
|
||||||
|
For a supplied `ses_...` ID, compare three sources:
|
||||||
|
|
||||||
|
- `opencode2 api get /api/session/active` and the Session/message endpoints for live server state.
|
||||||
|
- The database's ordered `event` rows for durable history.
|
||||||
|
- `packages/tui/src/context/data.tsx` and the relevant route for client projection and rendering.
|
||||||
|
|
||||||
|
Locate an uncertain database without modifying it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SESSION=ses_...
|
||||||
|
for db in ~/.local/share/opencode/*.db; do
|
||||||
|
sqlite3 "file:$db?mode=ro" "select 1 from session where id='$SESSION' limit 1" 2>/dev/null | grep -q 1 && printf '%s\n' "$db"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
## Logs
|
## Logs
|
||||||
|
|
||||||
- Log files live under `~/.local/share/opencode/log/`. In a local/dev checkout the active file is `opencode-local.log`; `opencode.log` is used for non-local (released) channel installs. Both are append-only, shared across every CLI and server process on the machine.
|
- Log files live under `~/.local/share/opencode/log/`. In a local/dev checkout the active file is `opencode-local.log`; `opencode.log` is used for non-local (released) channel installs. Both are append-only, shared across every CLI and server process on the machine.
|
||||||
|
|||||||
@@ -151,13 +151,15 @@ const table = sqliteTable("session", {
|
|||||||
|
|
||||||
## V2 Session Core
|
## V2 Session Core
|
||||||
|
|
||||||
|
- Keep durable events minimal: record irreducible new facts and do not repeat state derivable by folding the ordered aggregate history. Enrich projections and read models with previous or derived state when consumers need self-contained views.
|
||||||
- Keep durable prompt admission separate from model execution. `SessionV2.prompt(...)` admits one durable `session_input` row before scheduling advisory `SessionExecution.wake(sessionID)` unless `resume: false` requests admit-only behavior. The serialized runner promotes admitted inputs into visible user messages at safe boundaries.
|
- Keep durable prompt admission separate from model execution. `SessionV2.prompt(...)` admits one durable `session_input` row before scheduling advisory `SessionExecution.wake(sessionID)` unless `resume: false` requests admit-only behavior. The serialized runner promotes admitted inputs into visible user messages at safe boundaries.
|
||||||
- Reusing a Session ID adopts the existing Session. Reusing a prompt message ID reconciles an exact retry only when Session, prompt, and delivery mode match; conflicting reuse fails. Historical projected prompts lazily synthesize promoted inbox records during exact retry.
|
- Reusing a Session ID adopts the existing Session. Reusing a prompt message ID reconciles an exact retry only when Session, prompt, and delivery mode match; conflicting reuse fails. Historical projected prompts lazily synthesize promoted inbox records during exact retry.
|
||||||
- Keep `SessionExecution` process-global and Session-ID based. Its local implementation owns the process-local Session coordinator and discovers placement through `SessionStore` plus `LocationServiceMap.get(session.location)` only when a drain starts; no layer should take a Session ID. V2 interruption targets the active process-local ownership chain for that Session; idle or missing interruption is a no-op.
|
- Keep `SessionExecution` process-global and Session-ID based. Its local implementation owns the process-local Session coordinator and discovers placement through `SessionStore` plus `LocationServiceMap.get(session.location)` only when a drain starts; no layer should take a Session ID. V2 interruption targets the active process-local ownership chain for that Session; idle or missing interruption is a no-op.
|
||||||
- Keep `SessionRunner`, model resolution, tool registry, permissions, and filesystem Location-scoped. Omitted `Location.workspaceID` means implicit-local placement; explicit workspace identity remains reserved for future placement semantics.
|
- Keep `SessionRunner`, model resolution, tool registry, permissions, and filesystem Location-scoped. Omitted `Location.workspaceID` means implicit-local placement; explicit workspace identity remains reserved for future placement semantics.
|
||||||
- Preserve one explicit `llm.stream(request)` call per provider turn and reload projected history before durable continuation. Do not bridge through legacy `SessionPrompt.loop(...)` or delegate orchestration to an in-memory tool loop.
|
- Preserve one explicit `llm.stream(request)` call per step and reload projected history before durable continuation. Do not bridge through legacy `SessionPrompt.loop(...)` or delegate orchestration to an in-memory tool loop.
|
||||||
- Keep local Session drains process-local until clustering is implemented. `SessionRunCoordinator` joins explicit same-Session resumes, coalesces prompt wakeups, and allows different Sessions to run concurrently. Advisory wakes drain eligible durable inbox rows only; post-crash continuation recovery requires a separate explicit design before it may retry provider work. A drain has no durable identity or transcript boundary.
|
- Keep local Session drains process-local until clustering is implemented. `SessionRunCoordinator` joins explicit same-Session resumes, coalesces prompt wakeups, and allows different Sessions to run concurrently. Advisory wakes drain eligible durable inbox rows only; post-crash continuation recovery requires a separate explicit design before it may retry provider work. A drain has no durable identity or transcript boundary.
|
||||||
- Keep delivery vocabulary explicit. Prompts steer by default and promote at the next safe provider-turn boundary while the current drain requires continuation. An explicit `queue` input remains pending until the Session would otherwise become idle; promote one queued input at that boundary, then reevaluate continuation before promoting another. Promoting any new user input resets the selected agent's provider-turn allowance; a batch of steers resets it once.
|
- Keep delivery vocabulary explicit. Prompts steer by default and promote at the next safe step boundary while the current drain requires continuation. An explicit `queue` input remains pending until the Session would otherwise become idle; promote one queued input at that boundary, then reevaluate continuation before promoting another. Promoting any new user input resets the selected agent's step allowance; a batch of steers resets it once.
|
||||||
|
- One step is one logical LLM call; its durable record covers only the model-visible span. Do not write "provider turn", and do not use bare "turn" for a single call: "turn" is reserved for the future assistant-turn unit containing all steps from prompt promotion until the session would go idle.
|
||||||
- Keep EventV2 replay owner claims separate from clustered Session execution ownership.
|
- Keep EventV2 replay owner claims separate from clustered Session execution ownership.
|
||||||
- Keep the System Context algebra and built-ins in `src/system-context`; keep Context Source producers with their observed domains, and keep Session History selection plus Context Checkpoint persistence Session-owned. The runner composes all context producers explicitly in `loadSystemContext`; there is no context registry.
|
- Keep the System Context algebra and built-ins in `src/system-context`; keep Context Source producers with their observed domains, and keep Session History selection plus Context Checkpoint persistence Session-owned. The runner composes all context producers explicitly in `loadSystemContext`; there is no context registry.
|
||||||
- The durable Applied record is what the model was last told, per source. Reconcile narrates drift as chronological System updates and never rewrites the baseline; only completed compaction rebaselines, and move or committed revert resets the checkpoint. Unavailable sources keep the model's prior belief, blocking only a session's first baseline.
|
- The durable Applied record is what the model was last told, per source. Reconcile narrates drift as chronological System updates and never rewrites the baseline; only completed compaction rebaselines, and move or committed revert resets the checkpoint. Unavailable sources keep the model's prior belief, blocking only a session's first baseline.
|
||||||
|
|||||||
+38
-23
@@ -9,7 +9,7 @@ The structured collection of contextual facts presented to the model as initial
|
|||||||
_Avoid_: System prompt
|
_Avoid_: System prompt
|
||||||
|
|
||||||
**Session History**:
|
**Session History**:
|
||||||
The projected chronological conversation selected for a provider turn after applying the active compaction and **Context Epoch** cutoffs.
|
The projected chronological conversation selected for a **Step** after applying the active compaction and **Context Epoch** cutoffs.
|
||||||
_Avoid_: Session Context
|
_Avoid_: Session Context
|
||||||
|
|
||||||
**Context Source**:
|
**Context Source**:
|
||||||
@@ -31,13 +31,13 @@ The full **System Context** rendered at the start of a **Context Epoch**.
|
|||||||
_Avoid_: Live system prompt
|
_Avoid_: Live system prompt
|
||||||
|
|
||||||
**Context Snapshot**:
|
**Context Snapshot**:
|
||||||
The overwriteable model-hidden JSON state used to compare each **Context Source** with the value last admitted to a provider turn.
|
The overwriteable model-hidden JSON state used to compare each **Context Source** with the value last admitted to a **Step**.
|
||||||
|
|
||||||
**Unavailable Context**:
|
**Unavailable Context**:
|
||||||
An expected temporary inability to observe a **Context Source** value; the runtime retains its prior effective state and emits no update, or omits it until first successfully loaded.
|
An expected temporary inability to observe a **Context Source** value; the runtime retains its prior effective state and emits no update, or omits it until first successfully loaded.
|
||||||
|
|
||||||
**Safe Provider-Turn Boundary**:
|
**Safe Step Boundary**:
|
||||||
The point immediately before a provider call, after durable input promotion and any required tool settlement, where context changes may be admitted chronologically.
|
The point immediately before a provider request, after durable input promotion and any required tool settlement, where context changes may be admitted chronologically.
|
||||||
|
|
||||||
**Admitted Prompt**:
|
**Admitted Prompt**:
|
||||||
A durable user input accepted into the Session inbox but not yet included in **Session History**.
|
A durable user input accepted into the Session inbox but not yet included in **Session History**.
|
||||||
@@ -45,11 +45,24 @@ A durable user input accepted into the Session inbox but not yet included in **S
|
|||||||
**Prompt Promotion**:
|
**Prompt Promotion**:
|
||||||
The durable transition that removes an **Admitted Prompt** from pending input and appends its user message to **Session History**.
|
The durable transition that removes an **Admitted Prompt** from pending input and appends its user message to **Session History**.
|
||||||
|
|
||||||
**Provider Turn**:
|
**Step**:
|
||||||
One request to a model provider and the response projected from that request.
|
One logical LLM call spanning pre-flight context checkpoint preparation, input promotion, request build, and compaction check; the provider stream; and tool settlement.
|
||||||
|
_Avoid_: provider turn, turn (unqualified)
|
||||||
|
|
||||||
|
**Physical Attempt**:
|
||||||
|
One actual provider request on the wire in service of a **Step**; most Steps have one Physical Attempt, while overflow-triggered compaction recovery may give one Step two.
|
||||||
|
|
||||||
|
**Assistant Turn**:
|
||||||
|
A reserved name for the not-yet-modeled unit containing all **Steps** from prompt promotion until the assistant yields the floor; do not reify it until something durable needs it.
|
||||||
|
|
||||||
|
**Settlement**:
|
||||||
|
The terminal transition for a unit of work: Step and tool settlement are durable, while drain and execution settlement are coordinator-observed.
|
||||||
|
|
||||||
|
**Execution**:
|
||||||
|
One session-scoped coordinator busy period from first wake until idle. An Execution is process-local coordination rather than a durable domain entity.
|
||||||
|
|
||||||
**Session Drain**:
|
**Session Drain**:
|
||||||
One process-local execution span that promotes eligible input and runs required **Provider Turns** until no immediate continuation remains. A Session Drain has no durable identity or transcript boundary.
|
One process-local execution span that promotes eligible input and runs required **Steps** until no immediate continuation remains. A Session Drain has no durable identity or transcript boundary.
|
||||||
|
|
||||||
**Model Tool Output**:
|
**Model Tool Output**:
|
||||||
The bounded projection of a Core-executed tool result persisted in Session history and replayed to the model. A tool may shape this projection semantically, but the Tool Registry enforces the final size limit.
|
The bounded projection of a Core-executed tool result persisted in Session history and replayed to the model. A tool may shape this projection semantically, but the Tool Registry enforces the final size limit.
|
||||||
@@ -89,23 +102,25 @@ _Avoid_: Response envelope
|
|||||||
|
|
||||||
- A **System Context** is an opaque carrier composed from zero or more **Context Sources**.
|
- A **System Context** is an opaque carrier composed from zero or more **Context Sources**.
|
||||||
- **Session History** contains projected conversational messages and admitted **Mid-Conversation System Messages**; the active **Baseline System Context** remains separate provider-request state.
|
- **Session History** contains projected conversational messages and admitted **Mid-Conversation System Messages**; the active **Baseline System Context** remains separate provider-request state.
|
||||||
- The **System Context Registry** uses stable-keyed scoped contributions to assemble the current **System Context**; contributor removal naturally removes its sources at the next **Safe Provider-Turn Boundary**.
|
- The **System Context Registry** uses stable-keyed scoped contributions to assemble the current **System Context**; contributor removal naturally removes its sources at the next **Safe Step Boundary**.
|
||||||
- A changed **Context Source** may produce one **Mid-Conversation System Message** containing its newly effective state.
|
- A changed **Context Source** may produce one **Mid-Conversation System Message** containing its newly effective state.
|
||||||
- A **Mid-Conversation System Message** persists the exact combined rendered text sent to the model.
|
- A **Mid-Conversation System Message** persists the exact combined rendered text sent to the model.
|
||||||
- The current **Context Snapshot** advances atomically with the corresponding durable **Mid-Conversation System Message**.
|
- The current **Context Snapshot** advances atomically with the corresponding durable **Mid-Conversation System Message**.
|
||||||
- A **Context Snapshot** stores one codec-encoded JSON value and, for removable dynamic sources, a pre-rendered removal message per stable **Context Source** key.
|
- A **Context Snapshot** stores one codec-encoded JSON value and, for removable dynamic sources, a pre-rendered removal message per stable **Context Source** key.
|
||||||
- Changes from multiple **Context Sources** admitted at one safe boundary combine into one **Mid-Conversation System Message**.
|
- Changes from multiple **Context Sources** admitted at one safe boundary combine into one **Mid-Conversation System Message**.
|
||||||
- Context changes are sampled and admitted lazily at a **Safe Provider-Turn Boundary**, never pushed asynchronously when their source changes.
|
- Context changes are sampled and admitted lazily at a **Safe Step Boundary**, never pushed asynchronously when their source changes.
|
||||||
- At a **Safe Provider-Turn Boundary**, newly promoted user input or settled tool results precede any combined **Mid-Conversation System Message**.
|
- At a **Safe Step Boundary**, newly promoted user input or settled tool results precede any combined **Mid-Conversation System Message**.
|
||||||
- An **Admitted Prompt** is replayable pending input, not yet model-visible **Session History**.
|
- An **Admitted Prompt** is replayable pending input, not yet model-visible **Session History**.
|
||||||
- **Prompt Promotion** atomically consumes the pending inbox entry and appends its model-visible user message.
|
- **Prompt Promotion** atomically consumes the pending inbox entry and appends its model-visible user message.
|
||||||
- Steering prompts promote at the next **Safe Provider-Turn Boundary** while the current **Session Drain** still requires continuation. Promoting any newly admitted user input resets the selected agent's provider-turn allowance; multiple prompts promoted at one boundary reset it once.
|
- Steering prompts promote at the next **Safe Step Boundary** while the current **Session Drain** still requires continuation. Promoting any newly admitted user input resets the selected agent's step allowance; multiple prompts promoted at one boundary reset it once.
|
||||||
- A queued prompt does not promote while the current **Session Drain** requires continuation. The runner promotes one queued prompt when the Session would otherwise become idle, then reevaluates continuation before promoting another.
|
- A queued prompt does not promote while the current **Session Drain** requires continuation. The runner promotes one queued prompt when the Session would otherwise become idle, then reevaluates continuation before promoting another.
|
||||||
- A **Session Drain** is process-local coordination rather than a durable domain entity. Durable recovery must reason from prompts, projected history, provider attempts, and tool state rather than inventing an enclosing execution identity.
|
- A **Session Drain** is process-local coordination rather than a durable domain entity. Durable recovery must reason from prompts, projected history, physical attempts, and tool state rather than inventing an enclosing execution identity.
|
||||||
- The first provider turn renders the latest complete **Baseline System Context** and initializes its **Context Snapshot** without emitting a redundant **Mid-Conversation System Message**; unavailable initial context blocks the turn instead of persisting an incomplete baseline.
|
- An **Execution** contains one or more **Session Drains**; a **Session Drain** contains one reserved assistant-turn span at a time; that span contains **Steps**; and each **Step** contains one or more **Physical Attempts** plus any tool calls it requires.
|
||||||
|
- A **Step** record covers only the model-visible span from first assistant output through tool settlement; pre-flight leaves no record, and one Step settles at most one record.
|
||||||
|
- The first **Step** renders the latest complete **Baseline System Context** and initializes its **Context Snapshot** without emitting a redundant **Mid-Conversation System Message**; unavailable initial context blocks the Step instead of persisting an incomplete baseline.
|
||||||
- Initial **System Context** preparation precedes the first durable input promotion so an unavailable baseline leaves that input pending and retryable; ordinary reconciliation remains after promotion.
|
- Initial **System Context** preparation precedes the first durable input promotion so an unavailable baseline leaves that input pending and retryable; ordinary reconciliation remains after promotion.
|
||||||
- Compaction starts a new **Context Epoch** with a freshly rendered **Baseline System Context** and **Context Snapshot**; prior **Mid-Conversation System Messages** remain durable audit history but leave projected model history.
|
- Compaction starts a new **Context Epoch** with a freshly rendered **Baseline System Context** and **Context Snapshot**; prior **Mid-Conversation System Messages** remain durable audit history but leave projected model history.
|
||||||
- A newly registered core or plugin-defined **Context Source** absent from the current snapshot emits its baseline rendering once at the next **Safe Provider-Turn Boundary**.
|
- A newly registered core or plugin-defined **Context Source** absent from the current snapshot emits its baseline rendering once at the next **Safe Step Boundary**.
|
||||||
- **Context Source** keys are stable and namespaced; duplicate keys fail composition. `SystemContext.combine(...)` preserves caller order; the **System Context Registry** evaluates producers concurrently and combines them in stable contribution-key order so rendered context remains deterministic.
|
- **Context Source** keys are stable and namespaced; duplicate keys fail composition. `SystemContext.combine(...)` preserves caller order; the **System Context Registry** evaluates producers concurrently and combines them in stable contribution-key order so rendered context remains deterministic.
|
||||||
- Each **Context Source** loader returns one coherent typed value. `SystemContext.make(...)` hides that value type so differently typed sources compose uniformly. Its codec compares and stores that value; its pure renderers produce model-visible baseline, update, and removal text only when needed.
|
- Each **Context Source** loader returns one coherent typed value. `SystemContext.make(...)` hides that value type so differently typed sources compose uniformly. Its codec compares and stores that value; its pure renderers produce model-visible baseline, update, and removal text only when needed.
|
||||||
- `SystemContext.initialize(...)` observes a composed **System Context** once and produces a fresh **Baseline System Context** with its **Context Snapshot**.
|
- `SystemContext.initialize(...)` observes a composed **System Context** once and produces a fresh **Baseline System Context** with its **Context Snapshot**.
|
||||||
@@ -113,26 +128,26 @@ _Avoid_: Response envelope
|
|||||||
- `SystemContext.replace(...)` renders a fresh generation after completed compaction or another baseline-replacing transition; it reports replacement blocked while previously admitted context is unavailable.
|
- `SystemContext.replace(...)` renders a fresh generation after completed compaction or another baseline-replacing transition; it reports replacement blocked while previously admitted context is unavailable.
|
||||||
- **Unavailable Context** uses stale-while-revalidate semantics and is distinct from a successfully loaded absence, which may emit removal text.
|
- **Unavailable Context** uses stale-while-revalidate semantics and is distinct from a successfully loaded absence, which may emit removal text.
|
||||||
- Ordinary **Context Source** loaders return values directly; loaders that intentionally use stale-while-revalidate may explicitly return **Unavailable Context**.
|
- Ordinary **Context Source** loaders return values directly; loaders that intentionally use stale-while-revalidate may explicitly return **Unavailable Context**.
|
||||||
- Nested project instruction discovery after successful reads remains a follow-up; when implemented, discovered instructions must be admitted durably at the next **Safe Provider-Turn Boundary**.
|
- Nested project instruction discovery after successful reads remains a follow-up; when implemented, discovered instructions must be admitted durably at the next **Safe Step Boundary**.
|
||||||
- Location-scoped services naturally re-resolve effective context when a moved session next runs in its destination location.
|
- Location-scoped services naturally re-resolve effective context when a moved session next runs in its destination location.
|
||||||
- Moving a Session clears its active **Context Epoch**, so the destination must initialize a complete baseline before another prompt can promote.
|
- Moving a Session clears its active **Context Epoch**, so the destination must initialize a complete baseline before another prompt can promote.
|
||||||
- Instruction discovery, source identity, persistence, and file loading belong to the instruction service; the **System Context** abstraction only composes effectful producers and renders loaded values.
|
- Instruction discovery, source identity, persistence, and file loading belong to the instruction service; the **System Context** abstraction only composes effectful producers and renders loaded values.
|
||||||
- The first instruction-service slice observes global and upward project `AGENTS.md` files as one ordered aggregate **Context Source** at each **Safe Provider-Turn Boundary**.
|
- The first instruction-service slice observes global and upward project `AGENTS.md` files as one ordered aggregate **Context Source** at each **Safe Step Boundary**.
|
||||||
- Built-in and instruction context producers register through the **System Context Registry** with stable contribution keys. Plugin-defined context registration and hot-reload lifecycle remain a follow-up built on the same scoped registry seam.
|
- Built-in and instruction context producers register through the **System Context Registry** with stable contribution keys. Plugin-defined context registration and hot-reload lifecycle remain a follow-up built on the same scoped registry seam.
|
||||||
- Selected-agent available-skill guidance is a **Context Source** composed with Location-wide registry sources immediately before Context Epoch admission. It lists only names and descriptions permitted for that agent; skill bodies and locations are exposed only through the permission-checked `skill` tool.
|
- Selected-agent available-skill guidance is a **Context Source** composed with Location-wide registry sources immediately before Context Epoch admission. It lists only names and descriptions permitted for that agent; skill bodies and locations are exposed only through the permission-checked `skill` tool.
|
||||||
- The selected agent and model are sampled when a provider turn starts. Changes admitted after that boundary apply to the next provider turn and do not restart the current turn.
|
- The selected agent and model are sampled when a **Step** starts. Changes admitted after that boundary apply to the next Step and do not restart the current Step.
|
||||||
- Selected-agent available-skill guidance remains a **Context Source**. An agent switch that changes that guidance produces a **Mid-Conversation System Message** while preserving the current baseline.
|
- Selected-agent available-skill guidance remains a **Context Source**. An agent switch that changes that guidance produces a **Mid-Conversation System Message** while preserving the current baseline.
|
||||||
- Local tool authorization and pending permission requests retain the effective agent of the provider turn that issued the call; a later agent switch cannot change that call's policy.
|
- Local tool authorization and pending permission requests retain the effective agent of the **Step** that issued the call; a later agent switch cannot change that call's policy.
|
||||||
- Context source changes never wake idle sessions; the next naturally scheduled **Safe Provider-Turn Boundary** loads and compares current values lazily.
|
- Context source changes never wake idle sessions; the next naturally scheduled **Safe Step Boundary** loads and compares current values lazily.
|
||||||
- Once admitted, a **Mid-Conversation System Message** remains durable even if the following provider attempt fails and is replayed unchanged on retry.
|
- Once admitted, a **Mid-Conversation System Message** remains durable even if the following **Physical Attempt** fails and is replayed unchanged on retry.
|
||||||
- **Mid-Conversation System Messages** remain durable Session-message history; normal user-facing transcript surfaces may hide them.
|
- **Mid-Conversation System Messages** remain durable Session-message history; normal user-facing transcript surfaces may hide them.
|
||||||
- The date **Context Source** initially preserves host-local calendar-date behavior; a configured user timezone may replace that default later.
|
- The date **Context Source** initially preserves host-local calendar-date behavior; a configured user timezone may replace that default later.
|
||||||
- A **Context Epoch** begins with one immutable **Baseline System Context**.
|
- A **Context Epoch** begins with one immutable **Baseline System Context**.
|
||||||
- A **Baseline System Context** is stored durably and reused verbatim across process restarts within its **Context Epoch**.
|
- A **Baseline System Context** is stored durably and reused verbatim across process restarts within its **Context Epoch**.
|
||||||
- A **Baseline System Context** durably preserves the exact joined text used for the active provider-cache prefix.
|
- A **Baseline System Context** durably preserves the exact joined text used for the active provider-cache prefix.
|
||||||
- Completed compaction starts a new **Context Epoch** on the next provider attempt, folding the current complete **System Context** into a fresh baseline and removing earlier **Mid-Conversation System Messages** from active model history.
|
- Completed compaction starts a new **Context Epoch** on the next **Physical Attempt**, folding the current complete **System Context** into a fresh baseline and removing earlier **Mid-Conversation System Messages** from active model history.
|
||||||
- A model/provider switch preserves the current **Context Epoch** and chronological conversation history; the new selection applies to the next provider turn.
|
- A model/provider switch preserves the current **Context Epoch** and chronological conversation history; the new selection applies to the next **Step**.
|
||||||
- **Native Continuation Metadata** remains in durable history. Provider-turn projection includes it only for a successful exact originating provider/model match; failed turns and incompatible models omit opaque metadata, while non-empty visible reasoning lowers to ordinary assistant text after a model switch. This conservative relation may widen only when recorded provider tests establish compatibility.
|
- **Native Continuation Metadata** remains in durable history. Step projection includes it only for a successful exact originating provider/model match; failed Steps and incompatible models omit opaque metadata, while non-empty visible reasoning lowers to ordinary assistant text after a model switch. This conservative relation may widen only when recorded provider tests establish compatibility.
|
||||||
- **Model Request Options** remain provider-semantic through Catalog resolution. The Session runner maps them into the LLM package's provider-option namespace; the selected protocol adapter alone owns provider wire encoding.
|
- **Model Request Options** remain provider-semantic through Catalog resolution. The Session runner maps them into the LLM package's provider-option namespace; the selected protocol adapter alone owns provider wire encoding.
|
||||||
- **Generation Controls**, protocol-semantic **Model Request Options**, and compatibility request body fields are separate Catalog domains. A shared ingestion adapter partitions legacy and models.dev AI-SDK-shaped options before routing.
|
- **Generation Controls**, protocol-semantic **Model Request Options**, and compatibility request body fields are separate Catalog domains. A shared ingestion adapter partitions legacy and models.dev AI-SDK-shaped options before routing.
|
||||||
- The **PTY Environment** is a server concern rather than a Core PTY concern. PTY creation merges caller values, then the host overlay, then Core-forced terminal invariants such as `TERM` and `OPENCODE_TERMINAL`.
|
- The **PTY Environment** is a server concern rather than a Core PTY concern. PTY creation merges caller values, then the host overlay, then Core-forced terminal invariants such as `TERM` and `OPENCODE_TERMINAL`.
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@actions/artifact": "5.0.1",
|
"@actions/artifact": "5.0.1",
|
||||||
|
"@ast-grep/cli": "0.44.0",
|
||||||
"@tsconfig/bun": "catalog:",
|
"@tsconfig/bun": "catalog:",
|
||||||
"@types/mime-types": "3.0.1",
|
"@types/mime-types": "3.0.1",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
@@ -121,14 +122,14 @@
|
|||||||
},
|
},
|
||||||
"packages/client": {
|
"packages/client": {
|
||||||
"name": "@opencode-ai/client",
|
"name": "@opencode-ai/client",
|
||||||
|
"version": "1.17.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/protocol": "workspace:*",
|
"@opencode-ai/protocol": "workspace:*",
|
||||||
"@opencode-ai/schema": "workspace:*",
|
"@opencode-ai/schema": "workspace:*",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@effect/platform-node": "catalog:",
|
||||||
"@opencode-ai/httpapi-codegen": "workspace:*",
|
"@opencode-ai/httpapi-codegen": "workspace:*",
|
||||||
"@opencode-ai/server": "workspace:*",
|
|
||||||
"@tsconfig/bun": "catalog:",
|
"@tsconfig/bun": "catalog:",
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
@@ -597,7 +598,6 @@
|
|||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
"@openauthjs/openauth": "catalog:",
|
"@openauthjs/openauth": "catalog:",
|
||||||
"@opencode-ai/client": "workspace:*",
|
"@opencode-ai/client": "workspace:*",
|
||||||
"@opencode-ai/codemode": "workspace:*",
|
|
||||||
"@opencode-ai/llm": "workspace:*",
|
"@opencode-ai/llm": "workspace:*",
|
||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
"@opencode-ai/protocol": "workspace:*",
|
"@opencode-ai/protocol": "workspace:*",
|
||||||
@@ -694,6 +694,7 @@
|
|||||||
"version": "1.17.13",
|
"version": "1.17.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "3.0.8",
|
"@ai-sdk/provider": "3.0.8",
|
||||||
|
"@opencode-ai/client": "workspace:*",
|
||||||
"@opencode-ai/llm": "workspace:*",
|
"@opencode-ai/llm": "workspace:*",
|
||||||
"@opencode-ai/protocol": "workspace:*",
|
"@opencode-ai/protocol": "workspace:*",
|
||||||
"@opencode-ai/schema": "workspace:*",
|
"@opencode-ai/schema": "workspace:*",
|
||||||
@@ -795,6 +796,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/protocol": "workspace:*",
|
"@opencode-ai/protocol": "workspace:*",
|
||||||
|
"@opencode-ai/simulation": "workspace:*",
|
||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -848,6 +850,21 @@
|
|||||||
"vite": "catalog:",
|
"vite": "catalog:",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"packages/simulation": {
|
||||||
|
"name": "@opencode-ai/simulation",
|
||||||
|
"version": "1.17.13",
|
||||||
|
"dependencies": {
|
||||||
|
"@opencode-ai/core": "workspace:*",
|
||||||
|
"@opencode-ai/llm": "workspace:*",
|
||||||
|
"@opentui/core": "catalog:",
|
||||||
|
"effect": "catalog:",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tsconfig/bun": "catalog:",
|
||||||
|
"@types/bun": "catalog:",
|
||||||
|
"@typescript/native-preview": "catalog:",
|
||||||
|
},
|
||||||
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.17.13",
|
"version": "1.17.13",
|
||||||
@@ -962,6 +979,7 @@
|
|||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
|
"@opencode-ai/simulation": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@opentui/core": "catalog:",
|
"@opentui/core": "catalog:",
|
||||||
"@opentui/keymap": "catalog:",
|
"@opentui/keymap": "catalog:",
|
||||||
@@ -1236,6 +1254,22 @@
|
|||||||
|
|
||||||
"@anycable/core": ["@anycable/core@0.9.2", "", { "dependencies": { "nanoevents": "^7.0.1" } }, "sha512-x5ZXDcW/N4cxWl93CnbHs/u7qq4793jS2kNPWm+duPrXlrva+ml2ZGT7X9tuOBKzyIHf60zWCdIK7TUgMPAwXA=="],
|
"@anycable/core": ["@anycable/core@0.9.2", "", { "dependencies": { "nanoevents": "^7.0.1" } }, "sha512-x5ZXDcW/N4cxWl93CnbHs/u7qq4793jS2kNPWm+duPrXlrva+ml2ZGT7X9tuOBKzyIHf60zWCdIK7TUgMPAwXA=="],
|
||||||
|
|
||||||
|
"@ast-grep/cli": ["@ast-grep/cli@0.44.0", "", { "dependencies": { "detect-libc": "2.1.2" }, "optionalDependencies": { "@ast-grep/cli-darwin-arm64": "0.44.0", "@ast-grep/cli-darwin-x64": "0.44.0", "@ast-grep/cli-linux-arm64-gnu": "0.44.0", "@ast-grep/cli-linux-x64-gnu": "0.44.0", "@ast-grep/cli-win32-arm64-msvc": "0.44.0", "@ast-grep/cli-win32-ia32-msvc": "0.44.0", "@ast-grep/cli-win32-x64-msvc": "0.44.0" }, "bin": { "sg": "sg", "ast-grep": "ast-grep" } }, "sha512-Jf4PuP7XjzsMa3m9gYxmzV8KyWZc4w1ZzKe/t0+90wWxmSasQJe6AtMkJxHEi98MGgfAF1nWziqjDd0/6EsBjA=="],
|
||||||
|
|
||||||
|
"@ast-grep/cli-darwin-arm64": ["@ast-grep/cli-darwin-arm64@0.44.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bF7euu/hF/cYg4510z8110vh60rrqfrBdsfRqVGd6xqNSPENu7CJnTVN/Z4Nk5U1NM8YKzUD+dYx1ySUJ0CUNQ=="],
|
||||||
|
|
||||||
|
"@ast-grep/cli-darwin-x64": ["@ast-grep/cli-darwin-x64@0.44.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-0fI9caQGp1dFcmBATNlVytIRdAeYb91v1D2xjMIi1bSX+l8Uj846JUiaimUGBuBZmyFq+BScoWM4RnprEmZMpQ=="],
|
||||||
|
|
||||||
|
"@ast-grep/cli-linux-arm64-gnu": ["@ast-grep/cli-linux-arm64-gnu@0.44.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-JB6EUnqEtGGtyg1GqNquld/++1CvaWD7r84IwwhddX1qx0NmDoHyn2mKd8vnQ24Z0RkV3g7y7foMLakELbGtDw=="],
|
||||||
|
|
||||||
|
"@ast-grep/cli-linux-x64-gnu": ["@ast-grep/cli-linux-x64-gnu@0.44.0", "", { "os": "linux", "cpu": "x64" }, "sha512-rNL0LsI682D9EMzfaGVEtZa1xaqTtGb2I+Zk4ZzidX6u+fF7f79wdqyKahKjXzoIrGkuhkoL3gcyLKAtQd9+qg=="],
|
||||||
|
|
||||||
|
"@ast-grep/cli-win32-arm64-msvc": ["@ast-grep/cli-win32-arm64-msvc@0.44.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-lqD0MhGQAddh2YoV/brKQ6GVcFLmRiTBwIElutwedaUvRCdasTGukFPYuSWk/iI8Kv19xom6s7l+mGuZ7v+xwQ=="],
|
||||||
|
|
||||||
|
"@ast-grep/cli-win32-ia32-msvc": ["@ast-grep/cli-win32-ia32-msvc@0.44.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-ZJrnS+2OkNfwyr6yrN69glP67uybBxDvl9mqZvh1J44vB3OFn9U9c+cVAoZIAo7JD5F4rZNxwyu3gcy4+xuwEA=="],
|
||||||
|
|
||||||
|
"@ast-grep/cli-win32-x64-msvc": ["@ast-grep/cli-win32-x64-msvc@0.44.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OJEo7f95YYaSuS1byUB7ZctbzxoA7/wCoAol+pt6pvfdW/8Wq+L1qU28glwx7dQ0HgTsnPZbWpXQwmZpCBHhZg=="],
|
||||||
|
|
||||||
"@astrojs/check": ["@astrojs/check@0.9.6", "", { "dependencies": { "@astrojs/language-server": "^2.16.1", "chokidar": "^4.0.1", "kleur": "^4.1.5", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": "^5.0.0" }, "bin": { "astro-check": "bin/astro-check.js" } }, "sha512-jlaEu5SxvSgmfGIFfNgcn5/f+29H61NJzEMfAZ82Xopr4XBchXB1GVlcJsE+elUlsYSbXlptZLX+JMG3b/wZEA=="],
|
"@astrojs/check": ["@astrojs/check@0.9.6", "", { "dependencies": { "@astrojs/language-server": "^2.16.1", "chokidar": "^4.0.1", "kleur": "^4.1.5", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": "^5.0.0" }, "bin": { "astro-check": "bin/astro-check.js" } }, "sha512-jlaEu5SxvSgmfGIFfNgcn5/f+29H61NJzEMfAZ82Xopr4XBchXB1GVlcJsE+elUlsYSbXlptZLX+JMG3b/wZEA=="],
|
||||||
|
|
||||||
"@astrojs/cloudflare": ["@astrojs/cloudflare@12.6.3", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.1", "@astrojs/underscore-redirects": "1.0.0", "@cloudflare/workers-types": "^4.20250507.0", "tinyglobby": "^0.2.13", "vite": "^6.3.5", "wrangler": "^4.14.1" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-xhJptF5tU2k5eo70nIMyL1Udma0CqmUEnGSlGyFflLqSY82CRQI6nWZ/xZt0ZvmXuErUjIx0YYQNfZsz5CNjLQ=="],
|
"@astrojs/cloudflare": ["@astrojs/cloudflare@12.6.3", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.1", "@astrojs/underscore-redirects": "1.0.0", "@cloudflare/workers-types": "^4.20250507.0", "tinyglobby": "^0.2.13", "vite": "^6.3.5", "wrangler": "^4.14.1" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-xhJptF5tU2k5eo70nIMyL1Udma0CqmUEnGSlGyFflLqSY82CRQI6nWZ/xZt0ZvmXuErUjIx0YYQNfZsz5CNjLQ=="],
|
||||||
@@ -2002,6 +2036,8 @@
|
|||||||
|
|
||||||
"@opencode-ai/session-ui": ["@opencode-ai/session-ui@workspace:packages/session-ui"],
|
"@opencode-ai/session-ui": ["@opencode-ai/session-ui@workspace:packages/session-ui"],
|
||||||
|
|
||||||
|
"@opencode-ai/simulation": ["@opencode-ai/simulation@workspace:packages/simulation"],
|
||||||
|
|
||||||
"@opencode-ai/slack": ["@opencode-ai/slack@workspace:packages/slack"],
|
"@opencode-ai/slack": ["@opencode-ai/slack@workspace:packages/slack"],
|
||||||
|
|
||||||
"@opencode-ai/stats-app": ["@opencode-ai/stats-app@workspace:packages/stats/app"],
|
"@opencode-ai/stats-app": ["@opencode-ai/stats-app@workspace:packages/stats/app"],
|
||||||
@@ -3490,7 +3526,7 @@
|
|||||||
|
|
||||||
"destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="],
|
"destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="],
|
||||||
|
|
||||||
"detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
|
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||||
|
|
||||||
"detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="],
|
"detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="],
|
||||||
|
|
||||||
@@ -6048,6 +6084,8 @@
|
|||||||
|
|
||||||
"@oxc-resolver/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
|
"@oxc-resolver/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
|
||||||
|
|
||||||
|
"@parcel/watcher/detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
|
||||||
|
|
||||||
"@pierre/diffs/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="],
|
"@pierre/diffs/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="],
|
||||||
|
|
||||||
"@pierre/diffs/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="],
|
"@pierre/diffs/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="],
|
||||||
@@ -6114,8 +6152,6 @@
|
|||||||
|
|
||||||
"@storybook/csf-plugin/unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="],
|
"@storybook/csf-plugin/unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
|
||||||
@@ -6328,8 +6364,6 @@
|
|||||||
|
|
||||||
"light-my-request/process-warning": ["process-warning@4.0.1", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="],
|
"light-my-request/process-warning": ["process-warning@4.0.1", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="],
|
||||||
|
|
||||||
"lightningcss/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
|
||||||
|
|
||||||
"matcher/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
"matcher/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||||
|
|
||||||
"md-to-react-email/marked": ["marked@7.0.4", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ=="],
|
"md-to-react-email/marked": ["marked@7.0.4", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ=="],
|
||||||
@@ -6360,8 +6394,6 @@
|
|||||||
|
|
||||||
"node-gyp/undici": ["undici@6.26.0", "", {}, "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A=="],
|
"node-gyp/undici": ["undici@6.26.0", "", {}, "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A=="],
|
||||||
|
|
||||||
"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=="],
|
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
|
||||||
|
|
||||||
"nypm/citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="],
|
"nypm/citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="],
|
||||||
@@ -6428,8 +6460,6 @@
|
|||||||
|
|
||||||
"serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
|
"serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
|
||||||
|
|
||||||
"sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
|
||||||
|
|
||||||
"shiki/@shikijs/core": ["@shikijs/core@4.2.0", "", { "dependencies": { "@shikijs/primitive": "4.2.0", "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ=="],
|
"shiki/@shikijs/core": ["@shikijs/core@4.2.0", "", { "dependencies": { "@shikijs/primitive": "4.2.0", "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ=="],
|
||||||
|
|
||||||
"shiki/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="],
|
"shiki/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="],
|
||||||
@@ -7078,8 +7108,6 @@
|
|||||||
|
|
||||||
"opencode-gitlab-auth/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
|
"opencode-gitlab-auth/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
|
||||||
|
|
||||||
"openid-client/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
|
||||||
|
|
||||||
"p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
"p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||||
|
|
||||||
"pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
|
"pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
"dev:stats": "bun sst shell --stage=production -- bun run --cwd packages/stats/app dev",
|
"dev:stats": "bun sst shell --stage=production -- bun run --cwd packages/stats/app dev",
|
||||||
"dev:storybook": "bun --cwd packages/storybook storybook",
|
"dev:storybook": "bun --cwd packages/storybook storybook",
|
||||||
"lint": "oxlint",
|
"lint": "oxlint",
|
||||||
|
"lint:effect-patterns": "ast-grep scan -c script/ast-grep/sgconfig.yml packages/core/src packages/server/src packages/protocol/src packages/cli/src",
|
||||||
|
"test:lint-rules": "ast-grep test -c script/ast-grep/sgconfig.yml",
|
||||||
"typecheck": "bun turbo typecheck",
|
"typecheck": "bun turbo typecheck",
|
||||||
"upgrade-opentui": "bun run script/upgrade-opentui.ts",
|
"upgrade-opentui": "bun run script/upgrade-opentui.ts",
|
||||||
"postinstall": "bun run --cwd packages/core fix-node-pty",
|
"postinstall": "bun run --cwd packages/core fix-node-pty",
|
||||||
@@ -95,6 +97,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@actions/artifact": "5.0.1",
|
"@actions/artifact": "5.0.1",
|
||||||
|
"@ast-grep/cli": "0.44.0",
|
||||||
"@tsconfig/bun": "catalog:",
|
"@tsconfig/bun": "catalog:",
|
||||||
"@types/mime-types": "3.0.1",
|
"@types/mime-types": "3.0.1",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ export function DialogCustomProvider(props: Props) {
|
|||||||
>
|
>
|
||||||
<div class="flex flex-col gap-6 px-2.5 pb-3 overflow-y-auto max-h-[60vh]">
|
<div class="flex flex-col gap-6 px-2.5 pb-3 overflow-y-auto max-h-[60vh]">
|
||||||
<div class="px-2.5 flex gap-4 items-center">
|
<div class="px-2.5 flex gap-4 items-center">
|
||||||
<ProviderIcon id="synthetic" class="size-5 shrink-0 icon-strong-base" />
|
<ProviderIcon id="session.synthetic" class="size-5 shrink-0 icon-strong-base" />
|
||||||
<div class="text-16-medium text-text-strong">{language.t("provider.custom.title")}</div>
|
<div class="text-16-medium text-text-strong">{language.t("provider.custom.title")}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ const SettingsProvidersContent: Component = () => {
|
|||||||
>
|
>
|
||||||
<div class="flex flex-col min-w-0">
|
<div class="flex flex-col min-w-0">
|
||||||
<div class="flex flex-wrap items-center gap-x-3 gap-y-1">
|
<div class="flex flex-wrap items-center gap-x-3 gap-y-1">
|
||||||
<ProviderIcon id="synthetic" class="size-5 shrink-0 icon-strong-base" />
|
<ProviderIcon id="session.synthetic" class="size-5 shrink-0 icon-strong-base" />
|
||||||
<span class="text-14-medium text-text-strong">{language.t("provider.custom.title")}</span>
|
<span class="text-14-medium text-text-strong">{language.t("provider.custom.title")}</span>
|
||||||
<Tag>{language.t("settings.providers.tag.custom")}</Tag>
|
<Tag>{language.t("settings.providers.tag.custom")}</Tag>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ export const SettingsProvidersV2: Component = () => {
|
|||||||
<div class="settings-v2-provider-row" data-component="custom-provider-section">
|
<div class="settings-v2-provider-row" data-component="custom-provider-section">
|
||||||
<div class="settings-v2-provider-lead">
|
<div class="settings-v2-provider-lead">
|
||||||
<ProviderIcon
|
<ProviderIcon
|
||||||
id="synthetic"
|
id="session.synthetic"
|
||||||
width={PROVIDER_ICON_SIZE}
|
width={PROVIDER_ICON_SIZE}
|
||||||
height={PROVIDER_ICON_SIZE}
|
height={PROVIDER_ICON_SIZE}
|
||||||
class="settings-v2-provider-icon shrink-0"
|
class="settings-v2-provider-icon shrink-0"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ describe("file watcher invalidation", () => {
|
|||||||
const refresh: string[] = []
|
const refresh: string[] = []
|
||||||
invalidateFromWatcher(
|
invalidateFromWatcher(
|
||||||
{
|
{
|
||||||
type: "file.watcher.updated",
|
type: "filesystem.changed",
|
||||||
properties: {
|
properties: {
|
||||||
file: "src/new.ts",
|
file: "src/new.ts",
|
||||||
event: "add",
|
event: "add",
|
||||||
@@ -32,7 +32,7 @@ describe("file watcher invalidation", () => {
|
|||||||
|
|
||||||
invalidateFromWatcher(
|
invalidateFromWatcher(
|
||||||
{
|
{
|
||||||
type: "file.watcher.updated",
|
type: "filesystem.changed",
|
||||||
properties: {
|
properties: {
|
||||||
file: "src/open.ts",
|
file: "src/open.ts",
|
||||||
event: "change",
|
event: "change",
|
||||||
@@ -63,7 +63,7 @@ describe("file watcher invalidation", () => {
|
|||||||
|
|
||||||
invalidateFromWatcher(
|
invalidateFromWatcher(
|
||||||
{
|
{
|
||||||
type: "file.watcher.updated",
|
type: "filesystem.changed",
|
||||||
properties: {
|
properties: {
|
||||||
file: "src",
|
file: "src",
|
||||||
event: "change",
|
event: "change",
|
||||||
@@ -81,7 +81,7 @@ describe("file watcher invalidation", () => {
|
|||||||
|
|
||||||
invalidateFromWatcher(
|
invalidateFromWatcher(
|
||||||
{
|
{
|
||||||
type: "file.watcher.updated",
|
type: "filesystem.changed",
|
||||||
properties: {
|
properties: {
|
||||||
file: "src/file.ts",
|
file: "src/file.ts",
|
||||||
event: "change",
|
event: "change",
|
||||||
@@ -111,7 +111,7 @@ describe("file watcher invalidation", () => {
|
|||||||
|
|
||||||
invalidateFromWatcher(
|
invalidateFromWatcher(
|
||||||
{
|
{
|
||||||
type: "file.watcher.updated",
|
type: "filesystem.changed",
|
||||||
properties: {
|
properties: {
|
||||||
file: ".git/index.lock",
|
file: ".git/index.lock",
|
||||||
event: "change",
|
event: "change",
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ type WatcherOps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function invalidateFromWatcher(event: WatcherEvent, ops: WatcherOps) {
|
export function invalidateFromWatcher(event: WatcherEvent, ops: WatcherOps) {
|
||||||
if (event.type !== "file.watcher.updated") return
|
if (event.type !== "filesystem.changed") return
|
||||||
const props =
|
const props =
|
||||||
typeof event.properties === "object" && event.properties ? (event.properties as Record<string, unknown>) : undefined
|
typeof event.properties === "object" && event.properties ? (event.properties as Record<string, unknown>) : undefined
|
||||||
const rawPath = typeof props?.file === "string" ? props.file : undefined
|
const rawPath = typeof props?.file === "string" ? props.file : undefined
|
||||||
|
|||||||
@@ -820,7 +820,7 @@ export default function Page() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const stopVcs = sdk().event.listen((evt) => {
|
const stopVcs = sdk().event.listen((evt) => {
|
||||||
if (evt.details.type !== "file.watcher.updated") return
|
if (evt.details.type !== "filesystem.changed") return
|
||||||
const props =
|
const props =
|
||||||
typeof evt.details.properties === "object" && evt.details.properties
|
typeof evt.details.properties === "object" && evt.details.properties
|
||||||
? (evt.details.properties as Record<string, unknown>)
|
? (evt.details.properties as Record<string, unknown>)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { Commands } from "../commands"
|
|||||||
import { Runtime } from "../../framework/runtime"
|
import { Runtime } from "../../framework/runtime"
|
||||||
import { Service } from "@opencode-ai/client/effect"
|
import { Service } from "@opencode-ai/client/effect"
|
||||||
import { ServiceConfig } from "../../services/service-config"
|
import { ServiceConfig } from "../../services/service-config"
|
||||||
import type { Transport } from "@opencode-ai/client/effect"
|
|
||||||
|
|
||||||
const methods = new Set(["delete", "get", "head", "options", "patch", "post", "put"])
|
const methods = new Set(["delete", "get", "head", "options", "patch", "post", "put"])
|
||||||
|
|
||||||
@@ -61,7 +60,7 @@ export function rawRequest(input: readonly string[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveRequest(
|
function resolveRequest(
|
||||||
transport: Transport,
|
transport: Service.Transport,
|
||||||
input: readonly string[],
|
input: readonly string[],
|
||||||
params: Record<string, string>,
|
params: Record<string, string>,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import * as Effect from "effect/Effect"
|
import { Effect } from "effect"
|
||||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||||
import { Commands } from "../../commands"
|
import { Commands } from "../../commands"
|
||||||
import { Runtime } from "../../../framework/runtime"
|
import { Runtime } from "../../../framework/runtime"
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { NodeFileSystem } from "@effect/platform-node"
|
import { NodeFileSystem } from "@effect/platform-node"
|
||||||
import { Commands } from "../commands"
|
import { Commands } from "../commands"
|
||||||
import { Runtime } from "../../framework/runtime"
|
import { Runtime } from "../../framework/runtime"
|
||||||
import { Effect, Option } from "effect"
|
import { Effect, Option, Redacted } from "effect"
|
||||||
import { Service } from "@opencode-ai/client/effect"
|
import { Service } from "@opencode-ai/client/effect"
|
||||||
import type { Transport } from "@opencode-ai/client/effect"
|
import { Env } from "../../env"
|
||||||
import { ServiceConfig } from "../../services/service-config"
|
import { ServiceConfig } from "../../services/service-config"
|
||||||
import { Standalone } from "../../services/standalone"
|
import { Standalone } from "../../services/standalone"
|
||||||
import { Updater } from "../../services/updater"
|
import { Updater } from "../../services/updater"
|
||||||
@@ -19,11 +19,29 @@ export default Runtime.handler(Commands, (input) =>
|
|||||||
return yield* Effect.fail(new Error("--server and --standalone cannot be combined"))
|
return yield* Effect.fail(new Error("--server and --standalone cannot be combined"))
|
||||||
const transport = yield* Effect.gen(function* () {
|
const transport = yield* Effect.gen(function* () {
|
||||||
if (server !== undefined) {
|
if (server !== undefined) {
|
||||||
const password = process.env["OPENCODE_SERVER_PASSWORD"]
|
const password = yield* Env.password
|
||||||
return {
|
const explicit = {
|
||||||
url: server,
|
url: server,
|
||||||
headers: password ? { authorization: "Basic " + btoa("opencode:" + password) } : undefined,
|
headers: password
|
||||||
} satisfies Transport
|
? { authorization: "Basic " + btoa("opencode:" + Redacted.value(password)) }
|
||||||
|
: undefined,
|
||||||
|
} satisfies Service.Transport
|
||||||
|
// Fail loudly before entering the TUI: an explicit server that is
|
||||||
|
// unreachable or rejects auth should not present as reconnect churn.
|
||||||
|
const response = yield* Effect.tryPromise(() =>
|
||||||
|
fetch(new URL("/api/health", server), { headers: explicit.headers, signal: AbortSignal.timeout(5_000) }),
|
||||||
|
).pipe(Effect.mapError((cause) => new Error(`Could not reach server at ${server}`, { cause })))
|
||||||
|
if (response.status === 401)
|
||||||
|
return yield* Effect.fail(
|
||||||
|
new Error(
|
||||||
|
password
|
||||||
|
? `Server at ${server} rejected the password`
|
||||||
|
: `Server at ${server} requires a password; set OPENCODE_PASSWORD`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if (!response.ok)
|
||||||
|
return yield* Effect.fail(new Error(`Server at ${server} responded with status ${response.status}`))
|
||||||
|
return explicit
|
||||||
}
|
}
|
||||||
if (input.standalone) return yield* Standalone.transport()
|
if (input.standalone) return yield* Standalone.transport()
|
||||||
const options = yield* ServiceConfig.options()
|
const options = yield* ServiceConfig.options()
|
||||||
@@ -45,6 +63,24 @@ export default Runtime.handler(Commands, (input) =>
|
|||||||
}).pipe(Effect.provide(NodeFileSystem.layer)),
|
}).pipe(Effect.provide(NodeFileSystem.layer)),
|
||||||
)
|
)
|
||||||
: () => Promise.resolve(transport)
|
: () => Promise.resolve(transport)
|
||||||
yield* runTui(transport, { continue: input.continue, sessionID: Option.getOrUndefined(input.session) }, discover)
|
// Restart the managed service in place; start() resolves once the
|
||||||
|
// replacement is healthy and the reconnect loop reattaches on its own.
|
||||||
|
// Only meaningful in service mode: --server is not ours to restart and a
|
||||||
|
// standalone child cannot be respawned.
|
||||||
|
const reload = serviceOptions
|
||||||
|
? () =>
|
||||||
|
Effect.runPromise(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* Service.stop(serviceOptions)
|
||||||
|
yield* Service.start(serviceOptions)
|
||||||
|
}).pipe(Effect.provide(NodeFileSystem.layer)),
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
yield* runTui(
|
||||||
|
transport,
|
||||||
|
{ continue: input.continue, sessionID: Option.getOrUndefined(input.session) },
|
||||||
|
discover,
|
||||||
|
reload,
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { EOL } from "node:os"
|
import { EOL } from "node:os"
|
||||||
import * as Effect from "effect/Effect"
|
import { Effect } from "effect"
|
||||||
import { createOpencodeClient, type McpServer } from "@opencode-ai/sdk/v2/client"
|
import { createOpencodeClient, type McpServer } from "@opencode-ai/sdk/v2/client"
|
||||||
import { Commands } from "../../commands"
|
import { Commands } from "../../commands"
|
||||||
import { Runtime } from "../../../framework/runtime"
|
import { Runtime } from "../../../framework/runtime"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as Effect from "effect/Effect"
|
import { Effect } from "effect"
|
||||||
import { Commands } from "../commands"
|
import { Commands } from "../commands"
|
||||||
import { Runtime } from "../../framework/runtime"
|
import { Runtime } from "../../framework/runtime"
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { Credential } from "@opencode-ai/core/credential"
|
|||||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
||||||
|
import { Project } from "@opencode-ai/core/project"
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { Context, FileSystem, Layer, Option, Schedule, Schema } from "effect"
|
import { Context, Effect, FileSystem, Layer, Option, Redacted, Schedule, Schema } from "effect"
|
||||||
import * as Effect from "effect/Effect"
|
|
||||||
import { HttpRouter, HttpServer } from "effect/unstable/http"
|
import { HttpRouter, HttpServer } from "effect/unstable/http"
|
||||||
import { createServer } from "node:http"
|
import { createServer } from "node:http"
|
||||||
import { createRoutes } from "@opencode-ai/server/routes"
|
import { createRoutes } from "@opencode-ai/server/routes"
|
||||||
@@ -14,6 +14,8 @@ import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
|||||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||||
import { Commands } from "../commands"
|
import { Commands } from "../commands"
|
||||||
import { Runtime } from "../../framework/runtime"
|
import { Runtime } from "../../framework/runtime"
|
||||||
|
import { Service } from "@opencode-ai/client/effect"
|
||||||
|
import { Env } from "../../env"
|
||||||
import { ServiceConfig } from "../../services/service-config"
|
import { ServiceConfig } from "../../services/service-config"
|
||||||
import { Updater } from "../../services/updater"
|
import { Updater } from "../../services/updater"
|
||||||
import { randomBytes, randomUUID } from "crypto"
|
import { randomBytes, randomUUID } from "crypto"
|
||||||
@@ -25,12 +27,19 @@ export default Runtime.handler(
|
|||||||
if (input.service) yield* Effect.sync(() => process.chdir(Global.Path.home))
|
if (input.service) yield* Effect.sync(() => process.chdir(Global.Path.home))
|
||||||
return yield* Effect.scoped(
|
return yield* Effect.scoped(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const standalonePassword = process.env.OPENCODE_SERVER_PASSWORD
|
const standalonePassword = yield* Env.password
|
||||||
if (input.stdio) delete process.env.OPENCODE_SERVER_PASSWORD
|
// Keep the lease credential out of the environment inherited by any
|
||||||
|
// process this server spawns.
|
||||||
|
if (input.stdio) {
|
||||||
|
delete process.env.OPENCODE_PASSWORD
|
||||||
|
delete process.env.OPENCODE_SERVER_PASSWORD
|
||||||
|
}
|
||||||
const config = input.service ? yield* ServiceConfig.read() : {}
|
const config = input.service ? yield* ServiceConfig.read() : {}
|
||||||
const password = input.service
|
const password = input.service
|
||||||
? yield* ServiceConfig.password()
|
? yield* ServiceConfig.password()
|
||||||
: standalonePassword || randomBytes(32).toString("base64url")
|
: standalonePassword
|
||||||
|
? Redacted.value(standalonePassword)
|
||||||
|
: randomBytes(32).toString("base64url")
|
||||||
if (!password) return yield* Effect.fail(new Error("Missing server password"))
|
if (!password) return yield* Effect.fail(new Error("Missing server password"))
|
||||||
const hostname = Option.getOrUndefined(input.hostname) ?? config.hostname ?? "127.0.0.1"
|
const hostname = Option.getOrUndefined(input.hostname) ?? config.hostname ?? "127.0.0.1"
|
||||||
const port = Option.isSome(input.port)
|
const port = Option.isSome(input.port)
|
||||||
@@ -51,7 +60,7 @@ export default Runtime.handler(
|
|||||||
if (!input.service && !input.stdio && !standalonePassword) console.log(`server password ${password}`)
|
if (!input.service && !input.stdio && !standalonePassword) console.log(`server password ${password}`)
|
||||||
const updater = yield* Updater.Service
|
const updater = yield* Updater.Service
|
||||||
yield* updater.check().pipe(Effect.schedule(Schedule.spaced("10 minutes")), Effect.forkScoped)
|
yield* updater.check().pipe(Effect.schedule(Schedule.spaced("10 minutes")), Effect.forkScoped)
|
||||||
return yield* (input.stdio ? waitForStdinClose() : Effect.never)
|
return yield* input.stdio ? waitForStdinClose() : Effect.never
|
||||||
}).pipe(Effect.annotateLogs({ role: "server" })),
|
}).pipe(Effect.annotateLogs({ role: "server" })),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
@@ -63,8 +72,11 @@ export default Runtime.handler(
|
|||||||
// not a startup lock: the atomic rename elects the latest writer, the watcher
|
// not a startup lock: the atomic rename elects the latest writer, the watcher
|
||||||
// self-evicts losers, and the finalizer id-guard keeps an exiting server from
|
// self-evicts losers, and the finalizer id-guard keeps an exiting server from
|
||||||
// deleting its successor's registration.
|
// deleting its successor's registration.
|
||||||
const RegistrationId = Schema.Struct({ id: Schema.optional(Schema.String) })
|
// Written and read through Service.Info so the file the server registers is
|
||||||
const decodeRegistrationId = Schema.decodeUnknownEffect(Schema.fromJsonString(RegistrationId))
|
// provably the contract clients discover with.
|
||||||
|
const infoJson = Schema.fromJsonString(Service.Info)
|
||||||
|
const encodeInfo = Schema.encodeEffect(infoJson)
|
||||||
|
const decodeInfo = Schema.decodeUnknownEffect(infoJson)
|
||||||
|
|
||||||
const register = Effect.fnUntraced(function* (address: HttpServer.Address) {
|
const register = Effect.fnUntraced(function* (address: HttpServer.Address) {
|
||||||
const fs = yield* FileSystem.FileSystem
|
const fs = yield* FileSystem.FileSystem
|
||||||
@@ -73,20 +85,17 @@ const register = Effect.fnUntraced(function* (address: HttpServer.Address) {
|
|||||||
const secret = yield* ServiceConfig.password()
|
const secret = yield* ServiceConfig.password()
|
||||||
const temp = file + "." + id + ".tmp"
|
const temp = file + "." + id + ".tmp"
|
||||||
yield* fs.makeDirectory(path.dirname(file), { recursive: true })
|
yield* fs.makeDirectory(path.dirname(file), { recursive: true })
|
||||||
yield* fs.writeFileString(
|
const encoded = yield* encodeInfo({
|
||||||
temp,
|
|
||||||
JSON.stringify({
|
|
||||||
id,
|
id,
|
||||||
version: InstallationVersion,
|
version: InstallationVersion,
|
||||||
url: HttpServer.formatAddress(address),
|
url: HttpServer.formatAddress(address),
|
||||||
pid: process.pid,
|
pid: process.pid,
|
||||||
password: secret,
|
password: secret,
|
||||||
}),
|
})
|
||||||
{ mode: 0o600 },
|
yield* fs.writeFileString(temp, encoded, { mode: 0o600 })
|
||||||
)
|
|
||||||
yield* fs.rename(temp, file)
|
yield* fs.rename(temp, file)
|
||||||
const currentID = fs.readFileString(file).pipe(
|
const currentID = fs.readFileString(file).pipe(
|
||||||
Effect.flatMap(decodeRegistrationId),
|
Effect.flatMap(decodeInfo),
|
||||||
Effect.map((info) => info.id),
|
Effect.map((info) => info.id),
|
||||||
Effect.orElseSucceed(() => undefined),
|
Effect.orElseSucceed(() => undefined),
|
||||||
)
|
)
|
||||||
@@ -134,9 +143,9 @@ function listen(hostname: string, port: Option.Option<number>, password: string)
|
|||||||
function bind(hostname: string, port: number, password: string) {
|
function bind(hostname: string, port: number, password: string) {
|
||||||
const server = createServer()
|
const server = createServer()
|
||||||
return Layer.build(
|
return Layer.build(
|
||||||
HttpRouter.serve(createRoutes(password), { disableListenLog: true, disableLogger: true }).pipe(
|
HttpRouter.serve(createRoutes(password), { disableListenLog: true }).pipe(
|
||||||
Layer.provideMerge(NodeHttpServer.layer(() => server, { port, host: hostname })),
|
Layer.provideMerge(NodeHttpServer.layer(() => server, { port, host: hostname })),
|
||||||
Layer.provide(AppNodeBuilder.build(LayerNode.group([Credential.node, PermissionSaved.node]))),
|
Layer.provide(AppNodeBuilder.build(LayerNode.group([Credential.node, PermissionSaved.node, Project.node]))),
|
||||||
),
|
),
|
||||||
).pipe(
|
).pipe(
|
||||||
Effect.tap(() => Effect.addFinalizer(() => Effect.sync(() => server.closeAllConnections()))),
|
Effect.tap(() => Effect.addFinalizer(() => Effect.sync(() => server.closeAllConnections()))),
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import { Option } from "effect"
|
import { Effect, Option } from "effect"
|
||||||
import * as Effect from "effect/Effect"
|
|
||||||
import { Commands } from "../../commands"
|
import { Commands } from "../../commands"
|
||||||
import { Runtime } from "../../../framework/runtime"
|
import { Runtime } from "../../../framework/runtime"
|
||||||
import { ServiceConfig } from "../../../services/service-config"
|
import { ServiceConfig } from "../../../services/service-config"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import * as Effect from "effect/Effect"
|
import { Effect } from "effect"
|
||||||
import { Service } from "@opencode-ai/client/effect"
|
import { Service } from "@opencode-ai/client/effect"
|
||||||
import { Commands } from "../../commands"
|
import { Commands } from "../../commands"
|
||||||
import { Runtime } from "../../../framework/runtime"
|
import { Runtime } from "../../../framework/runtime"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as Effect from "effect/Effect"
|
import { Effect } from "effect"
|
||||||
import { Commands } from "../../commands"
|
import { Commands } from "../../commands"
|
||||||
import { Runtime } from "../../../framework/runtime"
|
import { Runtime } from "../../../framework/runtime"
|
||||||
import { ServiceConfig } from "../../../services/service-config"
|
import { ServiceConfig } from "../../../services/service-config"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import * as Effect from "effect/Effect"
|
import { Effect } from "effect"
|
||||||
import { Service } from "@opencode-ai/client/effect"
|
import { Service } from "@opencode-ai/client/effect"
|
||||||
import { Commands } from "../../commands"
|
import { Commands } from "../../commands"
|
||||||
import { Runtime } from "../../../framework/runtime"
|
import { Runtime } from "../../../framework/runtime"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import * as Effect from "effect/Effect"
|
import { Effect } from "effect"
|
||||||
import { Service } from "@opencode-ai/client/effect"
|
import { Service } from "@opencode-ai/client/effect"
|
||||||
import { Commands } from "../../commands"
|
import { Commands } from "../../commands"
|
||||||
import { Runtime } from "../../../framework/runtime"
|
import { Runtime } from "../../../framework/runtime"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as Effect from "effect/Effect"
|
import { Effect } from "effect"
|
||||||
import { Service } from "@opencode-ai/client/effect"
|
import { Service } from "@opencode-ai/client/effect"
|
||||||
import { Commands } from "../../commands"
|
import { Commands } from "../../commands"
|
||||||
import { Runtime } from "../../../framework/runtime"
|
import { Runtime } from "../../../framework/runtime"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as Effect from "effect/Effect"
|
import { Effect } from "effect"
|
||||||
import { Commands } from "../../commands"
|
import { Commands } from "../../commands"
|
||||||
import { Runtime } from "../../../framework/runtime"
|
import { Runtime } from "../../../framework/runtime"
|
||||||
import { ServiceConfig } from "../../../services/service-config"
|
import { ServiceConfig } from "../../../services/service-config"
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Config } from "effect"
|
||||||
|
|
||||||
|
// Every environment variable the CLI reads, in one place. Consumers yield
|
||||||
|
// these instead of touching process.env so the full surface stays visible,
|
||||||
|
// typed, and redacted where secret.
|
||||||
|
|
||||||
|
// The opencode server password: sent by clients connecting to an explicit
|
||||||
|
// --server, and adopted by a manually run or standalone server. The legacy
|
||||||
|
// name is still honored.
|
||||||
|
export const password = Config.redacted("OPENCODE_PASSWORD").pipe(
|
||||||
|
Config.orElse(() => Config.redacted("OPENCODE_SERVER_PASSWORD")),
|
||||||
|
Config.withDefault(undefined),
|
||||||
|
)
|
||||||
|
|
||||||
|
export * as Env from "./env"
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import * as Effect from "effect/Effect"
|
import { Effect, FileSystem, Scope } from "effect"
|
||||||
import * as Command from "effect/unstable/cli/Command"
|
import { Command } from "effect/unstable/cli"
|
||||||
import { Spec } from "./spec"
|
import { Spec } from "./spec"
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { Updater } from "../services/updater"
|
import { Updater } from "../services/updater"
|
||||||
import { FileSystem, Scope } from "effect"
|
|
||||||
|
|
||||||
export type Input<Value> =
|
export type Input<Value> =
|
||||||
Value extends Spec.Node<infer _Name, infer Command, infer _Commands>
|
Value extends Spec.Node<infer _Name, infer Command, infer _Commands>
|
||||||
@@ -12,11 +11,21 @@ export type Input<Value> =
|
|||||||
? Input
|
? Input
|
||||||
: never
|
: never
|
||||||
|
|
||||||
type RuntimeHandler = (input: unknown) => Effect.Effect<void, unknown, FileSystem.FileSystem | Global.Service | Updater.Service | Scope.Scope>
|
type RuntimeHandler = (
|
||||||
|
input: unknown,
|
||||||
|
) => Effect.Effect<void, unknown, FileSystem.FileSystem | Global.Service | Updater.Service | Scope.Scope>
|
||||||
type Loader<Node extends Spec.Any> = () => Promise<{
|
type Loader<Node extends Spec.Any> = () => Promise<{
|
||||||
default: (input: Input<Node>) => Effect.Effect<void, any, FileSystem.FileSystem | Global.Service | Updater.Service | Scope.Scope>
|
default: (
|
||||||
|
input: Input<Node>,
|
||||||
|
) => Effect.Effect<void, any, FileSystem.FileSystem | Global.Service | Updater.Service | Scope.Scope>
|
||||||
}>
|
}>
|
||||||
type ProvidedCommand = Command.Command<string, unknown, unknown, unknown, FileSystem.FileSystem | Global.Service | Updater.Service | Scope.Scope>
|
type ProvidedCommand = Command.Command<
|
||||||
|
string,
|
||||||
|
unknown,
|
||||||
|
unknown,
|
||||||
|
unknown,
|
||||||
|
FileSystem.FileSystem | Global.Service | Updater.Service | Scope.Scope
|
||||||
|
>
|
||||||
|
|
||||||
export type Handlers<Node extends Spec.Any> = keyof Node["commands"] extends never
|
export type Handlers<Node extends Spec.Any> = keyof Node["commands"] extends never
|
||||||
? Loader<Node>
|
? Loader<Node>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as Command from "effect/unstable/cli/Command"
|
import { Command } from "effect/unstable/cli"
|
||||||
|
|
||||||
type Options<Config extends Command.Command.Config, Commands extends ReadonlyArray<Any>> = {
|
type Options<Config extends Command.Command.Config, Commands extends ReadonlyArray<Any>> = {
|
||||||
readonly description?: string
|
readonly description?: string
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
#!/usr/bin/env bun
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
import * as NodeRuntime from "@effect/platform-node/NodeRuntime"
|
import { NodeFileSystem, NodeRuntime, NodeServices } from "@effect/platform-node"
|
||||||
import * as NodeServices from "@effect/platform-node/NodeServices"
|
import { Effect, Layer, Logger, References } from "effect"
|
||||||
import { NodeFileSystem } from "@effect/platform-node"
|
|
||||||
import * as Effect from "effect/Effect"
|
|
||||||
import { Layer, Logger, References } from "effect"
|
|
||||||
import { Commands } from "./commands/commands"
|
import { Commands } from "./commands/commands"
|
||||||
import { Runtime } from "./framework/runtime"
|
import { Runtime } from "./framework/runtime"
|
||||||
import { Logging } from "@opencode-ai/core/observability/logging"
|
import { Logging } from "@opencode-ai/core/observability/logging"
|
||||||
@@ -46,7 +43,12 @@ const Handlers = Runtime.handlers(Commands, {
|
|||||||
serve: () => import("./commands/handlers/serve"),
|
serve: () => import("./commands/handlers/serve"),
|
||||||
})
|
})
|
||||||
|
|
||||||
Effect.logInfo("cli starting", { version: InstallationVersion, channel: InstallationChannel, local: InstallationLocal }).pipe(
|
Effect.logInfo("cli starting", {
|
||||||
|
version: InstallationVersion,
|
||||||
|
channel: InstallationChannel,
|
||||||
|
local: InstallationLocal,
|
||||||
|
args: process.argv.slice(2),
|
||||||
|
}).pipe(
|
||||||
Effect.flatMap(() => Runtime.run(Commands, Handlers, { version: InstallationVersion })),
|
Effect.flatMap(() => Runtime.run(Commands, Handlers, { version: InstallationVersion })),
|
||||||
Effect.annotateLogs({ role: "cli" }),
|
Effect.annotateLogs({ role: "cli" }),
|
||||||
Effect.provide(Updater.layer),
|
Effect.provide(Updater.layer),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Effect, FileSystem, Schema } from "effect"
|
|||||||
import { randomBytes } from "crypto"
|
import { randomBytes } from "crypto"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
||||||
// The CLI's service configuration file, plus the ServiceOptions binding that
|
// The CLI's service configuration file, plus the Service.Options binding that
|
||||||
// points the client package's service operations at this CLI: which
|
// points the client package's service operations at this CLI: which
|
||||||
// registration file (by channel), which version, and how to spawn opencode.
|
// registration file (by channel), which version, and how to spawn opencode.
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ function command(password: string) {
|
|||||||
if (!compiled && entrypoint.length === 0) throw new Error("Failed to resolve CLI entrypoint")
|
if (!compiled && entrypoint.length === 0) throw new Error("Failed to resolve CLI entrypoint")
|
||||||
return ChildProcess.make(process.execPath, [...entrypoint, "serve", "--stdio", "--port", "0"], {
|
return ChildProcess.make(process.execPath, [...entrypoint, "serve", "--stdio", "--port", "0"], {
|
||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
env: { OPENCODE_SERVER_PASSWORD: password },
|
// Explicit entry wins over anything inherited, so a user-exported
|
||||||
|
// OPENCODE_PASSWORD cannot shadow the child's lease credential.
|
||||||
|
env: { OPENCODE_PASSWORD: password },
|
||||||
extendEnv: true,
|
extendEnv: true,
|
||||||
// The server treats EOF on this pipe as the end of its ownership lease.
|
// The server treats EOF on this pipe as the end of its ownership lease.
|
||||||
// The OS closes it even when the TUI is killed before Effect finalizers run.
|
// The OS closes it even when the TUI is killed before Effect finalizers run.
|
||||||
|
|||||||
@@ -5,11 +5,16 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
|||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { loadBuiltinPlugins } from "@opencode-ai/tui/builtins"
|
import { loadBuiltinPlugins } from "@opencode-ai/tui/builtins"
|
||||||
import { OpenCode } from "@opencode-ai/client/promise"
|
import { OpenCode } from "@opencode-ai/client/promise"
|
||||||
import type { Transport } from "@opencode-ai/client/effect"
|
import type { Service } from "@opencode-ai/client/effect"
|
||||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||||
import type { Args } from "@opencode-ai/tui/context/args"
|
import type { Args } from "@opencode-ai/tui/context/args"
|
||||||
|
|
||||||
export function runTui(transport: Transport, args: Args, discover?: () => Promise<Transport>) {
|
export function runTui(
|
||||||
|
transport: Service.Transport,
|
||||||
|
args: Args,
|
||||||
|
discover?: () => Promise<Service.Transport>,
|
||||||
|
reload?: () => Promise<void>,
|
||||||
|
) {
|
||||||
const config = TuiConfig.resolve({}, { terminalSuspend: false })
|
const config = TuiConfig.resolve({}, { terminalSuspend: false })
|
||||||
let disposeSlots: (() => void) | undefined
|
let disposeSlots: (() => void) | undefined
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
@@ -33,6 +38,7 @@ export function runTui(transport: Transport, args: Args, discover?: () => Promis
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
|
reload,
|
||||||
args,
|
args,
|
||||||
config,
|
config,
|
||||||
pluginHost: {
|
pluginHost: {
|
||||||
|
|||||||
@@ -1,16 +1,30 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/client",
|
"name": "@opencode-ai/client",
|
||||||
"private": true,
|
"version": "1.17.13",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/anomalyco/opencode.git",
|
||||||
|
"directory": "packages/client"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
"exports": {
|
"exports": {
|
||||||
"./promise": "./src/promise/index.ts",
|
"./promise": "./src/promise/index.ts",
|
||||||
"./effect": "./src/effect/index.ts"
|
"./promise/api": "./src/promise/api.ts",
|
||||||
|
"./effect": "./src/effect/index.ts",
|
||||||
|
"./effect/api": "./src/effect/api.ts"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"build": "bun run script/build-package.ts",
|
||||||
"generate": "bun run script/build.ts",
|
"generate": "bun run script/build.ts",
|
||||||
"check:generated": "bun run generate && git diff --exit-code -- src/promise/generated src/effect/generated",
|
"check:generated": "bun run generate && git diff --exit-code -- src/promise/generated src/effect/generated src/effect/api",
|
||||||
"test": "bun test --timeout 5000",
|
"test": "bun test --timeout 5000",
|
||||||
"typecheck": "tsgo --noEmit"
|
"typecheck": "tsgo --noEmit"
|
||||||
},
|
},
|
||||||
@@ -28,9 +42,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
|
||||||
"@opencode-ai/httpapi-codegen": "workspace:*",
|
"@opencode-ai/httpapi-codegen": "workspace:*",
|
||||||
"@opencode-ai/server": "workspace:*",
|
|
||||||
"@tsconfig/bun": "catalog:",
|
"@tsconfig/bun": "catalog:",
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
import { $ } from "bun"
|
||||||
|
import { fileURLToPath } from "node:url"
|
||||||
|
|
||||||
|
process.chdir(fileURLToPath(new URL("..", import.meta.url)))
|
||||||
|
|
||||||
|
await $`rm -rf dist`
|
||||||
|
await $`bun tsc -p tsconfig.build.json`
|
||||||
@@ -32,8 +32,8 @@ await Effect.runPromise(
|
|||||||
fileURLToPath(new URL("../src/effect/generated", import.meta.url)),
|
fileURLToPath(new URL("../src/effect/generated", import.meta.url)),
|
||||||
),
|
),
|
||||||
write(
|
write(
|
||||||
emitEffectShape(effectContract, { module: "@opencode-ai/protocol/client", api: "ClientApi" }),
|
emitEffectShape(effectContract, { module: "../../contract", api: "ClientApi" }),
|
||||||
fileURLToPath(new URL("../../plugin/src/v2/effect/generated", import.meta.url)),
|
fileURLToPath(new URL("../src/effect/api", import.meta.url)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
{ concurrency: 3, discard: true },
|
{ concurrency: 3, discard: true },
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
import { Script } from "@opencode-ai/script"
|
||||||
|
import { $ } from "bun"
|
||||||
|
import { rm } from "node:fs/promises"
|
||||||
|
import { fileURLToPath } from "node:url"
|
||||||
|
|
||||||
|
process.chdir(fileURLToPath(new URL("..", import.meta.url)))
|
||||||
|
|
||||||
|
const originalText = await Bun.file("package.json").text()
|
||||||
|
const pkg = JSON.parse(originalText) as {
|
||||||
|
name: string
|
||||||
|
version: string
|
||||||
|
exports: Record<string, string | { import: string; types: string }>
|
||||||
|
}
|
||||||
|
const tarball = `${pkg.name.replace("@", "").replace("/", "-")}-${pkg.version}.tgz`
|
||||||
|
|
||||||
|
if ((await $`npm view ${pkg.name}@${pkg.version} version`.nothrow()).exitCode === 0) {
|
||||||
|
console.log(`already published ${pkg.name}@${pkg.version}`)
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await $`bun run typecheck`
|
||||||
|
await $`bun run build`
|
||||||
|
pkg.exports = Object.fromEntries(
|
||||||
|
Object.entries(pkg.exports).map(([key, value]) => {
|
||||||
|
if (typeof value !== "string") return [key, value]
|
||||||
|
return [
|
||||||
|
key,
|
||||||
|
{
|
||||||
|
import: value.replace("./src/", "./dist/").replace(/\.ts$/, ".js"),
|
||||||
|
types: value.replace("./src/", "./dist/").replace(/\.ts$/, ".d.ts"),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
await Bun.write("package.json", JSON.stringify(pkg, null, 2) + "\n")
|
||||||
|
await rm(tarball, { force: true })
|
||||||
|
await $`bun pm pack`
|
||||||
|
await $`npm publish ${tarball} --tag ${Script.channel} --access public`
|
||||||
|
} finally {
|
||||||
|
await Bun.write("package.json", originalText)
|
||||||
|
await rm(tarball, { force: true })
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import type { ModelApi, ProviderApi } from "./api/api.js"
|
||||||
|
|
||||||
|
export type * from "./api/api.js"
|
||||||
|
|
||||||
|
export interface CatalogApi<E = never> {
|
||||||
|
readonly provider: ProviderApi<E>
|
||||||
|
readonly model: ModelApi<E>
|
||||||
|
}
|
||||||
+72
-62
@@ -1,7 +1,7 @@
|
|||||||
// Generated by @opencode-ai/httpapi-codegen. Do not edit.
|
// Generated by @opencode-ai/httpapi-codegen. Do not edit.
|
||||||
import type { Effect, Stream } from "effect"
|
import type { Effect, Stream } from "effect"
|
||||||
import type { HttpApiClient } from "effect/unstable/httpapi"
|
import type { HttpApiClient } from "effect/unstable/httpapi"
|
||||||
import type { ClientApi } from "@opencode-ai/protocol/client"
|
import type { ClientApi } from "../../contract"
|
||||||
|
|
||||||
type RawClient = HttpApiClient.ForApi<typeof ClientApi>
|
type RawClient = HttpApiClient.ForApi<typeof ClientApi>
|
||||||
type EffectValue<A> = A extends Effect.Effect<infer Success, any, any> ? Success : never
|
type EffectValue<A> = A extends Effect.Effect<infer Success, any, any> ? Success : never
|
||||||
@@ -153,96 +153,105 @@ export type Endpoint4_11Input = {
|
|||||||
export type Endpoint4_11Output = EffectValue<ReturnType<RawClient["server.session"]["session.synthetic"]>>
|
export type Endpoint4_11Output = EffectValue<ReturnType<RawClient["server.session"]["session.synthetic"]>>
|
||||||
export type SessionSyntheticOperation<E = never> = (input: Endpoint4_11Input) => Effect.Effect<Endpoint4_11Output, E>
|
export type SessionSyntheticOperation<E = never> = (input: Endpoint4_11Input) => Effect.Effect<Endpoint4_11Output, E>
|
||||||
|
|
||||||
type Endpoint4_12Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
|
type Endpoint4_12Request = Parameters<RawClient["server.session"]["session.shell"]>[0]
|
||||||
export type Endpoint4_12Input = { readonly sessionID: Endpoint4_12Request["params"]["sessionID"] }
|
export type Endpoint4_12Input = {
|
||||||
export type Endpoint4_12Output = EffectValue<ReturnType<RawClient["server.session"]["session.compact"]>>
|
readonly sessionID: Endpoint4_12Request["params"]["sessionID"]
|
||||||
export type SessionCompactOperation<E = never> = (input: Endpoint4_12Input) => Effect.Effect<Endpoint4_12Output, E>
|
readonly id?: Endpoint4_12Request["payload"]["id"]
|
||||||
|
readonly command: Endpoint4_12Request["payload"]["command"]
|
||||||
type Endpoint4_13Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
|
|
||||||
export type Endpoint4_13Input = { readonly sessionID: Endpoint4_13Request["params"]["sessionID"] }
|
|
||||||
export type Endpoint4_13Output = EffectValue<ReturnType<RawClient["server.session"]["session.wait"]>>
|
|
||||||
export type SessionWaitOperation<E = never> = (input: Endpoint4_13Input) => Effect.Effect<Endpoint4_13Output, E>
|
|
||||||
|
|
||||||
type Endpoint4_14Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
|
|
||||||
export type Endpoint4_14Input = {
|
|
||||||
readonly sessionID: Endpoint4_14Request["params"]["sessionID"]
|
|
||||||
readonly messageID: Endpoint4_14Request["payload"]["messageID"]
|
|
||||||
readonly files?: Endpoint4_14Request["payload"]["files"]
|
|
||||||
}
|
}
|
||||||
export type Endpoint4_14Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.stage"]>>["data"]
|
export type Endpoint4_12Output = EffectValue<ReturnType<RawClient["server.session"]["session.shell"]>>
|
||||||
export type SessionRevertStageOperation<E = never> = (input: Endpoint4_14Input) => Effect.Effect<Endpoint4_14Output, E>
|
export type SessionShellOperation<E = never> = (input: Endpoint4_12Input) => Effect.Effect<Endpoint4_12Output, E>
|
||||||
|
|
||||||
type Endpoint4_15Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
|
type Endpoint4_13Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
|
||||||
export type Endpoint4_15Input = { readonly sessionID: Endpoint4_15Request["params"]["sessionID"] }
|
export type Endpoint4_13Input = { readonly sessionID: Endpoint4_13Request["params"]["sessionID"] }
|
||||||
export type Endpoint4_15Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.clear"]>>
|
export type Endpoint4_13Output = EffectValue<ReturnType<RawClient["server.session"]["session.compact"]>>
|
||||||
export type SessionRevertClearOperation<E = never> = (input: Endpoint4_15Input) => Effect.Effect<Endpoint4_15Output, E>
|
export type SessionCompactOperation<E = never> = (input: Endpoint4_13Input) => Effect.Effect<Endpoint4_13Output, E>
|
||||||
|
|
||||||
type Endpoint4_16Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
|
type Endpoint4_14Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
|
||||||
|
export type Endpoint4_14Input = { readonly sessionID: Endpoint4_14Request["params"]["sessionID"] }
|
||||||
|
export type Endpoint4_14Output = EffectValue<ReturnType<RawClient["server.session"]["session.wait"]>>
|
||||||
|
export type SessionWaitOperation<E = never> = (input: Endpoint4_14Input) => Effect.Effect<Endpoint4_14Output, E>
|
||||||
|
|
||||||
|
type Endpoint4_15Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
|
||||||
|
export type Endpoint4_15Input = {
|
||||||
|
readonly sessionID: Endpoint4_15Request["params"]["sessionID"]
|
||||||
|
readonly messageID: Endpoint4_15Request["payload"]["messageID"]
|
||||||
|
readonly files?: Endpoint4_15Request["payload"]["files"]
|
||||||
|
}
|
||||||
|
export type Endpoint4_15Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.stage"]>>["data"]
|
||||||
|
export type SessionRevertStageOperation<E = never> = (input: Endpoint4_15Input) => Effect.Effect<Endpoint4_15Output, E>
|
||||||
|
|
||||||
|
type Endpoint4_16Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
|
||||||
export type Endpoint4_16Input = { readonly sessionID: Endpoint4_16Request["params"]["sessionID"] }
|
export type Endpoint4_16Input = { readonly sessionID: Endpoint4_16Request["params"]["sessionID"] }
|
||||||
export type Endpoint4_16Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.commit"]>>
|
export type Endpoint4_16Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.clear"]>>
|
||||||
export type SessionRevertCommitOperation<E = never> = (input: Endpoint4_16Input) => Effect.Effect<Endpoint4_16Output, E>
|
export type SessionRevertClearOperation<E = never> = (input: Endpoint4_16Input) => Effect.Effect<Endpoint4_16Output, E>
|
||||||
|
|
||||||
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.context"]>[0]
|
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
|
||||||
export type Endpoint4_17Input = { readonly sessionID: Endpoint4_17Request["params"]["sessionID"] }
|
export type Endpoint4_17Input = { readonly sessionID: Endpoint4_17Request["params"]["sessionID"] }
|
||||||
export type Endpoint4_17Output = EffectValue<ReturnType<RawClient["server.session"]["session.context"]>>["data"]
|
export type Endpoint4_17Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.commit"]>>
|
||||||
export type SessionContextOperation<E = never> = (input: Endpoint4_17Input) => Effect.Effect<Endpoint4_17Output, E>
|
export type SessionRevertCommitOperation<E = never> = (input: Endpoint4_17Input) => Effect.Effect<Endpoint4_17Output, E>
|
||||||
|
|
||||||
type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.context.entry.list"]>[0]
|
type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.context"]>[0]
|
||||||
export type Endpoint4_18Input = { readonly sessionID: Endpoint4_18Request["params"]["sessionID"] }
|
export type Endpoint4_18Input = { readonly sessionID: Endpoint4_18Request["params"]["sessionID"] }
|
||||||
export type Endpoint4_18Output = EffectValue<
|
export type Endpoint4_18Output = EffectValue<ReturnType<RawClient["server.session"]["session.context"]>>["data"]
|
||||||
|
export type SessionContextOperation<E = never> = (input: Endpoint4_18Input) => Effect.Effect<Endpoint4_18Output, E>
|
||||||
|
|
||||||
|
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.context.entry.list"]>[0]
|
||||||
|
export type Endpoint4_19Input = { readonly sessionID: Endpoint4_19Request["params"]["sessionID"] }
|
||||||
|
export type Endpoint4_19Output = EffectValue<
|
||||||
ReturnType<RawClient["server.session"]["session.context.entry.list"]>
|
ReturnType<RawClient["server.session"]["session.context.entry.list"]>
|
||||||
>["data"]
|
>["data"]
|
||||||
export type SessionListContextEntriesOperation<E = never> = (
|
export type SessionListContextEntriesOperation<E = never> = (
|
||||||
input: Endpoint4_18Input,
|
|
||||||
) => Effect.Effect<Endpoint4_18Output, E>
|
|
||||||
|
|
||||||
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.context.entry.put"]>[0]
|
|
||||||
export type Endpoint4_19Input = {
|
|
||||||
readonly sessionID: Endpoint4_19Request["params"]["sessionID"]
|
|
||||||
readonly key: Endpoint4_19Request["params"]["key"]
|
|
||||||
readonly value: Endpoint4_19Request["payload"]["value"]
|
|
||||||
}
|
|
||||||
export type Endpoint4_19Output = EffectValue<ReturnType<RawClient["server.session"]["session.context.entry.put"]>>
|
|
||||||
export type SessionPutContextEntryOperation<E = never> = (
|
|
||||||
input: Endpoint4_19Input,
|
input: Endpoint4_19Input,
|
||||||
) => Effect.Effect<Endpoint4_19Output, E>
|
) => Effect.Effect<Endpoint4_19Output, E>
|
||||||
|
|
||||||
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.context.entry.remove"]>[0]
|
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.context.entry.put"]>[0]
|
||||||
export type Endpoint4_20Input = {
|
export type Endpoint4_20Input = {
|
||||||
readonly sessionID: Endpoint4_20Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_20Request["params"]["sessionID"]
|
||||||
readonly key: Endpoint4_20Request["params"]["key"]
|
readonly key: Endpoint4_20Request["params"]["key"]
|
||||||
|
readonly value: Endpoint4_20Request["payload"]["value"]
|
||||||
}
|
}
|
||||||
export type Endpoint4_20Output = EffectValue<ReturnType<RawClient["server.session"]["session.context.entry.remove"]>>
|
export type Endpoint4_20Output = EffectValue<ReturnType<RawClient["server.session"]["session.context.entry.put"]>>
|
||||||
export type SessionRemoveContextEntryOperation<E = never> = (
|
export type SessionPutContextEntryOperation<E = never> = (
|
||||||
input: Endpoint4_20Input,
|
input: Endpoint4_20Input,
|
||||||
) => Effect.Effect<Endpoint4_20Output, E>
|
) => Effect.Effect<Endpoint4_20Output, E>
|
||||||
|
|
||||||
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.log"]>[0]
|
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.context.entry.remove"]>[0]
|
||||||
export type Endpoint4_21Input = {
|
export type Endpoint4_21Input = {
|
||||||
readonly sessionID: Endpoint4_21Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_21Request["params"]["sessionID"]
|
||||||
readonly after?: Endpoint4_21Request["query"]["after"]
|
readonly key: Endpoint4_21Request["params"]["key"]
|
||||||
readonly follow?: Endpoint4_21Request["query"]["follow"]
|
|
||||||
}
|
}
|
||||||
export type Endpoint4_21Output = StreamValue<EffectValue<ReturnType<RawClient["server.session"]["session.log"]>>>
|
export type Endpoint4_21Output = EffectValue<ReturnType<RawClient["server.session"]["session.context.entry.remove"]>>
|
||||||
export type SessionLogOperation<E = never> = (input: Endpoint4_21Input) => Stream.Stream<Endpoint4_21Output, E>
|
export type SessionRemoveContextEntryOperation<E = never> = (
|
||||||
|
input: Endpoint4_21Input,
|
||||||
|
) => Effect.Effect<Endpoint4_21Output, E>
|
||||||
|
|
||||||
type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
|
type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.log"]>[0]
|
||||||
export type Endpoint4_22Input = { readonly sessionID: Endpoint4_22Request["params"]["sessionID"] }
|
export type Endpoint4_22Input = {
|
||||||
export type Endpoint4_22Output = EffectValue<ReturnType<RawClient["server.session"]["session.interrupt"]>>
|
readonly sessionID: Endpoint4_22Request["params"]["sessionID"]
|
||||||
export type SessionInterruptOperation<E = never> = (input: Endpoint4_22Input) => Effect.Effect<Endpoint4_22Output, E>
|
readonly after?: Endpoint4_22Request["query"]["after"]
|
||||||
|
readonly follow?: Endpoint4_22Request["query"]["follow"]
|
||||||
|
}
|
||||||
|
export type Endpoint4_22Output = StreamValue<EffectValue<ReturnType<RawClient["server.session"]["session.log"]>>>
|
||||||
|
export type SessionLogOperation<E = never> = (input: Endpoint4_22Input) => Stream.Stream<Endpoint4_22Output, E>
|
||||||
|
|
||||||
type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.background"]>[0]
|
type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
|
||||||
export type Endpoint4_23Input = { readonly sessionID: Endpoint4_23Request["params"]["sessionID"] }
|
export type Endpoint4_23Input = { readonly sessionID: Endpoint4_23Request["params"]["sessionID"] }
|
||||||
export type Endpoint4_23Output = EffectValue<ReturnType<RawClient["server.session"]["session.background"]>>
|
export type Endpoint4_23Output = EffectValue<ReturnType<RawClient["server.session"]["session.interrupt"]>>
|
||||||
export type SessionBackgroundOperation<E = never> = (input: Endpoint4_23Input) => Effect.Effect<Endpoint4_23Output, E>
|
export type SessionInterruptOperation<E = never> = (input: Endpoint4_23Input) => Effect.Effect<Endpoint4_23Output, E>
|
||||||
|
|
||||||
type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.message"]>[0]
|
type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.background"]>[0]
|
||||||
export type Endpoint4_24Input = {
|
export type Endpoint4_24Input = { readonly sessionID: Endpoint4_24Request["params"]["sessionID"] }
|
||||||
readonly sessionID: Endpoint4_24Request["params"]["sessionID"]
|
export type Endpoint4_24Output = EffectValue<ReturnType<RawClient["server.session"]["session.background"]>>
|
||||||
readonly messageID: Endpoint4_24Request["params"]["messageID"]
|
export type SessionBackgroundOperation<E = never> = (input: Endpoint4_24Input) => Effect.Effect<Endpoint4_24Output, E>
|
||||||
|
|
||||||
|
type Endpoint4_25Request = Parameters<RawClient["server.session"]["session.message"]>[0]
|
||||||
|
export type Endpoint4_25Input = {
|
||||||
|
readonly sessionID: Endpoint4_25Request["params"]["sessionID"]
|
||||||
|
readonly messageID: Endpoint4_25Request["params"]["messageID"]
|
||||||
}
|
}
|
||||||
export type Endpoint4_24Output = EffectValue<ReturnType<RawClient["server.session"]["session.message"]>>["data"]
|
export type Endpoint4_25Output = EffectValue<ReturnType<RawClient["server.session"]["session.message"]>>["data"]
|
||||||
export type SessionMessageOperation<E = never> = (input: Endpoint4_24Input) => Effect.Effect<Endpoint4_24Output, E>
|
export type SessionMessageOperation<E = never> = (input: Endpoint4_25Input) => Effect.Effect<Endpoint4_25Output, E>
|
||||||
|
|
||||||
export interface SessionApi<E = never> {
|
export interface SessionApi<E = never> {
|
||||||
readonly list: SessionListOperation<E>
|
readonly list: SessionListOperation<E>
|
||||||
@@ -257,6 +266,7 @@ export interface SessionApi<E = never> {
|
|||||||
readonly command: SessionCommandOperation<E>
|
readonly command: SessionCommandOperation<E>
|
||||||
readonly skill: SessionSkillOperation<E>
|
readonly skill: SessionSkillOperation<E>
|
||||||
readonly synthetic: SessionSyntheticOperation<E>
|
readonly synthetic: SessionSyntheticOperation<E>
|
||||||
|
readonly shell: SessionShellOperation<E>
|
||||||
readonly compact: SessionCompactOperation<E>
|
readonly compact: SessionCompactOperation<E>
|
||||||
readonly wait: SessionWaitOperation<E>
|
readonly wait: SessionWaitOperation<E>
|
||||||
readonly revertStage: SessionRevertStageOperation<E>
|
readonly revertStage: SessionRevertStageOperation<E>
|
||||||
@@ -208,23 +208,35 @@ const Endpoint4_11 = (raw: RawClient["server.session"]) => (input: Endpoint4_11I
|
|||||||
payload: { text: input["text"], description: input["description"], metadata: input["metadata"] },
|
payload: { text: input["text"], description: input["description"], metadata: input["metadata"] },
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint4_12Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
|
type Endpoint4_12Request = Parameters<RawClient["server.session"]["session.shell"]>[0]
|
||||||
type Endpoint4_12Input = { readonly sessionID: Endpoint4_12Request["params"]["sessionID"] }
|
type Endpoint4_12Input = {
|
||||||
|
readonly sessionID: Endpoint4_12Request["params"]["sessionID"]
|
||||||
|
readonly id?: Endpoint4_12Request["payload"]["id"]
|
||||||
|
readonly command: Endpoint4_12Request["payload"]["command"]
|
||||||
|
}
|
||||||
const Endpoint4_12 = (raw: RawClient["server.session"]) => (input: Endpoint4_12Input) =>
|
const Endpoint4_12 = (raw: RawClient["server.session"]) => (input: Endpoint4_12Input) =>
|
||||||
raw["session.compact"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
raw["session.shell"]({
|
||||||
|
params: { sessionID: input["sessionID"] },
|
||||||
|
payload: { id: input["id"], command: input["command"] },
|
||||||
|
}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint4_13Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
|
type Endpoint4_13Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
|
||||||
type Endpoint4_13Input = { readonly sessionID: Endpoint4_13Request["params"]["sessionID"] }
|
type Endpoint4_13Input = { readonly sessionID: Endpoint4_13Request["params"]["sessionID"] }
|
||||||
const Endpoint4_13 = (raw: RawClient["server.session"]) => (input: Endpoint4_13Input) =>
|
const Endpoint4_13 = (raw: RawClient["server.session"]) => (input: Endpoint4_13Input) =>
|
||||||
|
raw["session.compact"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
|
type Endpoint4_14Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
|
||||||
|
type Endpoint4_14Input = { readonly sessionID: Endpoint4_14Request["params"]["sessionID"] }
|
||||||
|
const Endpoint4_14 = (raw: RawClient["server.session"]) => (input: Endpoint4_14Input) =>
|
||||||
raw["session.wait"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
raw["session.wait"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint4_14Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
|
type Endpoint4_15Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
|
||||||
type Endpoint4_14Input = {
|
type Endpoint4_15Input = {
|
||||||
readonly sessionID: Endpoint4_14Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_15Request["params"]["sessionID"]
|
||||||
readonly messageID: Endpoint4_14Request["payload"]["messageID"]
|
readonly messageID: Endpoint4_15Request["payload"]["messageID"]
|
||||||
readonly files?: Endpoint4_14Request["payload"]["files"]
|
readonly files?: Endpoint4_15Request["payload"]["files"]
|
||||||
}
|
}
|
||||||
const Endpoint4_14 = (raw: RawClient["server.session"]) => (input: Endpoint4_14Input) =>
|
const Endpoint4_15 = (raw: RawClient["server.session"]) => (input: Endpoint4_15Input) =>
|
||||||
raw["session.revert.stage"]({
|
raw["session.revert.stage"]({
|
||||||
params: { sessionID: input["sessionID"] },
|
params: { sessionID: input["sessionID"] },
|
||||||
payload: { messageID: input["messageID"], files: input["files"] },
|
payload: { messageID: input["messageID"], files: input["files"] },
|
||||||
@@ -233,61 +245,61 @@ const Endpoint4_14 = (raw: RawClient["server.session"]) => (input: Endpoint4_14I
|
|||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint4_15Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
|
type Endpoint4_16Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
|
||||||
type Endpoint4_15Input = { readonly sessionID: Endpoint4_15Request["params"]["sessionID"] }
|
|
||||||
const Endpoint4_15 = (raw: RawClient["server.session"]) => (input: Endpoint4_15Input) =>
|
|
||||||
raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
type Endpoint4_16Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
|
|
||||||
type Endpoint4_16Input = { readonly sessionID: Endpoint4_16Request["params"]["sessionID"] }
|
type Endpoint4_16Input = { readonly sessionID: Endpoint4_16Request["params"]["sessionID"] }
|
||||||
const Endpoint4_16 = (raw: RawClient["server.session"]) => (input: Endpoint4_16Input) =>
|
const Endpoint4_16 = (raw: RawClient["server.session"]) => (input: Endpoint4_16Input) =>
|
||||||
raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.context"]>[0]
|
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
|
||||||
type Endpoint4_17Input = { readonly sessionID: Endpoint4_17Request["params"]["sessionID"] }
|
type Endpoint4_17Input = { readonly sessionID: Endpoint4_17Request["params"]["sessionID"] }
|
||||||
const Endpoint4_17 = (raw: RawClient["server.session"]) => (input: Endpoint4_17Input) =>
|
const Endpoint4_17 = (raw: RawClient["server.session"]) => (input: Endpoint4_17Input) =>
|
||||||
|
raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
|
type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.context"]>[0]
|
||||||
|
type Endpoint4_18Input = { readonly sessionID: Endpoint4_18Request["params"]["sessionID"] }
|
||||||
|
const Endpoint4_18 = (raw: RawClient["server.session"]) => (input: Endpoint4_18Input) =>
|
||||||
raw["session.context"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
raw["session.context"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.context.entry.list"]>[0]
|
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.context.entry.list"]>[0]
|
||||||
type Endpoint4_18Input = { readonly sessionID: Endpoint4_18Request["params"]["sessionID"] }
|
type Endpoint4_19Input = { readonly sessionID: Endpoint4_19Request["params"]["sessionID"] }
|
||||||
const Endpoint4_18 = (raw: RawClient["server.session"]) => (input: Endpoint4_18Input) =>
|
const Endpoint4_19 = (raw: RawClient["server.session"]) => (input: Endpoint4_19Input) =>
|
||||||
raw["session.context.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
raw["session.context.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.context.entry.put"]>[0]
|
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.context.entry.put"]>[0]
|
||||||
type Endpoint4_19Input = {
|
type Endpoint4_20Input = {
|
||||||
readonly sessionID: Endpoint4_19Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_20Request["params"]["sessionID"]
|
||||||
readonly key: Endpoint4_19Request["params"]["key"]
|
readonly key: Endpoint4_20Request["params"]["key"]
|
||||||
readonly value: Endpoint4_19Request["payload"]["value"]
|
readonly value: Endpoint4_20Request["payload"]["value"]
|
||||||
}
|
}
|
||||||
const Endpoint4_19 = (raw: RawClient["server.session"]) => (input: Endpoint4_19Input) =>
|
const Endpoint4_20 = (raw: RawClient["server.session"]) => (input: Endpoint4_20Input) =>
|
||||||
raw["session.context.entry.put"]({
|
raw["session.context.entry.put"]({
|
||||||
params: { sessionID: input["sessionID"], key: input["key"] },
|
params: { sessionID: input["sessionID"], key: input["key"] },
|
||||||
payload: { value: input["value"] },
|
payload: { value: input["value"] },
|
||||||
}).pipe(Effect.mapError(mapClientError))
|
}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.context.entry.remove"]>[0]
|
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.context.entry.remove"]>[0]
|
||||||
type Endpoint4_20Input = {
|
type Endpoint4_21Input = {
|
||||||
readonly sessionID: Endpoint4_20Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_21Request["params"]["sessionID"]
|
||||||
readonly key: Endpoint4_20Request["params"]["key"]
|
readonly key: Endpoint4_21Request["params"]["key"]
|
||||||
}
|
}
|
||||||
const Endpoint4_20 = (raw: RawClient["server.session"]) => (input: Endpoint4_20Input) =>
|
const Endpoint4_21 = (raw: RawClient["server.session"]) => (input: Endpoint4_21Input) =>
|
||||||
raw["session.context.entry.remove"]({ params: { sessionID: input["sessionID"], key: input["key"] } }).pipe(
|
raw["session.context.entry.remove"]({ params: { sessionID: input["sessionID"], key: input["key"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.log"]>[0]
|
type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.log"]>[0]
|
||||||
type Endpoint4_21Input = {
|
type Endpoint4_22Input = {
|
||||||
readonly sessionID: Endpoint4_21Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_22Request["params"]["sessionID"]
|
||||||
readonly after?: Endpoint4_21Request["query"]["after"]
|
readonly after?: Endpoint4_22Request["query"]["after"]
|
||||||
readonly follow?: Endpoint4_21Request["query"]["follow"]
|
readonly follow?: Endpoint4_22Request["query"]["follow"]
|
||||||
}
|
}
|
||||||
const Endpoint4_21 = (raw: RawClient["server.session"]) => (input: Endpoint4_21Input) =>
|
const Endpoint4_22 = (raw: RawClient["server.session"]) => (input: Endpoint4_22Input) =>
|
||||||
Stream.unwrap(
|
Stream.unwrap(
|
||||||
raw["session.log"]({
|
raw["session.log"]({
|
||||||
params: { sessionID: input["sessionID"] },
|
params: { sessionID: input["sessionID"] },
|
||||||
@@ -298,22 +310,22 @@ const Endpoint4_21 = (raw: RawClient["server.session"]) => (input: Endpoint4_21I
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
|
type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
|
||||||
type Endpoint4_22Input = { readonly sessionID: Endpoint4_22Request["params"]["sessionID"] }
|
|
||||||
const Endpoint4_22 = (raw: RawClient["server.session"]) => (input: Endpoint4_22Input) =>
|
|
||||||
raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
|
||||||
|
|
||||||
type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.background"]>[0]
|
|
||||||
type Endpoint4_23Input = { readonly sessionID: Endpoint4_23Request["params"]["sessionID"] }
|
type Endpoint4_23Input = { readonly sessionID: Endpoint4_23Request["params"]["sessionID"] }
|
||||||
const Endpoint4_23 = (raw: RawClient["server.session"]) => (input: Endpoint4_23Input) =>
|
const Endpoint4_23 = (raw: RawClient["server.session"]) => (input: Endpoint4_23Input) =>
|
||||||
|
raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
|
type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.background"]>[0]
|
||||||
|
type Endpoint4_24Input = { readonly sessionID: Endpoint4_24Request["params"]["sessionID"] }
|
||||||
|
const Endpoint4_24 = (raw: RawClient["server.session"]) => (input: Endpoint4_24Input) =>
|
||||||
raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.message"]>[0]
|
type Endpoint4_25Request = Parameters<RawClient["server.session"]["session.message"]>[0]
|
||||||
type Endpoint4_24Input = {
|
type Endpoint4_25Input = {
|
||||||
readonly sessionID: Endpoint4_24Request["params"]["sessionID"]
|
readonly sessionID: Endpoint4_25Request["params"]["sessionID"]
|
||||||
readonly messageID: Endpoint4_24Request["params"]["messageID"]
|
readonly messageID: Endpoint4_25Request["params"]["messageID"]
|
||||||
}
|
}
|
||||||
const Endpoint4_24 = (raw: RawClient["server.session"]) => (input: Endpoint4_24Input) =>
|
const Endpoint4_25 = (raw: RawClient["server.session"]) => (input: Endpoint4_25Input) =>
|
||||||
raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe(
|
raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
@@ -332,19 +344,20 @@ const adaptGroup4 = (raw: RawClient["server.session"]) => ({
|
|||||||
command: Endpoint4_9(raw),
|
command: Endpoint4_9(raw),
|
||||||
skill: Endpoint4_10(raw),
|
skill: Endpoint4_10(raw),
|
||||||
synthetic: Endpoint4_11(raw),
|
synthetic: Endpoint4_11(raw),
|
||||||
compact: Endpoint4_12(raw),
|
shell: Endpoint4_12(raw),
|
||||||
wait: Endpoint4_13(raw),
|
compact: Endpoint4_13(raw),
|
||||||
revertStage: Endpoint4_14(raw),
|
wait: Endpoint4_14(raw),
|
||||||
revertClear: Endpoint4_15(raw),
|
revertStage: Endpoint4_15(raw),
|
||||||
revertCommit: Endpoint4_16(raw),
|
revertClear: Endpoint4_16(raw),
|
||||||
context: Endpoint4_17(raw),
|
revertCommit: Endpoint4_17(raw),
|
||||||
listContextEntries: Endpoint4_18(raw),
|
context: Endpoint4_18(raw),
|
||||||
putContextEntry: Endpoint4_19(raw),
|
listContextEntries: Endpoint4_19(raw),
|
||||||
removeContextEntry: Endpoint4_20(raw),
|
putContextEntry: Endpoint4_20(raw),
|
||||||
log: Endpoint4_21(raw),
|
removeContextEntry: Endpoint4_21(raw),
|
||||||
interrupt: Endpoint4_22(raw),
|
log: Endpoint4_22(raw),
|
||||||
background: Endpoint4_23(raw),
|
interrupt: Endpoint4_23(raw),
|
||||||
message: Endpoint4_24(raw),
|
background: Endpoint4_24(raw),
|
||||||
|
message: Endpoint4_25(raw),
|
||||||
})
|
})
|
||||||
|
|
||||||
type Endpoint5_0Request = Parameters<RawClient["server.message"]["session.messages"]>[0]
|
type Endpoint5_0Request = Parameters<RawClient["server.message"]["session.messages"]>[0]
|
||||||
|
|||||||
@@ -1,8 +1,23 @@
|
|||||||
// TODO: Keep additional network capabilities inside Schema and Protocol as the client grows; /effect must never import
|
// TODO: Keep additional network capabilities inside Schema and Protocol as the client grows; /effect must never import
|
||||||
// Core or Server. Preserve these datatype exports so internal model reorganizations do not require caller migrations.
|
// Core or Server. Preserve these datatype exports so internal model reorganizations do not require caller migrations.
|
||||||
|
import type { Effect } from "effect"
|
||||||
|
|
||||||
export * from "./generated/index"
|
export * from "./generated/index"
|
||||||
|
export type {
|
||||||
|
AgentApi,
|
||||||
|
AppApi,
|
||||||
|
CatalogApi,
|
||||||
|
CommandApi,
|
||||||
|
EventApi,
|
||||||
|
IntegrationApi,
|
||||||
|
ModelApi,
|
||||||
|
PluginApi,
|
||||||
|
ProviderApi,
|
||||||
|
ReferenceApi,
|
||||||
|
SessionApi,
|
||||||
|
SkillApi,
|
||||||
|
} from "./api.js"
|
||||||
export { Service } from "./service.js"
|
export { Service } from "./service.js"
|
||||||
export type { Transport, ServiceOptions } from "./service.js"
|
|
||||||
export { Agent } from "@opencode-ai/schema/agent"
|
export { Agent } from "@opencode-ai/schema/agent"
|
||||||
export { Command } from "@opencode-ai/schema/command"
|
export { Command } from "@opencode-ai/schema/command"
|
||||||
export { Credential } from "@opencode-ai/schema/credential"
|
export { Credential } from "@opencode-ai/schema/credential"
|
||||||
@@ -28,3 +43,4 @@ export { SessionMessage } from "@opencode-ai/schema/session-message"
|
|||||||
export { Skill } from "@opencode-ai/schema/skill"
|
export { Skill } from "@opencode-ai/schema/skill"
|
||||||
export { Prompt } from "@opencode-ai/schema/prompt"
|
export { Prompt } from "@opencode-ai/schema/prompt"
|
||||||
export type { OpenCodeEvent } from "@opencode-ai/protocol/groups/event"
|
export type { OpenCodeEvent } from "@opencode-ai/protocol/groups/event"
|
||||||
|
export type OpenCodeClient = Effect.Success<ReturnType<typeof import("./generated/client").make>>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export type Transport = {
|
|||||||
readonly headers?: RequestInit["headers"]
|
readonly headers?: RequestInit["headers"]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServiceOptions = {
|
export type Options = {
|
||||||
// Absolute path to the service registration file. Defaults to
|
// Absolute path to the service registration file. Defaults to
|
||||||
// opencode/service.json in the XDG state directory.
|
// opencode/service.json in the XDG state directory.
|
||||||
readonly file?: string
|
readonly file?: string
|
||||||
@@ -30,21 +30,21 @@ export type ServiceOptions = {
|
|||||||
|
|
||||||
// Read-only lookup: registration file plus health check and version gate.
|
// Read-only lookup: registration file plus health check and version gate.
|
||||||
// Never spawns; escalation to start() is the caller's policy.
|
// Never spawns; escalation to start() is the caller's policy.
|
||||||
export const discover = Effect.fn("service.discover")(function* (options: ServiceOptions = {}) {
|
export const discover = Effect.fn("service.discover")(function* (options: Options = {}) {
|
||||||
const registration = yield* read(options.file)
|
const info = yield* read(options.file)
|
||||||
if (registration === undefined) return undefined
|
if (info === undefined) return undefined
|
||||||
if (options.version !== undefined && registration.version !== options.version) return undefined
|
if (options.version !== undefined && info.version !== options.version) return undefined
|
||||||
const found = yield* probe(registration)
|
const found = yield* probe(info)
|
||||||
return found?.transport
|
return found?.transport
|
||||||
})
|
})
|
||||||
|
|
||||||
// Idempotent ensure-running: reuses a healthy compatible server, replaces a
|
// Idempotent ensure-running: reuses a healthy compatible server, replaces a
|
||||||
// version-mismatched one, and otherwise spawns the service command detached.
|
// version-mismatched one, and otherwise spawns the service command detached.
|
||||||
export const start = Effect.fn("service.start")(function* (options: ServiceOptions = {}) {
|
export const start = Effect.fn("service.start")(function* (options: Options = {}) {
|
||||||
const compatible = yield* discover(options)
|
const compatible = yield* discover(options)
|
||||||
if (compatible !== undefined) return compatible
|
if (compatible !== undefined) return compatible
|
||||||
const mismatched = yield* find(options)
|
const mismatched = yield* find(options)
|
||||||
if (mismatched !== undefined) yield* kill(mismatched.registration, options).pipe(Effect.ignore)
|
if (mismatched !== undefined) yield* kill(mismatched.info, options).pipe(Effect.ignore)
|
||||||
|
|
||||||
const [command, ...args] = options.command ?? ["opencode", "serve", "--service"]
|
const [command, ...args] = options.command ?? ["opencode", "serve", "--service"]
|
||||||
if (command === undefined) return yield* Effect.fail(new Error("Missing service command"))
|
if (command === undefined) return yield* Effect.fail(new Error("Missing service command"))
|
||||||
@@ -64,10 +64,10 @@ export const start = Effect.fn("service.start")(function* (options: ServiceOptio
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export const stop = Effect.fn("service.stop")(function* (options: ServiceOptions = {}) {
|
export const stop = Effect.fn("service.stop")(function* (options: Options = {}) {
|
||||||
const fs = yield* FileSystem.FileSystem
|
const fs = yield* FileSystem.FileSystem
|
||||||
const existing = yield* find(options)
|
const existing = yield* find(options)
|
||||||
if (existing !== undefined) yield* kill(existing.registration, options)
|
if (existing !== undefined) yield* kill(existing.info, options)
|
||||||
yield* fs.remove(options.file ?? fallback()).pipe(Effect.ignore)
|
yield* fs.remove(options.file ?? fallback()).pipe(Effect.ignore)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -80,18 +80,18 @@ function auth(password: string): RequestInit["headers"] {
|
|||||||
return { authorization: "Basic " + btoa("opencode:" + password) }
|
return { authorization: "Basic " + btoa("opencode:" + password) }
|
||||||
}
|
}
|
||||||
|
|
||||||
const Registration = Schema.Struct({
|
export const Info = Schema.Struct({
|
||||||
id: Schema.optional(Schema.String),
|
id: Schema.optional(Schema.String),
|
||||||
version: Schema.optional(Schema.String),
|
version: Schema.optional(Schema.String),
|
||||||
url: Schema.String,
|
url: Schema.String,
|
||||||
pid: Schema.Int.check(Schema.isGreaterThan(0)),
|
pid: Schema.Int.check(Schema.isGreaterThan(0)),
|
||||||
password: Schema.optional(Schema.String),
|
password: Schema.optional(Schema.String),
|
||||||
})
|
})
|
||||||
type Registration = typeof Registration.Type
|
export type Info = typeof Info.Type
|
||||||
|
|
||||||
const decode = Schema.decodeUnknownEffect(Schema.fromJsonString(Registration))
|
const decode = Schema.decodeUnknownEffect(Schema.fromJsonString(Info))
|
||||||
|
|
||||||
// A missing or corrupt file means no valid registration; callers treat both
|
// A missing or corrupt file means no valid info; callers treat both
|
||||||
// the same (the registering server self-evicts, clients rediscover).
|
// the same (the registering server self-evicts, clients rediscover).
|
||||||
const read = Effect.fnUntraced(function* (file?: string) {
|
const read = Effect.fnUntraced(function* (file?: string) {
|
||||||
const fs = yield* FileSystem.FileSystem
|
const fs = yield* FileSystem.FileSystem
|
||||||
@@ -101,14 +101,14 @@ const read = Effect.fnUntraced(function* (file?: string) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
type LocalService = {
|
type LocalService = {
|
||||||
readonly registration: Registration
|
readonly info: Info
|
||||||
readonly transport: Transport
|
readonly transport: Transport
|
||||||
}
|
}
|
||||||
|
|
||||||
const probe = Effect.fnUntraced(function* (registration: Registration) {
|
const probe = Effect.fnUntraced(function* (info: Info) {
|
||||||
const headers = registration.password === undefined ? undefined : auth(registration.password)
|
const headers = info.password === undefined ? undefined : auth(info.password)
|
||||||
const healthy = yield* Effect.tryPromise(() =>
|
const healthy = yield* Effect.tryPromise(() =>
|
||||||
fetch(new URL("/api/health", registration.url), {
|
fetch(new URL("/api/health", info.url), {
|
||||||
headers,
|
headers,
|
||||||
signal: AbortSignal.timeout(2_000),
|
signal: AbortSignal.timeout(2_000),
|
||||||
}),
|
}),
|
||||||
@@ -117,15 +117,15 @@ const probe = Effect.fnUntraced(function* (registration: Registration) {
|
|||||||
Effect.orElseSucceed(() => false),
|
Effect.orElseSucceed(() => false),
|
||||||
)
|
)
|
||||||
if (!healthy) return undefined
|
if (!healthy) return undefined
|
||||||
return { registration, transport: { url: registration.url, headers } } satisfies LocalService
|
return { info, transport: { url: info.url, headers } } satisfies LocalService
|
||||||
})
|
})
|
||||||
|
|
||||||
// Health-checked lookup without the version gate: lifecycle operations must be
|
// Health-checked lookup without the version gate: lifecycle operations must be
|
||||||
// able to see (and replace or stop) a server from a different version.
|
// able to see (and replace or stop) a server from a different version.
|
||||||
const find = Effect.fnUntraced(function* (options: ServiceOptions) {
|
const find = Effect.fnUntraced(function* (options: Options) {
|
||||||
const registration = yield* read(options.file)
|
const info = yield* read(options.file)
|
||||||
if (registration === undefined) return undefined
|
if (info === undefined) return undefined
|
||||||
return yield* probe(registration)
|
return yield* probe(info)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 50ms cadence bounded at ~5s, shared by stop escalation and start readiness.
|
// 50ms cadence bounded at ~5s, shared by stop escalation and start readiness.
|
||||||
@@ -142,22 +142,22 @@ const stopped = Effect.fnUntraced(function* (pid: number) {
|
|||||||
return yield* Effect.fail(new Error(`Server process ${pid} is still running`))
|
return yield* Effect.fail(new Error(`Server process ${pid} is still running`))
|
||||||
})
|
})
|
||||||
|
|
||||||
function same(left: Registration, right: Registration) {
|
function same(left: Info, right: Info) {
|
||||||
return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid
|
return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid
|
||||||
}
|
}
|
||||||
|
|
||||||
const kill = Effect.fnUntraced(function* (info: Registration, options: ServiceOptions) {
|
const kill = Effect.fnUntraced(function* (info: Info, options: Options) {
|
||||||
// A stale registration may point at a PID that has since been reused by
|
// A stale registration may point at a PID that has since been reused by
|
||||||
// another process. Only signal the PID after authenticating the server.
|
// another process. Only signal the PID after authenticating the server.
|
||||||
const current = yield* find(options)
|
const current = yield* find(options)
|
||||||
if (current === undefined || !same(current.registration, info)) return
|
if (current === undefined || !same(current.info, info)) return
|
||||||
|
|
||||||
yield* signal(info.pid, "SIGTERM")
|
yield* signal(info.pid, "SIGTERM")
|
||||||
const done = yield* stopped(info.pid).pipe(Effect.retry(poll), Effect.option)
|
const done = yield* stopped(info.pid).pipe(Effect.retry(poll), Effect.option)
|
||||||
if (Option.isSome(done)) return
|
if (Option.isSome(done)) return
|
||||||
|
|
||||||
const latest = yield* find(options)
|
const latest = yield* find(options)
|
||||||
if (latest === undefined || !same(latest.registration, info)) return
|
if (latest === undefined || !same(latest.info, info)) return
|
||||||
yield* signal(info.pid, "SIGKILL")
|
yield* signal(info.pid, "SIGKILL")
|
||||||
yield* stopped(info.pid).pipe(Effect.retry(poll))
|
yield* stopped(info.pid).pipe(Effect.retry(poll))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import type {
|
||||||
|
AgentApi as EffectAgentApi,
|
||||||
|
CommandApi as EffectCommandApi,
|
||||||
|
EventApi as EffectEventApi,
|
||||||
|
IntegrationApi as EffectIntegrationApi,
|
||||||
|
ModelApi as EffectModelApi,
|
||||||
|
PluginApi as EffectPluginApi,
|
||||||
|
ProviderApi as EffectProviderApi,
|
||||||
|
ReferenceApi as EffectReferenceApi,
|
||||||
|
SessionApi as EffectSessionApi,
|
||||||
|
SkillApi as EffectSkillApi,
|
||||||
|
} from "../effect/api/api.js"
|
||||||
|
import type { Effect, Stream } from "effect"
|
||||||
|
|
||||||
|
type PromisifyOperation<Operation> = Operation extends (
|
||||||
|
...args: infer Args
|
||||||
|
) => Effect.Effect<infer Success, unknown, unknown>
|
||||||
|
? (...args: Args) => Promise<Success>
|
||||||
|
: Operation extends (...args: infer Args) => Stream.Stream<infer Success, unknown, unknown>
|
||||||
|
? (...args: Args) => AsyncIterable<Success>
|
||||||
|
: Operation
|
||||||
|
|
||||||
|
type PromisifyApi<Api> = {
|
||||||
|
readonly [Name in keyof Api]: PromisifyOperation<Api[Name]>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AgentApi = PromisifyApi<EffectAgentApi<unknown>>
|
||||||
|
export type CommandApi = PromisifyApi<EffectCommandApi<unknown>>
|
||||||
|
export type EventApi = PromisifyApi<EffectEventApi<unknown>>
|
||||||
|
export type IntegrationApi = PromisifyApi<EffectIntegrationApi<unknown>>
|
||||||
|
export type ModelApi = PromisifyApi<EffectModelApi<unknown>>
|
||||||
|
export type PluginApi = PromisifyApi<EffectPluginApi<unknown>>
|
||||||
|
export type ProviderApi = PromisifyApi<EffectProviderApi<unknown>>
|
||||||
|
export type ReferenceApi = PromisifyApi<EffectReferenceApi<unknown>>
|
||||||
|
export type SessionApi = PromisifyApi<EffectSessionApi<unknown>>
|
||||||
|
export type SkillApi = PromisifyApi<EffectSkillApi<unknown>>
|
||||||
|
|
||||||
|
export interface CatalogApi {
|
||||||
|
readonly provider: ProviderApi
|
||||||
|
readonly model: ModelApi
|
||||||
|
}
|
||||||
@@ -29,6 +29,8 @@ import type {
|
|||||||
SessionSkillOutput,
|
SessionSkillOutput,
|
||||||
SessionSyntheticInput,
|
SessionSyntheticInput,
|
||||||
SessionSyntheticOutput,
|
SessionSyntheticOutput,
|
||||||
|
SessionShellInput,
|
||||||
|
SessionShellOutput,
|
||||||
SessionCompactInput,
|
SessionCompactInput,
|
||||||
SessionCompactOutput,
|
SessionCompactOutput,
|
||||||
SessionWaitInput,
|
SessionWaitInput,
|
||||||
@@ -525,6 +527,18 @@ export function make(options: ClientOptions) {
|
|||||||
},
|
},
|
||||||
requestOptions,
|
requestOptions,
|
||||||
),
|
),
|
||||||
|
shell: (input: SessionShellInput, requestOptions?: RequestOptions) =>
|
||||||
|
request<SessionShellOutput>(
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
path: `/api/session/${encodeURIComponent(input.sessionID)}/shell`,
|
||||||
|
body: { id: input["id"], command: input["command"] },
|
||||||
|
successStatus: 204,
|
||||||
|
declaredStatuses: [404, 400, 401],
|
||||||
|
empty: true,
|
||||||
|
},
|
||||||
|
requestOptions,
|
||||||
|
),
|
||||||
compact: (input: SessionCompactInput, requestOptions?: RequestOptions) =>
|
compact: (input: SessionCompactInput, requestOptions?: RequestOptions) =>
|
||||||
request<SessionCompactOutput>(
|
request<SessionCompactOutput>(
|
||||||
{
|
{
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,16 @@
|
|||||||
export * from "./generated/index"
|
export * from "./generated/index"
|
||||||
export type { Transport } from "../effect/service.js"
|
export type {
|
||||||
|
AgentApi,
|
||||||
|
CatalogApi,
|
||||||
|
CommandApi,
|
||||||
|
EventApi,
|
||||||
|
IntegrationApi,
|
||||||
|
ModelApi,
|
||||||
|
PluginApi,
|
||||||
|
ProviderApi,
|
||||||
|
ReferenceApi,
|
||||||
|
SessionApi,
|
||||||
|
SkillApi,
|
||||||
|
} from "./api.js"
|
||||||
export type { EventSubscribeOutput as OpenCodeEvent } from "./generated/types"
|
export type { EventSubscribeOutput as OpenCodeEvent } from "./generated/types"
|
||||||
export type OpenCodeClient = ReturnType<typeof import("./generated/client").make>
|
export type OpenCodeClient = ReturnType<typeof import("./generated/client").make>
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { Effect } from "effect"
|
||||||
|
import { OpenCode as EffectOpenCode, type AppApi as EffectApi } from "../src/effect"
|
||||||
|
|
||||||
|
type EffectClient = Effect.Success<ReturnType<typeof EffectOpenCode.make>>
|
||||||
|
|
||||||
|
declare const effectClient: EffectClient
|
||||||
|
|
||||||
|
const effectApi: EffectApi<unknown> = effectClient
|
||||||
|
|
||||||
|
void effectApi
|
||||||
@@ -1,7 +1,17 @@
|
|||||||
import { expect, test } from "bun:test"
|
import { expect, test } from "bun:test"
|
||||||
import { DateTime, Effect, Stream } from "effect"
|
import { DateTime, Effect, Stream } from "effect"
|
||||||
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
||||||
import { AbsolutePath, Agent, Event, Location, Model, OpenCode, Prompt, Session, SessionMessage } from "../src/effect/index"
|
import {
|
||||||
|
AbsolutePath,
|
||||||
|
Agent,
|
||||||
|
Event,
|
||||||
|
Location,
|
||||||
|
Model,
|
||||||
|
OpenCode,
|
||||||
|
Prompt,
|
||||||
|
Session,
|
||||||
|
SessionMessage,
|
||||||
|
} from "../src/effect/index"
|
||||||
|
|
||||||
const synced = { type: "log.synced" as const, aggregateID: "ses_test", seq: Event.Seq.make(1) }
|
const synced = { type: "log.synced" as const, aggregateID: "ses_test", seq: Event.Seq.make(1) }
|
||||||
|
|
||||||
@@ -23,7 +33,7 @@ test("event.subscribe exposes and decodes the native Effect event stream", async
|
|||||||
HttpClientResponse.fromWeb(
|
HttpClientResponse.fromWeb(
|
||||||
request,
|
request,
|
||||||
new Response(
|
new Response(
|
||||||
`data: ${JSON.stringify({ id: "evt_connected", type: "server.connected", data: {} })}\n\n` +
|
`data: ${JSON.stringify({ id: "evt_connected", created: 0, type: "server.connected", data: {} })}\n\n` +
|
||||||
`data: ${JSON.stringify(modelSwitchedEvent)}\n\n`,
|
`data: ${JSON.stringify(modelSwitchedEvent)}\n\n`,
|
||||||
{ headers: { "content-type": "text/event-stream" } },
|
{ headers: { "content-type": "text/event-stream" } },
|
||||||
),
|
),
|
||||||
@@ -35,10 +45,10 @@ test("event.subscribe exposes and decodes the native Effect event stream", async
|
|||||||
return yield* client.event.subscribe().pipe(Stream.runCollect)
|
return yield* client.event.subscribe().pipe(Stream.runCollect)
|
||||||
}).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise)
|
}).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise)
|
||||||
|
|
||||||
expect(Array.from(events).map((event) => event.type)).toEqual(["server.connected", "session.next.model.switched"])
|
expect(Array.from(events).map((event) => event.type)).toEqual(["server.connected", "session.model.selected"])
|
||||||
const durable = events[1]
|
const durable = events[1]
|
||||||
if (durable?.type !== "session.next.model.switched") throw new Error("Expected model event")
|
if (durable?.type !== "session.model.selected") throw new Error("Expected model event")
|
||||||
expect(DateTime.toEpochMillis(durable.data.timestamp)).toBe(1_717_171_717_000)
|
expect(DateTime.toEpochMillis(durable.created)).toBe(1_717_171_717_000)
|
||||||
expect(durable.durable).toEqual({ aggregateID: "ses_test", seq: 1, version: 1 })
|
expect(durable.durable).toEqual({ aggregateID: "ses_test", seq: 1, version: 1 })
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -149,8 +159,8 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
|
|||||||
expect(result.context).toEqual([])
|
expect(result.context).toEqual([])
|
||||||
expect(logQueries[0]).toEqual({ after: "0" })
|
expect(logQueries[0]).toEqual({ after: "0" })
|
||||||
const logged = Array.from(result.log)
|
const logged = Array.from(result.log)
|
||||||
expect(logged.map((item) => item.type)).toEqual(["session.next.model.switched", "log.synced"])
|
expect(logged.map((item) => item.type)).toEqual(["session.model.selected", "log.synced"])
|
||||||
expect(logged[0]?.type === "session.next.model.switched" && DateTime.toEpochMillis(logged[0].data.timestamp)).toBe(
|
expect(logged[0]?.type === "session.model.selected" && DateTime.toEpochMillis(logged[0].created)).toBe(
|
||||||
1_717_171_717_000,
|
1_717_171_717_000,
|
||||||
)
|
)
|
||||||
expect(logged.at(-1)).toEqual(synced)
|
expect(logged.at(-1)).toEqual(synced)
|
||||||
@@ -217,12 +227,11 @@ const modelSwitchedMessage = {
|
|||||||
|
|
||||||
const modelSwitchedEvent = {
|
const modelSwitchedEvent = {
|
||||||
id: "evt_model",
|
id: "evt_model",
|
||||||
type: "session.next.model.switched",
|
created: 1_717_171_717_000,
|
||||||
|
type: "session.model.selected",
|
||||||
durable: { aggregateID: "ses_test", seq: 1, version: 1 },
|
durable: { aggregateID: "ses_test", seq: 1, version: 1 },
|
||||||
data: {
|
data: {
|
||||||
timestamp: 1_717_171_717_000,
|
|
||||||
sessionID: "ses_test",
|
sessionID: "ses_test",
|
||||||
messageID: "msg_model",
|
|
||||||
model: { id: "claude", providerID: "anthropic" },
|
model: { id: "claude", providerID: "anthropic" },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ test("event.subscribe exposes the Promise event stream wire projection", async (
|
|||||||
baseUrl: "http://localhost:3000",
|
baseUrl: "http://localhost:3000",
|
||||||
fetch: async () =>
|
fetch: async () =>
|
||||||
new Response(
|
new Response(
|
||||||
`: heartbeat\n\ndata: ${JSON.stringify({ id: "evt_connected", type: "server.connected", data: {} })}\n\n` +
|
`: heartbeat\n\ndata: ${JSON.stringify({ id: "evt_connected", created: 0, type: "server.connected", data: {} })}\n\n` +
|
||||||
`data: ${JSON.stringify(modelSwitchedEvent)}\n\n`,
|
`data: ${JSON.stringify(modelSwitchedEvent)}\n\n`,
|
||||||
{ headers: { "content-type": "text/event-stream" } },
|
{ headers: { "content-type": "text/event-stream" } },
|
||||||
),
|
),
|
||||||
@@ -159,8 +159,8 @@ test("event.subscribe exposes the Promise event stream wire projection", async (
|
|||||||
const events = []
|
const events = []
|
||||||
for await (const event of client.event.subscribe()) events.push(event)
|
for await (const event of client.event.subscribe()) events.push(event)
|
||||||
|
|
||||||
expect(events).toEqual([{ id: "evt_connected", type: "server.connected", data: {} }, modelSwitchedEvent])
|
expect(events).toEqual([{ id: "evt_connected", created: 0, type: "server.connected", data: {} }, modelSwitchedEvent])
|
||||||
expect(events[1]?.type === "session.next.model.switched" && events[1].data.timestamp).toBe(1_717_171_717_000)
|
expect(events[1]?.type === "session.model.selected" && events[1].created).toBe(1_717_171_717_000)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("event.subscribe terminates on malformed Promise SSE data", async () => {
|
test("event.subscribe terminates on malformed Promise SSE data", async () => {
|
||||||
@@ -328,12 +328,11 @@ const synced = { type: "log.synced", aggregateID: "ses_test", seq: 1 }
|
|||||||
|
|
||||||
const modelSwitchedEvent = {
|
const modelSwitchedEvent = {
|
||||||
id: "evt_model",
|
id: "evt_model",
|
||||||
type: "session.next.model.switched",
|
created: 1_717_171_717_000,
|
||||||
|
type: "session.model.selected",
|
||||||
durable: { aggregateID: "ses_test", seq: 1, version: 1 },
|
durable: { aggregateID: "ses_test", seq: 1, version: 1 },
|
||||||
data: {
|
data: {
|
||||||
timestamp: 1_717_171_717_000,
|
|
||||||
sessionID: "ses_test",
|
sessionID: "ses_test",
|
||||||
messageID: "msg_model",
|
|
||||||
model: { id: "claude", providerID: "anthropic" },
|
model: { id: "claude", providerID: "anthropic" },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "dist",
|
||||||
|
"noEmit": false,
|
||||||
|
"declaration": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
"extends": "@tsconfig/bun/tsconfig.json",
|
"extends": "@tsconfig/bun/tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||||
|
"allowImportingTsExtensions": false,
|
||||||
|
"allowJs": false,
|
||||||
"noUncheckedIndexedAccess": false
|
"noUncheckedIndexedAccess": false
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
|
|||||||
@@ -458,8 +458,8 @@ export const discoveryPlan = <R>(
|
|||||||
|
|
||||||
// Section order is deliberate: workflow first (the top is the least likely part of a long
|
// Section order is deliberate: workflow first (the top is the least likely part of a long
|
||||||
// description to be truncated or skimmed away), then rules, then syntax, with the budgeted
|
// description to be truncated or skimmed away), then rules, then syntax, with the budgeted
|
||||||
// catalog at the bottom. Example call forms use explicit `<namespace>.<tool>` placeholders -
|
// catalog at the bottom. Example call forms use placeholders - never a real or fabricated
|
||||||
// never a real or fabricated tool name.
|
// tool name - and show both dot and bracket notation so non-identifier names are not normalized.
|
||||||
const intro = [
|
const intro = [
|
||||||
"Write a CodeMode program to answer the request. Return code only.",
|
"Write a CodeMode program to answer the request. Return code only.",
|
||||||
empty
|
empty
|
||||||
@@ -467,6 +467,7 @@ export const discoveryPlan = <R>(
|
|||||||
: complete
|
: complete
|
||||||
? "Execute JavaScript in a confined runtime. Inside this program, `tools` contains only the host-provided tools listed below; surrounding agent tools are not available unless listed here."
|
? "Execute JavaScript in a confined runtime. Inside this program, `tools` contains only the host-provided tools listed below; surrounding agent tools are not available unless listed here."
|
||||||
: "Execute JavaScript in a confined runtime. Inside this program, `tools` contains only the host-provided tools listed or searchable below; surrounding agent tools are not available unless listed here.",
|
: "Execute JavaScript in a confined runtime. Inside this program, `tools` contains only the host-provided tools listed or searchable below; surrounding agent tools are not available unless listed here.",
|
||||||
|
...(empty ? [] : ["Do not infer or normalize tool names; use only exact signatures shown below or returned by search."]),
|
||||||
]
|
]
|
||||||
|
|
||||||
// The search step exists only when search is advertised (PARTIAL catalog); a COMPLETE
|
// The search step exists only when search is advertised (PARTIAL catalog); a COMPLETE
|
||||||
@@ -480,14 +481,14 @@ export const discoveryPlan = <R>(
|
|||||||
...(complete
|
...(complete
|
||||||
? [
|
? [
|
||||||
"1. Pick a tool from the list under `## Available tools` - each line is the exact call signature; use it as-is rather than guessing segments.",
|
"1. Pick a tool from the list under `## Available tools` - each line is the exact call signature; use it as-is rather than guessing segments.",
|
||||||
"2. Call it using the exact signature shown: `const res = await tools.<namespace>.<tool>(input)` - bracket notation may appear for names that are not JavaScript identifiers.",
|
'2. Call it using the exact signature shown; bracket notation and quotes are part of the path.',
|
||||||
'3. Parse text results: `const data = typeof res === "string" ? JSON.parse(res) : res` - most tools return JSON as a string.',
|
'3. Parse text results: `const data = typeof res === "string" ? JSON.parse(res) : res` - most tools return JSON as a string.',
|
||||||
"4. Return only the fields you need: `return { <field>: data.<field> }` - raw payloads get truncated and waste context.",
|
"4. Return only the fields you need: `return { <field>: data.<field> }` - raw payloads get truncated and waste context.",
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
'1. Find a tool (skip when it is already listed below): `const { items } = await tools.$codemode.search({ query: "<intent + key nouns>" })` - short phrases like "list issues" work best.',
|
'1. If the exact signature is not listed below, first search: `const { items } = await tools.$codemode.search({ query: "<intent + key nouns>" })`.',
|
||||||
"2. Read the matches: each item is `{ path, description, signature }` - read the description before using an unfamiliar tool.",
|
"2. Read the matches: each item is `{ path, description, signature }` - read the description before using an unfamiliar tool.",
|
||||||
"3. Call it with the result's `path` as-is (never guess segments): `const res = await tools.<namespace>.<tool>(input)` - bracket notation may appear for names that are not JavaScript identifiers.",
|
"3. Call the result's `path` as-is; bracket notation and quotes are part of the path.",
|
||||||
'4. Parse text results: `const data = typeof res === "string" ? JSON.parse(res) : res` - most tools return JSON as a string.',
|
'4. Parse text results: `const data = typeof res === "string" ? JSON.parse(res) : res` - most tools return JSON as a string.',
|
||||||
"5. Return only the fields you need: `return { <field>: data.<field> }` - raw payloads get truncated and waste context.",
|
"5. Return only the fields you need: `return { <field>: data.<field> }` - raw payloads get truncated and waste context.",
|
||||||
]),
|
]),
|
||||||
@@ -504,7 +505,7 @@ export const discoveryPlan = <R>(
|
|||||||
: "- Only tools listed here or returned by `tools.$codemode.search` are available inside `tools`; tools from the surrounding agent/runtime are not implicitly exposed.",
|
: "- Only tools listed here or returned by `tools.$codemode.search` are available inside `tools`; tools from the surrounding agent/runtime are not implicitly exposed.",
|
||||||
"- Filter, aggregate, and transform collections in code - never return them raw or call a tool per item across messages.",
|
"- Filter, aggregate, and transform collections in code - never return them raw or call a tool per item across messages.",
|
||||||
"- A result typed `Promise<unknown>` has no guaranteed shape - verify what actually came back before relying on its fields.",
|
"- A result typed `Promise<unknown>` has no guaranteed shape - verify what actually came back before relying on its fields.",
|
||||||
"- Run independent calls in parallel: `await Promise.all(items.map((item) => tools.<namespace>.<tool>(item)))`.",
|
'- Run independent calls in parallel: `await Promise.all(items.map((item) => tools.<namespace>.<tool>(item)))`, or use `tools.<namespace>["tool-name"](item)` when the listed signature uses bracket notation.',
|
||||||
"- `Object.keys(tools)` lists namespaces; `Object.keys(tools.<namespace>)` lists its tools; `for...in` works on both.",
|
"- `Object.keys(tools)` lists namespaces; `Object.keys(tools.<namespace>)` lists its tools; `for...in` works on both.",
|
||||||
...(complete
|
...(complete
|
||||||
? []
|
? []
|
||||||
|
|||||||
@@ -622,12 +622,12 @@ describe("CodeMode public contract", () => {
|
|||||||
)
|
)
|
||||||
expect(instructions).toContain("Return only the fields you need")
|
expect(instructions).toContain("Return only the fields you need")
|
||||||
expect(instructions).toContain("raw payloads get truncated and waste context")
|
expect(instructions).toContain("raw payloads get truncated and waste context")
|
||||||
expect(instructions).toContain("`const res = await tools.<namespace>.<tool>(input)`")
|
expect(instructions).toContain("Do not infer or normalize tool names")
|
||||||
|
expect(instructions).toContain("bracket notation and quotes are part of the path")
|
||||||
expect(instructions).toContain("surrounding agent tools are not available unless listed here")
|
expect(instructions).toContain("surrounding agent tools are not available unless listed here")
|
||||||
expect(instructions).toContain("Only tools listed here are available inside `tools`")
|
expect(instructions).toContain("Only tools listed here are available inside `tools`")
|
||||||
expect(instructions).toContain("bracket notation may appear for names that are not JavaScript identifiers")
|
// Placeholders use generic namespace/tool/field names only - no fabricated real tools
|
||||||
// Placeholders use the <namespace>.<tool>/<field> style ONLY - no fabricated tool
|
// and no real catalog tools cherry-picked into example lines.
|
||||||
// names, and no real catalog tools cherry-picked into example lines.
|
|
||||||
expect(instructions).toContain("`return { <field>: data.<field> }`")
|
expect(instructions).toContain("`return { <field>: data.<field> }`")
|
||||||
expect(instructions).not.toContain("total_count")
|
expect(instructions).not.toContain("total_count")
|
||||||
expect(instructions).not.toContain("list_issues")
|
expect(instructions).not.toContain("list_issues")
|
||||||
@@ -640,7 +640,7 @@ describe("CodeMode public contract", () => {
|
|||||||
// PARTIAL: the workflow starts with search (with query-style guidance that is clearly
|
// PARTIAL: the workflow starts with search (with query-style guidance that is clearly
|
||||||
// a query string, never a tool name) and the browse-namespace rule appears.
|
// a query string, never a tool name) and the browse-namespace rule appears.
|
||||||
expect(partial).toContain(
|
expect(partial).toContain(
|
||||||
'1. Find a tool (skip when it is already listed below): `const { items } = await tools.$codemode.search({ query: "<intent + key nouns>" })` - short phrases like "list issues" work best.',
|
'1. If the exact signature is not listed below, first search: `const { items } = await tools.$codemode.search({ query: "<intent + key nouns>" })`.',
|
||||||
)
|
)
|
||||||
expect(partial).toContain(
|
expect(partial).toContain(
|
||||||
"Only tools listed here or returned by `tools.$codemode.search` are available inside `tools`",
|
"Only tools listed here or returned by `tools.$codemode.search` are available inside `tools`",
|
||||||
|
|||||||
@@ -339,3 +339,43 @@ describe("pretty signatures in search results", () => {
|
|||||||
expect(instructions).not.toContain("/**")
|
expect(instructions).not.toContain("/**")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("non-identifier tool paths", () => {
|
||||||
|
const resolveLibrary = Tool.make({
|
||||||
|
description: "Resolve a Context7 library ID",
|
||||||
|
input: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
query: { type: "string" },
|
||||||
|
libraryName: { type: "string" },
|
||||||
|
},
|
||||||
|
required: ["query", "libraryName"],
|
||||||
|
} as const,
|
||||||
|
run: () => Effect.succeed("/reactjs/react.dev"),
|
||||||
|
})
|
||||||
|
const runtime = CodeMode.make({ tools: { context7: { "resolve-library-id": resolveLibrary } } })
|
||||||
|
|
||||||
|
test("inline catalog uses bracket notation for dashed tool names", () => {
|
||||||
|
const instructions = runtime.instructions()
|
||||||
|
|
||||||
|
expect(instructions).toContain(
|
||||||
|
'tools.context7["resolve-library-id"](input: { query: string; libraryName: string }): Promise<unknown>',
|
||||||
|
)
|
||||||
|
expect(instructions).toContain("Do not infer or normalize tool names")
|
||||||
|
expect(instructions).toContain("bracket notation and quotes are part of the path")
|
||||||
|
expect(instructions).not.toContain("tools.context7.resolve-library-id")
|
||||||
|
expect(instructions).not.toContain("tools.context7.resolve_library_id")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("search results return callable bracket-notation paths and signatures", async () => {
|
||||||
|
const result = await Effect.runPromise(
|
||||||
|
runtime.execute(`return await tools.$codemode.search({ query: "resolve library" })`),
|
||||||
|
)
|
||||||
|
expect(result.ok).toBe(true)
|
||||||
|
if (!result.ok) throw new Error("search failed")
|
||||||
|
|
||||||
|
const value = result.value as { items: Array<{ path: string; signature: string }> }
|
||||||
|
expect(value.items[0]?.path).toBe('tools.context7["resolve-library-id"]')
|
||||||
|
expect(value.items[0]?.signature).toContain('tools.context7["resolve-library-id"](input: {')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "sqlite",
|
"dialect": "sqlite",
|
||||||
"id": "22e57fed-b9b8-4e94-a3b4-f94bece680a8",
|
"id": "96e9fe64-d810-4102-8f79-3317a88bb6d2",
|
||||||
"prevIds": [
|
"prevIds": [
|
||||||
"f14a9b18-8207-487e-a3d3-227e629ba9ad"
|
"22e57fed-b9b8-4e94-a3b4-f94bece680a8"
|
||||||
],
|
],
|
||||||
"ddl": [
|
"ddl": [
|
||||||
{
|
{
|
||||||
@@ -526,6 +526,16 @@
|
|||||||
"entityType": "columns",
|
"entityType": "columns",
|
||||||
"table": "event"
|
"table": "event"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": null,
|
||||||
|
"generated": null,
|
||||||
|
"name": "created",
|
||||||
|
"entityType": "columns",
|
||||||
|
"table": "event"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"notNull": true,
|
"notNull": true,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export * as AccountV2 from "./account"
|
export * as AccountV2 from "./account"
|
||||||
|
|
||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import type * as HttpClientError from "effect/unstable/http/HttpClientError"
|
import type { HttpClientError } from "effect/unstable/http"
|
||||||
|
|
||||||
export const ID = Schema.String.pipe(Schema.brand("AccountID"))
|
export const ID = Schema.String.pipe(Schema.brand("AccountID"))
|
||||||
export type ID = Schema.Schema.Type<typeof ID>
|
export type ID = Schema.Schema.Type<typeof ID>
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
export * as Catalog from "./catalog"
|
export * as Catalog from "./catalog"
|
||||||
|
|
||||||
import { makeLocationNode } from "./effect/app-node"
|
import { makeLocationNode } from "./effect/app-node"
|
||||||
import { Array, Context, Effect, Layer, Option, Order, pipe, Schema } from "effect"
|
import { Array, Context, Effect, Layer, Option, Order, pipe } from "effect"
|
||||||
import { Catalog } from "@opencode-ai/schema/catalog"
|
import { Catalog } from "@opencode-ai/schema/catalog"
|
||||||
import { ModelV2 } from "./model"
|
import { ModelV2 } from "./model"
|
||||||
import { ProviderV2 } from "./provider"
|
import { ProviderV2 } from "./provider"
|
||||||
import { EventV2 } from "./event"
|
import { EventV2 } from "./event"
|
||||||
import { Policy } from "./policy"
|
|
||||||
import { State } from "./state"
|
import { State } from "./state"
|
||||||
import { Integration } from "./integration"
|
import { Integration } from "./integration"
|
||||||
|
|
||||||
@@ -17,8 +16,6 @@ export type ProviderRecord = {
|
|||||||
|
|
||||||
export type DefaultModel = { providerID: ProviderV2.ID; modelID: ModelV2.ID }
|
export type DefaultModel = { providerID: ProviderV2.ID; modelID: ModelV2.ID }
|
||||||
|
|
||||||
export const PolicyActions = Schema.Literals(["provider.use"])
|
|
||||||
|
|
||||||
export const Event = Catalog.Event
|
export const Event = Catalog.Event
|
||||||
|
|
||||||
type Data = {
|
type Data = {
|
||||||
@@ -65,7 +62,6 @@ const layer = Layer.effect(
|
|||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const policy = yield* Policy.Service
|
|
||||||
const integrations = yield* Integration.Service
|
const integrations = yield* Integration.Service
|
||||||
|
|
||||||
const available = (provider: ProviderV2.Info, integration: Integration.Info | undefined) => {
|
const available = (provider: ProviderV2.Info, integration: Integration.Info | undefined) => {
|
||||||
@@ -159,13 +155,6 @@ const layer = Layer.effect(
|
|||||||
return result
|
return result
|
||||||
},
|
},
|
||||||
finalize: Effect.fn("CatalogV2.finalize")(function* (catalog) {
|
finalize: Effect.fn("CatalogV2.finalize")(function* (catalog) {
|
||||||
if (policy.hasStatements()) {
|
|
||||||
for (const record of [...catalog.provider.list()]) {
|
|
||||||
if ((yield* policy.evaluate("provider.use", record.provider.id, "allow")) === "deny") {
|
|
||||||
catalog.provider.remove(record.provider.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
yield* events.publish(Event.Updated, {})
|
yield* events.publish(Event.Updated, {})
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
@@ -294,4 +283,4 @@ const layer = Layer.effect(
|
|||||||
|
|
||||||
const SMALL_MODEL_RE = /\b(nano|flash|lite|mini|haiku|small|fast)\b/
|
const SMALL_MODEL_RE = /\b(nano|flash|lite|mini|haiku|small|fast)\b/
|
||||||
|
|
||||||
export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node, Policy.node, Integration.node] })
|
export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node, Integration.node] })
|
||||||
|
|||||||
+57
-21
@@ -3,18 +3,19 @@ export * as Config from "./config"
|
|||||||
import { makeLocationNode } from "./effect/app-node"
|
import { makeLocationNode } from "./effect/app-node"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { type ParseError, parse } from "jsonc-parser"
|
import { type ParseError, parse } from "jsonc-parser"
|
||||||
import { Context, Effect, Layer, Option, Schema } from "effect"
|
import { Context, Effect, Fiber, Layer, Option, PubSub, Schema, Stream } from "effect"
|
||||||
import { Permission } from "@opencode-ai/schema/permission"
|
import { Permission } from "@opencode-ai/schema/permission"
|
||||||
|
import { Config as ConfigSchema } from "@opencode-ai/schema/config"
|
||||||
|
import { EventV2 } from "./event"
|
||||||
|
import { Watcher } from "./filesystem/watcher"
|
||||||
import { FSUtil } from "./fs-util"
|
import { FSUtil } from "./fs-util"
|
||||||
import { Global } from "./global"
|
import { Global } from "./global"
|
||||||
import { Location } from "./location"
|
import { Location } from "./location"
|
||||||
import { Policy } from "./policy"
|
|
||||||
import { AbsolutePath } from "./schema"
|
import { AbsolutePath } from "./schema"
|
||||||
import { ConfigAgent } from "./config/agent"
|
import { ConfigAgent } from "./config/agent"
|
||||||
import { ConfigAttachments } from "./config/attachments"
|
import { ConfigAttachments } from "./config/attachments"
|
||||||
import { ConfigCompaction } from "./config/compaction"
|
import { ConfigCompaction } from "./config/compaction"
|
||||||
import { ConfigCommand } from "./config/command"
|
import { ConfigCommand } from "./config/command"
|
||||||
import { ConfigExperimental } from "./config/experimental"
|
|
||||||
import { ConfigFormatter } from "./config/formatter"
|
import { ConfigFormatter } from "./config/formatter"
|
||||||
import { ConfigLSP } from "./config/lsp"
|
import { ConfigLSP } from "./config/lsp"
|
||||||
import { ConfigMCP } from "./config/mcp"
|
import { ConfigMCP } from "./config/mcp"
|
||||||
@@ -22,6 +23,7 @@ import { ConfigPlugin } from "./config/plugin"
|
|||||||
import { ConfigProvider } from "./config/provider"
|
import { ConfigProvider } from "./config/provider"
|
||||||
import { ConfigReference } from "./config/reference"
|
import { ConfigReference } from "./config/reference"
|
||||||
import { ConfigToolOutput } from "./config/tool-output"
|
import { ConfigToolOutput } from "./config/tool-output"
|
||||||
|
import { ConfigVariable } from "./config/variable"
|
||||||
import { ConfigWatcher } from "./config/watcher"
|
import { ConfigWatcher } from "./config/watcher"
|
||||||
import { ConfigV1 } from "./v1/config/config"
|
import { ConfigV1 } from "./v1/config/config"
|
||||||
import { ConfigMigrateV1 } from "./v1/config/migrate"
|
import { ConfigMigrateV1 } from "./v1/config/migrate"
|
||||||
@@ -102,7 +104,6 @@ export class Info extends Schema.Class<Info>("Config.Info")({
|
|||||||
plugins: ConfigPlugin.Plugins.pipe(Schema.optional).annotate({
|
plugins: ConfigPlugin.Plugins.pipe(Schema.optional).annotate({
|
||||||
description: "Ordered external plugin packages to load",
|
description: "Ordered external plugin packages to load",
|
||||||
}),
|
}),
|
||||||
experimental: ConfigExperimental.Experimental.pipe(Schema.optional),
|
|
||||||
providers: Schema.Record(Schema.String, ConfigProvider.Info).pipe(Schema.optional),
|
providers: Schema.Record(Schema.String, ConfigProvider.Info).pipe(Schema.optional),
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
@@ -138,7 +139,8 @@ const layer = Layer.effect(
|
|||||||
const fs = yield* FSUtil.Service
|
const fs = yield* FSUtil.Service
|
||||||
const global = yield* Global.Service
|
const global = yield* Global.Service
|
||||||
const location = yield* Location.Service
|
const location = yield* Location.Service
|
||||||
const policy = yield* Policy.Service
|
const watcher = yield* Watcher.Service
|
||||||
|
const events = yield* EventV2.Service
|
||||||
const names = ["opencode.json", "opencode.jsonc"]
|
const names = ["opencode.json", "opencode.jsonc"]
|
||||||
const decodeOptions = { errors: "all", onExcessProperty: "ignore", propertyOrder: "original" } as const
|
const decodeOptions = { errors: "all", onExcessProperty: "ignore", propertyOrder: "original" } as const
|
||||||
const decodeInfo = Schema.decodeUnknownOption(Info, decodeOptions)
|
const decodeInfo = Schema.decodeUnknownOption(Info, decodeOptions)
|
||||||
@@ -147,9 +149,10 @@ const layer = Layer.effect(
|
|||||||
const loadFile = Effect.fnUntraced(function* (filepath: string) {
|
const loadFile = Effect.fnUntraced(function* (filepath: string) {
|
||||||
const text = yield* fs.readFileStringSafe(filepath)
|
const text = yield* fs.readFileStringSafe(filepath)
|
||||||
if (!text) return
|
if (!text) return
|
||||||
|
const substituted = yield* ConfigVariable.substitute({ type: "path", path: filepath, text })
|
||||||
|
|
||||||
const errors: ParseError[] = []
|
const errors: ParseError[] = []
|
||||||
const input: unknown = parse(text, errors, { allowTrailingComma: true })
|
const input: unknown = parse(substituted, errors, { allowTrailingComma: true })
|
||||||
if (errors.length) return
|
if (errors.length) return
|
||||||
|
|
||||||
const info = Option.getOrUndefined(
|
const info = Option.getOrUndefined(
|
||||||
@@ -170,10 +173,9 @@ const layer = Layer.effect(
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const discover = Effect.fn("Config.discover")(function* () {
|
||||||
const globalDirectory = AbsolutePath.make(global.config)
|
const globalDirectory = AbsolutePath.make(global.config)
|
||||||
const locationIsGlobal = path.resolve(location.directory) === path.resolve(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 discovered = locationIsGlobal
|
const discovered = locationIsGlobal
|
||||||
? []
|
? []
|
||||||
: yield* fs
|
: yield* fs
|
||||||
@@ -190,25 +192,59 @@ const layer = Layer.effect(
|
|||||||
.toReversed()
|
.toReversed()
|
||||||
.map((directory) => AbsolutePath.make(directory)),
|
.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 = discovered.filter((item) => path.basename(item) !== ".opencode").toReversed()
|
const directPaths = discovered.filter((item) => path.basename(item) !== ".opencode").toReversed()
|
||||||
const direct = yield* Effect.forEach(directPaths, loadFile).pipe(
|
const direct = yield* Effect.forEach(directPaths, loadFile).pipe(
|
||||||
Effect.orDie,
|
Effect.orDie,
|
||||||
Effect.map((configs) => configs.filter((config): config is Document => config !== undefined)),
|
Effect.map((configs) => configs.filter((config): config is Document => config !== undefined)),
|
||||||
)
|
)
|
||||||
const supplementary = yield* Effect.forEach(directories, loadDirectory).pipe(Effect.orDie)
|
const supplementary = yield* Effect.forEach(directories, loadDirectory).pipe(Effect.orDie)
|
||||||
// Apply general settings first and more specific settings last:
|
return {
|
||||||
// global config, project files, then `.opencode` files.
|
entries: [...(supplementary[0] ?? []), ...direct, ...supplementary.slice(1).flat()],
|
||||||
const configs = [...(supplementary[0] ?? []), ...direct, ...supplementary.slice(1).flat()]
|
directories,
|
||||||
// Rules use the opposite order so a user-global rule can override a
|
files: directPaths,
|
||||||
// repository rule. Statement order inside each file stays unchanged.
|
}
|
||||||
yield* policy.load(
|
})
|
||||||
configs
|
|
||||||
.filter((config): config is Document => config.type === "document")
|
const initial = yield* discover()
|
||||||
.toReversed()
|
let configs = initial.entries
|
||||||
.flatMap((config) => config.info.experimental?.policies ?? []),
|
const updates = yield* PubSub.unbounded<Watcher.Update>()
|
||||||
|
const subscriptions = new Map<string, Effect.Effect<unknown>>()
|
||||||
|
const targets = (snapshot: typeof initial) => [
|
||||||
|
...snapshot.directories.map((path) => ({ path, type: "directory" as const })),
|
||||||
|
...snapshot.files
|
||||||
|
.filter((file) => !snapshot.directories.some((directory) => FSUtil.contains(directory, file)))
|
||||||
|
.map((path) => ({ path, type: "file" as const })),
|
||||||
|
]
|
||||||
|
const reconcile = Effect.fn("Config.reconcileWatches")(function* (snapshot: typeof initial) {
|
||||||
|
const next = new Map(targets(snapshot).map((target) => [JSON.stringify(target), target]))
|
||||||
|
for (const [key, stop] of subscriptions) {
|
||||||
|
if (next.has(key)) continue
|
||||||
|
yield* stop
|
||||||
|
subscriptions.delete(key)
|
||||||
|
}
|
||||||
|
for (const [key, target] of next) {
|
||||||
|
if (subscriptions.has(key)) continue
|
||||||
|
const fiber = yield* watcher.subscribe(target).pipe(
|
||||||
|
Stream.runForEach((update) => PubSub.publish(updates, update)),
|
||||||
|
Effect.forkScoped({ startImmediately: true }),
|
||||||
)
|
)
|
||||||
|
subscriptions.set(key, Fiber.interrupt(fiber))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
yield* Stream.fromPubSub(updates).pipe(
|
||||||
|
Stream.debounce("100 millis"),
|
||||||
|
Stream.runForEach((update) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const next = yield* discover()
|
||||||
|
configs = next.entries
|
||||||
|
yield* reconcile(next)
|
||||||
|
yield* events.publish(ConfigSchema.Event.Updated, {})
|
||||||
|
}).pipe(Effect.catchCause((cause) => Effect.logError("failed to reload config", { path: update.path, cause }))),
|
||||||
|
),
|
||||||
|
Effect.forkScoped({ startImmediately: true }),
|
||||||
|
)
|
||||||
|
yield* reconcile(initial)
|
||||||
|
|
||||||
return Service.of({
|
return Service.of({
|
||||||
entries: Effect.fn("Config.entries")(function* () {
|
entries: Effect.fn("Config.entries")(function* () {
|
||||||
@@ -221,5 +257,5 @@ const layer = Layer.effect(
|
|||||||
export const node = makeLocationNode({
|
export const node = makeLocationNode({
|
||||||
service: Service,
|
service: Service,
|
||||||
layer,
|
layer,
|
||||||
deps: [FSUtil.node, Global.node, Location.node, Policy.node],
|
deps: [Watcher.node, EventV2.node, FSUtil.node, Global.node, Location.node],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
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),
|
|
||||||
}) {}
|
|
||||||
@@ -2,7 +2,7 @@ export * as ConfigAgentPlugin from "./agent"
|
|||||||
|
|
||||||
import { define } from "../../plugin/internal"
|
import { define } from "../../plugin/internal"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Effect, Option, Schema } from "effect"
|
import { Effect, Option, Schema, Stream } from "effect"
|
||||||
import { AgentV2 } from "../../agent"
|
import { AgentV2 } from "../../agent"
|
||||||
import { Config } from "../../config"
|
import { Config } from "../../config"
|
||||||
import { ConfigAgent } from "../agent"
|
import { ConfigAgent } from "../agent"
|
||||||
@@ -38,9 +38,8 @@ export const Plugin = define({
|
|||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
const config = yield* Config.Service
|
const config = yield* Config.Service
|
||||||
const fs = yield* FSUtil.Service
|
const fs = yield* FSUtil.Service
|
||||||
yield* ctx.agent.transform(
|
const load = Effect.fn("ConfigAgentPlugin.load")(function* () {
|
||||||
Effect.fn(function* (draft) {
|
return yield* Effect.forEach(yield* config.entries(), (entry) => {
|
||||||
const documents = yield* Effect.forEach(yield* config.entries(), (entry) => {
|
|
||||||
if (entry.type === "document") return Effect.succeed([entry])
|
if (entry.type === "document") return Effect.succeed([entry])
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const files = yield* discover(fs, entry.path)
|
const files = yield* discover(fs, entry.path)
|
||||||
@@ -56,14 +55,17 @@ export const Plugin = define({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
}).pipe(Effect.map((documents) => documents.flat()))
|
}).pipe(Effect.map((documents) => documents.flat()))
|
||||||
const global = documents.flatMap((document) => document.info.permissions ?? [])
|
})
|
||||||
const configuredDefault = Config.latest(documents, "default_agent")
|
const loaded = { documents: yield* load() }
|
||||||
|
yield* ctx.agent.transform((draft) => {
|
||||||
|
const global = loaded.documents.flatMap((document) => document.info.permissions ?? [])
|
||||||
|
const configuredDefault = Config.latest(loaded.documents, "default_agent")
|
||||||
if (configuredDefault !== undefined) draft.default(AgentV2.ID.make(configuredDefault))
|
if (configuredDefault !== undefined) draft.default(AgentV2.ID.make(configuredDefault))
|
||||||
for (const current of draft.list()) {
|
for (const current of draft.list()) {
|
||||||
draft.update(current.id, (agent) => agent.permissions.push(...global))
|
draft.update(current.id, (agent) => agent.permissions.push(...global))
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const document of documents) {
|
for (const document of loaded.documents) {
|
||||||
for (const [id, item] of Object.entries(document.info.agents ?? {})) {
|
for (const [id, item] of Object.entries(document.info.agents ?? {})) {
|
||||||
const agentID = AgentV2.ID.make(id)
|
const agentID = AgentV2.ID.make(id)
|
||||||
if (item.disabled) {
|
if (item.disabled) {
|
||||||
@@ -95,7 +97,16 @@ export const Plugin = define({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
|
yield* ctx.event.subscribe().pipe(
|
||||||
|
Stream.filter((event) => event.type === "config.updated"),
|
||||||
|
Stream.runForEach(() =>
|
||||||
|
load().pipe(
|
||||||
|
Effect.tap((documents) => Effect.sync(() => (loaded.documents = documents))),
|
||||||
|
Effect.andThen(ctx.agent.reload()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Effect.forkScoped({ startImmediately: true }),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export * as ConfigCommandPlugin from "./command"
|
|||||||
|
|
||||||
import { define } from "../../plugin/internal"
|
import { define } from "../../plugin/internal"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Effect, Option, Schema } from "effect"
|
import { Effect, Option, Schema, Stream } from "effect"
|
||||||
import { CommandV2 } from "../../command"
|
import { CommandV2 } from "../../command"
|
||||||
import { Config } from "../../config"
|
import { Config } from "../../config"
|
||||||
import { FSUtil } from "../../fs-util"
|
import { FSUtil } from "../../fs-util"
|
||||||
@@ -17,9 +17,8 @@ export const Plugin = define({
|
|||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
const config = yield* Config.Service
|
const config = yield* Config.Service
|
||||||
const fs = yield* FSUtil.Service
|
const fs = yield* FSUtil.Service
|
||||||
yield* ctx.command.transform(
|
const load = Effect.fn("ConfigCommandPlugin.load")(function* () {
|
||||||
Effect.fn(function* (draft) {
|
return yield* Effect.forEach(yield* config.entries(), (entry) => {
|
||||||
const documents = yield* Effect.forEach(yield* config.entries(), (entry) => {
|
|
||||||
if (entry.type === "document") return Effect.succeed([{ commands: entry.info.commands }])
|
if (entry.type === "document") return Effect.succeed([{ commands: entry.info.commands }])
|
||||||
return loadDirectory(fs, entry.path).pipe(
|
return loadDirectory(fs, entry.path).pipe(
|
||||||
Effect.map((commands) => [
|
Effect.map((commands) => [
|
||||||
@@ -27,7 +26,10 @@ export const Plugin = define({
|
|||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
}).pipe(Effect.map((documents) => documents.flat()))
|
}).pipe(Effect.map((documents) => documents.flat()))
|
||||||
for (const document of documents) {
|
})
|
||||||
|
const loaded = { documents: yield* load() }
|
||||||
|
yield* ctx.command.transform((draft) => {
|
||||||
|
for (const document of loaded.documents) {
|
||||||
for (const [name, command] of Object.entries(document.commands ?? {})) {
|
for (const [name, command] of Object.entries(document.commands ?? {})) {
|
||||||
draft.update(name, (item) => {
|
draft.update(name, (item) => {
|
||||||
item.template = command.template
|
item.template = command.template
|
||||||
@@ -44,7 +46,16 @@ export const Plugin = define({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
|
yield* ctx.event.subscribe().pipe(
|
||||||
|
Stream.filter((event) => event.type === "config.updated"),
|
||||||
|
Stream.runForEach(() =>
|
||||||
|
load().pipe(
|
||||||
|
Effect.tap((documents) => Effect.sync(() => (loaded.documents = documents))),
|
||||||
|
Effect.andThen(ctx.command.reload()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Effect.forkScoped({ startImmediately: true }),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -42,8 +42,8 @@ export const Plugin = define({
|
|||||||
const fs = yield* FSUtil.Service
|
const fs = yield* FSUtil.Service
|
||||||
const location = yield* Location.Service
|
const location = yield* Location.Service
|
||||||
const npm = yield* Npm.Service
|
const npm = yield* Npm.Service
|
||||||
yield* Effect.gen(function* () {
|
const load = Effect.fn("ConfigExternalPlugin.load")(function* () {
|
||||||
const configured: { package: string; options?: Record<string, any> }[] = []
|
const configured: { package: string; options?: Record<string, unknown> }[] = []
|
||||||
|
|
||||||
for (const entry of yield* config.entries()) {
|
for (const entry of yield* config.entries()) {
|
||||||
if (entry.type === "document") {
|
if (entry.type === "document") {
|
||||||
@@ -98,8 +98,8 @@ export const Plugin = define({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const ref of configured) {
|
return yield* Effect.forEach(configured, (ref) =>
|
||||||
yield* Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const entrypoint = path.isAbsolute(ref.package)
|
const entrypoint = path.isAbsolute(ref.package)
|
||||||
? pathToFileURL(ref.package).href
|
? pathToFileURL(ref.package).href
|
||||||
: (yield* npm.add(ref.package)).entrypoint
|
: (yield* npm.add(ref.package)).entrypoint
|
||||||
@@ -108,13 +108,15 @@ export const Plugin = define({
|
|||||||
const mod = yield* Effect.promise(() => import(entrypoint))
|
const mod = yield* Effect.promise(() => import(entrypoint))
|
||||||
const value = (yield* Schema.decodeUnknownEffect(PluginModule)(mod)).default
|
const value = (yield* Schema.decodeUnknownEffect(PluginModule)(mod)).default
|
||||||
const plugin = "effect" in value ? value : PluginPromise.fromPromise(value)
|
const plugin = "effect" in value ? value : PluginPromise.fromPromise(value)
|
||||||
yield* ctx.plugin.add({
|
return {
|
||||||
id: plugin.id,
|
id: plugin.id,
|
||||||
effect: (host) => plugin.effect({ ...host, options: ref.options ?? {} }),
|
effect: (host: Parameters<typeof plugin.effect>[0]) =>
|
||||||
})
|
plugin.effect({ ...host, options: ref.options ?? {} }),
|
||||||
}).pipe(Effect.ignoreCause)
|
|
||||||
}
|
}
|
||||||
|
}).pipe(Effect.catchCause(() => Effect.succeed(undefined))),
|
||||||
|
).pipe(Effect.map((plugins) => plugins.filter((plugin) => plugin !== undefined)))
|
||||||
})
|
})
|
||||||
|
for (const plugin of yield* load()) yield* ctx.plugin.add(plugin)
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export * as ConfigProviderPlugin from "./provider"
|
export * as ConfigProviderPlugin from "./provider"
|
||||||
|
|
||||||
import { define } from "../../plugin/internal"
|
import { define } from "../../plugin/internal"
|
||||||
import { Effect } from "effect"
|
import { Effect, Stream } from "effect"
|
||||||
import { Config } from "../../config"
|
import { Config } from "../../config"
|
||||||
import { ModelV2 } from "../../model"
|
import { ModelV2 } from "../../model"
|
||||||
|
|
||||||
@@ -9,9 +9,9 @@ export const Plugin = define({
|
|||||||
id: "config-provider",
|
id: "config-provider",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
const config = yield* Config.Service
|
const config = yield* Config.Service
|
||||||
yield* ctx.integration.transform(
|
const loaded = { entries: yield* config.entries() }
|
||||||
Effect.fn(function* (integrations) {
|
yield* ctx.integration.transform((integrations) => {
|
||||||
const files = (yield* config.entries()).filter((entry): entry is Config.Document => entry.type === "document")
|
const files = loaded.entries.filter((entry): entry is Config.Document => entry.type === "document")
|
||||||
const configuredIntegrations = new Set(
|
const configuredIntegrations = new Set(
|
||||||
files.flatMap((file) =>
|
files.flatMap((file) =>
|
||||||
Object.entries(file.info.providers ?? {}).flatMap(([id, provider]) =>
|
Object.entries(file.info.providers ?? {}).flatMap(([id, provider]) =>
|
||||||
@@ -34,14 +34,11 @@ export const Plugin = define({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
)
|
|
||||||
|
|
||||||
yield* ctx.catalog.transform(
|
yield* ctx.catalog.transform((catalog) => {
|
||||||
Effect.fn(function* (catalog) {
|
const files = loaded.entries.filter((entry): entry is Config.Document => entry.type === "document")
|
||||||
const entries = yield* config.entries()
|
const configuredDefault = Config.latest(loaded.entries, "model")
|
||||||
const files = entries.filter((entry): entry is Config.Document => entry.type === "document")
|
|
||||||
const configuredDefault = Config.latest(entries, "model")
|
|
||||||
if (configuredDefault !== undefined) {
|
if (configuredDefault !== undefined) {
|
||||||
const model = ModelV2.parse(configuredDefault)
|
const model = ModelV2.parse(configuredDefault)
|
||||||
catalog.model.default.set(model.providerID, model.modelID)
|
catalog.model.default.set(model.providerID, model.modelID)
|
||||||
@@ -110,7 +107,17 @@ export const Plugin = define({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
|
yield* ctx.event.subscribe().pipe(
|
||||||
|
Stream.filter((event) => event.type === "config.updated"),
|
||||||
|
Stream.runForEach(() =>
|
||||||
|
config.entries().pipe(
|
||||||
|
Effect.tap((entries) => Effect.sync(() => (loaded.entries = entries))),
|
||||||
|
Effect.andThen(ctx.integration.reload()),
|
||||||
|
Effect.andThen(ctx.catalog.reload()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Effect.forkScoped({ startImmediately: true }),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export * as ConfigReferencePlugin from "./reference"
|
|||||||
|
|
||||||
import { define } from "../../plugin/internal"
|
import { define } from "../../plugin/internal"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Effect } from "effect"
|
import { Effect, Stream } from "effect"
|
||||||
import { Config } from "../../config"
|
import { Config } from "../../config"
|
||||||
import { ConfigReference } from "../reference"
|
import { ConfigReference } from "../reference"
|
||||||
import { Reference } from "../../reference"
|
import { Reference } from "../../reference"
|
||||||
@@ -16,12 +16,10 @@ export const Plugin = define({
|
|||||||
const config = yield* Config.Service
|
const config = yield* Config.Service
|
||||||
const location = yield* Location.Service
|
const location = yield* Location.Service
|
||||||
const global = yield* Global.Service
|
const global = yield* Global.Service
|
||||||
yield* ctx.reference.transform(
|
const loaded = { entries: yield* config.entries() }
|
||||||
Effect.fn(function* (draft) {
|
yield* ctx.reference.transform((draft) => {
|
||||||
const entries = new Map<string, Reference.Source>()
|
const entries = new Map<string, Reference.Source>()
|
||||||
for (const doc of (yield* config.entries()).filter(
|
for (const doc of loaded.entries.filter((entry): entry is Config.Document => entry.type === "document")) {
|
||||||
(entry): entry is Config.Document => entry.type === "document",
|
|
||||||
)) {
|
|
||||||
const directory = doc.path ? path.dirname(doc.path) : location.directory
|
const directory = doc.path ? path.dirname(doc.path) : location.directory
|
||||||
for (const [name, entry] of Object.entries(doc.info.references ?? {})) {
|
for (const [name, entry] of Object.entries(doc.info.references ?? {})) {
|
||||||
if (!validAlias(name)) continue
|
if (!validAlias(name)) continue
|
||||||
@@ -49,7 +47,16 @@ export const Plugin = define({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const [name, source] of entries) draft.add(name, source)
|
for (const [name, source] of entries) draft.add(name, source)
|
||||||
}),
|
})
|
||||||
|
yield* ctx.event.subscribe().pipe(
|
||||||
|
Stream.filter((event) => event.type === "config.updated"),
|
||||||
|
Stream.runForEach(() =>
|
||||||
|
config.entries().pipe(
|
||||||
|
Effect.tap((entries) => Effect.sync(() => (loaded.entries = entries))),
|
||||||
|
Effect.andThen(ctx.reference.reload()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Effect.forkScoped({ startImmediately: true }),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export * as ConfigSkillPlugin from "./skill"
|
|||||||
|
|
||||||
import { define } from "../../plugin/internal"
|
import { define } from "../../plugin/internal"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Effect } from "effect"
|
import { Effect, Stream } from "effect"
|
||||||
import { Config } from "../../config"
|
import { Config } from "../../config"
|
||||||
import { AbsolutePath } from "../../schema"
|
import { AbsolutePath } from "../../schema"
|
||||||
import { SkillV2 } from "../../skill"
|
import { SkillV2 } from "../../skill"
|
||||||
@@ -15,11 +15,10 @@ export const Plugin = define({
|
|||||||
const config = yield* Config.Service
|
const config = yield* Config.Service
|
||||||
const global = yield* Global.Service
|
const global = yield* Global.Service
|
||||||
const location = yield* Location.Service
|
const location = yield* Location.Service
|
||||||
yield* ctx.skill.transform(
|
const loaded = { entries: yield* config.entries() }
|
||||||
Effect.fn(function* (draft) {
|
yield* ctx.skill.transform((draft) => {
|
||||||
const entries = yield* config.entries()
|
const directories = loaded.entries.flatMap((entry) => (entry.type === "directory" ? [entry.path] : []))
|
||||||
const directories = entries.flatMap((entry) => (entry.type === "directory" ? [entry.path] : []))
|
const items = loaded.entries.flatMap((entry) => (entry.type === "document" ? (entry.info.skills ?? []) : []))
|
||||||
const items = entries.flatMap((entry) => (entry.type === "document" ? (entry.info.skills ?? []) : []))
|
|
||||||
for (const directory of directories) {
|
for (const directory of directories) {
|
||||||
draft.source(
|
draft.source(
|
||||||
SkillV2.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) }),
|
SkillV2.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) }),
|
||||||
@@ -44,7 +43,16 @@ export const Plugin = define({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
|
yield* ctx.event.subscribe().pipe(
|
||||||
|
Stream.filter((event) => event.type === "config.updated"),
|
||||||
|
Stream.runForEach(() =>
|
||||||
|
config.entries().pipe(
|
||||||
|
Effect.tap((entries) => Effect.sync(() => (loaded.entries = entries))),
|
||||||
|
Effect.andThen(ctx.skill.reload()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Effect.forkScoped({ startImmediately: true }),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
export * as ConfigVariable from "./variable"
|
||||||
|
|
||||||
|
import os from "os"
|
||||||
|
import path from "path"
|
||||||
|
import { Effect } from "effect"
|
||||||
|
import { FSUtil } from "../fs-util"
|
||||||
|
import { InvalidError } from "../v1/config/error"
|
||||||
|
|
||||||
|
type ParseSource =
|
||||||
|
| {
|
||||||
|
type: "path"
|
||||||
|
path: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "virtual"
|
||||||
|
source: string
|
||||||
|
dir: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubstituteInput = ParseSource & {
|
||||||
|
text: string
|
||||||
|
missing?: "error" | "empty"
|
||||||
|
env?: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Apply {env:VAR} and {file:path} substitutions to config text. */
|
||||||
|
export const substitute = Effect.fn("ConfigVariable.substitute")(function* (input: SubstituteInput) {
|
||||||
|
const text = input.text.replace(
|
||||||
|
/\{env:([^}]+)\}/g,
|
||||||
|
(_, varName: string) => (input.env?.[varName] ?? process.env[varName]) || "",
|
||||||
|
)
|
||||||
|
if (!text.includes("{file:")) return text
|
||||||
|
return yield* substituteFiles(input, text)
|
||||||
|
})
|
||||||
|
|
||||||
|
const substituteFiles = Effect.fnUntraced(function* (input: SubstituteInput, text: string) {
|
||||||
|
const fs = yield* FSUtil.Service
|
||||||
|
const configDir = input.type === "path" ? path.dirname(input.path) : input.dir
|
||||||
|
const configSource = input.type === "path" ? input.path : input.source
|
||||||
|
const matches = Array.from(text.matchAll(/\{file:[^}]+\}/g))
|
||||||
|
let out = ""
|
||||||
|
let cursor = 0
|
||||||
|
|
||||||
|
for (const match of matches) {
|
||||||
|
const token = match[0]
|
||||||
|
const index = match.index
|
||||||
|
out += text.slice(cursor, index)
|
||||||
|
|
||||||
|
const lineStart = text.lastIndexOf("\n", index - 1) + 1
|
||||||
|
const prefix = text.slice(lineStart, index).trimStart()
|
||||||
|
if (prefix.startsWith("//")) {
|
||||||
|
out += token
|
||||||
|
cursor = index + token.length
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = token.replace(/^\{file:/, "").replace(/\}$/, "")
|
||||||
|
const expandedPath = filePath.startsWith("~/") ? path.join(os.homedir(), filePath.slice(2)) : filePath
|
||||||
|
const resolvedPath = path.isAbsolute(expandedPath) ? expandedPath : path.resolve(configDir, expandedPath)
|
||||||
|
const fileContent = yield* fs.readFileString(resolvedPath).pipe(
|
||||||
|
Effect.catch((error) => {
|
||||||
|
if (input.missing === "empty") return Effect.succeed("")
|
||||||
|
|
||||||
|
const message = `bad file reference: "${token}"`
|
||||||
|
return Effect.fail(
|
||||||
|
new InvalidError(
|
||||||
|
{
|
||||||
|
path: configSource,
|
||||||
|
message:
|
||||||
|
error._tag === "PlatformError" && error.reason._tag === "NotFound"
|
||||||
|
? `${message} ${resolvedPath} does not exist`
|
||||||
|
: message,
|
||||||
|
},
|
||||||
|
{ cause: error },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
out += JSON.stringify(fileContent.trim()).slice(1, -1)
|
||||||
|
cursor = index + token.length
|
||||||
|
}
|
||||||
|
|
||||||
|
return out + text.slice(cursor)
|
||||||
|
})
|
||||||
@@ -106,8 +106,7 @@ const layer = Layer.effect(
|
|||||||
yield* events.publish(SessionEvent.Moved, {
|
yield* events.publish(SessionEvent.Moved, {
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
location: Location.Ref.make({ directory }),
|
location: Location.Ref.make({ directory }),
|
||||||
subdirectory: RelativePath.make(path.relative(destination.directory, directory).replaceAll("\\", "/")),
|
subpath: RelativePath.make(path.relative(destination.directory, directory).replaceAll("\\", "/")),
|
||||||
timestamp: yield* DateTime.now,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (patch) {
|
if (patch) {
|
||||||
|
|||||||
@@ -1,26 +1,17 @@
|
|||||||
import type * as Arr from "effect/Array"
|
import type { NonEmptyReadonlyArray } from "effect/Array"
|
||||||
import { NodeFileSystem, NodeSink, NodeStream } from "@effect/platform-node"
|
import { NodeFileSystem, NodePath, NodeSink, NodeStream } from "@effect/platform-node"
|
||||||
import * as NodePath from "@effect/platform-node/NodePath"
|
import { Deferred, Effect, Exit, FileSystem, Layer, Path, PlatformError, Predicate, Sink, Stream } from "effect"
|
||||||
import * as Deferred from "effect/Deferred"
|
import type { Scope } from "effect"
|
||||||
import * as Effect from "effect/Effect"
|
import { ChildProcess } from "effect/unstable/process"
|
||||||
import * as Exit from "effect/Exit"
|
|
||||||
import * as FileSystem from "effect/FileSystem"
|
|
||||||
import * as Layer from "effect/Layer"
|
|
||||||
import * as Path from "effect/Path"
|
|
||||||
import * as PlatformError from "effect/PlatformError"
|
|
||||||
import * as Predicate from "effect/Predicate"
|
|
||||||
import type * as Scope from "effect/Scope"
|
|
||||||
import * as Sink from "effect/Sink"
|
|
||||||
import * as Stream from "effect/Stream"
|
|
||||||
import * as ChildProcess from "effect/unstable/process/ChildProcess"
|
|
||||||
import type { ChildProcessHandle } from "effect/unstable/process/ChildProcessSpawner"
|
|
||||||
import {
|
import {
|
||||||
ChildProcessSpawner,
|
ChildProcessSpawner,
|
||||||
ExitCode,
|
ExitCode,
|
||||||
make as makeSpawner,
|
make,
|
||||||
makeHandle,
|
makeHandle,
|
||||||
ProcessId,
|
ProcessId,
|
||||||
|
type ChildProcessHandle,
|
||||||
} from "effect/unstable/process/ChildProcessSpawner"
|
} from "effect/unstable/process/ChildProcessSpawner"
|
||||||
|
// ast-grep-ignore: no-star-import
|
||||||
import * as NodeChildProcess from "node:child_process"
|
import * as NodeChildProcess from "node:child_process"
|
||||||
import { PassThrough } from "node:stream"
|
import { PassThrough } from "node:stream"
|
||||||
import launch from "cross-spawn"
|
import launch from "cross-spawn"
|
||||||
@@ -71,7 +62,7 @@ const flatten = (command: ChildProcess.Command) => {
|
|||||||
if (commands.length === 0) throw new Error("flatten produced empty commands array")
|
if (commands.length === 0) throw new Error("flatten produced empty commands array")
|
||||||
const [head, ...tail] = commands
|
const [head, ...tail] = commands
|
||||||
return {
|
return {
|
||||||
commands: [head, ...tail] as Arr.NonEmptyReadonlyArray<ChildProcess.StandardCommand>,
|
commands: [head, ...tail] as NonEmptyReadonlyArray<ChildProcess.StandardCommand>,
|
||||||
opts,
|
opts,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,7 +87,7 @@ const toPlatformError = (
|
|||||||
|
|
||||||
type ExitSignal = Deferred.Deferred<readonly [code: number | null, signal: NodeJS.Signals | null]>
|
type ExitSignal = Deferred.Deferred<readonly [code: number | null, signal: NodeJS.Signals | null]>
|
||||||
|
|
||||||
export const make = Effect.gen(function* () {
|
const makeCrossSpawnSpawner = Effect.gen(function* () {
|
||||||
const fs = yield* FileSystem.FileSystem
|
const fs = yield* FileSystem.FileSystem
|
||||||
const path = yield* Path.Path
|
const path = yield* Path.Path
|
||||||
|
|
||||||
@@ -494,12 +485,12 @@ export const make = Effect.gen(function* () {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
return makeSpawner(spawnCommand)
|
return make(spawnCommand)
|
||||||
})
|
})
|
||||||
|
|
||||||
const layer: Layer.Layer<ChildProcessSpawner, never, FileSystem.FileSystem | Path.Path> = Layer.effect(
|
const layer: Layer.Layer<ChildProcessSpawner, never, FileSystem.FileSystem | Path.Path> = Layer.effect(
|
||||||
ChildProcessSpawner,
|
ChildProcessSpawner,
|
||||||
make,
|
makeCrossSpawnSpawner,
|
||||||
)
|
)
|
||||||
|
|
||||||
export const node = makeGlobalNode({ service: ChildProcessSpawner, layer, deps: [filesystem, path] })
|
export const node = makeGlobalNode({ service: ChildProcessSpawner, layer, deps: [filesystem, path] })
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export * as Database from "./database"
|
export * as Database from "./database"
|
||||||
|
|
||||||
import { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite"
|
import { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite"
|
||||||
import { layer as sqliteLayer } from "#sqlite"
|
import { layer } from "#sqlite"
|
||||||
import { Context, Effect, Layer } from "effect"
|
import { Context, Effect, Layer } from "effect"
|
||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
import { Flag } from "../flag/flag"
|
import { Flag } from "../flag/flag"
|
||||||
@@ -19,7 +19,7 @@ export interface Interface {
|
|||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/storage/Database") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/storage/Database") {}
|
||||||
|
|
||||||
const layer = Layer.effect(
|
const databaseLayer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const db = yield* makeDatabase
|
const db = yield* makeDatabase
|
||||||
@@ -37,7 +37,7 @@ const layer = Layer.effect(
|
|||||||
)
|
)
|
||||||
|
|
||||||
export function layerFromPath(filename: string) {
|
export function layerFromPath(filename: string) {
|
||||||
return layer.pipe(Layer.provide(sqliteLayer({ filename })))
|
return databaseLayer.pipe(Layer.provide(layer({ filename })))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function path() {
|
export function path() {
|
||||||
|
|||||||
+3
@@ -41,5 +41,8 @@ export const migrations = (
|
|||||||
import("./migration/20260622170816_reset_v2_session_state"),
|
import("./migration/20260622170816_reset_v2_session_state"),
|
||||||
import("./migration/20260622202450_simplify_session_input"),
|
import("./migration/20260622202450_simplify_session_input"),
|
||||||
import("./migration/20260702134641_add_session_context_entry"),
|
import("./migration/20260702134641_add_session_context_entry"),
|
||||||
|
import("./migration/20260703090000_reset_v2_event_rename_sweep"),
|
||||||
|
import("./migration/20260703181610_event_created_column"),
|
||||||
|
import("./migration/20260703190000_reset_v2_shell_event_payloads"),
|
||||||
])
|
])
|
||||||
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
|
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export function apply(db: Database) {
|
|||||||
sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'`,
|
sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'`,
|
||||||
)
|
)
|
||||||
if (tables.some((table) => table.name === "session")) return yield* applyOnly(db, migrations)
|
if (tables.some((table) => table.name === "session")) return yield* applyOnly(db, migrations)
|
||||||
if (tables.length > 0) return yield* Effect.die("Database is not empty and has no session table")
|
if (tables.length > 0) return yield* Effect.die(new Error("Database is not empty and has no session table"))
|
||||||
yield* db.transaction((tx) =>
|
yield* db.transaction((tx) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* schema.up(tx)
|
yield* schema.up(tx)
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { Effect } from "effect"
|
||||||
|
import type { DatabaseMigration } from "../migration"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
id: "20260703090000_reset_v2_event_rename_sweep",
|
||||||
|
up(tx) {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
yield* tx.run(`DELETE FROM \`session_input\`;`)
|
||||||
|
yield* tx.run(`DELETE FROM \`session_message\`;`)
|
||||||
|
yield* tx.run(`DELETE FROM \`event\`;`)
|
||||||
|
yield* tx.run(`DELETE FROM \`event_sequence\`;`)
|
||||||
|
// `created` column is added by the generated 20260703181610_event_created_column
|
||||||
|
// migration, which runs after this wipe (NOT NULL without default is safe on the
|
||||||
|
// emptied table).
|
||||||
|
})
|
||||||
|
},
|
||||||
|
} satisfies DatabaseMigration.Migration
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { Effect } from "effect"
|
||||||
|
import type { DatabaseMigration } from "../migration"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
id: "20260703181610_event_created_column",
|
||||||
|
up(tx) {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
yield* tx.run(`ALTER TABLE \`event\` ADD \`created\` integer NOT NULL;`)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
} satisfies DatabaseMigration.Migration
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Effect } from "effect"
|
||||||
|
import type { DatabaseMigration } from "../migration"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
id: "20260703190000_reset_v2_shell_event_payloads",
|
||||||
|
up(tx) {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
yield* tx.run(`DELETE FROM \`session_input\`;`)
|
||||||
|
yield* tx.run(`DELETE FROM \`session_message\`;`)
|
||||||
|
yield* tx.run(`DELETE FROM \`event\`;`)
|
||||||
|
yield* tx.run(`DELETE FROM \`event_sequence\`;`)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
} satisfies DatabaseMigration.Migration
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import nodePath from "path"
|
import nodePath from "path"
|
||||||
import { customType } from "drizzle-orm/sqlite-core"
|
import { customType } from "drizzle-orm/sqlite-core"
|
||||||
|
import { Schema } from "effect"
|
||||||
import { AbsolutePath } from "../schema"
|
import { AbsolutePath } from "../schema"
|
||||||
|
|
||||||
function storagePath(input: string) {
|
function storagePath(input: string) {
|
||||||
@@ -74,6 +75,8 @@ export const pathColumn = customType<{
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const decodeAbsoluteArray = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Array(Schema.String)))
|
||||||
|
|
||||||
export const absoluteArrayColumn = customType<{
|
export const absoluteArrayColumn = customType<{
|
||||||
data: AbsolutePath[]
|
data: AbsolutePath[]
|
||||||
driverData: string
|
driverData: string
|
||||||
@@ -86,6 +89,6 @@ export const absoluteArrayColumn = customType<{
|
|||||||
return JSON.stringify(input.map(absolute))
|
return JSON.stringify(input.map(absolute))
|
||||||
},
|
},
|
||||||
fromDriver(input) {
|
fromDriver(input) {
|
||||||
return (JSON.parse(input) as string[]).map((item) => AbsolutePath.make(toPlatform(absolute(item))))
|
return decodeAbsoluteArray(input).map((item) => AbsolutePath.make(toPlatform(absolute(item))))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ export default {
|
|||||||
\`id\` text PRIMARY KEY,
|
\`id\` text PRIMARY KEY,
|
||||||
\`aggregate_id\` text NOT NULL,
|
\`aggregate_id\` text NOT NULL,
|
||||||
\`seq\` integer NOT NULL,
|
\`seq\` integer NOT NULL,
|
||||||
|
\`created\` integer NOT NULL,
|
||||||
\`type\` text NOT NULL,
|
\`type\` text NOT NULL,
|
||||||
\`data\` text NOT NULL,
|
\`data\` text NOT NULL,
|
||||||
CONSTRAINT \`fk_event_aggregate_id_event_sequence_aggregate_id_fk\` FOREIGN KEY (\`aggregate_id\`) REFERENCES \`event_sequence\`(\`aggregate_id\`) ON DELETE CASCADE
|
CONSTRAINT \`fk_event_aggregate_id_event_sequence_aggregate_id_fk\` FOREIGN KEY (\`aggregate_id\`) REFERENCES \`event_sequence\`(\`aggregate_id\`) ON DELETE CASCADE
|
||||||
|
|||||||
@@ -1,18 +1,11 @@
|
|||||||
import { Database } from "bun:sqlite"
|
import { Database } from "bun:sqlite"
|
||||||
import { drizzle } from "drizzle-orm/bun-sqlite"
|
import { drizzle } from "drizzle-orm/bun-sqlite"
|
||||||
import * as Context from "effect/Context"
|
import { Context, Effect, Fiber, Layer, Scope, Semaphore, Stream } from "effect"
|
||||||
import * as Effect from "effect/Effect"
|
|
||||||
import * as Fiber from "effect/Fiber"
|
|
||||||
import { identity } from "effect/Function"
|
import { identity } from "effect/Function"
|
||||||
import * as Layer from "effect/Layer"
|
import { Reactivity } from "effect/unstable/reactivity"
|
||||||
import * as Scope from "effect/Scope"
|
import { SqlClient, Statement } from "effect/unstable/sql"
|
||||||
import * as Semaphore from "effect/Semaphore"
|
|
||||||
import * as Stream from "effect/Stream"
|
|
||||||
import * as Reactivity from "effect/unstable/reactivity/Reactivity"
|
|
||||||
import * as Client from "effect/unstable/sql/SqlClient"
|
|
||||||
import type { Connection } from "effect/unstable/sql/SqlConnection"
|
import type { Connection } from "effect/unstable/sql/SqlConnection"
|
||||||
import { classifySqliteError, SqlError } from "effect/unstable/sql/SqlError"
|
import { classifySqliteError, SqlError } from "effect/unstable/sql/SqlError"
|
||||||
import * as Statement from "effect/unstable/sql/Statement"
|
|
||||||
import { Sqlite } from "./sqlite"
|
import { Sqlite } from "./sqlite"
|
||||||
|
|
||||||
const ATTR_DB_SYSTEM_NAME = "db.system.name"
|
const ATTR_DB_SYSTEM_NAME = "db.system.name"
|
||||||
@@ -20,7 +13,7 @@ const ATTR_DB_SYSTEM_NAME = "db.system.name"
|
|||||||
const TypeId = "~@opencode-ai/core/database/SqliteBun" as const
|
const TypeId = "~@opencode-ai/core/database/SqliteBun" as const
|
||||||
type TypeId = typeof TypeId
|
type TypeId = typeof TypeId
|
||||||
|
|
||||||
interface SqliteClient extends Client.SqlClient {
|
interface SqliteClient extends SqlClient.SqlClient {
|
||||||
readonly [TypeId]: TypeId
|
readonly [TypeId]: TypeId
|
||||||
readonly config: Config
|
readonly config: Config
|
||||||
readonly export: Effect.Effect<Uint8Array, SqlError>
|
readonly export: Effect.Effect<Uint8Array, SqlError>
|
||||||
@@ -57,7 +50,7 @@ const make = (options: Config) =>
|
|||||||
Effect.withFiber<Array<Record<string, unknown>>, SqlError>((fiber) => {
|
Effect.withFiber<Array<Record<string, unknown>>, SqlError>((fiber) => {
|
||||||
const statement = native.query(query)
|
const statement = native.query(query)
|
||||||
// @ts-ignore bun-types missing safeIntegers method, fixed in https://github.com/oven-sh/bun/pull/26627
|
// @ts-ignore bun-types missing safeIntegers method, fixed in https://github.com/oven-sh/bun/pull/26627
|
||||||
statement.safeIntegers(Context.get(fiber.context, Client.SafeIntegers))
|
statement.safeIntegers(Context.get(fiber.context, SqlClient.SafeIntegers))
|
||||||
try {
|
try {
|
||||||
return Effect.succeed((statement.all(...(params as any)) ?? []) as Array<Record<string, unknown>>)
|
return Effect.succeed((statement.all(...(params as any)) ?? []) as Array<Record<string, unknown>>)
|
||||||
} catch (cause) {
|
} catch (cause) {
|
||||||
@@ -73,7 +66,7 @@ const make = (options: Config) =>
|
|||||||
Effect.withFiber<Array<unknown[]>, SqlError>((fiber) => {
|
Effect.withFiber<Array<unknown[]>, SqlError>((fiber) => {
|
||||||
const statement = native.query(query)
|
const statement = native.query(query)
|
||||||
// @ts-ignore bun-types missing safeIntegers method, fixed in https://github.com/oven-sh/bun/pull/26627
|
// @ts-ignore bun-types missing safeIntegers method, fixed in https://github.com/oven-sh/bun/pull/26627
|
||||||
statement.safeIntegers(Context.get(fiber.context, Client.SafeIntegers))
|
statement.safeIntegers(Context.get(fiber.context, SqlClient.SafeIntegers))
|
||||||
try {
|
try {
|
||||||
return Effect.succeed((statement.values(...(params as any)) ?? []) as Array<unknown[]>)
|
return Effect.succeed((statement.values(...(params as any)) ?? []) as Array<unknown[]>)
|
||||||
} catch (cause) {
|
} catch (cause) {
|
||||||
@@ -130,7 +123,7 @@ const make = (options: Config) =>
|
|||||||
})
|
})
|
||||||
|
|
||||||
const client = Object.assign(
|
const client = Object.assign(
|
||||||
(yield* Client.make({
|
(yield* SqlClient.make({
|
||||||
acquirer,
|
acquirer,
|
||||||
compiler,
|
compiler,
|
||||||
transactionAcquirer,
|
transactionAcquirer,
|
||||||
@@ -166,7 +159,7 @@ const nativeLayer = (config: Config) =>
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const sqliteLayer = (config: Config) => Layer.effect(Client.SqlClient, make(config))
|
const sqliteLayer = (config: Config) => Layer.effect(SqlClient.SqlClient, make(config))
|
||||||
|
|
||||||
const drizzleLayer = Layer.effect(
|
const drizzleLayer = Layer.effect(
|
||||||
Sqlite.Drizzle,
|
Sqlite.Drizzle,
|
||||||
|
|||||||
@@ -1,18 +1,11 @@
|
|||||||
import { DatabaseSync, type SQLInputValue } from "node:sqlite"
|
import { DatabaseSync, type SQLInputValue } from "node:sqlite"
|
||||||
import { drizzle } from "drizzle-orm/node-sqlite"
|
import { drizzle } from "drizzle-orm/node-sqlite"
|
||||||
import * as Context from "effect/Context"
|
import { Context, Effect, Fiber, Layer, Scope, Semaphore, Stream } from "effect"
|
||||||
import * as Effect from "effect/Effect"
|
|
||||||
import * as Fiber from "effect/Fiber"
|
|
||||||
import { identity } from "effect/Function"
|
import { identity } from "effect/Function"
|
||||||
import * as Layer from "effect/Layer"
|
import { Reactivity } from "effect/unstable/reactivity"
|
||||||
import * as Scope from "effect/Scope"
|
import { SqlClient, Statement } from "effect/unstable/sql"
|
||||||
import * as Semaphore from "effect/Semaphore"
|
|
||||||
import * as Stream from "effect/Stream"
|
|
||||||
import * as Reactivity from "effect/unstable/reactivity/Reactivity"
|
|
||||||
import * as Client from "effect/unstable/sql/SqlClient"
|
|
||||||
import type { Connection } from "effect/unstable/sql/SqlConnection"
|
import type { Connection } from "effect/unstable/sql/SqlConnection"
|
||||||
import { classifySqliteError, SqlError } from "effect/unstable/sql/SqlError"
|
import { classifySqliteError, SqlError } from "effect/unstable/sql/SqlError"
|
||||||
import * as Statement from "effect/unstable/sql/Statement"
|
|
||||||
import { Sqlite } from "./sqlite"
|
import { Sqlite } from "./sqlite"
|
||||||
|
|
||||||
const ATTR_DB_SYSTEM_NAME = "db.system.name"
|
const ATTR_DB_SYSTEM_NAME = "db.system.name"
|
||||||
@@ -20,7 +13,7 @@ const ATTR_DB_SYSTEM_NAME = "db.system.name"
|
|||||||
const TypeId = "~@opencode-ai/core/database/SqliteNode" as const
|
const TypeId = "~@opencode-ai/core/database/SqliteNode" as const
|
||||||
type TypeId = typeof TypeId
|
type TypeId = typeof TypeId
|
||||||
|
|
||||||
interface SqliteClient extends Client.SqlClient {
|
interface SqliteClient extends SqlClient.SqlClient {
|
||||||
readonly [TypeId]: TypeId
|
readonly [TypeId]: TypeId
|
||||||
readonly config: Config
|
readonly config: Config
|
||||||
readonly loadExtension: (path: string) => Effect.Effect<void, SqlError>
|
readonly loadExtension: (path: string) => Effect.Effect<void, SqlError>
|
||||||
@@ -56,7 +49,7 @@ const make = (options: Config) =>
|
|||||||
const run = (query: string, params: ReadonlyArray<unknown> = []) =>
|
const run = (query: string, params: ReadonlyArray<unknown> = []) =>
|
||||||
Effect.withFiber<Array<Record<string, unknown>>, SqlError>((fiber) => {
|
Effect.withFiber<Array<Record<string, unknown>>, SqlError>((fiber) => {
|
||||||
const statement = native.prepare(query)
|
const statement = native.prepare(query)
|
||||||
statement.setReadBigInts(Context.get(fiber.context, Client.SafeIntegers))
|
statement.setReadBigInts(Context.get(fiber.context, SqlClient.SafeIntegers))
|
||||||
try {
|
try {
|
||||||
return Effect.succeed(statement.all(...(params as SQLInputValue[])) as Array<Record<string, unknown>>)
|
return Effect.succeed(statement.all(...(params as SQLInputValue[])) as Array<Record<string, unknown>>)
|
||||||
} catch (cause) {
|
} catch (cause) {
|
||||||
@@ -71,7 +64,7 @@ const make = (options: Config) =>
|
|||||||
const runValues = (query: string, params: ReadonlyArray<unknown> = []) =>
|
const runValues = (query: string, params: ReadonlyArray<unknown> = []) =>
|
||||||
Effect.withFiber<ReadonlyArray<ReadonlyArray<unknown>>, SqlError>((fiber) => {
|
Effect.withFiber<ReadonlyArray<ReadonlyArray<unknown>>, SqlError>((fiber) => {
|
||||||
const statement = native.prepare(query)
|
const statement = native.prepare(query)
|
||||||
statement.setReadBigInts(Context.get(fiber.context, Client.SafeIntegers))
|
statement.setReadBigInts(Context.get(fiber.context, SqlClient.SafeIntegers))
|
||||||
statement.setReturnArrays(true)
|
statement.setReturnArrays(true)
|
||||||
try {
|
try {
|
||||||
return Effect.succeed(
|
return Effect.succeed(
|
||||||
@@ -124,7 +117,7 @@ const make = (options: Config) =>
|
|||||||
})
|
})
|
||||||
|
|
||||||
const client = Object.assign(
|
const client = Object.assign(
|
||||||
(yield* Client.make({
|
(yield* SqlClient.make({
|
||||||
acquirer,
|
acquirer,
|
||||||
compiler,
|
compiler,
|
||||||
transactionAcquirer,
|
transactionAcquirer,
|
||||||
@@ -161,7 +154,7 @@ const nativeLayer = (config: Config) =>
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const sqliteLayer = (config: Config) => Layer.effect(Client.SqlClient, make(config))
|
const sqliteLayer = (config: Config) => Layer.effect(SqlClient.SqlClient, make(config))
|
||||||
|
|
||||||
const drizzleLayer = Layer.effect(
|
const drizzleLayer = Layer.effect(
|
||||||
Sqlite.Drizzle,
|
Sqlite.Drizzle,
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ export function hoist<A, E, T extends Tag, const Items extends Replacements = re
|
|||||||
}
|
}
|
||||||
if (node.tag === tag) {
|
if (node.tag === tag) {
|
||||||
const existing = hoisted.get(node.name)
|
const existing = hoisted.get(node.name)
|
||||||
if (existing && existing !== node) {
|
if (existing && existing.implementation !== node.implementation) {
|
||||||
throw new Error(`Tag ${tag} has conflicting implementations for ${node.name}`)
|
throw new Error(`Tag ${tag} has conflicting implementations for ${node.name}`)
|
||||||
}
|
}
|
||||||
hoisted.set(node.name, rewriteReplacementDependencies(node, replacementMap))
|
hoisted.set(node.name, rewriteReplacementDependencies(node, replacementMap))
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export * as EventV2 from "./event"
|
export * as EventV2 from "./event"
|
||||||
|
|
||||||
import { Cause, Context, Effect, Layer, Option, PubSub, Queue, Schema, Stream } from "effect"
|
import { Cause, Context, DateTime, Effect, Layer, Option, PubSub, Queue, Schema, Stream } from "effect"
|
||||||
import { Event } from "@opencode-ai/schema/event"
|
import { Event } from "@opencode-ai/schema/event"
|
||||||
import type { Data, Definition, Payload } from "@opencode-ai/schema/event"
|
import type { Data, Definition, Payload } from "@opencode-ai/schema/event"
|
||||||
import type { EventLog } from "@opencode-ai/schema/event-log"
|
import type { EventLog } from "@opencode-ai/schema/event-log"
|
||||||
@@ -55,6 +55,7 @@ export const reserveSequence = Effect.fn("EventV2.reserveSequence")(function* (
|
|||||||
export type SerializedEvent = {
|
export type SerializedEvent = {
|
||||||
readonly id: ID
|
readonly id: ID
|
||||||
readonly type: string
|
readonly type: string
|
||||||
|
readonly created?: DateTime.Utc
|
||||||
readonly seq: number
|
readonly seq: number
|
||||||
readonly aggregateID: string
|
readonly aggregateID: string
|
||||||
readonly data: Record<string, unknown>
|
readonly data: Record<string, unknown>
|
||||||
@@ -81,6 +82,7 @@ const decodeSerializedEvent = (event: SerializedEvent): Payload => {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
id: event.id,
|
id: event.id,
|
||||||
|
created: event.created ?? DateTime.makeUnsafe(0),
|
||||||
type: definition.type,
|
type: definition.type,
|
||||||
durable: envelope(event.aggregateID, event.seq, definition.durable.version),
|
durable: envelope(event.aggregateID, event.seq, definition.durable.version),
|
||||||
data: Schema.decodeUnknownSync(definition.data)(event.data),
|
data: Schema.decodeUnknownSync(definition.data)(event.data),
|
||||||
@@ -92,8 +94,9 @@ export class SubscriberOverflowError extends Schema.TaggedErrorClass<SubscriberO
|
|||||||
{ capacity: Schema.Int },
|
{ capacity: Schema.Int },
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
export const define = Event.define
|
|
||||||
export const versionedType = Event.versionedType
|
export const versionedType = Event.versionedType
|
||||||
|
export const durable = Event.durable
|
||||||
|
export const ephemeral = Event.ephemeral
|
||||||
|
|
||||||
export interface PublishOptions {
|
export interface PublishOptions {
|
||||||
readonly id?: ID
|
readonly id?: ID
|
||||||
@@ -294,6 +297,7 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
if (
|
if (
|
||||||
stored?.id === event.id &&
|
stored?.id === event.id &&
|
||||||
stored.type === versionedType(definition.type, durable.version) &&
|
stored.type === versionedType(definition.type, durable.version) &&
|
||||||
|
stored.created === DateTime.toEpochMillis(event.created ?? DateTime.makeUnsafe(0)) &&
|
||||||
isDeepStrictEqual(stored.data, encoded)
|
isDeepStrictEqual(stored.data, encoded)
|
||||||
) {
|
) {
|
||||||
if (input.ownerID && row?.ownerID == null) {
|
if (input.ownerID && row?.ownerID == null) {
|
||||||
@@ -365,6 +369,7 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
id: event.id,
|
id: event.id,
|
||||||
aggregate_id: aggregateID,
|
aggregate_id: aggregateID,
|
||||||
seq,
|
seq,
|
||||||
|
created: DateTime.toEpochMillis(event.created ?? DateTime.makeUnsafe(0)),
|
||||||
type: versionedType(definition.type, durable.version),
|
type: versionedType(definition.type, durable.version),
|
||||||
data: encoded,
|
data: encoded,
|
||||||
},
|
},
|
||||||
@@ -470,6 +475,7 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
definition,
|
definition,
|
||||||
{
|
{
|
||||||
id: options?.id ?? ID.create(),
|
id: options?.id ?? ID.create(),
|
||||||
|
created: yield* DateTime.now,
|
||||||
...(options?.metadata ? { metadata: options.metadata } : {}),
|
...(options?.metadata ? { metadata: options.metadata } : {}),
|
||||||
type: definition.type,
|
type: definition.type,
|
||||||
...(location ? { location } : {}),
|
...(location ? { location } : {}),
|
||||||
@@ -493,6 +499,7 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
} else {
|
} else {
|
||||||
const payload = {
|
const payload = {
|
||||||
id: event.id,
|
id: event.id,
|
||||||
|
created: event.created ?? DateTime.makeUnsafe(0),
|
||||||
type: definition.type,
|
type: definition.type,
|
||||||
data: Schema.decodeUnknownSync(definition.data)(event.data),
|
data: Schema.decodeUnknownSync(definition.data)(event.data),
|
||||||
} as Payload
|
} as Payload
|
||||||
@@ -609,6 +616,7 @@ export const layerWith = (options?: LayerOptions) =>
|
|||||||
return [
|
return [
|
||||||
decodeSerializedEvent({
|
decodeSerializedEvent({
|
||||||
id: event.id,
|
id: event.id,
|
||||||
|
created: DateTime.makeUnsafe(event.created),
|
||||||
aggregateID: event.aggregate_id,
|
aggregateID: event.aggregate_id,
|
||||||
seq: event.seq,
|
seq: event.seq,
|
||||||
type: event.type,
|
type: event.type,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export const EventTable = sqliteTable(
|
|||||||
.notNull()
|
.notNull()
|
||||||
.references(() => EventSequenceTable.aggregate_id, { onDelete: "cascade" }),
|
.references(() => EventSequenceTable.aggregate_id, { onDelete: "cascade" }),
|
||||||
seq: integer().notNull(),
|
seq: integer().notNull(),
|
||||||
|
created: integer().notNull(),
|
||||||
type: text().notNull(),
|
type: text().notNull(),
|
||||||
data: text({ mode: "json" }).$type<Record<string, unknown>>().notNull(),
|
data: text({ mode: "json" }).$type<Record<string, unknown>>().notNull(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const FILES = [
|
|||||||
"**/.nyc_output/**",
|
"**/.nyc_output/**",
|
||||||
]
|
]
|
||||||
|
|
||||||
export const PATTERNS = [...FILES, ...FOLDERS]
|
export const PATTERNS = [...FILES, ...FOLDERS, `**/{${Array.from(FOLDERS).join(",")}}/**`]
|
||||||
|
|
||||||
export function match(filepath: string, opts?: { extra?: string[]; whitelist?: string[] }) {
|
export function match(filepath: string, opts?: { extra?: string[]; whitelist?: string[] }) {
|
||||||
for (const pattern of opts?.whitelist || []) {
|
for (const pattern of opts?.whitelist || []) {
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
export * as LocationWatcher from "./location-watcher"
|
||||||
|
|
||||||
|
import { makeLocationNode } from "../effect/app-node"
|
||||||
|
import { Context, Effect, Layer, Stream } from "effect"
|
||||||
|
import { FileSystem } from "@opencode-ai/schema/filesystem"
|
||||||
|
import os from "os"
|
||||||
|
import path from "path"
|
||||||
|
import { Config } from "../config"
|
||||||
|
import { EventV2 } from "../event"
|
||||||
|
import { FSUtil } from "../fs-util"
|
||||||
|
import { Git } from "../git"
|
||||||
|
import { Location } from "../location"
|
||||||
|
import { Watcher } from "./watcher"
|
||||||
|
import { Ignore } from "./ignore"
|
||||||
|
import { Protected } from "./protected"
|
||||||
|
|
||||||
|
function protecteds(dir: string) {
|
||||||
|
return Protected.paths().filter((item) => {
|
||||||
|
const relative = path.relative(dir, item)
|
||||||
|
return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Interface {}
|
||||||
|
|
||||||
|
export class Service extends Context.Service<Service, Interface>()("@opencode/LocationWatcher") {}
|
||||||
|
|
||||||
|
const layer = Layer.effect(
|
||||||
|
Service,
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const location = yield* Location.Service
|
||||||
|
const watcher = yield* Watcher.Service
|
||||||
|
const events = yield* EventV2.Service
|
||||||
|
const fs = yield* FSUtil.Service
|
||||||
|
const git = yield* Git.Service
|
||||||
|
const configService = yield* Config.Service
|
||||||
|
const config = (yield* configService.entries())
|
||||||
|
.filter((entry): entry is Config.Document => entry.type === "document")
|
||||||
|
.flatMap((item) => item.info.watcher?.ignore ?? [])
|
||||||
|
const publish = (update: { type: "create" | "update" | "delete"; path: string }) =>
|
||||||
|
events.publish(FileSystem.Event.Changed, {
|
||||||
|
file: update.path,
|
||||||
|
event: update.type === "create" ? "add" : update.type === "update" ? "change" : "unlink",
|
||||||
|
})
|
||||||
|
|
||||||
|
if (path.resolve(location.directory) !== path.resolve(os.homedir())) {
|
||||||
|
yield* watcher
|
||||||
|
.subscribe({
|
||||||
|
path: location.directory,
|
||||||
|
type: "directory",
|
||||||
|
ignore: [...Ignore.PATTERNS, ...config, ...protecteds(location.directory)],
|
||||||
|
})
|
||||||
|
.pipe(Stream.runForEach(publish), Effect.forkScoped({ startImmediately: true }))
|
||||||
|
} else {
|
||||||
|
yield* Effect.logInfo("location watcher skipped home directory", { directory: location.directory })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (location.vcs?.type === "git") {
|
||||||
|
const resolved = (yield* git.repo.discover(location.directory))?.gitDirectory
|
||||||
|
const vcs = resolved ? yield* fs.realPath(resolved).pipe(Effect.catch(() => Effect.succeed(resolved))) : undefined
|
||||||
|
if (vcs && !config.includes(".git") && !config.includes(vcs) && (!resolved || !config.includes(resolved))) {
|
||||||
|
const ignore = (yield* fs.readDirectoryEntries(vcs).pipe(Effect.catch(() => Effect.succeed([])))).flatMap(
|
||||||
|
(entry) => (entry.name === "HEAD" ? [] : [entry.name]),
|
||||||
|
)
|
||||||
|
yield* watcher
|
||||||
|
.subscribe({ path: vcs, type: "directory", ignore })
|
||||||
|
.pipe(Stream.runForEach(publish), Effect.forkScoped({ startImmediately: true }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Service.of({})
|
||||||
|
}).pipe(
|
||||||
|
Effect.catchCause((cause) =>
|
||||||
|
Effect.logError("failed to init location watcher service", { cause }).pipe(Effect.as(Service.of({}))),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const node = makeLocationNode({
|
||||||
|
service: Service,
|
||||||
|
layer,
|
||||||
|
deps: [Watcher.node, FSUtil.node, Location.node, Config.node, Git.node, EventV2.node],
|
||||||
|
})
|
||||||
@@ -3,26 +3,20 @@ export * as Watcher from "./watcher"
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { createWrapper } from "@parcel/watcher/wrapper"
|
import { createWrapper } from "@parcel/watcher/wrapper"
|
||||||
import type ParcelWatcher from "@parcel/watcher"
|
import type ParcelWatcher from "@parcel/watcher"
|
||||||
import { makeLocationNode } from "../effect/app-node"
|
import { FileSystem } from "@opencode-ai/schema/filesystem"
|
||||||
import { Cause, Context, Effect, Layer } from "effect"
|
import { makeGlobalNode } from "../effect/app-node"
|
||||||
import { FileSystemWatcher } from "@opencode-ai/schema/filesystem-watcher"
|
import { Cause, Context, Effect, Layer, PubSub, Scope, Stream } from "effect"
|
||||||
import os from "os"
|
import { KeyedMutex } from "../effect/keyed-mutex"
|
||||||
import path from "path"
|
|
||||||
import { Config } from "../config"
|
|
||||||
import { EventV2 } from "../event"
|
|
||||||
import { Flag } from "../flag/flag"
|
import { Flag } from "../flag/flag"
|
||||||
import { FSUtil } from "../fs-util"
|
|
||||||
import { Git } from "../git"
|
|
||||||
import { Location } from "../location"
|
|
||||||
import { lazy } from "../util/lazy"
|
import { lazy } from "../util/lazy"
|
||||||
import { Ignore } from "./ignore"
|
import { watch as watchFileSystem } from "node:fs"
|
||||||
import { Protected } from "./protected"
|
import path from "path"
|
||||||
|
|
||||||
declare const OPENCODE_LIBC: string | undefined
|
declare const OPENCODE_LIBC: string | undefined
|
||||||
|
|
||||||
const SUBSCRIBE_TIMEOUT_MS = 10_000
|
const SUBSCRIBE_TIMEOUT_MS = 10_000
|
||||||
|
|
||||||
export const Event = FileSystemWatcher.Event
|
export const Event = { Updated: FileSystem.Event.Changed }
|
||||||
|
|
||||||
const watcher = lazy((): typeof import("@parcel/watcher") | undefined => {
|
const watcher = lazy((): typeof import("@parcel/watcher") | undefined => {
|
||||||
try {
|
try {
|
||||||
@@ -42,107 +36,134 @@ function getBackend() {
|
|||||||
if (process.platform === "linux") return "inotify"
|
if (process.platform === "linux") return "inotify"
|
||||||
}
|
}
|
||||||
|
|
||||||
function protecteds(dir: string) {
|
export const hasNativeBinding = () => !!watcher()
|
||||||
return Protected.paths().filter((item) => {
|
export type Update = ParcelWatcher.Event
|
||||||
const relative = path.relative(dir, item)
|
|
||||||
return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative)
|
export type WatchInput =
|
||||||
})
|
| { readonly path: string; readonly type: "file" }
|
||||||
|
| { readonly path: string; readonly type: "directory"; readonly ignore?: readonly string[] }
|
||||||
|
|
||||||
|
export interface Interface {
|
||||||
|
readonly subscribe: (input: WatchInput) => Stream.Stream<Update>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const hasNativeBinding = () => !!watcher()
|
export class Service extends Context.Service<Service, Interface>()("@opencode/Watcher") {}
|
||||||
|
|
||||||
export interface Interface {}
|
|
||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/FileWatcher") {}
|
|
||||||
|
|
||||||
const layer = Layer.effect(
|
const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
if (Flag.OPENCODE_DISABLE_FILEWATCHER) return Service.of({})
|
|
||||||
|
|
||||||
const backend = getBackend()
|
const backend = getBackend()
|
||||||
const location = yield* Location.Service
|
const native = watcher()
|
||||||
if (path.resolve(location.directory) === path.resolve(os.homedir())) {
|
if (Flag.OPENCODE_DISABLE_FILEWATCHER) {
|
||||||
yield* Effect.logInfo("watcher skipped home directory", { directory: location.directory })
|
return Service.of({ subscribe: () => Stream.empty })
|
||||||
return Service.of({})
|
|
||||||
}
|
}
|
||||||
if (!backend) {
|
|
||||||
yield* Effect.logError("watcher backend not supported", {
|
type Entry = {
|
||||||
directory: location.directory,
|
readonly pubsub: PubSub.PubSub<Update>
|
||||||
platform: process.platform,
|
readonly subscription: { readonly unsubscribe: () => Promise<void> }
|
||||||
|
refs: number
|
||||||
|
}
|
||||||
|
const entries = new Map<string, Entry>()
|
||||||
|
const locks = KeyedMutex.makeUnsafe<string>()
|
||||||
|
|
||||||
|
const acquire = Effect.fn("Watcher.acquire")(function* (input: WatchInput) {
|
||||||
|
const scope = yield* Scope.Scope
|
||||||
|
const target = path.resolve(input.path)
|
||||||
|
const directory = input.type === "file" ? path.dirname(target) : target
|
||||||
|
const ignore = [...new Set(input.type === "directory" ? (input.ignore ?? []) : [])].toSorted()
|
||||||
|
const id = JSON.stringify([input.type, target, ignore])
|
||||||
|
const pubsub = yield* locks.withLock(id)(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const existing = entries.get(id)
|
||||||
|
if (existing) {
|
||||||
|
existing.refs++
|
||||||
|
return existing.pubsub
|
||||||
|
}
|
||||||
|
const pubsub = yield* PubSub.unbounded<Update>()
|
||||||
|
const subscription = yield* input.type === "file"
|
||||||
|
? Effect.sync(() => {
|
||||||
|
const subscription = watchFileSystem(directory, { recursive: false }, (_event, file) => {
|
||||||
|
if (file && path.resolve(directory, file.toString()) !== target) return
|
||||||
|
PubSub.publishUnsafe(pubsub, {
|
||||||
|
path: target,
|
||||||
|
type: "update",
|
||||||
|
} satisfies Update)
|
||||||
})
|
})
|
||||||
return Service.of({})
|
if ("on" in subscription && typeof subscription.on === "function") {
|
||||||
|
subscription.on("error", (error: unknown) =>
|
||||||
|
Effect.runFork(Effect.logError("watcher callback failed", { path: target, error })),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
return { unsubscribe: () => Promise.resolve(subscription.close()) }
|
||||||
const w = watcher()
|
})
|
||||||
if (!w) return Service.of({})
|
: subscribeDirectory(native, backend, directory, ignore, pubsub)
|
||||||
|
if (subscription) {
|
||||||
yield* Effect.logInfo("watcher backend", { directory: location.directory, platform: process.platform, backend })
|
entries.set(id, { pubsub, subscription, refs: 1 })
|
||||||
const events = yield* EventV2.Service
|
yield* Effect.logInfo("watcher started", {
|
||||||
const fs = yield* FSUtil.Service
|
path: target,
|
||||||
const git = yield* Git.Service
|
type: input.type,
|
||||||
const context = yield* Effect.context()
|
backend: input.type === "file" ? "node" : backend,
|
||||||
const runFork = Effect.runForkWith(context)
|
ignores: ignore.length,
|
||||||
const subscriptions: ParcelWatcher.AsyncSubscription[] = []
|
})
|
||||||
yield* Effect.addFinalizer(() =>
|
return pubsub
|
||||||
Effect.promise(() => Promise.allSettled(subscriptions.map((subscription) => subscription.unsubscribe()))),
|
}
|
||||||
|
yield* PubSub.shutdown(pubsub)
|
||||||
|
return pubsub
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const callback: ParcelWatcher.SubscribeCallback = (_error, updates) => {
|
yield* Scope.addFinalizer(
|
||||||
if (_error) runFork(Effect.logError("watcher callback failed", { error: _error }))
|
scope,
|
||||||
for (const update of updates) {
|
locks.withLock(id)(
|
||||||
if (update.type === "create") runFork(events.publish(Event.Updated, { file: update.path, event: "add" }))
|
Effect.gen(function* () {
|
||||||
if (update.type === "update") runFork(events.publish(Event.Updated, { file: update.path, event: "change" }))
|
const entry = entries.get(id)
|
||||||
if (update.type === "delete") runFork(events.publish(Event.Updated, { file: update.path, event: "unlink" }))
|
if (!entry) return
|
||||||
}
|
entry.refs--
|
||||||
}
|
if (entry.refs > 0) return
|
||||||
|
entries.delete(id)
|
||||||
|
yield* Effect.promise(() => entry.subscription.unsubscribe()).pipe(Effect.ignore)
|
||||||
|
yield* PubSub.shutdown(entry.pubsub)
|
||||||
|
yield* Effect.logInfo("watcher stopped", { path: target, type: input.type })
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return pubsub
|
||||||
|
})
|
||||||
|
|
||||||
const subscribe = (directory: string, ignore: string[]) => {
|
const subscribe = (input: WatchInput) =>
|
||||||
const pending = w.subscribe(directory, callback, { ignore, backend })
|
Stream.unwrap(acquire(input).pipe(Effect.map((pubsub) => Stream.fromPubSub(pubsub))))
|
||||||
|
|
||||||
|
return Service.of({ subscribe })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const node = makeGlobalNode({ service: Service, layer, deps: [] })
|
||||||
|
|
||||||
|
function subscribeDirectory(
|
||||||
|
native: typeof import("@parcel/watcher") | undefined,
|
||||||
|
backend: ParcelWatcher.BackendType | undefined,
|
||||||
|
directory: string,
|
||||||
|
ignore: string[],
|
||||||
|
pubsub: PubSub.PubSub<Update>,
|
||||||
|
) {
|
||||||
|
if (!native || !backend) {
|
||||||
|
return Effect.logError("watcher backend not supported", { directory, platform: process.platform }).pipe(
|
||||||
|
Effect.as(undefined),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const callback: ParcelWatcher.SubscribeCallback = (error, updates) => {
|
||||||
|
if (error) Effect.runFork(Effect.logError("watcher callback failed", { error }))
|
||||||
|
for (const update of updates) PubSub.publishUnsafe(pubsub, update)
|
||||||
|
}
|
||||||
|
const pending = native.subscribe(directory, callback, { ignore, backend })
|
||||||
return Effect.promise(() => pending).pipe(
|
return Effect.promise(() => pending).pipe(
|
||||||
Effect.tap((subscription) =>
|
|
||||||
Effect.sync(() => subscriptions.push(subscription)).pipe(
|
|
||||||
Effect.andThen(Effect.logInfo("watcher subscribed", { directory, backend, ignores: ignore.length })),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Effect.timeout(SUBSCRIBE_TIMEOUT_MS),
|
Effect.timeout(SUBSCRIBE_TIMEOUT_MS),
|
||||||
Effect.catchCause((cause) => {
|
Effect.catchCause((cause) => {
|
||||||
pending.then((subscription) => subscription.unsubscribe()).catch(() => {})
|
pending.then((subscription) => subscription.unsubscribe()).catch(() => {})
|
||||||
return Effect.logError("failed to subscribe", { directory, cause: Cause.pretty(cause) })
|
return Effect.logError("failed to subscribe", {
|
||||||
|
directory,
|
||||||
|
cause: Cause.pretty(cause),
|
||||||
|
}).pipe(Effect.as(undefined))
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = (yield* (yield* Config.Service).entries())
|
|
||||||
.filter((entry): entry is Config.Document => entry.type === "document")
|
|
||||||
.flatMap((item) => item.info.watcher?.ignore ?? [])
|
|
||||||
yield* Effect.forkScoped(
|
|
||||||
subscribe(location.directory, [...Ignore.PATTERNS, ...config, ...protecteds(location.directory)]),
|
|
||||||
)
|
|
||||||
|
|
||||||
if (location.vcs?.type === "git") {
|
|
||||||
const resolved = (yield* git.repo.discover(location.directory))?.gitDirectory
|
|
||||||
const vcs = resolved ? yield* fs.realPath(resolved).pipe(Effect.catch(() => Effect.succeed(resolved))) : undefined
|
|
||||||
if (vcs && !config.includes(".git") && !config.includes(vcs) && (!resolved || !config.includes(resolved))) {
|
|
||||||
const ignore = (yield* fs.readDirectoryEntries(vcs).pipe(Effect.catch(() => Effect.succeed([])))).flatMap(
|
|
||||||
(entry) => (entry.name === "HEAD" ? [] : [entry.name]),
|
|
||||||
)
|
|
||||||
yield* Effect.forkScoped(subscribe(vcs, ignore))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Service.of({})
|
|
||||||
}).pipe(
|
|
||||||
Effect.catchCause((cause) => {
|
|
||||||
return Effect.logError("failed to init watcher service", { cause: Cause.pretty(cause) }).pipe(
|
|
||||||
Effect.as(Service.of({})),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
export const node = makeLocationNode({
|
|
||||||
service: Service,
|
|
||||||
layer,
|
|
||||||
deps: [FSUtil.node, Location.node, Config.node, Git.node, EventV2.node],
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -98,10 +98,14 @@ export const layer = Layer.effect(
|
|||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const forms = yield* Cache.makeWith<ID, Entry>(() => Effect.die("Form cache must be used via set/getSuccess, never get"), {
|
const forms = yield* Cache.makeWith<ID, Entry>(
|
||||||
|
() => Effect.die(new Error("Form cache must be used via set/getSuccess, never get")),
|
||||||
|
{
|
||||||
capacity: Number.MAX_SAFE_INTEGER,
|
capacity: Number.MAX_SAFE_INTEGER,
|
||||||
timeToLive: (exit) => (Exit.isSuccess(exit) && exit.value.state.status === "pending" ? Duration.infinity : RETENTION),
|
timeToLive: (exit) =>
|
||||||
})
|
Exit.isSuccess(exit) && exit.value.state.status === "pending" ? Duration.infinity : RETENTION,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const find = Effect.fn("Form.find")(function* (id: ID) {
|
const find = Effect.fn("Form.find")(function* (id: ID) {
|
||||||
return yield* Cache.getSuccess(forms, id).pipe(
|
return yield* Cache.getSuccess(forms, id).pipe(
|
||||||
@@ -131,7 +135,9 @@ export const layer = Layer.effect(
|
|||||||
...(input.metadata === undefined ? {} : { metadata: input.metadata }),
|
...(input.metadata === undefined ? {} : { metadata: input.metadata }),
|
||||||
}
|
}
|
||||||
const form: Info =
|
const form: Info =
|
||||||
input.mode === "form" ? { ...base, mode: "form", fields: input.fields } : { ...base, mode: "url", url: input.url }
|
input.mode === "form"
|
||||||
|
? { ...base, mode: "form", fields: input.fields }
|
||||||
|
: { ...base, mode: "url", url: input.url }
|
||||||
const entry: Entry = {
|
const entry: Entry = {
|
||||||
form,
|
form,
|
||||||
state: { status: "pending" },
|
state: { status: "pending" },
|
||||||
@@ -149,7 +155,9 @@ export const layer = Layer.effect(
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const form = yield* create(input)
|
const form = yield* create(input)
|
||||||
const entry = yield* find(form.id).pipe(Effect.orDie)
|
const entry = yield* find(form.id).pipe(Effect.orDie)
|
||||||
return yield* restore(Deferred.await(entry.deferred)).pipe(Effect.onInterrupt(() => Effect.ignore(cancel(form.id))))
|
return yield* restore(Deferred.await(entry.deferred)).pipe(
|
||||||
|
Effect.onInterrupt(() => Effect.ignore(cancel(form.id))),
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -301,7 +309,8 @@ function validateField(field: Form.Field, value: Form.Value): string | undefined
|
|||||||
return `Form field has invalid pattern: ${field.key}`
|
return `Form field has invalid pattern: ${field.key}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (field.format === "email" && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return `Expected email for form field: ${field.key}`
|
if (field.format === "email" && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value))
|
||||||
|
return `Expected email for form field: ${field.key}`
|
||||||
if (field.format === "uri" && !isUri(value)) return `Expected URI for form field: ${field.key}`
|
if (field.format === "uri" && !isUri(value)) return `Expected URI for form field: ${field.key}`
|
||||||
if (field.format === "date" && !isDate(value)) return `Expected date for form field: ${field.key}`
|
if (field.format === "date" && !isDate(value)) return `Expected date for form field: ${field.key}`
|
||||||
if (field.format === "date-time" && !isDateTime(value)) return `Expected date-time for form field: ${field.key}`
|
if (field.format === "date-time" && !isDateTime(value)) return `Expected date-time for form field: ${field.key}`
|
||||||
@@ -324,8 +333,10 @@ function validateField(field: Form.Field, value: Form.Value): string | undefined
|
|||||||
if (field.type === "multiselect") {
|
if (field.type === "multiselect") {
|
||||||
if (!isStringArray(value)) return `Expected string array for form field: ${field.key}`
|
if (!isStringArray(value)) return `Expected string array for form field: ${field.key}`
|
||||||
if (field.required && value.length === 0) return `Missing required form field: ${field.key}`
|
if (field.required && value.length === 0) return `Missing required form field: ${field.key}`
|
||||||
if (field.minItems !== undefined && value.length < field.minItems) return `Too few selections for form field: ${field.key}`
|
if (field.minItems !== undefined && value.length < field.minItems)
|
||||||
if (field.maxItems !== undefined && value.length > field.maxItems) return `Too many selections for form field: ${field.key}`
|
return `Too few selections for form field: ${field.key}`
|
||||||
|
if (field.maxItems !== undefined && value.length > field.maxItems)
|
||||||
|
return `Too many selections for form field: ${field.key}`
|
||||||
if (!field.custom && value.some((item) => !field.options.some((option) => option.value === item))) {
|
if (!field.custom && value.some((item) => !field.options.some((option) => option.value === item))) {
|
||||||
return `Invalid option for form field: ${field.key}`
|
return `Invalid option for form field: ${field.key}`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { NodeFileSystem } from "@effect/platform-node"
|
import { NodeFileSystem } from "@effect/platform-node"
|
||||||
import { dirname, isAbsolute, join, relative, resolve as pathResolve, sep } from "path"
|
import path, { dirname, isAbsolute, join, relative, sep } from "path"
|
||||||
import { realpathSync } from "fs"
|
import { realpathSync } from "fs"
|
||||||
import * as NFS from "fs/promises"
|
import { readdir } from "fs/promises"
|
||||||
import { lookup } from "mime-types"
|
import { lookup } from "mime-types"
|
||||||
import { Context, Effect, FileSystem, Layer, Schema } from "effect"
|
import { Context, Effect, FileSystem, Layer, Schema } from "effect"
|
||||||
import type { PlatformError } from "effect/PlatformError"
|
import type { PlatformError } from "effect/PlatformError"
|
||||||
@@ -38,6 +38,7 @@ export namespace FSUtil {
|
|||||||
readonly ensureDir: (path: string) => Effect.Effect<void, Error>
|
readonly ensureDir: (path: string) => Effect.Effect<void, Error>
|
||||||
readonly writeWithDirs: (path: string, content: string | Uint8Array, mode?: number) => Effect.Effect<void, Error>
|
readonly writeWithDirs: (path: string, content: string | Uint8Array, mode?: number) => Effect.Effect<void, Error>
|
||||||
readonly readDirectoryEntries: (path: string) => Effect.Effect<DirEntry[], Error>
|
readonly readDirectoryEntries: (path: string) => Effect.Effect<DirEntry[], Error>
|
||||||
|
readonly resolve: (path: string) => Effect.Effect<string>
|
||||||
readonly findUp: (target: string, start: string, stop?: string) => Effect.Effect<string[], Error>
|
readonly findUp: (target: string, start: string, stop?: string) => Effect.Effect<string[], Error>
|
||||||
readonly up: (options: { targets: string[]; start: string; stop?: string }) => Effect.Effect<string[], Error>
|
readonly up: (options: { targets: string[]; start: string; stop?: string }) => Effect.Effect<string[], Error>
|
||||||
readonly globUp: (pattern: string, start: string, stop?: string) => Effect.Effect<string[], Error>
|
readonly globUp: (pattern: string, start: string, stop?: string) => Effect.Effect<string[], Error>
|
||||||
@@ -49,7 +50,9 @@ export namespace FSUtil {
|
|||||||
|
|
||||||
export const use = serviceUse(Service)
|
export const use = serviceUse(Service)
|
||||||
|
|
||||||
const layer = Layer.effect(
|
// Exported so simulation can wrap this layer and override the methods that
|
||||||
|
// bypass the injected FileSystem (readDirectoryEntries, glob, globUp).
|
||||||
|
export const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const fs = yield* FileSystem.FileSystem
|
const fs = yield* FileSystem.FileSystem
|
||||||
@@ -77,7 +80,7 @@ export namespace FSUtil {
|
|||||||
const readDirectoryEntries = Effect.fn("FileSystem.readDirectoryEntries")(function* (dirPath: string) {
|
const readDirectoryEntries = Effect.fn("FileSystem.readDirectoryEntries")(function* (dirPath: string) {
|
||||||
return yield* Effect.tryPromise({
|
return yield* Effect.tryPromise({
|
||||||
try: async () => {
|
try: async () => {
|
||||||
const entries = await NFS.readdir(dirPath, { withFileTypes: true })
|
const entries = await readdir(dirPath, { withFileTypes: true })
|
||||||
return entries.map(
|
return entries.map(
|
||||||
(e): DirEntry => ({
|
(e): DirEntry => ({
|
||||||
name: e.name,
|
name: e.name,
|
||||||
@@ -89,6 +92,14 @@ export namespace FSUtil {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const resolve = Effect.fn("FileSystem.resolve")(function* (input: string) {
|
||||||
|
const resolved = path.resolve(windowsPath(input))
|
||||||
|
return yield* fs.realPath(resolved).pipe(
|
||||||
|
Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(resolved)),
|
||||||
|
Effect.orDie,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
const readJson = Effect.fn("FileSystem.readJson")(function* (path: string) {
|
const readJson = Effect.fn("FileSystem.readJson")(function* (path: string) {
|
||||||
const text = yield* fs.readFileString(path)
|
const text = yield* fs.readFileString(path)
|
||||||
return yield* Effect.try({
|
return yield* Effect.try({
|
||||||
@@ -187,6 +198,7 @@ export namespace FSUtil {
|
|||||||
isDir,
|
isDir,
|
||||||
isFile,
|
isFile,
|
||||||
readDirectoryEntries,
|
readDirectoryEntries,
|
||||||
|
resolve,
|
||||||
readJson,
|
readJson,
|
||||||
writeJson,
|
writeJson,
|
||||||
ensureDir,
|
ensureDir,
|
||||||
@@ -209,7 +221,7 @@ export namespace FSUtil {
|
|||||||
|
|
||||||
export function normalizePath(p: string): string {
|
export function normalizePath(p: string): string {
|
||||||
if (process.platform !== "win32") return p
|
if (process.platform !== "win32") return p
|
||||||
const resolved = pathResolve(windowsPath(p))
|
const resolved = path.resolve(windowsPath(p))
|
||||||
try {
|
try {
|
||||||
return realpathSync.native(resolved)
|
return realpathSync.native(resolved)
|
||||||
} catch {
|
} catch {
|
||||||
@@ -227,7 +239,7 @@ export namespace FSUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function resolve(p: string): string {
|
export function resolve(p: string): string {
|
||||||
const resolved = pathResolve(windowsPath(p))
|
const resolved = path.resolve(windowsPath(p))
|
||||||
try {
|
try {
|
||||||
return normalizePath(realpathSync(resolved))
|
return normalizePath(realpathSync(resolved))
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { create as createIdentifier } from "@opencode-ai/schema/identifier"
|
import { create } from "@opencode-ai/schema/identifier"
|
||||||
|
|
||||||
const prefixes = {
|
const prefixes = {
|
||||||
job: "job",
|
job: "job",
|
||||||
@@ -23,7 +23,7 @@ export function descending(prefix: keyof typeof prefixes, given?: string) {
|
|||||||
|
|
||||||
function generateID(prefix: keyof typeof prefixes, direction: "descending" | "ascending", given?: string): string {
|
function generateID(prefix: keyof typeof prefixes, direction: "descending" | "ascending", given?: string): string {
|
||||||
if (!given) {
|
if (!given) {
|
||||||
return create(prefixes[prefix], direction)
|
return createID(prefixes[prefix], direction)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!given.startsWith(prefixes[prefix])) {
|
if (!given.startsWith(prefixes[prefix])) {
|
||||||
@@ -32,10 +32,12 @@ function generateID(prefix: keyof typeof prefixes, direction: "descending" | "as
|
|||||||
return given
|
return given
|
||||||
}
|
}
|
||||||
|
|
||||||
export function create(prefix: string, direction: "descending" | "ascending", timestamp?: number): string {
|
function createID(prefix: string, direction: "descending" | "ascending", timestamp?: number): string {
|
||||||
return prefix + "_" + createIdentifier(direction === "descending", timestamp)
|
return prefix + "_" + create(direction === "descending", timestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { createID as create }
|
||||||
|
|
||||||
/** Extract timestamp from an ascending ID. Does not work with descending IDs. */
|
/** Extract timestamp from an ascending ID. Does not work with descending IDs. */
|
||||||
export function timestamp(id: string): number {
|
export function timestamp(id: string): number {
|
||||||
const prefix = id.split("_")[0]
|
const prefix = id.split("_")[0]
|
||||||
|
|||||||
@@ -43,22 +43,24 @@ const layer = Layer.effect(
|
|||||||
})
|
})
|
||||||
|
|
||||||
const observe = Effect.fn("InstructionContext.observe")(function* () {
|
const observe = Effect.fn("InstructionContext.observe")(function* () {
|
||||||
const start = FSUtil.resolve(location.directory)
|
const start = yield* fs.resolve(location.directory)
|
||||||
const stop = FSUtil.resolve(location.project.directory)
|
const stop = yield* fs.resolve(location.project.directory)
|
||||||
const fromProject = relative(stop, start)
|
const fromProject = relative(stop, start)
|
||||||
const insideProject =
|
const insideProject =
|
||||||
fromProject === "" || (fromProject !== ".." && !fromProject.startsWith(`..${sep}`) && !isAbsolute(fromProject))
|
fromProject === "" || (fromProject !== ".." && !fromProject.startsWith(`..${sep}`) && !isAbsolute(fromProject))
|
||||||
const discovered = new Set(
|
const discovered = new Set(
|
||||||
(Flag.OPENCODE_DISABLE_PROJECT_CONFIG || !insideProject
|
yield* Effect.forEach(
|
||||||
|
Flag.OPENCODE_DISABLE_PROJECT_CONFIG || !insideProject
|
||||||
? []
|
? []
|
||||||
: yield* fs.up({
|
: yield* fs.up({
|
||||||
targets: ["AGENTS.md"],
|
targets: ["AGENTS.md"],
|
||||||
start,
|
start,
|
||||||
stop,
|
stop,
|
||||||
})
|
}),
|
||||||
).map(FSUtil.resolve),
|
fs.resolve,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
const paths = Array.dedupe([FSUtil.resolve(join(global.config, "AGENTS.md")), ...discovered])
|
const paths = Array.dedupe([yield* fs.resolve(join(global.config, "AGENTS.md")), ...discovered])
|
||||||
const files = yield* Effect.forEach(
|
const files = yield* Effect.forEach(
|
||||||
paths,
|
paths,
|
||||||
(path) =>
|
(path) =>
|
||||||
|
|||||||
@@ -406,7 +406,7 @@ const layer = Layer.effect(
|
|||||||
.get()
|
.get()
|
||||||
.integrations.get(input.integrationID)
|
.integrations.get(input.integrationID)
|
||||||
?.methods.some((method) => method.type === "key")
|
?.methods.some((method) => method.type === "key")
|
||||||
if (!method) return yield* Effect.die(`Key method not found: ${input.integrationID}`)
|
if (!method) return yield* Effect.die(new Error(`Key method not found: ${input.integrationID}`))
|
||||||
yield* credentials.create({
|
yield* credentials.create({
|
||||||
integrationID: input.integrationID,
|
integrationID: input.integrationID,
|
||||||
label: input.label,
|
label: input.label,
|
||||||
@@ -418,7 +418,7 @@ const layer = Layer.effect(
|
|||||||
oauth: Effect.fn("Integration.connection.oauth")(function* (input) {
|
oauth: Effect.fn("Integration.connection.oauth")(function* (input) {
|
||||||
const method = state.get().integrations.get(input.integrationID)?.implementations.get(input.methodID)
|
const method = state.get().integrations.get(input.integrationID)?.implementations.get(input.methodID)
|
||||||
if (!method) {
|
if (!method) {
|
||||||
return yield* Effect.die(`OAuth method not found: ${input.integrationID}/${input.methodID}`)
|
return yield* Effect.die(new Error(`OAuth method not found: ${input.integrationID}/${input.methodID}`))
|
||||||
}
|
}
|
||||||
const attemptScope = yield* Scope.fork(scope)
|
const attemptScope = yield* Scope.fork(scope)
|
||||||
const authorization = yield* authorize(method.authorize(input.inputs)).pipe(
|
const authorization = yield* authorize(method.authorize(input.inputs)).pipe(
|
||||||
@@ -475,7 +475,7 @@ const layer = Layer.effect(
|
|||||||
attempt: {
|
attempt: {
|
||||||
status: Effect.fn("Integration.attempt.status")(function* (attemptID) {
|
status: Effect.fn("Integration.attempt.status")(function* (attemptID) {
|
||||||
const attempt = (yield* SynchronizedRef.get(attempts)).get(attemptID)
|
const attempt = (yield* SynchronizedRef.get(attempts)).get(attemptID)
|
||||||
if (!attempt) return yield* Effect.die(`OAuth attempt not found: ${attemptID}`)
|
if (!attempt) return yield* Effect.die(new Error(`OAuth attempt not found: ${attemptID}`))
|
||||||
if (attempt.status === "failed") {
|
if (attempt.status === "failed") {
|
||||||
return { status: attempt.status, message: attempt.message ?? "Authorization failed", time: attempt.time }
|
return { status: attempt.status, message: attempt.message ?? "Authorization failed", time: attempt.time }
|
||||||
}
|
}
|
||||||
@@ -488,12 +488,13 @@ const layer = Layer.effect(
|
|||||||
if (match.authorization.mode === "code" && input.code === undefined) return [match, current]
|
if (match.authorization.mode === "code" && input.code === undefined) return [match, current]
|
||||||
return [match, new Map(current).set(input.attemptID, { ...match, completing: true })]
|
return [match, new Map(current).set(input.attemptID, { ...match, completing: true })]
|
||||||
})
|
})
|
||||||
if (!attempt) return yield* Effect.die(`OAuth attempt not found: ${input.attemptID}`)
|
if (!attempt) return yield* Effect.die(new Error(`OAuth attempt not found: ${input.attemptID}`))
|
||||||
if (attempt.status !== "pending") return
|
if (attempt.status !== "pending") return
|
||||||
if (attempt.authorization.mode === "code" && input.code === undefined) {
|
if (attempt.authorization.mode === "code" && input.code === undefined) {
|
||||||
return yield* new CodeRequiredError({ attemptID: input.attemptID })
|
return yield* new CodeRequiredError({ attemptID: input.attemptID })
|
||||||
}
|
}
|
||||||
if (attempt.completing) return yield* Effect.die(`OAuth attempt already completing: ${input.attemptID}`)
|
if (attempt.completing)
|
||||||
|
return yield* Effect.die(new Error(`OAuth attempt already completing: ${input.attemptID}`))
|
||||||
const callback =
|
const callback =
|
||||||
attempt.authorization.mode === "auto"
|
attempt.authorization.mode === "auto"
|
||||||
? attempt.authorization.callback
|
? attempt.authorization.callback
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { FileSystem } from "./filesystem"
|
|||||||
import { FileSystemSearch } from "./filesystem/search"
|
import { FileSystemSearch } from "./filesystem/search"
|
||||||
import { Generate } from "./generate"
|
import { Generate } from "./generate"
|
||||||
import { Form } from "./form"
|
import { Form } from "./form"
|
||||||
import { Watcher } from "./filesystem/watcher"
|
import { LocationWatcher } from "./filesystem/location-watcher"
|
||||||
import { Image } from "./image"
|
import { Image } from "./image"
|
||||||
import { Integration } from "./integration"
|
import { Integration } from "./integration"
|
||||||
import { Location } from "./location"
|
import { Location } from "./location"
|
||||||
@@ -21,15 +21,13 @@ import { MCP } from "./mcp/index"
|
|||||||
import { PermissionV2 } from "./permission"
|
import { PermissionV2 } from "./permission"
|
||||||
import { PluginV2 } from "./plugin"
|
import { PluginV2 } from "./plugin"
|
||||||
import { PluginInternal } from "./plugin/internal"
|
import { PluginInternal } from "./plugin/internal"
|
||||||
import { Policy } from "./policy"
|
|
||||||
import { Project } from "./project"
|
|
||||||
import { ProjectCopy } from "./project/copy"
|
import { ProjectCopy } from "./project/copy"
|
||||||
import { Pty } from "./pty"
|
import { Pty } from "./pty"
|
||||||
import { QuestionV2 } from "./question"
|
import { QuestionV2 } from "./question"
|
||||||
import { Shell } from "./shell"
|
import { Shell } from "./shell"
|
||||||
import { Reference } from "./reference"
|
import { Reference } from "./reference"
|
||||||
import { ReferenceGuidance } from "./reference/guidance"
|
import { ReferenceGuidance } from "./reference/guidance"
|
||||||
import * as SessionRunnerLLM from "./session/runner/llm"
|
import { SessionRunnerLLM } from "./session/runner/llm"
|
||||||
import { SessionRunnerModel } from "./session/runner/model"
|
import { SessionRunnerModel } from "./session/runner/model"
|
||||||
import { SessionCompaction } from "./session/compaction"
|
import { SessionCompaction } from "./session/compaction"
|
||||||
import { SessionTitle } from "./session/title"
|
import { SessionTitle } from "./session/title"
|
||||||
@@ -49,10 +47,8 @@ import { Vcs } from "./vcs"
|
|||||||
|
|
||||||
export { LocationServiceMap } from "./location-service-map"
|
export { LocationServiceMap } from "./location-service-map"
|
||||||
|
|
||||||
export const locationServices = LayerNode.group([
|
const locationServiceNodes = [
|
||||||
Project.node,
|
|
||||||
Location.node,
|
Location.node,
|
||||||
Policy.node,
|
|
||||||
Config.node,
|
Config.node,
|
||||||
AgentV2.node,
|
AgentV2.node,
|
||||||
CommandV2.node,
|
CommandV2.node,
|
||||||
@@ -66,7 +62,7 @@ export const locationServices = LayerNode.group([
|
|||||||
ProjectCopy.refreshNode,
|
ProjectCopy.refreshNode,
|
||||||
FileSystemSearch.node,
|
FileSystemSearch.node,
|
||||||
FileSystem.node,
|
FileSystem.node,
|
||||||
Watcher.node,
|
LocationWatcher.node,
|
||||||
Pty.node,
|
Pty.node,
|
||||||
Shell.node,
|
Shell.node,
|
||||||
SkillV2.node,
|
SkillV2.node,
|
||||||
@@ -96,7 +92,9 @@ export const locationServices = LayerNode.group([
|
|||||||
Snapshot.node,
|
Snapshot.node,
|
||||||
SessionRunnerLLM.node,
|
SessionRunnerLLM.node,
|
||||||
Vcs.node,
|
Vcs.node,
|
||||||
])
|
] as const satisfies readonly Node.LocationNode<unknown, unknown>[]
|
||||||
|
|
||||||
|
export const locationServices = LayerNode.group<typeof locationServiceNodes>(locationServiceNodes)
|
||||||
|
|
||||||
export type LocationServices = LayerNode.Output<typeof locationServices>
|
export type LocationServices = LayerNode.Output<typeof locationServices>
|
||||||
export type LocationError = LayerNode.Error<typeof locationServices>
|
export type LocationError = LayerNode.Error<typeof locationServices>
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import { InstallationVersion } from "../installation/version"
|
|||||||
|
|
||||||
const DEFAULT_STARTUP_TIMEOUT = 30_000
|
const DEFAULT_STARTUP_TIMEOUT = 30_000
|
||||||
const DEFAULT_REQUEST_TIMEOUT = 30_000
|
const DEFAULT_REQUEST_TIMEOUT = 30_000
|
||||||
const DEFAULT_TOOL_CALL_TIMEOUT = 100_000_000
|
|
||||||
|
|
||||||
type Transport = StdioClientTransport | StreamableHTTPClientTransport
|
type Transport = StdioClientTransport | StreamableHTTPClientTransport
|
||||||
|
|
||||||
@@ -47,7 +46,11 @@ const TolerantListPromptsResult = ListPromptsResultSchema.extend({
|
|||||||
|
|
||||||
export class NeedsAuthError extends Schema.TaggedErrorClass<NeedsAuthError>()("MCP.NeedsAuthError", {
|
export class NeedsAuthError extends Schema.TaggedErrorClass<NeedsAuthError>()("MCP.NeedsAuthError", {
|
||||||
server: Schema.String,
|
server: Schema.String,
|
||||||
}) {}
|
}) {
|
||||||
|
override get message() {
|
||||||
|
return `MCP server requires authentication: ${this.server}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class ConnectError extends Schema.TaggedErrorClass<ConnectError>()("MCP.ConnectError", {
|
export class ConnectError extends Schema.TaggedErrorClass<ConnectError>()("MCP.ConnectError", {
|
||||||
server: Schema.String,
|
server: Schema.String,
|
||||||
@@ -282,8 +285,8 @@ export const connect = Effect.fnUntraced(function* (
|
|||||||
client.callTool(
|
client.callTool(
|
||||||
{ name: input.name, arguments: input.args ?? {} },
|
{ name: input.name, arguments: input.args ?? {} },
|
||||||
CallToolResultSchema,
|
CallToolResultSchema,
|
||||||
// Human-driven tools can exceed the SDK default; use an effectively infinite timeout.
|
// Keep progress tokens available without imposing a client timeout on tool execution.
|
||||||
{ signal, timeout: DEFAULT_TOOL_CALL_TIMEOUT, resetTimeoutOnProgress: true, onprogress: () => {} },
|
{ signal, resetTimeoutOnProgress: true, onprogress: () => {} },
|
||||||
),
|
),
|
||||||
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
|
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
|
||||||
}).pipe(
|
}).pipe(
|
||||||
|
|||||||
@@ -127,7 +127,11 @@ export class ResourceContent extends Schema.Class<ResourceContent>("MCP.Resource
|
|||||||
|
|
||||||
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("MCP.NotFoundError", {
|
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("MCP.NotFoundError", {
|
||||||
server: ServerName,
|
server: ServerName,
|
||||||
}) {}
|
}) {
|
||||||
|
override get message() {
|
||||||
|
return `MCP server not found: ${this.server}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class ToolCallError extends Schema.TaggedErrorClass<ToolCallError>()("MCP.ToolCallError", {
|
export class ToolCallError extends Schema.TaggedErrorClass<ToolCallError>()("MCP.ToolCallError", {
|
||||||
server: ServerName,
|
server: ServerName,
|
||||||
@@ -201,7 +205,7 @@ export const layer = Layer.effect(
|
|||||||
for (const [name, server] of Object.entries(entry.info.mcp?.servers ?? {})) {
|
for (const [name, server] of Object.entries(entry.info.mcp?.servers ?? {})) {
|
||||||
runtime.set(ServerName.make(name), {
|
runtime.set(ServerName.make(name), {
|
||||||
config: { ...server, timeout: { ...timeout, ...server.timeout } },
|
config: { ...server, timeout: { ...timeout, ...server.timeout } },
|
||||||
status: { status: "disconnected" },
|
status: { status: "pending" },
|
||||||
startup: Deferred.makeUnsafe<void>(),
|
startup: Deferred.makeUnsafe<void>(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const Cost = Schema.Struct({
|
|||||||
const ReasoningOption = Schema.Union([
|
const ReasoningOption = Schema.Union([
|
||||||
Schema.Struct({
|
Schema.Struct({
|
||||||
type: Schema.Literal("effort"),
|
type: Schema.Literal("effort"),
|
||||||
values: Schema.Array(Schema.String),
|
values: Schema.Array(Schema.Union([Schema.String, Schema.Null])),
|
||||||
}),
|
}),
|
||||||
Schema.Struct({
|
Schema.Struct({
|
||||||
type: Schema.Literal("toggle"),
|
type: Schema.Literal("toggle"),
|
||||||
@@ -67,7 +67,7 @@ export const Model = Schema.Struct({
|
|||||||
attachment: Schema.Boolean,
|
attachment: Schema.Boolean,
|
||||||
reasoning: Schema.Boolean,
|
reasoning: Schema.Boolean,
|
||||||
reasoning_options: Schema.optional(Schema.Array(ReasoningOption)),
|
reasoning_options: Schema.optional(Schema.Array(ReasoningOption)),
|
||||||
temperature: Schema.Boolean,
|
temperature: Schema.optional(Schema.Boolean),
|
||||||
tool_call: Schema.Boolean,
|
tool_call: Schema.Boolean,
|
||||||
interleaved: Schema.optional(
|
interleaved: Schema.optional(
|
||||||
Schema.Union([
|
Schema.Union([
|
||||||
@@ -125,6 +125,10 @@ export const Provider = Schema.Struct({
|
|||||||
|
|
||||||
export type Provider = Schema.Schema.Type<typeof Provider>
|
export type Provider = Schema.Schema.Type<typeof Provider>
|
||||||
|
|
||||||
|
const Providers = Schema.Record(Schema.String, Provider)
|
||||||
|
const decodeProviders = Schema.decodeUnknownEffect(Schema.fromJsonString(Providers))
|
||||||
|
const decodeProvidersUnknown = Schema.decodeUnknownEffect(Providers)
|
||||||
|
|
||||||
export const Event = ModelsDev.Event
|
export const Event = ModelsDev.Event
|
||||||
|
|
||||||
declare const OPENCODE_MODELS_DEV: Record<string, Provider> | undefined
|
declare const OPENCODE_MODELS_DEV: Record<string, Provider> | undefined
|
||||||
@@ -176,6 +180,7 @@ const layer = Layer.effect(
|
|||||||
})
|
})
|
||||||
|
|
||||||
const loadFromDisk = fs.readJson(Flag.OPENCODE_MODELS_PATH ?? filepath).pipe(
|
const loadFromDisk = fs.readJson(Flag.OPENCODE_MODELS_PATH ?? filepath).pipe(
|
||||||
|
Effect.flatMap(decodeProvidersUnknown),
|
||||||
Effect.catch((error) => {
|
Effect.catch((error) => {
|
||||||
if (
|
if (
|
||||||
Flag.OPENCODE_MODELS_PATH === undefined &&
|
Flag.OPENCODE_MODELS_PATH === undefined &&
|
||||||
@@ -186,11 +191,17 @@ const layer = Layer.effect(
|
|||||||
}
|
}
|
||||||
return Effect.succeed(undefined)
|
return Effect.succeed(undefined)
|
||||||
}),
|
}),
|
||||||
Effect.map((v) => v as Record<string, Provider> | undefined),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const loadSnapshot = Effect.sync(() =>
|
const loadSnapshot = Effect.sync(() =>
|
||||||
typeof OPENCODE_MODELS_DEV === "undefined" ? undefined : OPENCODE_MODELS_DEV,
|
typeof OPENCODE_MODELS_DEV === "undefined" ? undefined : OPENCODE_MODELS_DEV,
|
||||||
|
).pipe(
|
||||||
|
Effect.flatMap((snapshot) =>
|
||||||
|
snapshot === undefined ? Effect.succeed(undefined) : decodeProvidersUnknown(snapshot),
|
||||||
|
),
|
||||||
|
Effect.catch((cause) =>
|
||||||
|
Effect.logWarning("bundled models snapshot failed schema decode", { cause }).pipe(Effect.as(undefined)),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const fetchAndWrite = Effect.fn("ModelsDev.fetchAndWrite")(function* () {
|
const fetchAndWrite = Effect.fn("ModelsDev.fetchAndWrite")(function* () {
|
||||||
@@ -221,7 +232,7 @@ const layer = Layer.effect(
|
|||||||
return yield* fetchAndWrite()
|
return yield* fetchAndWrite()
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
return JSON.parse(text) as Record<string, Provider>
|
return yield* decodeProviders(text)
|
||||||
}).pipe(Effect.withSpan("ModelsDev.populate"), Effect.orDie)
|
}).pipe(Effect.withSpan("ModelsDev.populate"), Effect.orDie)
|
||||||
|
|
||||||
const [cachedGet, invalidate] = yield* Effect.cachedInvalidateWithTTL(populate, Duration.infinity)
|
const [cachedGet, invalidate] = yield* Effect.cachedInvalidateWithTTL(populate, Duration.infinity)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export * as PermissionV2 from "./permission"
|
export * as PermissionV2 from "./permission"
|
||||||
|
|
||||||
import { makeLocationNode } from "./effect/app-node"
|
import { makeLocationNode } from "./effect/app-node"
|
||||||
import { Context, Deferred, Effect as EffectRuntime, Layer, Schema } from "effect"
|
import { Context, Deferred, Effect, Layer, Schema } from "effect"
|
||||||
import { Permission } from "@opencode-ai/schema/permission"
|
import { Permission } from "@opencode-ai/schema/permission"
|
||||||
import { EventV2 } from "./event"
|
import { EventV2 } from "./event"
|
||||||
import { Location } from "./location"
|
import { Location } from "./location"
|
||||||
@@ -11,7 +11,9 @@ import { SessionStore } from "./session/store"
|
|||||||
import { Wildcard } from "./util/wildcard"
|
import { Wildcard } from "./util/wildcard"
|
||||||
import { PermissionSaved } from "./permission/saved"
|
import { PermissionSaved } from "./permission/saved"
|
||||||
|
|
||||||
export { Effect, Rule, Ruleset } from "@opencode-ai/schema/permission"
|
const PermissionEffect = Permission.Effect
|
||||||
|
export { PermissionEffect as Effect }
|
||||||
|
export { Rule, Ruleset } from "@opencode-ai/schema/permission"
|
||||||
const missingAgentPermissions: Permission.Ruleset = [{ action: "*", resource: "*", effect: "deny" }]
|
const missingAgentPermissions: Permission.Ruleset = [{ action: "*", resource: "*", effect: "deny" }]
|
||||||
|
|
||||||
export const ID = Permission.ID
|
export const ID = Permission.ID
|
||||||
@@ -90,12 +92,12 @@ export function merge(...rulesets: Permission.Ruleset[]): Permission.Ruleset {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
readonly ask: (input: AssertInput) => EffectRuntime.Effect<AskResult, SessionV2.NotFoundError>
|
readonly ask: (input: AssertInput) => Effect.Effect<AskResult, SessionV2.NotFoundError>
|
||||||
readonly assert: (input: AssertInput) => EffectRuntime.Effect<void, Error | SessionV2.NotFoundError>
|
readonly assert: (input: AssertInput) => Effect.Effect<void, Error | SessionV2.NotFoundError>
|
||||||
readonly reply: (input: ReplyInput) => EffectRuntime.Effect<void, NotFoundError>
|
readonly reply: (input: ReplyInput) => Effect.Effect<void, NotFoundError>
|
||||||
readonly get: (id: ID) => EffectRuntime.Effect<Request | undefined>
|
readonly get: (id: ID) => Effect.Effect<Request | undefined>
|
||||||
readonly forSession: (sessionID: SessionV2.ID) => EffectRuntime.Effect<ReadonlyArray<Request>>
|
readonly forSession: (sessionID: SessionV2.ID) => Effect.Effect<ReadonlyArray<Request>>
|
||||||
readonly list: () => EffectRuntime.Effect<ReadonlyArray<Request>>
|
readonly list: () => Effect.Effect<ReadonlyArray<Request>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Permission") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Permission") {}
|
||||||
@@ -108,7 +110,7 @@ interface Pending {
|
|||||||
|
|
||||||
const layer = Layer.effect(
|
const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
EffectRuntime.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const location = yield* Location.Service
|
const location = yield* Location.Service
|
||||||
const agents = yield* AgentV2.Service
|
const agents = yield* AgentV2.Service
|
||||||
@@ -116,28 +118,25 @@ const layer = Layer.effect(
|
|||||||
const saved = yield* PermissionSaved.Service
|
const saved = yield* PermissionSaved.Service
|
||||||
const pending = new Map<ID, Pending>()
|
const pending = new Map<ID, Pending>()
|
||||||
|
|
||||||
yield* EffectRuntime.addFinalizer(() =>
|
yield* Effect.addFinalizer(() =>
|
||||||
EffectRuntime.forEach(pending.values(), (item) => Deferred.fail(item.deferred, new RejectedError()), {
|
Effect.forEach(pending.values(), (item) => Deferred.fail(item.deferred, new RejectedError()), {
|
||||||
discard: true,
|
discard: true,
|
||||||
}).pipe(
|
}).pipe(
|
||||||
EffectRuntime.ensuring(
|
Effect.ensuring(
|
||||||
EffectRuntime.sync(() => {
|
Effect.sync(() => {
|
||||||
pending.clear()
|
pending.clear()
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const savedRules = EffectRuntime.fnUntraced(function* () {
|
const savedRules = Effect.fnUntraced(function* () {
|
||||||
return (yield* saved.list({ projectID: location.project.id })).map(
|
return (yield* saved.list({ projectID: location.project.id })).map(
|
||||||
(item): Permission.Rule => ({ action: item.action, resource: item.resource, effect: "allow" }),
|
(item): Permission.Rule => ({ action: item.action, resource: item.resource, effect: "allow" }),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const configured = EffectRuntime.fn("PermissionV2.configured")(function* (
|
const configured = Effect.fn("PermissionV2.configured")(function* (sessionID: SessionV2.ID, agentID?: AgentV2.ID) {
|
||||||
sessionID: SessionV2.ID,
|
|
||||||
agentID?: AgentV2.ID,
|
|
||||||
) {
|
|
||||||
const session = yield* sessions.get(sessionID)
|
const session = yield* sessions.get(sessionID)
|
||||||
if (!session) return yield* new SessionV2.NotFoundError({ sessionID })
|
if (!session) return yield* new SessionV2.NotFoundError({ sessionID })
|
||||||
const agent = yield* agents.resolve(agentID ?? session.agent)
|
const agent = yield* agents.resolve(agentID ?? session.agent)
|
||||||
@@ -152,7 +151,7 @@ const layer = Layer.effect(
|
|||||||
return rules.filter((rule) => Wildcard.match(input.action, rule.action))
|
return rules.filter((rule) => Wildcard.match(input.action, rule.action))
|
||||||
}
|
}
|
||||||
|
|
||||||
const evaluateInput = EffectRuntime.fnUntraced(function* (input: AssertInput) {
|
const evaluateInput = Effect.fnUntraced(function* (input: AssertInput) {
|
||||||
const rules = yield* configured(input.sessionID, input.agent)
|
const rules = yield* configured(input.sessionID, input.agent)
|
||||||
if (denied(input, rules)) return { effect: "deny" as const, rules }
|
if (denied(input, rules)) return { effect: "deny" as const, rules }
|
||||||
const all = [...rules, ...(yield* savedRules())]
|
const all = [...rules, ...(yield* savedRules())]
|
||||||
@@ -174,29 +173,30 @@ const layer = Layer.effect(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const create = (request: Request, agent?: AgentV2.ID) =>
|
const create = (request: Request, agent?: AgentV2.ID) =>
|
||||||
EffectRuntime.uninterruptible(
|
Effect.uninterruptible(
|
||||||
EffectRuntime.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const deferred = yield* Deferred.make<void, RejectedError | CorrectedError>()
|
const deferred = yield* Deferred.make<void, RejectedError | CorrectedError>()
|
||||||
const item = { request, agent, deferred }
|
const item = { request, agent, deferred }
|
||||||
if (pending.has(request.id)) return yield* EffectRuntime.die(`Duplicate pending permission ID: ${request.id}`)
|
if (pending.has(request.id))
|
||||||
|
return yield* Effect.die(new Error(`Duplicate pending permission ID: ${request.id}`))
|
||||||
pending.set(request.id, item)
|
pending.set(request.id, item)
|
||||||
yield* events
|
yield* events
|
||||||
.publish(Event.Asked, request)
|
.publish(Event.Asked, request)
|
||||||
.pipe(EffectRuntime.onError(() => EffectRuntime.sync(() => pending.delete(request.id))))
|
.pipe(Effect.onError(() => Effect.sync(() => pending.delete(request.id))))
|
||||||
return item
|
return item
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const ask = EffectRuntime.fn("PermissionV2.ask")(function* (input: AssertInput) {
|
const ask = Effect.fn("PermissionV2.ask")(function* (input: AssertInput) {
|
||||||
const result = yield* evaluateInput(input)
|
const result = yield* evaluateInput(input)
|
||||||
const value = request(input)
|
const value = request(input)
|
||||||
if (result.effect === "ask") yield* create(value, input.agent)
|
if (result.effect === "ask") yield* create(value, input.agent)
|
||||||
return { id: value.id, effect: result.effect }
|
return { id: value.id, effect: result.effect }
|
||||||
})
|
})
|
||||||
|
|
||||||
const assert = EffectRuntime.fn("PermissionV2.assert")((input: AssertInput) =>
|
const assert = Effect.fn("PermissionV2.assert")((input: AssertInput) =>
|
||||||
EffectRuntime.uninterruptibleMask((restore) =>
|
Effect.uninterruptibleMask((restore) =>
|
||||||
EffectRuntime.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const result = yield* evaluateInput(input)
|
const result = yield* evaluateInput(input)
|
||||||
if (result.effect === "deny") {
|
if (result.effect === "deny") {
|
||||||
return yield* new DeniedError({
|
return yield* new DeniedError({
|
||||||
@@ -206,8 +206,8 @@ const layer = Layer.effect(
|
|||||||
if (result.effect === "allow") return
|
if (result.effect === "allow") return
|
||||||
const item = yield* create(request(input), input.agent)
|
const item = yield* create(request(input), input.agent)
|
||||||
return yield* restore(Deferred.await(item.deferred)).pipe(
|
return yield* restore(Deferred.await(item.deferred)).pipe(
|
||||||
EffectRuntime.ensuring(
|
Effect.ensuring(
|
||||||
EffectRuntime.sync(() => {
|
Effect.sync(() => {
|
||||||
pending.delete(item.request.id)
|
pending.delete(item.request.id)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -216,9 +216,9 @@ const layer = Layer.effect(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const reply = EffectRuntime.fn("PermissionV2.reply")((input: ReplyInput) =>
|
const reply = Effect.fn("PermissionV2.reply")((input: ReplyInput) =>
|
||||||
EffectRuntime.uninterruptible(
|
Effect.uninterruptible(
|
||||||
EffectRuntime.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const existing = pending.get(input.requestID)
|
const existing = pending.get(input.requestID)
|
||||||
if (!existing) return yield* new NotFoundError({ requestID: input.requestID })
|
if (!existing) return yield* new NotFoundError({ requestID: input.requestID })
|
||||||
yield* events.publish(Event.Replied, {
|
yield* events.publish(Event.Replied, {
|
||||||
@@ -261,7 +261,7 @@ const layer = Layer.effect(
|
|||||||
for (const [id, item] of pending) {
|
for (const [id, item] of pending) {
|
||||||
const input = { ...item.request }
|
const input = { ...item.request }
|
||||||
const rules = yield* configured(item.request.sessionID, item.agent).pipe(
|
const rules = yield* configured(item.request.sessionID, item.agent).pipe(
|
||||||
EffectRuntime.catchTag("Session.NotFoundError", () => EffectRuntime.succeed(undefined)),
|
Effect.catchTag("Session.NotFoundError", () => Effect.succeed(undefined)),
|
||||||
)
|
)
|
||||||
if (!rules) continue
|
if (!rules) continue
|
||||||
if (denied(input, rules)) continue
|
if (denied(input, rules)) continue
|
||||||
@@ -284,15 +284,15 @@ const layer = Layer.effect(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const list = EffectRuntime.fn("PermissionV2.list")(function* () {
|
const list = Effect.fn("PermissionV2.list")(function* () {
|
||||||
return Array.from(pending.values(), (item) => item.request)
|
return Array.from(pending.values(), (item) => item.request)
|
||||||
})
|
})
|
||||||
|
|
||||||
const get = EffectRuntime.fn("PermissionV2.get")(function* (id: ID) {
|
const get = Effect.fn("PermissionV2.get")(function* (id: ID) {
|
||||||
return pending.get(id)?.request
|
return pending.get(id)?.request
|
||||||
})
|
})
|
||||||
|
|
||||||
const forSession = EffectRuntime.fn("PermissionV2.forSession")(function* (sessionID: SessionV2.ID) {
|
const forSession = Effect.fn("PermissionV2.forSession")(function* (sessionID: SessionV2.ID) {
|
||||||
return Array.from(pending.values(), (item) => item.request).filter((request) => request.sessionID === sessionID)
|
return Array.from(pending.values(), (item) => item.request).filter((request) => request.sessionID === sessionID)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ const layer = Layer.effect(
|
|||||||
let host: Parameters<PluginDefinition["effect"]>[0]
|
let host: Parameters<PluginDefinition["effect"]>[0]
|
||||||
|
|
||||||
const add = Effect.fn("Plugin.add")(function* (id: ID, effect: PluginDefinition["effect"]) {
|
const add = Effect.fn("Plugin.add")(function* (id: ID, effect: PluginDefinition["effect"]) {
|
||||||
if (loading.has(id)) return yield* Effect.die(`Plugin load cycle detected for ${id}`)
|
if (loading.has(id)) return yield* Effect.die(new Error(`Plugin load cycle detected for ${id}`))
|
||||||
|
|
||||||
yield* locks.withLock(id)(
|
yield* locks.withLock(id)(
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
@@ -90,7 +90,7 @@ const layer = Layer.effect(
|
|||||||
})
|
})
|
||||||
|
|
||||||
const remove = Effect.fn("Plugin.remove")(function* (id: ID) {
|
const remove = Effect.fn("Plugin.remove")(function* (id: ID) {
|
||||||
if (loading.has(id)) return yield* Effect.die(`Cannot remove plugin ${id} while it is loading`)
|
if (loading.has(id)) return yield* Effect.die(new Error(`Cannot remove plugin ${id} while it is loading`))
|
||||||
|
|
||||||
yield* locks.withLock(id)(
|
yield* locks.withLock(id)(
|
||||||
State.batch(
|
State.batch(
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
export * as PluginHost from "./host"
|
export * as PluginHost from "./host"
|
||||||
|
|
||||||
import type { PluginContext as Interface } from "@opencode-ai/plugin/v2/effect"
|
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
|
||||||
import { Effect, Schema } from "effect"
|
import { EventManifest } from "@opencode-ai/schema/event-manifest"
|
||||||
|
import { Effect, Schema, Stream } from "effect"
|
||||||
import { AgentV2 } from "../agent"
|
import { AgentV2 } from "../agent"
|
||||||
import { AISDK } from "../aisdk"
|
import { AISDK } from "../aisdk"
|
||||||
import { Catalog } from "../catalog"
|
import { Catalog } from "../catalog"
|
||||||
import { CommandV2 } from "../command"
|
import { CommandV2 } from "../command"
|
||||||
import { Credential } from "../credential"
|
import { Credential } from "../credential"
|
||||||
|
import { EventV2 } from "../event"
|
||||||
import { Integration } from "../integration"
|
import { Integration } from "../integration"
|
||||||
import { Location } from "../location"
|
import { Location } from "../location"
|
||||||
import { ModelV2 } from "../model"
|
import { ModelV2 } from "../model"
|
||||||
@@ -21,12 +23,14 @@ import { ToolHooks } from "../tool/hooks"
|
|||||||
import { WorkspaceV2 } from "../workspace"
|
import { WorkspaceV2 } from "../workspace"
|
||||||
|
|
||||||
const mutable = <T>(value: T) => value as DeepMutable<T>
|
const mutable = <T>(value: T) => value as DeepMutable<T>
|
||||||
|
const isEvent = Schema.is(Schema.Union(EventManifest.ServerDefinitions))
|
||||||
|
|
||||||
export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Interface) {
|
export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Interface) {
|
||||||
const agents = yield* AgentV2.Service
|
const agents = yield* AgentV2.Service
|
||||||
const aisdk = yield* AISDK.Service
|
const aisdk = yield* AISDK.Service
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
const commands = yield* CommandV2.Service
|
const commands = yield* CommandV2.Service
|
||||||
|
const events = yield* EventV2.Service
|
||||||
const integration = yield* Integration.Service
|
const integration = yield* Integration.Service
|
||||||
const location = yield* Location.Service
|
const location = yield* Location.Service
|
||||||
const reference = yield* Reference.Service
|
const reference = yield* Reference.Service
|
||||||
@@ -40,7 +44,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
|||||||
workspaceID: location.workspaceID,
|
workspaceID: location.workspaceID,
|
||||||
project: location.project,
|
project: location.project,
|
||||||
})
|
})
|
||||||
const locationRef = (input?: Parameters<Interface["agent"]["list"]>[0]) =>
|
const locationRef = (input?: Parameters<PluginContext["agent"]["list"]>[0]) =>
|
||||||
input?.location === undefined
|
input?.location === undefined
|
||||||
? undefined
|
? undefined
|
||||||
: Location.Ref.make({
|
: Location.Ref.make({
|
||||||
@@ -52,6 +56,8 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
|||||||
})
|
})
|
||||||
const isCurrentLocation = (ref: Location.Ref) =>
|
const isCurrentLocation = (ref: Location.Ref) =>
|
||||||
ref.directory === location.directory && ref.workspaceID === location.workspaceID
|
ref.directory === location.directory && ref.workspaceID === location.workspaceID
|
||||||
|
const response = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
||||||
|
effect.pipe(Effect.map((data) => ({ location: locationInfo(), data })))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
options: {},
|
options: {},
|
||||||
@@ -63,15 +69,15 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
|||||||
},
|
},
|
||||||
reload: agents.reload,
|
reload: agents.reload,
|
||||||
transform: (callback) =>
|
transform: (callback) =>
|
||||||
agents.transform((draft) =>
|
agents.transform((draft) => {
|
||||||
callback({
|
callback({
|
||||||
list: () => mutable(draft.list()),
|
list: () => mutable(draft.list()),
|
||||||
get: (id) => mutable(draft.get(AgentV2.ID.make(id))),
|
get: (id) => mutable(draft.get(AgentV2.ID.make(id))),
|
||||||
default: (id) => draft.default(id === undefined ? undefined : AgentV2.ID.make(id)),
|
default: (id) => draft.default(id === undefined ? undefined : AgentV2.ID.make(id)),
|
||||||
update: (id, update) => draft.update(AgentV2.ID.make(id), update),
|
update: (id, update) => draft.update(AgentV2.ID.make(id), update),
|
||||||
remove: (id) => draft.remove(AgentV2.ID.make(id)),
|
remove: (id) => draft.remove(AgentV2.ID.make(id)),
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
),
|
|
||||||
},
|
},
|
||||||
aisdk: {
|
aisdk: {
|
||||||
sdk: (callback) =>
|
sdk: (callback) =>
|
||||||
@@ -102,9 +108,26 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
catalog: {
|
catalog: {
|
||||||
|
provider: {
|
||||||
|
list: () => response(catalog.provider.available()),
|
||||||
|
get: (input) =>
|
||||||
|
catalog.provider
|
||||||
|
.get(ProviderV2.ID.make(input.providerID))
|
||||||
|
.pipe(
|
||||||
|
Effect.flatMap((provider) =>
|
||||||
|
provider === undefined
|
||||||
|
? Effect.fail(new Error(`Provider not found: ${input.providerID}`))
|
||||||
|
: response(Effect.succeed(provider)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
model: {
|
||||||
|
list: () => response(catalog.model.available()),
|
||||||
|
default: () => response(catalog.model.default()),
|
||||||
|
},
|
||||||
reload: catalog.reload,
|
reload: catalog.reload,
|
||||||
transform: (callback) =>
|
transform: (callback) =>
|
||||||
catalog.transform((draft) =>
|
catalog.transform((draft) => {
|
||||||
callback({
|
callback({
|
||||||
provider: {
|
provider: {
|
||||||
list: () => mutable(draft.provider.list()),
|
list: () => mutable(draft.provider.list()),
|
||||||
@@ -125,14 +148,42 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
|||||||
draft.model.default.set(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
|
draft.model.default.set(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
),
|
|
||||||
},
|
},
|
||||||
command: {
|
command: {
|
||||||
|
list: () => response(commands.list()),
|
||||||
reload: commands.reload,
|
reload: commands.reload,
|
||||||
transform: commands.transform,
|
transform: (callback) =>
|
||||||
|
commands.transform((draft) => {
|
||||||
|
callback(draft)
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
subscribe: () => events.live().pipe(Stream.filter(isEvent)),
|
||||||
},
|
},
|
||||||
integration: {
|
integration: {
|
||||||
|
list: () => response(integration.list()),
|
||||||
|
get: (input) => response(integration.get(Integration.ID.make(input.integrationID))),
|
||||||
|
connectKey: (input) =>
|
||||||
|
integration.connection.key({
|
||||||
|
integrationID: Integration.ID.make(input.integrationID),
|
||||||
|
key: input.key,
|
||||||
|
label: input.label,
|
||||||
|
}),
|
||||||
|
connectOauth: (input) =>
|
||||||
|
response(
|
||||||
|
integration.connection.oauth({
|
||||||
|
integrationID: Integration.ID.make(input.integrationID),
|
||||||
|
methodID: Integration.MethodID.make(input.methodID),
|
||||||
|
inputs: input.inputs,
|
||||||
|
label: input.label,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
attemptStatus: (input) => response(integration.attempt.status(Integration.AttemptID.make(input.attemptID))),
|
||||||
|
attemptComplete: (input) =>
|
||||||
|
integration.attempt.complete({ attemptID: Integration.AttemptID.make(input.attemptID), code: input.code }),
|
||||||
|
attemptCancel: (input) => integration.attempt.cancel(Integration.AttemptID.make(input.attemptID)),
|
||||||
reload: integration.reload,
|
reload: integration.reload,
|
||||||
connection: {
|
connection: {
|
||||||
active: (id) => integration.connection.active(Integration.ID.make(id)),
|
active: (id) => integration.connection.active(Integration.ID.make(id)),
|
||||||
@@ -142,7 +193,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
transform: (callback) =>
|
transform: (callback) =>
|
||||||
integration.transform((draft) =>
|
integration.transform((draft) => {
|
||||||
callback({
|
callback({
|
||||||
list: () => mutable(draft.list()),
|
list: () => mutable(draft.list()),
|
||||||
get: (id) => mutable(draft.get(Integration.ID.make(id))),
|
get: (id) => mutable(draft.get(Integration.ID.make(id))),
|
||||||
@@ -219,36 +270,39 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
|||||||
remove: (id, method) =>
|
remove: (id, method) =>
|
||||||
draft.method.remove(Integration.ID.make(id), Schema.decodeUnknownSync(Integration.Method)(method)),
|
draft.method.remove(Integration.ID.make(id), Schema.decodeUnknownSync(Integration.Method)(method)),
|
||||||
},
|
},
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
),
|
|
||||||
},
|
},
|
||||||
plugin: {
|
plugin: {
|
||||||
|
list: () => response(plugin.list()),
|
||||||
add: (input) => plugin.add(PluginV2.ID.make(input.id), input.effect),
|
add: (input) => plugin.add(PluginV2.ID.make(input.id), input.effect),
|
||||||
remove: (id) => plugin.remove(PluginV2.ID.make(id)),
|
remove: (id) => plugin.remove(PluginV2.ID.make(id)),
|
||||||
},
|
},
|
||||||
reference: {
|
reference: {
|
||||||
|
list: () => response(reference.list()),
|
||||||
reload: reference.reload,
|
reload: reference.reload,
|
||||||
transform: (callback) =>
|
transform: (callback) =>
|
||||||
reference.transform((draft) =>
|
reference.transform((draft) => {
|
||||||
callback({
|
callback({
|
||||||
add: (name, source) => draft.add(name, Schema.decodeUnknownSync(Reference.Source)(source)),
|
add: (name, source) => draft.add(name, Schema.decodeUnknownSync(Reference.Source)(source)),
|
||||||
remove: draft.remove,
|
remove: draft.remove,
|
||||||
list: draft.list,
|
list: draft.list,
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
),
|
|
||||||
},
|
},
|
||||||
skill: {
|
skill: {
|
||||||
|
list: () => response(skill.list()),
|
||||||
reload: skill.reload,
|
reload: skill.reload,
|
||||||
transform: (callback) =>
|
transform: (callback) =>
|
||||||
skill.transform((draft) =>
|
skill.transform((draft) => {
|
||||||
callback({
|
callback({
|
||||||
source: (source) => draft.source(Schema.decodeUnknownSync(SkillV2.Source)(source)),
|
source: (source) => draft.source(Schema.decodeUnknownSync(SkillV2.Source)(source)),
|
||||||
list: draft.list,
|
list: draft.list,
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
),
|
|
||||||
},
|
},
|
||||||
tool: {
|
tool: {
|
||||||
register: (input) => tools.register(input),
|
register: (input, options) => tools.register(input, options),
|
||||||
execute: {
|
execute: {
|
||||||
before: (callback) =>
|
before: (callback) =>
|
||||||
toolHooks.hook.before((event) => {
|
toolHooks.hook.before((event) => {
|
||||||
@@ -305,5 +359,5 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
|||||||
command: runtime.session.command,
|
command: runtime.session.command,
|
||||||
interrupt: (input) => runtime.session.interrupt(input.sessionID),
|
interrupt: (input) => runtime.session.interrupt(input.sessionID),
|
||||||
},
|
},
|
||||||
} satisfies Interface
|
} satisfies PluginContext
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -201,10 +201,9 @@ export const ModelsDevPlugin = define({
|
|||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
const modelsDev = yield* ModelsDev.Service
|
const modelsDev = yield* ModelsDev.Service
|
||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
yield* ctx.integration.transform(
|
const loaded = { data: yield* modelsDev.get() }
|
||||||
Effect.fn(function* (integrations) {
|
yield* ctx.integration.transform((integrations) => {
|
||||||
const data = yield* modelsDev.get()
|
for (const item of Object.values(loaded.data)) {
|
||||||
for (const item of Object.values(data)) {
|
|
||||||
if (item.env.length === 0) continue
|
if (item.env.length === 0) continue
|
||||||
const integrationID = item.id
|
const integrationID = item.id
|
||||||
integrations.update(integrationID, (integration) => (integration.name = item.name))
|
integrations.update(integrationID, (integration) => (integration.name = item.name))
|
||||||
@@ -217,12 +216,9 @@ export const ModelsDevPlugin = define({
|
|||||||
method: { type: "env", names: [...item.env] },
|
method: { type: "env", names: [...item.env] },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
)
|
yield* ctx.catalog.transform((catalog) => {
|
||||||
yield* ctx.catalog.transform(
|
for (const item of Object.values(loaded.data)) {
|
||||||
Effect.fn(function* (catalog) {
|
|
||||||
const data = yield* modelsDev.get()
|
|
||||||
for (const item of Object.values(data)) {
|
|
||||||
const providerID = ProviderV2.ID.make(item.id)
|
const providerID = ProviderV2.ID.make(item.id)
|
||||||
catalog.provider.update(providerID, (provider) => {
|
catalog.provider.update(providerID, (provider) => {
|
||||||
provider.name = item.name
|
provider.name = item.name
|
||||||
@@ -255,10 +251,15 @@ export const ModelsDevPlugin = define({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
)
|
|
||||||
yield* events.subscribe(ModelsDev.Event.Refreshed).pipe(
|
yield* events.subscribe(ModelsDev.Event.Refreshed).pipe(
|
||||||
Stream.runForEach(() => ctx.integration.reload().pipe(Effect.andThen(ctx.catalog.reload()))),
|
Stream.runForEach(() =>
|
||||||
|
modelsDev.get().pipe(
|
||||||
|
Effect.tap((data) => Effect.sync(() => (loaded.data = data))),
|
||||||
|
Effect.andThen(ctx.integration.reload()),
|
||||||
|
Effect.andThen(ctx.catalog.reload()),
|
||||||
|
),
|
||||||
|
),
|
||||||
Effect.forkScoped({ startImmediately: true }),
|
Effect.forkScoped({ startImmediately: true }),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
export * as PluginPromise from "./promise"
|
export * as PluginPromise from "./promise"
|
||||||
|
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||||
import type { Plugin, PluginContext, Registration } from "@opencode-ai/plugin/v2/promise"
|
import type { Plugin, PluginContext } from "@opencode-ai/plugin/v2/promise"
|
||||||
import { Effect, Scope } from "effect"
|
import { Effect, Scope, Stream } from "effect"
|
||||||
|
|
||||||
// The Effect host hands back this registration shape; mirror it structurally so
|
|
||||||
// we do not have to alias the Effect package's `Registration` against the Promise one.
|
|
||||||
type HostRegistration = { readonly dispose: Effect.Effect<void> }
|
type HostRegistration = { readonly dispose: Effect.Effect<void> }
|
||||||
|
type Registration = { readonly dispose: () => Promise<void> }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adapts a Promise plugin into an Effect plugin so the existing Effect-only
|
* Adapts a Promise plugin into an Effect plugin so the existing Effect-only
|
||||||
@@ -31,20 +30,23 @@ export function fromPromise(plugin: Plugin) {
|
|||||||
dispose: () => Effect.runPromiseWith(context)(registration.dispose),
|
dispose: () => Effect.runPromiseWith(context)(registration.dispose),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const run = (effect: Effect.Effect<void>) => Effect.runPromiseWith(context)(effect)
|
const run = <A, E>(effect: Effect.Effect<A, E>) => Effect.runPromiseWith(context)(effect)
|
||||||
|
|
||||||
const transform =
|
const transform =
|
||||||
<Draft>(domain: {
|
<Draft>(domain: {
|
||||||
transform: (
|
transform: (callback: (draft: Draft) => void) => Effect.Effect<HostRegistration, never, Scope.Scope>
|
||||||
callback: (draft: Draft) => Effect.Effect<void> | void,
|
|
||||||
) => Effect.Effect<HostRegistration, never, Scope.Scope>
|
|
||||||
}) =>
|
}) =>
|
||||||
(callback: (draft: Draft) => Promise<void> | void) =>
|
(callback: (draft: Draft) => void) =>
|
||||||
register(domain.transform((draft) => Effect.promise(() => Promise.resolve(callback(draft)))))
|
register(
|
||||||
|
domain.transform((draft) => {
|
||||||
|
callback(draft)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
const context2: PluginContext = {
|
const context2: PluginContext = {
|
||||||
options: host.options,
|
options: host.options,
|
||||||
agent: {
|
agent: {
|
||||||
|
list: (input) => run(host.agent.list(input)),
|
||||||
transform: transform(host.agent),
|
transform: transform(host.agent),
|
||||||
reload: () => run(host.agent.reload()),
|
reload: () => run(host.agent.reload()),
|
||||||
},
|
},
|
||||||
@@ -55,14 +57,33 @@ export function fromPromise(plugin: Plugin) {
|
|||||||
register(host.aisdk.language((event) => Effect.promise(() => Promise.resolve(callback(event))))),
|
register(host.aisdk.language((event) => Effect.promise(() => Promise.resolve(callback(event))))),
|
||||||
},
|
},
|
||||||
catalog: {
|
catalog: {
|
||||||
|
provider: {
|
||||||
|
list: (input) => run(host.catalog.provider.list(input)),
|
||||||
|
get: (input) => run(host.catalog.provider.get(input)),
|
||||||
|
},
|
||||||
|
model: {
|
||||||
|
list: (input) => run(host.catalog.model.list(input)),
|
||||||
|
default: (input) => run(host.catalog.model.default(input)),
|
||||||
|
},
|
||||||
transform: transform(host.catalog),
|
transform: transform(host.catalog),
|
||||||
reload: () => run(host.catalog.reload()),
|
reload: () => run(host.catalog.reload()),
|
||||||
},
|
},
|
||||||
command: {
|
command: {
|
||||||
|
list: (input) => run(host.command.list(input)),
|
||||||
transform: transform(host.command),
|
transform: transform(host.command),
|
||||||
reload: () => run(host.command.reload()),
|
reload: () => run(host.command.reload()),
|
||||||
},
|
},
|
||||||
|
event: {
|
||||||
|
subscribe: () => Stream.toAsyncIterable(host.event.subscribe()),
|
||||||
|
},
|
||||||
integration: {
|
integration: {
|
||||||
|
list: (input) => run(host.integration.list(input)),
|
||||||
|
get: (input) => run(host.integration.get(input)),
|
||||||
|
connectKey: (input) => run(host.integration.connectKey(input)),
|
||||||
|
connectOauth: (input) => run(host.integration.connectOauth(input)),
|
||||||
|
attemptStatus: (input) => run(host.integration.attemptStatus(input)),
|
||||||
|
attemptComplete: (input) => run(host.integration.attemptComplete(input)),
|
||||||
|
attemptCancel: (input) => run(host.integration.attemptCancel(input)),
|
||||||
transform: transform(host.integration),
|
transform: transform(host.integration),
|
||||||
reload: () => run(host.integration.reload()),
|
reload: () => run(host.integration.reload()),
|
||||||
connection: {
|
connection: {
|
||||||
@@ -71,6 +92,7 @@ export function fromPromise(plugin: Plugin) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugin: {
|
plugin: {
|
||||||
|
list: (input) => run(host.plugin.list(input)),
|
||||||
add: (input) => {
|
add: (input) => {
|
||||||
const child = fromPromise(input)
|
const child = fromPromise(input)
|
||||||
return run(host.plugin.add(child))
|
return run(host.plugin.add(child))
|
||||||
@@ -78,13 +100,22 @@ export function fromPromise(plugin: Plugin) {
|
|||||||
remove: (id) => run(host.plugin.remove(id)),
|
remove: (id) => run(host.plugin.remove(id)),
|
||||||
},
|
},
|
||||||
reference: {
|
reference: {
|
||||||
|
list: (input) => run(host.reference.list(input)),
|
||||||
transform: transform(host.reference),
|
transform: transform(host.reference),
|
||||||
reload: () => run(host.reference.reload()),
|
reload: () => run(host.reference.reload()),
|
||||||
},
|
},
|
||||||
skill: {
|
skill: {
|
||||||
|
list: (input) => run(host.skill.list(input)),
|
||||||
transform: transform(host.skill),
|
transform: transform(host.skill),
|
||||||
reload: () => run(host.skill.reload()),
|
reload: () => run(host.skill.reload()),
|
||||||
},
|
},
|
||||||
|
session: {
|
||||||
|
create: (input) => run(host.session.create(input)),
|
||||||
|
get: (input) => run(host.session.get(input)),
|
||||||
|
prompt: (input) => run(host.session.prompt(input)),
|
||||||
|
command: (input) => run(host.session.command(input)),
|
||||||
|
interrupt: (input) => run(host.session.interrupt(input)),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
yield* Effect.promise(() => Promise.resolve(plugin.setup(context2)))
|
yield* Effect.promise(() => Promise.resolve(plugin.setup(context2)))
|
||||||
|
|||||||
@@ -62,8 +62,7 @@ function selectMantleModel(sdk: MantleSDK, modelID: string) {
|
|||||||
export const AmazonBedrockPlugin = define({
|
export const AmazonBedrockPlugin = define({
|
||||||
id: "amazon-bedrock",
|
id: "amazon-bedrock",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.catalog.transform(
|
yield* ctx.catalog.transform((evt) => {
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
if (item.provider.api.type !== "aisdk") continue
|
if (item.provider.api.type !== "aisdk") continue
|
||||||
if (item.provider.api.package !== "@ai-sdk/amazon-bedrock") continue
|
if (item.provider.api.package !== "@ai-sdk/amazon-bedrock") continue
|
||||||
@@ -76,8 +75,7 @@ export const AmazonBedrockPlugin = define({
|
|||||||
delete provider.request.body.endpoint
|
delete provider.request.body.endpoint
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
)
|
|
||||||
yield* ctx.aisdk.sdk(
|
yield* ctx.aisdk.sdk(
|
||||||
Effect.fn(function* (evt) {
|
Effect.fn(function* (evt) {
|
||||||
if (!["@ai-sdk/amazon-bedrock", "@ai-sdk/amazon-bedrock/mantle"].includes(evt.package)) return
|
if (!["@ai-sdk/amazon-bedrock", "@ai-sdk/amazon-bedrock/mantle"].includes(evt.package)) return
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import { define } from "../internal"
|
|||||||
export const AnthropicPlugin = define({
|
export const AnthropicPlugin = define({
|
||||||
id: "anthropic",
|
id: "anthropic",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.catalog.transform(
|
yield* ctx.catalog.transform((evt) => {
|
||||||
Effect.fn(function* (evt) {
|
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
if (item.provider.api.type !== "aisdk") continue
|
if (item.provider.api.type !== "aisdk") continue
|
||||||
if (item.provider.api.package !== "@ai-sdk/anthropic") continue
|
if (item.provider.api.package !== "@ai-sdk/anthropic") continue
|
||||||
@@ -14,8 +13,7 @@ export const AnthropicPlugin = define({
|
|||||||
"interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14"
|
"interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
)
|
|
||||||
yield* ctx.aisdk.sdk(
|
yield* ctx.aisdk.sdk(
|
||||||
Effect.fn(function* (evt) {
|
Effect.fn(function* (evt) {
|
||||||
if (evt.package !== "@ai-sdk/anthropic") return
|
if (evt.package !== "@ai-sdk/anthropic") return
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user