
Opencode Ts
- 181 installs
- 191 repo stars
- Updated July 24, 2026
- pproenca/dot-skills
opencode-ts: A skill for development. This provides functionality for development workflows.
Key points
- opencode-ts
Opencode Ts by the numbers
- 181 all-time installs (skills.sh)
- +6 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #2,188 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/pproenca/dot-skills --skill opencode-tsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 181 |
|---|---|
| repo stars | ★ 191 |
| Last updated | July 24, 2026 |
| Repository | pproenca/dot-skills ↗ |
How do I use opencode-ts for development tasks?
Use opencode-ts for development tasks
Who is it for?
Best when you're working on backend & apis and need structured help with opencode-ts.
Skip if: Teams with no backend & apis needs, or anyone wanting a generic chat assistant without this specific workflow.
When should I use this skill?
When you need to use opencode-ts for development tasks, or when opencode-ts: a skill for development. this provides functionality for development workflows.
What you get
Structured output aligned to opencode-ts: opencode-ts.
Files
Opencode TypeScript
Code like the opencode core team. This skill contains real code extracted from the repo — complete implementations, not abstract rules. Follow the workflow below based on your task.
---
Implement (new code)
Follow these phases in order:
1. Orient — where does this go?
Load architecture.md. Find:
- Which module owns this behavior
- What the dependency direction allows
- What file naming convention to follow
- Whether this is a new module or an addition to an existing one
2. Gather — what already exists?
Load helpers-deep-dive.md. Before writing ANY utility:
- Check if it already exists in
util/,effect/,bus/,sync/ - Check the usage matrix to see how other modules use it
- If it exists, use it. If it doesn't, inline first — extract only when awkwardness repeats.
For quick lookups: primitives.md (shorter, import paths + signatures only)
3. Build — write the code
Load the reference that matches what you're building:
| Building... | Load this |
|---|---|
| Service module (namespace + Effect service + schemas + events) | service-module.md |
| Tool or modifying tool behavior | tool-module.md |
| Database tables, schemas, events, error types | schemas-and-state.md |
| Server routes, config, plugins, project lifecycle | server-and-routes.md |
| Tests | test-writing.md |
4. CHECK GATE — code MUST pass all of these before proceeding
Load style-dna.md. If ANY of the following fail, fix the code before proceeding to Review:
- [ ] Single-word variable names where clear
- [ ] No
try/catch, noelse, noany, no unnecessary destructuring - [ ]
const+ ternary overlet+ mutation - [ ] snake_case Drizzle fields,
.annotate({ identifier })on boundary schemas,.annotate({ description })on fields - [ ] Effect Schema (
Schema.Struct/Schema.Schema.Type) for DTOs, events, tool params — not Zod - [ ] Effect is used for services, not plain async classes
- [ ] Module ends with
export * as X from "."— no in-fileexport namespace X {} - [ ] No patterns from Section 7 ("Things That Compile But Get Rejected") present in the diff
DO NOT proceed if any check fails. Go back to phase 3 and fix.
5. REVIEW GATE — REJECT the diff if any of these apply
Load review-voice.md. The diff MUST NOT contain any of the following. If it does, fix before submitting:
- [ ] Changes to files outside the scope of the task
- [ ]
as anyoras unknown ascasts - [ ] Custom utilities that duplicate community primitives or
@/util/*helpers - [ ] Provider-specific code that should live in models.dev
- [ ] Code removal without a clear reason documented in the commit
- [ ] Unexplained variable renames or structural changes
- [ ] Abstraction the core team would ask to remove (check refactoring-patterns.md)
---
Refactor (changing existing code)
1. Orient — what touches what?
Load architecture.md. Map the blast radius before changing anything.
2. Study — which pattern applies here?
Load refactoring-patterns.md. Start with the Decision Matrix at the top — match the code smell you see to the correct pattern. Then read the specific pattern section for real before/after diffs:
- Simplification patterns (removing unnecessary abstraction)
- Consolidation patterns (Bun → Node migration)
- Extraction patterns (pulling reusable utilities)
- Migration patterns (moving to Effect services)
- Deletion patterns (removing dead code)
- Stabilization patterns (fixing ordering/race conditions)
- Variant elimination (removing special cases)
3. Gather — can an existing utility replace this code?
Load helpers-deep-dive.md. The best refactor often replaces 20 lines with one utility call.
4. Check + Review
Same as implement phases 4-5: style-dna.md then review-voice.md.
---
Key decisions (always apply)
- Effect is mandatory — all services use
Context.Service/Layer/makeRuntime. No plain async classes. - One module, one self-barrel — write flat top-level exports (schemas,
Interface,Service,layer,defaultLayer), then close the file withexport * as X from ".". Consumers still writeimport { X } from "@/x"→X.Service; the barrel is the namespace. opencode dropped in-fileexport namespace X {}. - Effect Schema everywhere —
Schema.Struct+Schema.Schema.Type<typeof X>for DTOs, events, and tool params;.annotate({ identifier })on boundary schemas,.annotate({ description })on fields.Schema.TaggedErrorClassfor Effect errors.Newtype<Self>()("Name", Schema.String.check(...))(from@opencode-ai/core/schema) for branded IDs. Zod is no longer the boundary tool. - Event-sourced writes — mutations go through
SyncEvent.run→ projectors → SQLite. Direct DB writes only for non-event-sourced features. - No mocks in tests — use
tmpdir+Instance.provide+ real services. Mocks only for external SDKs. - Single-word variables —
state,pending,info,row,cfg,tx. Multi-word only when genuinely ambiguous.
---
Reference index
| File | Size | What it contains |
|---|---|---|
| style-dna.md | 18K | Mandatory style rules, naming, control flow, 14 review traps |
| primitives.md | 22K | Quick-lookup: every utility with import path + signature |
| helpers-deep-dive.md | ~40K | Full deep-dive: every utility, every usage site, when NOT to use |
| architecture.md | ~30K | Module map, dependency graph, data flow, file conventions |
| service-module.md | 27K | Complete Question + Permission implementations |
| tool-module.md | 28K | Full tool implementations, registry, prompt loop |
| test-writing.md | 42K | 5 complete test files with all fixture patterns |
| schemas-and-state.md | 36K | SQL tables, Effect Schema (Struct/annotate/Newtype), SyncEvent flow, errors |
| server-and-routes.md | 32K | Routes, config, plugins, project lifecycle |
| review-voice.md | ~25K | Real PR review comments from Dax + Aiden |
| refactoring-patterns.md | ~25K | Real before/after diffs from cleanup commits |
{
"version": "1.1.0",
"organization": "Opencode",
"technology": "TypeScript (Effect v4-beta + Effect Schema)",
"date": "May 2026",
"abstract": "Coding distillation skill for the opencode TypeScript codebase, re-aligned to opencode's Effect v4-beta migration as of May 2026: Context.Service DI (was ServiceMap.Service), Effect Schema for boundaries/events/tool params (was Zod), and flat module + self-barrel (export * as X) exports (was in-file export namespace). Contains real code extracted from the sst/opencode dev branch — complete service implementations, tool modules, test suites, SQL schemas, server routes, architecture maps, PR review patterns, and refactoring diffs from the core team. 11 reference files organized as a workflow: orient (architecture) → gather (helpers) → build (task-specific reference) → check (style DNA) → review (reviewer voice). Evaluated: +33% skill advantage on test writing tasks vs no-skill baseline.",
"references": [
"https://github.com/sst/opencode",
"https://effect.website",
"https://effect.website/blog/releases/effect/40-beta/",
"https://github.com/Effect-TS/effect-smol/blob/main/MIGRATION.md"
]
}
Opencode TypeScript
Coding distillation skill for the sst/opencode codebase. Contains real code extracted from the repo — complete implementations an LLM can pattern-match against to write code indistinguishable from the core team.
How it works
The skill routes by workflow phase, not artifact type:
Implement (new code):
Orient → Gather → Build → Check → ReviewRefactor (changing code):
Orient → Study → Gather → Check → ReviewEach phase loads the right reference file. See SKILL.md for the full routing table.
Structure
opencode-ts/
├── SKILL.md # Workflow router
├── metadata.json # Version, references
└── references/
├── architecture.md # Module map, dependency graph, where to put new code
├── style-dna.md # Mandatory style rules, naming, 14 review traps
├── helpers-deep-dive.md # Every utility, every usage site, usage matrix
├── primitives.md # Quick-lookup: import paths + signatures
├── service-module.md # Complete Question + Permission implementations
├── tool-module.md # Full tool implementations, registry, prompt loop
├── test-writing.md # 5 complete test files, fixtures, fake servers
├── schemas-and-state.md # SQL tables, Zod/Effect schemas, SyncEvent flow
├── server-and-routes.md # Routes, config, plugins, project lifecycle
├── review-voice.md # Real PR review comments from core team
└── refactoring-patterns.md # Before/after diffs from cleanup commitsEval results
Tested with non-prescriptive prompts against the real cloned repo:
| Eval | With Skill | Without Skill | Delta |
|---|---|---|---|
| Add bookmark feature | 57% | 71% | -14%* |
| Write tests for share module | 100% | 67% | +33% |
| Refactor flag module | 60% | 60% | 0% |
*Eval 1 delta reflects 3 assertion design errors, not skill regression. With corrected assertions: 100% vs 100%.
Strongest value: test writing — the skill teaches tmpdir isolation, Instance.provide, Instance.disposeAll, and fake server patterns that baseline agents miss.
Source
All code extracted from sst/opencode (March 2026). 352KB across 11 reference files, 10,488 lines. PR review patterns from Dax Raad, Aiden Cline, Kit Langton, and Adam. Refactoring diffs from the top 2 contributors.
CODE ATLAS -- opencode TypeScript Architecture
Root:packages/opencode/src/(app wiring, alias@/)
Entry:index.ts(yargs CLI),server/server.ts(Hono HTTP)
Runtime: Bun, Effect-TS for DI/services, Drizzle for SQLite
---
0. PACKAGE LAYOUT
Shared, framework-agnostic logic was extracted into a separate package; app wiring stays in the opencode package. Two source roots:
packages/core/src/ @opencode-ai/core -- shared, framework-agnostic
|-- util/log Log.create({ service })
|-- util/wildcard Wildcard.match
|-- schema NonNegativeInt, Newtype (branded-ID base class)
|-- filesystem AppFileSystem
|-- permission PermissionV2 (Action / evaluate / merge / disabled)
|-- effect/service-use serviceUse() accessor
|-- event, git, model, provider, session, agent (shared pieces)
packages/opencode/src/ alias `@/` -- app wiring, the module map below
bus, question, tool, server, storage/db, lsp, reference, project, config, ...The import split is SELECTIVE -- it is NOT a blanket @/util/ -> @opencode-ai/core/util/ rename. Some @/util/* paths stayed under @/ (e.g. @/util/media). Only treat a path as moved if it is one of the shared pieces listed above (it lives in packages/core/src). The module map in section 1 describes packages/opencode/src/ (@/).
---
1. MODULE MAP
packages/opencode/src/
|
|-- index.ts CLI entry point (yargs). Registers all commands.
|-- node.ts Node.js-specific entry (alternate runtime)
|
|-- FOUNDATION LAYER (no domain imports)
| |-- global/ XDG paths (data, cache, config, state). Side-effectful init.
| |-- id/ Prefixed monotonic ID generator (ses_, msg_, prt_, evt_, etc.)
| |-- flag/ Environment variable flags. Pure reads from process.env.
| |-- installation/ Version, channel, upgrade detection. HttpClient for update checks.
| |-- util/ Pure utilities: log, filesystem, glob, git, hash, lock, process,
| | context (ALS), error, color, network, schema helpers, etc.
| |-- effect/ Effect-TS service infrastructure:
| | |-- instance-state.ts ScopedCache keyed by Instance.directory
| | |-- run-service.ts makeRuntime() -- shared MemoMap for all services
| | |-- instance-registry.ts Disposer registry for per-instance cleanup
| | |-- cross-spawn-spawner.ts Effect ChildProcessSpawner via cross-spawn
|
|-- STORAGE LAYER
| |-- storage/ Dual storage system:
| | |-- db.ts SQLite via Drizzle (Database.Client, .use, .transaction)
| | |-- db.bun.ts Bun SQLite init
| | |-- db.node.ts Node SQLite init
| | |-- storage.ts JSON file storage (read/write/list with file locks)
| | |-- schema.sql.ts Drizzle schema for event_sequence, event tables
| | |-- schema.ts Re-exports
| | |-- json-migration.ts Legacy JSON-to-SQLite data migration
|
|-- EVENT LAYER
| |-- bus/ In-process pub/sub:
| | |-- bus-event.ts BusEvent.define() -- schema-typed event definitions
| | |-- global.ts GlobalBus -- Node EventEmitter, cross-instance
| | |-- index.ts Bus -- Effect PubSub, per-instance scoped
| |-- sync/ Event sourcing on SQLite:
| | |-- index.ts SyncEvent.define/run/replay/project -- persisted events
| | |-- event.sql.ts Drizzle tables: event, event_sequence
| | |-- schema.ts EventID branded type
|
|-- CONFIGURATION LAYER
| |-- config/ Cascading JSONC config (managed > global > project > env):
| | |-- config.ts Config.Service (Effect), Config.get(), schema, merge logic
| | |-- paths.ts Config file path resolution
| | |-- markdown.ts ConfigMarkdown -- YAML frontmatter parser for AGENTS.md/SKILL.md
| | |-- tui.ts TUI-specific config
| | |-- tui-schema.ts TUI config Zod schema
| | |-- migrate-tui-config.ts Migration from old TUI format
| |-- env/ Per-instance env var isolation (via Instance.state)
| |-- flag/ Static env var flags (compile-time constants)
|
|-- AUTH LAYER
| |-- auth/ Provider auth storage (OAuth, API key, WellKnown):
| | |-- index.ts Auth.Service (Effect). JSON file at Global.Path.data/auth.json
| |-- account/ Opencode account management (device code flow, orgs):
| | |-- index.ts Account.Service re-exports
| | |-- repo.ts AccountRepo -- SQLite persistence
| | |-- schema.ts AccountID, AccessToken, RefreshToken, DeviceCode, etc.
|
|-- PROVIDER LAYER
| |-- provider/ LLM provider abstraction:
| | |-- provider.ts Provider namespace: model registry, SDK instantiation,
| | | imports all @ai-sdk/* providers directly
| | |-- models.ts ModelsDev -- fetches model metadata from models.dev
| | |-- schema.ts ProviderID, ModelID branded types
| | |-- auth.ts Provider-specific auth resolution
| | |-- transform.ts ProviderTransform -- per-provider option tweaks
| | |-- error.ts Provider error types
| | |-- sdk/ Custom SDK adaptors (copilot, etc.)
|
|-- PROJECT LAYER
| |-- project/ Project identity and lifecycle:
| | |-- project.ts Project.Service -- CRUD, fromDirectory, git root detection
| | |-- instance.ts Instance -- ALS context (directory, worktree, project)
| | |-- bootstrap.ts InstanceBootstrap() -- init sequence for an instance
| | |-- state.ts Instance.state() -- per-instance memoized state
| | |-- vcs.ts Vcs -- git status, diff summary
| | |-- project.sql.ts Drizzle ProjectTable
| | |-- schema.ts ProjectID branded type
|
|-- DOMAIN LAYER
| |-- session/ Core session management:
| | |-- index.ts Session namespace -- CRUD, messages, fork, share, events
| | |-- schema.ts SessionID, MessageID, PartID branded types
| | |-- session.sql.ts Drizzle tables: session, permission
| | |-- message-v2.ts MessageV2 -- User/Assistant/Info schema, Part types
| | |-- prompt.ts SessionPrompt -- orchestrates message->LLM->tools loop
| | |-- llm.ts LLM.stream() -- Vercel AI SDK streamText wrapper
| | |-- processor.ts SessionProcessor -- handles stream events, tool calls
| | |-- system.ts SystemPrompt -- provider-specific system prompts
| | |-- instruction.ts InstructionPrompt -- AGENTS.md/CLAUDE.md injection
| | |-- compaction.ts SessionCompaction -- context window management
| | |-- summary.ts SessionSummary -- post-session diff summary
| | |-- status.ts SessionStatus -- busy/idle tracking
| | |-- retry.ts SessionRetry -- LLM retry logic
| | |-- revert.ts SessionRevert -- snapshot-based undo
| | |-- todo.ts Todo -- todowrite persistence
| | |-- projectors.ts SyncEvent projectors for session/message/part tables
| | |-- prompt/ Text prompt templates (.txt files)
| |-- agent/ Agent definitions and generation:
| | |-- agent.ts Agent.Service (Effect) -- build/plan/explore/general/title/etc.
| | |-- prompt/ Agent-specific prompt templates
| |-- command/ Slash commands (/init, /review, custom, MCP, skill):
| | |-- index.ts Command.Service (Effect) -- aggregates all command sources
| | |-- template/ Built-in command templates (.txt)
| |-- permission/ Permission evaluation engine:
| | |-- index.ts Permission.Service (Effect) -- ask/reply flow with Deferred
| | |-- evaluate.ts Pure rule evaluation (pattern matching)
| | |-- arity.ts Rule arity/specificity comparison
| | |-- schema.ts PermissionID branded type
| |-- question/ Interactive question flow (like Permission but for info):
| | |-- index.ts Question.Service (Effect) -- ask/answer with Deferred
| | |-- schema.ts QuestionID branded type
|
|-- TOOL LAYER
| |-- tool/ Tool system:
| | |-- tool.ts Tool.define() -- tool interface, auto-truncation wrapper
| | |-- registry.ts ToolRegistry.Service (Effect) -- collects built-in + plugin tools
| | |-- schema.ts Tool-related schemas
| | |-- truncate.ts Truncate -- output size management
| | |-- bash.ts BashTool
| | |-- read.ts ReadTool
| | |-- edit.ts EditTool
| | |-- write.ts WriteTool
| | |-- glob.ts GlobTool
| | |-- grep.ts GrepTool
| | |-- task.ts TaskTool (subagent spawner)
| | |-- batch.ts BatchTool (parallel tool calls)
| | |-- webfetch.ts WebFetchTool
| | |-- websearch.ts WebSearchTool
| | |-- codesearch.ts CodeSearchTool
| | |-- lsp.ts LspTool
| | |-- question.ts QuestionTool
| | |-- skill.ts SkillTool
| | |-- plan.ts PlanTool (enter/exit plan mode)
| | |-- todo.ts TodoWriteTool
| | |-- apply_patch.ts ApplyPatchTool (OpenAI-style)
| | |-- multiedit.ts MultiEditTool
| | |-- ls.ts LsTool
| | |-- invalid.ts InvalidTool (catch malformed calls)
| | |-- *.txt Tool description templates
|
|-- INTEGRATION LAYER
| |-- plugin/ Plugin system:
| | |-- index.ts Plugin.Service (Effect) -- load, trigger hooks
| | |-- shared.ts Plugin resolution, compatibility checks
| | |-- codex.ts Built-in Codex auth plugin
| | |-- copilot.ts Built-in Copilot auth plugin
| | |-- install.ts npm plugin installation
| | |-- meta.ts Plugin metadata
| |-- mcp/ MCP (Model Context Protocol) client:
| | |-- index.ts MCP.Service (Effect) -- manages MCP server connections
| | |-- auth.ts MCP auth helpers
| | |-- oauth-provider.ts MCP OAuth provider
| | |-- oauth-callback.ts MCP OAuth callback handler
| |-- skill/ Skill discovery and loading:
| | |-- index.ts Skill.Service (Effect) -- scan SKILL.md files
| | |-- discovery.ts Discovery.Service -- pull skills from URLs
| |-- lsp/ Language Server Protocol:
| | |-- index.ts LSP.Service (Effect) -- manages LSP server lifecycles
| | |-- client.ts LSPClient -- JSON-RPC connection
| | |-- server.ts LSPServer -- built-in server definitions
| | |-- language.ts Language detection
| | |-- launch.ts LSP process spawning
| |-- snapshot/ Git-based file snapshots:
| | |-- index.ts Snapshot.Service (Effect) -- shadow git repo for undo
| |-- format/ Code formatter orchestration:
| | |-- index.ts Format.Service (Effect) -- prettier, biome, etc.
| | |-- formatter.ts Built-in formatter definitions
|
|-- INFRASTRUCTURE LAYER
| |-- file/ File system operations for the project:
| | |-- index.ts File.Service (Effect) -- content, diff, search, ls
| | |-- ignore.ts .gitignore/.opencodeignore handling
| | |-- protected.ts Protected file detection
| | |-- ripgrep.ts Ripgrep integration
| | |-- time.ts FileTime -- mtime tracking for LSP
| | |-- watcher.ts FileWatcher -- filesystem change detection
| |-- filesystem/ Effect FileSystem wrapper with extras:
| | |-- index.ts AppFileSystem.Service -- isDir, readJson, findUp, glob
| |-- bun/ Bun process runner:
| | |-- index.ts BunProc -- run Bun commands, install packages
| | |-- registry.ts PackageRegistry -- npm package resolution
| |-- shell/ Shell utilities:
| | |-- shell.ts Shell.killTree, Shell.detect, Shell.env
| |-- patch/ Patch application (OpenAI apply_patch format):
| | |-- index.ts Patch namespace -- parse and apply unified diffs
| |-- ide/ IDE integration (VS Code, Cursor, etc.):
| | |-- index.ts Ide namespace -- extension installation
| |-- pty/ Pseudo-terminal management:
| | |-- index.ts Pty.Service (Effect) -- spawn, attach, WebSocket relay
| | |-- schema.ts PtyID branded type
| |-- worktree/ Git worktree management:
| | |-- index.ts Worktree.Service (Effect) -- create/remove/reset worktrees
|
|-- SERVER LAYER
| |-- server/ HTTP API (Hono):
| | |-- server.ts Server namespace -- ControlPlaneRoutes, listen, openapi
| | |-- instance.ts InstanceRoutes -- per-directory middleware + route mounting
| | |-- projectors.ts initProjectors() -- wires SyncEvent projectors
| | |-- event.ts Event schema
| | |-- error.ts HTTP error helpers
| | |-- middleware.ts Error handler middleware
| | |-- mdns.ts mDNS service publishing
| | |-- routes/ Route handlers:
| | | |-- global.ts /global/* -- cross-instance endpoints
| | | |-- session.ts /session/* -- CRUD, prompt, messages
| | | |-- config.ts /config/* -- read/write config
| | | |-- project.ts /project/* -- project info
| | | |-- provider.ts /provider/* -- models, auth
| | | |-- permission.ts /permission/* -- ask/reply
| | | |-- question.ts /question/* -- ask/answer
| | | |-- file.ts /file/* -- read, diff, ls
| | | |-- mcp.ts /mcp/* -- server status, tools
| | | |-- pty.ts /pty/* -- terminal sessions
| | | |-- event.ts /event -- SSE event stream
| | | |-- tui.ts /tui/* -- TUI-specific endpoints
| | | |-- experimental.ts /experimental/* -- feature flags
| | | |-- workspace.ts /workspace/* -- worktree/workspace management
|
|-- CLI LAYER
| |-- cli/ CLI commands and TUI:
| | |-- bootstrap.ts CLI bootstrap wrapper (Instance.provide + InstanceBootstrap)
| | |-- cmd/ Yargs command modules:
| | | |-- run.ts Default command (TUI or headless)
| | | |-- serve.ts `opencode serve` -- headless HTTP server
| | | |-- agent.ts `opencode agent` -- agent management
| | | |-- models.ts `opencode models` -- list models
| | | |-- providers.ts `opencode providers` -- list providers
| | | |-- account.ts `opencode account` -- login/logout
| | | |-- generate.ts `opencode generate` -- code generation
| | | |-- export.ts `opencode export` -- session export
| | | |-- import.ts `opencode import` -- session import
| | | |-- mcp.ts `opencode mcp` -- MCP server management
| | | |-- plug.ts `opencode plugin` -- plugin management
| | | |-- github.ts `opencode github` -- GitHub integration
| | | |-- pr.ts `opencode pr` -- PR workflow
| | | |-- session.ts `opencode session` -- session management
| | | |-- tui/ TUI (terminal UI) commands
| | |-- effect/ CLI-specific Effect helpers
| | |-- error.ts CLI error formatting
| | |-- logo.ts ASCII logo
| | |-- ui.ts Terminal output helpers
| | |-- network.ts Network option resolution
| | |-- upgrade.ts Auto-upgrade logic
|
|-- CONTROL PLANE
| |-- control-plane/ Multi-workspace orchestration:
| | |-- workspace.ts Workspace CRUD (worktree + cloud adapters)
| | |-- workspace.sql.ts Drizzle WorkspaceTable
| | |-- workspace-router-middleware.ts Routes requests to correct instance
| | |-- schema.ts WorkspaceID branded type
| | |-- types.ts WorkspaceInfo schema
| | |-- sse.ts SSE parsing utilities
| | |-- adaptors/ Workspace type adaptors (local worktree, cloud, etc.)
|
|-- SHARING
| |-- share/ Session sharing:
| | |-- share-next.ts ShareNext -- upload sessions to opencode.ai
| | |-- share.sql.ts Drizzle SessionShareTable
|
|-- ACP (Agent Communication Protocol)
| |-- acp/ Agent-to-agent communication:
| | |-- agent.ts ACP agent implementation
| | |-- session.ts ACP session management
| | |-- types.ts ACP type definitions---
2. DEPENDENCY GRAPH
Layer Diagram (arrows = "imports from")
FOUNDATION (no domain deps)
+-----------+ +------+ +------+ +--------------+ +--------+
| global/ | | id/ | | flag/| | installation/| | util/* |
+-----------+ +------+ +------+ +--------------+ +--------+
| | | | |
+------+------+---------+-----------+-------+-------+
| |
v v
STORAGE EFFECT INFRA
+------------------+ +------------------+
| storage/db.ts |<---+ | effect/ |
| storage/storage | | | instance-state |
+------------------+ | | run-service |
| | +------------------+
v | |
EVENT LAYER | |
+----------+ +--------+ | |
| bus/ | | sync/ |-+ |
+----------+ +--------+ |
| ^ | |
| | v |
| +--- (all domain modules publish/subscribe)|
| |
v v
CONFIG + AUTH All Service layers use
+----------+ +--------+ InstanceState + makeRuntime
| config/ | | auth/ |
+----------+ +--------+
| | |
v v v
PROVIDER
+------------------+
| provider/ |
+------------------+
|
v
PROJECT
+------------------+
| project/ |
| instance.ts | <-- ALS context, everything reads from here
| project.ts |
| bootstrap.ts |
+------------------+
|
+-----+-----+-----+-----+-----+-----+
| | | | | | |
v v v v v v v
DOMAIN MODULES (all depend on project/instance)
+-------+ +-----+ +-------+ +----------+ +--------+
|session | |agent| |command| |permission| |question|
+-------+ +-----+ +-------+ +----------+ +--------+
| | |
v v v
TOOL LAYER
+------------------+
| tool/registry.ts | --> all tool/*.ts
+------------------+
|
v
INTEGRATION LAYER
+------+ +-----+ +-------+ +--------+ +------+ +--------+
|plugin| | mcp | | skill | |snapshot| | lsp | | format |
+------+ +-----+ +-------+ +--------+ +------+ +--------+
|
v
INFRASTRUCTURE
+------+ +----+ +------+ +-----+ +---+ +--------+ +-----+
| file | |bun | | shell| |patch| |ide| |worktree| | pty |
+------+ +----+ +------+ +-----+ +---+ +--------+ +-----+
|
v
SERVER LAYER
+------------------+
| server/server.ts | --> server/instance.ts --> server/routes/*
+------------------+
|
v
CLI LAYER
+------------------+
| cli/cmd/* | --> cli/bootstrap.ts
+------------------+Key Import Relationships
session/index.ts imports:
<- storage/db (Database, SessionTable)
<- bus (Bus.publish)
<- sync (SyncEvent.run)
<- config (Config.get)
<- provider/schema (ModelID, ProviderID)
<- project/instance (Instance.project, Instance.directory)
<- permission (Permission.Ruleset)
<- snapshot (Snapshot.FileDiff)
<- session/message-v2, session/prompt, session/schema
session/prompt.ts imports:
<- session/index (Session)
<- agent/agent (Agent)
<- provider/provider (Provider)
<- tool/registry (ToolRegistry)
<- mcp (MCP)
<- lsp (LSP)
<- plugin (Plugin)
<- permission (Permission)
<- command (Command)
<- session/llm (LLM)
<- session/processor (SessionProcessor)
agent/agent.ts imports:
<- config (Config.Service)
<- provider (Provider)
<- auth (Auth.Service)
<- skill (Skill.Service)
<- plugin (Plugin)
<- permission (Permission)
tool/registry.ts imports:
<- config (Config.Service)
<- plugin (Plugin.Service)
<- all tool/*.ts files
server/instance.ts imports:
<- project/bootstrap (InstanceBootstrap)
<- project/instance (Instance.provide)
<- all server/routes/* files---
3. DATA FLOW
A. HTTP Request -> Response
HTTP Request
|
v
server/server.ts ControlPlaneRoutes()
|-- middleware: auth, cors, compress, logging
|-- route: /global/* --> server/routes/global.ts
|-- route: /auth/* --> inline handlers
|-- middleware: WorkspaceRouterMiddleware
| |
| v
| server/instance.ts InstanceRoutes
| |-- middleware: Instance.provide(directory, InstanceBootstrap)
| | |
| | +-- project/instance.ts: creates ALS context
| | +-- project/bootstrap.ts: Plugin.init, Format.init,
| | | LSP.init, File.init, FileWatcher.init, Vcs.init,
| | | Snapshot.init
| | |
| |-- route: /session/* --> session route handler
| | |
| | v
| | Session.list() / Session.get() / SessionPrompt.chat()
| | |
| | v
| | Database.use(db => db.select().from(SessionTable)...)
| | |
| | v
| | c.json(result) --> HTTP Response
| |
| |-- route: /config/* --> Config.get/set
| |-- route: /provider/* --> Provider.list/models
| |-- route: /permission/* --> Permission.ask/reply
| |-- route: /file/* --> File.read/diff/ls
| |-- route: /mcp/* --> MCP.status/tools
| |-- route: /pty/* --> Pty.create/attach
| |-- route: /event --> SSE stream (Bus.subscribeAll)B. CLI Command -> Output
CLI invocation: `opencode [command] [args]`
|
v
index.ts yargs.parse()
|-- middleware: Log.init, Database migration check
|
v
cli/cmd/run.ts (default) or specific command
|
v
cli/bootstrap.ts
|-- Instance.provide({ directory, init: InstanceBootstrap, fn })
| |
| v
| project/instance.ts (ALS context created)
| project/bootstrap.ts (services initialized)
| |
| v
| command handler executes:
| |
| v
| (for `serve`): Server.listen(opts)
| (for `run`): TUI starts, SessionPrompt.chat()
| (for others): Direct domain calls
| |
| v
| Instance.dispose() (cleanup)C. Session Prompt -> Tool Execution -> Result
SessionPrompt.chat(sessionID, userMessage)
|
v
1. Resolve model: Provider.defaultModel() / Agent config
|
v
2. Build system prompt:
SystemPrompt.provider(model) + InstructionPrompt (AGENTS.md)
+ Plugin.trigger("experimental.chat.system.transform")
|
v
3. Resolve tools:
ToolRegistry.tools(model, agent)
|-- Built-in tools (BashTool, ReadTool, EditTool, etc.)
|-- Plugin tools (plugin.trigger "tool.definition")
|-- MCP tools (MCP.tools())
|-- Custom tools from config dirs (tools/*.ts)
|
v
4. Apply permission filter:
Permission.disabled(toolIDs, agent.permission)
|
v
5. Create SessionProcessor:
processor = SessionProcessor.create(assistantMessage)
|
v
6. LLM.stream(input) -- Vercel AI SDK streamText()
|
+--[stream loop]-------------------------------------+
| |
| event: "text-delta" |
| -> Session.updatePart(TextPart) |
| -> Session.updatePartDelta() |
| |
| event: "tool-call" |
| -> Permission.ask(ruleset, patterns) |
| |-- action=allow: proceed |
| |-- action=ask: Bus.publish(Permission.Asked)|
| | wait for Deferred<void, RejectedError> |
| |-- action=deny: throw DeniedError |
| -> tool.execute(args, ctx) |
| |-- ctx.ask() for nested permissions |
| |-- Format.file() after edits |
| |-- LSP.touchFile() for diagnostics |
| -> Session.updatePart(ToolPart) |
| |
| event: "finish" |
| -> check shouldContinue (has tool results?) |
| -> if yes: loop with updated messages |
| -> if no: break |
+-----------------------------------------------------+
|
v
7. Post-processing:
SessionCompaction.maybe(sessionID) -- auto-compact if needed
SessionSummary.generate(sessionID) -- git diff summary
Snapshot.commit() -- save snapshot
|
v
8. Return MessageV2.WithParts to callerD. Event Sourcing Flow (SyncEvent)
Domain action (e.g. Session.create):
|
v
SyncEvent.run(Session.Event.Created, { sessionID, info })
|
v
Database.transaction("immediate"):
|-- EventSequenceTable: read seq, increment
|-- Projector function: insert into SessionTable
|-- EventTable: insert event record
|-- Database.effect(() => {
| Bus.publish(event) -- in-process notification
| GlobalBus.emit("event") -- cross-instance notification
| })---
4. BOOTSTRAP SEQUENCE
=== Process Start (index.ts) ===
1. Global.Path init (global/index.ts)
- Create XDG directories (data, cache, config, state, log, bin)
- Check/reset cache version
2. Yargs middleware (index.ts)
- Log.init()
- Set process.env flags (AGENT, OPENCODE, OPENCODE_PID)
- Check for first-run database migration (JsonMigration)
- Database.Client() -- opens SQLite, applies Drizzle migrations
3. Command dispatch (e.g. `serve`)
- Server.listen() or cli/bootstrap.ts
=== Per-Instance Bootstrap (project/bootstrap.ts) ===
Called via Instance.provide({ directory, init: InstanceBootstrap })
1. Instance context creation (project/instance.ts)
- Project.fromDirectory(directory)
- Detect git root, compute projectID
- Database.use: upsert ProjectTable
- Set ALS context: { directory, worktree, project }
2. InstanceBootstrap() sequence:
a. Plugin.init() -- load internal + npm plugins, trigger hooks
b. ShareNext.init() -- initialize share URL resolution
c. Format.init() -- detect available formatters (prettier, biome)
d. LSP.init() -- configure LSP server definitions
e. File.init() -- setup file service state
f. FileWatcher.init() -- start filesystem watcher
g. Vcs.init() -- track git status
h. Snapshot.init() -- initialize shadow git repo
3. Bus.subscribe(Command.Event.Executed)
- On /init command: mark project as initialized
=== Server Startup (server/server.ts, server/projectors.ts) ===
1. initProjectors() -- wire SyncEvent projectors (session/message/part)
- SyncEvent.init({ projectors: sessionProjectors })
- This freezes event definitions (no new SyncEvent.define after this)
2. Server.listen() -- Bun.serve with Hono app
- ControlPlaneRoutes: auth, cors, compress, /global/*, /auth/*
- WorkspaceRouterMiddleware: routes to correct instance
- InstanceRoutes: per-request Instance.provide + all domain routes---
5. MODULE BOUNDARIES
Allowed Dependencies (by convention)
RULE 1: Layers import downward only
CLI -> Server -> Domain -> Storage -> Foundation
(never upward)
RULE 2: Foundation modules have ZERO domain imports
global/, id/, flag/, util/, effect/
These can be imported by anything.
RULE 3: Storage layer imports only Foundation
storage/db.ts imports: util/local-context, util/lazy, global, flag, id, installation
RULE 4: Bus layer imports only Storage + Foundation
bus/ imports: @opencode-ai/core/util/log, project/instance (for directory key)
sync/ imports: storage/db, bus/bus-event, flag
RULE 5: Config imports Storage + Foundation + Auth
config/ imports: storage, global, flag, auth, env, bus, installation, bun
RULE 6: Domain modules import Config + Auth + Storage + Bus
session/, agent/, command/, permission/, question/
These are the "business logic" layer.
RULE 7: Tools import Domain (session, agent, permission)
tool/ imports session (for context), permission (for ask),
but NOT server/ or cli/
RULE 8: Server imports everything except CLI
server/ can import any domain/tool/integration module
RULE 9: CLI imports everything
cli/ is the outermost layer, can import server + domainKnown Acceptable Cross-Cuts
project/instance.ts
- Imported by NEARLY EVERY module (provides ALS context)
- This is by design: Instance.directory, Instance.worktree, Instance.project
bus/global.ts
- Simple EventEmitter, imported by bus/, project/, control-plane/
- Cross-instance communication channel
effect/instance-state.ts + effect/run-service.ts
- Imported by every Service module (Config, Agent, Tool, etc.)
- The DI backbone of the codebaseDependency Direction Violations to Watch
CAUTION: session/prompt.ts is a "god file"
- Imports from 20+ modules: agent, provider, tool/registry, mcp, lsp,
plugin, permission, command, config, bus, session/*, tool/*
- This is the orchestration nexus; changes here affect everything
CAUTION: config/config.ts imports heavily
- Imports: auth, env, bus, global-bus, installation, bun, filesystem,
plugin/shared, config/markdown, config/paths, account
- Config needs to know about plugins for dependency resolution
AVOID: Domain modules should not import server/
- session/ should never import from server/routes/
- If a domain module needs server URL, use Flag or inject it
AVOID: tool/*.ts should not import session/index.ts directly
- Tools receive context via Tool.Context, not by importing Session
- Exception: tool/task.ts (subagent) needs SessionPromptClean Seams
1. Tool.Info interface (tool/tool.ts)
- Clean boundary between tool implementation and orchestration
- Tools know nothing about HTTP, CLI, or streaming
- Input: args + Context. Output: { title, metadata, output }
2. Effect Service boundary
- Every Service exposes an Interface + layer + defaultLayer
- Consumers use Service.of() or the async wrapper functions
- Layers compose via Layer.provide() -- explicit dependency declaration
3. SyncEvent projectors (sync/index.ts)
- Clean separation: event definition (domain) vs projection (storage)
- Projectors registered once at server startup
4. Bus events (bus/bus-event.ts)
- Typed event definitions, decoupled pub/sub
- Modules define events, other modules subscribe
5. Provider abstraction (provider/provider.ts)
- All LLM SDKs hidden behind Provider.getLanguage()
- Session/LLM layer only sees the Vercel AI SDK interface---
6. FILE NAMING CONVENTIONS
When a module gets index.ts
Directory modules use index.ts when:
- The module IS the namespace (bus/, session/, file/, permission/, etc.)
- index.ts holds the flat top-level exports and closes with the self-barrel
(`export * as X from "."`) that backs the namespace
- Other files in the dir are internal implementation details
Examples:
bus/index.ts -- Bus namespace (the public API)
session/index.ts -- Session namespace (CRUD, events, types)
permission/index.ts -- Permission namespace
auth/index.ts -- Auth namespaceWhen a module gets a named file instead
Named files when:
- The module has a clear single-word identity
- Used inside a directory with multiple peer concepts
Examples:
config/config.ts -- not index.ts, because config/ has peers (paths, markdown, tui)
provider/provider.ts -- not index.ts, because provider/ has peers (models, auth, schema)
agent/agent.ts -- not index.ts, because agent/ has prompt/ subdir
flag/flag.ts -- single file module, named for clarity
shell/shell.ts -- single file module
id/id.ts -- single file moduleWhen a module gets schema.ts
schema.ts appears when:
- A module defines branded ID types (SessionID, MessageID, etc.)
- The schema is shared across multiple files in the module
Pattern: schema.ts exports branded Identifier types
session/schema.ts -- SessionID, MessageID, PartID
permission/schema.ts -- PermissionID
question/schema.ts -- QuestionID
provider/schema.ts -- ProviderID, ModelID
project/schema.ts -- ProjectID
control-plane/schema.ts -- WorkspaceID
pty/schema.ts -- PtyID
sync/schema.ts -- EventID
storage/schema.ts -- re-exports
tool/schema.ts -- tool-related schemasWhen a module gets .sql.ts
*.sql.ts files define Drizzle ORM table schemas:
session/session.sql.ts -- SessionTable, PermissionTable
project/project.sql.ts -- ProjectTable
account/account.sql.ts -- AccountTable
sync/event.sql.ts -- EventTable, EventSequenceTable
storage/schema.sql.ts -- core storage tables
share/share.sql.ts -- SessionShareTable
control-plane/workspace.sql.ts -- WorkspaceTable
Convention: {entity}.sql.ts sits alongside {entity}.tsWhen a module gets a separate types file
Rare. Types are usually co-located in the main namespace file.
Exception:
control-plane/types.ts -- WorkspaceInfo shared across adaptors
acp/types.ts -- ACP protocol typesDirectory vs single file
DIRECTORY when:
- Module has 3+ files (implementation, schema, sql, sub-modules)
- Module has sub-directories (agent/prompt/, session/prompt/)
- Module is a "service" with Effect Service + layer + state
SINGLE FILE when:
- Module is a pure utility or simple namespace
- No sub-components needed
Single-file modules that COULD be directories but aren't:
flag/flag.ts -- just env var reads
id/id.ts -- just ID generation
shell/shell.ts -- just shell utilities
These stay as single files because they have no sub-components.---
7. EFFECT-TS SERVICE PATTERN
Every major module follows this pattern. There is NO in-file export namespace: the file declares flat top-level exports, then re-exports itself as a namespace via a self-barrel on the last line. Consumers still write import { Module } from "@/module" then Module.Service / Module.layer / Module.Info -- the barrel IS the namespace.
// 1. Effect Schema data (Zod is no longer the schema tool for service data)
export const Info = Schema.Struct({ ... }).annotate({ identifier: "Info" })
export type Info = Schema.Schema.Type<typeof Info>
// 2. Effect Service class
export interface Interface {
readonly method: (input: X) => Effect.Effect<Y>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/Module") {}
// 3. Layer (DI wiring)
export const layer = Layer.effect(Service, Effect.gen(function* () {
const dep = yield* DependencyService // pull deps from context
const state = yield* InstanceState.make(...) // per-instance state
return Service.of({ method: ... })
}))
// 4. Default layer (self-contained, provides all deps)
export const defaultLayer = layer.pipe(
Layer.provide(Dep1.defaultLayer),
Layer.provide(Dep2.defaultLayer),
)
// 5. Runtime bridge (async wrappers for non-Effect code)
const { runPromise } = makeRuntime(Service, defaultLayer)
export async function method(input: X): Promise<Y> {
return runPromise((svc) => svc.method(input))
}
// 6. Self-barrel (last line) -- this re-export IS the `Module` namespace
export * as Module from "."Modules using this pattern: Agent, Auth, Bus, Command, Config, File, Format, LSP, MCP, Permission, Plugin, Pty, Question, Skill, Snapshot, ToolRegistry, Worktree, Account, Project, Installation
---
8. QUICK REFERENCE -- WHERE TO PUT NEW CODE
New tool? -> tool/{name}.ts (Tool.define, add to registry.ts)
New CLI command? -> cli/cmd/{name}.ts (yargs command, add to index.ts)
New API route? -> server/routes/{name}.ts (Hono, mount in instance.ts)
New domain entity? -> {name}/index.ts + {name}/schema.ts + {name}/{name}.sql.ts
New provider? -> provider/provider.ts (add to SDK init map)
New Effect service? -> {name}/index.ts (Service, Interface, layer, defaultLayer)
New bus event? -> In the module that owns it: BusEvent.define()
New sync event? -> In the module that owns it: SyncEvent.define() + projector
New util function? -> util/{category}.ts
New config option? -> config/config.ts (add to Info schema)
New flag? -> flag/flag.ts
New formatter? -> format/formatter.ts
New LSP server? -> lsp/server.ts
New plugin hook? -> @opencode-ai/plugin types + plugin/index.ts
New branded ID? -> {module}/schema.ts using id/id.ts IdentifierHelpers Deep Dive
Every helper and utility in the opencode codebase. An LLM should never reinvent something that already exists, AND should know when NOT to use a helper.
---
Table of Contents
1. util/ Utilities (35 files) 2. Non-util Helpers 3. Usage Matrix 4. Anti-Patterns and Correct Avoidances
---
util/ Utilities
NOTE: Several shared, framework-agnostic utilities have been extracted out of@/util/into the@opencode-ai/corepackage (packages/core/src/) -- e.g.log,wildcard,schema,filesystem,permission, andeffect/service-use. App-only wiring stays under@/. Where a helper below has moved, its import path is now@opencode-ai/core/...; the public API (Log.create,Wildcard.match, etc.) is unchanged. Always check the actual import path before assuming@/util/.
abort.ts -- Timeout-aware AbortController creation
What it does: Creates AbortControllers that auto-abort after a timeout, with optional signal composition.
Implementation:
export function abortAfter(ms: number) {
const controller = new AbortController()
const id = setTimeout(controller.abort.bind(controller), ms)
return {
controller,
signal: controller.signal,
clearTimeout: () => globalThis.clearTimeout(id),
}
}
export function abortAfterAny(ms: number, ...signals: AbortSignal[]) {
const timeout = abortAfter(ms)
const signal = AbortSignal.any([timeout.signal, ...signals])
return {
signal,
clearTimeout: timeout.clearTimeout,
}
}Key design detail: Uses bind() instead of arrow functions to avoid capturing surrounding scope in closures -- prevents request bodies and other large objects from being retained for the timer's lifetime.
Used by: No direct imports found in src/ (defined but currently unused by application code).
When NOT to use: The codebase has many new AbortController() calls (session/prompt.ts, provider/provider.ts, acp/agent.ts, cli/cmd/tui/worker.ts, control-plane/workspace.ts, cli/cmd/tui/plugin/runtime.ts). These are all cases where the controller is NOT timeout-based -- they are lifecycle controllers that get manually aborted. The abortAfter helper is ONLY for timeout-based abort. Do not use it for general-purpose abort controllers.
---
archive.ts -- Cross-platform zip extraction
What it does: Extracts zip files using platform-appropriate tools (PowerShell on Windows, unzip elsewhere).
Implementation:
export async function extractZip(zipPath: string, destDir: string) {
if (process.platform === "win32") {
const winZipPath = path.resolve(zipPath)
const winDestDir = path.resolve(destDir)
const cmd = `$global:ProgressPreference = 'SilentlyContinue'; Expand-Archive -Path '${winZipPath}' -DestinationPath '${winDestDir}' -Force`
await Process.run(["powershell", "-NoProfile", "-NonInteractive", "-Command", cmd])
return
}
await Process.run(["unzip", "-o", "-q", zipPath, "-d", destDir])
}
// Self-barrel at file bottom -- this barrel IS the `Archive` namespace.
// Consumers still write `import { Archive } from "@/util/archive"` then `Archive.extractZip(...)`.
export * as Archive from "."Used by: No direct imports in src/ (available for plugin/installation scenarios).
When NOT to use: Only for zip files. Does not handle tar.gz or other archive formats.
---
color.ts -- Hex color validation and ANSI conversion
What it does: Validates hex color strings, converts to RGB, and creates ANSI bold escape sequences.
Implementation:
export function isValidHex(hex?: string): hex is string {
if (!hex) return false
return /^#[0-9a-fA-F]{6}$/.test(hex)
}
export function hexToRgb(hex: string): { r: number; g: number; b: number } {
const r = parseInt(hex.slice(1, 3), 16)
const g = parseInt(hex.slice(3, 5), 16)
const b = parseInt(hex.slice(5, 7), 16)
return { r, g, b }
}
export function hexToAnsiBold(hex?: string): string | undefined {
if (!isValidHex(hex)) return undefined
const { r, g, b } = hexToRgb(hex)
return `\x1b[38;2;${r};${g};${b}m\x1b[1m`
}
// Self-barrel at file bottom -- the barrel IS the `Color` namespace.
export * as Color from "."Used by: No direct imports found (TUI theming infrastructure).
When NOT to use: Only supports 6-digit hex (#RRGGBB). Does not handle 3-digit hex, named colors, or HSL.
---
local-context.ts -- AsyncLocalStorage-based context propagation
What it does: Creates typed async context values using Node.js AsyncLocalStorage. Provides use() to read and provide() to set context.
Implementation:
export class NotFound extends Error {
constructor(public override readonly name: string) {
super(`No context found for ${name}`)
}
}
export function create<T>(name: string) {
const storage = new AsyncLocalStorage<T>()
return {
use() {
const result = storage.getStore()
if (!result) throw new NotFound(name)
return result
},
provide<R>(value: T, fn: () => R) {
return storage.run(value, fn)
},
}
}
// Self-barrel at file bottom -- the barrel IS the `LocalContext` namespace.
// It was renamed from `Context` to `LocalContext` precisely so it no longer
// collides with Effect's `Context` / `Context.Service` (the DI namespace).
export * as LocalContext from "."Used by:
storage/db.ts-- Database transaction context (LocalContext.create<{ tx: TxOrDb, effects: ... }>("database"))cli/cmd/tui/thread.ts-- TUI thread contextcli/cmd/tui/plugin/runtime.ts-- Plugin runtime contextcli/cmd/tui/component/textarea-keybindings.ts-- Keybinding context
Real call pattern:
// Define context
const ctx = LocalContext.create<{ tx: TxOrDb; effects: (() => void)[] }>("database")
// Provide context
ctx.provide({ tx, effects }, () => callback(tx))
// Use context (throws LocalContext.NotFound if not in scope)
const { tx } = ctx.use()When NOT to use: This is for vanilla JS/TS contexts. The Effect-based modules use Effect's own Context / Layer system instead. Do NOT use this LocalContext.create (the @/util/local-context AsyncLocalStorage wrapper) inside Effect service code -- use Effect's Context.Service and layers. (Effect's Context.Service is the DI namespace; @/util/local-context is opencode's separate AsyncLocalStorage helper.)
---
data-url.ts -- Decode data: URLs
What it does: Decodes base64 and percent-encoded data URLs to UTF-8 strings.
Implementation:
export function decodeDataUrl(url: string) {
const idx = url.indexOf(",")
if (idx === -1) return ""
const head = url.slice(0, idx)
const body = url.slice(idx + 1)
if (head.includes(";base64")) return Buffer.from(body, "base64").toString("utf8")
return decodeURIComponent(body)
}Used by:
session/prompt.ts-- Decoding inline data URLs in prompt content
When NOT to use: Only returns strings (UTF-8). Not suitable for binary data URL decoding.
---
defer.ts -- Resource cleanup via using / await using
What it does: Creates disposable objects for using / await using syntax, wrapping cleanup functions.
Implementation:
export function defer<T extends () => void | Promise<void>>(
fn: T,
): T extends () => Promise<void> ? { [Symbol.asyncDispose]: () => Promise<void> } : { [Symbol.dispose]: () => void } {
return {
[Symbol.dispose]() { fn() },
[Symbol.asyncDispose]() { return Promise.resolve(fn()) },
} as any
}Used by:
tool/task.ts-- Cleanup after task tool executioncli/cmd/tui/util/editor.ts-- Editor temp file cleanup
Real call pattern:
await using _ = defer(async () => {
// cleanup logic here
})When NOT to use: The Lock module already returns disposables. Flock.acquire already returns async disposables. Do not wrap these in defer -- they have their own [Symbol.asyncDispose].
---
effect-http-client.ts -- Retry wrapper for Effect HTTP client
What it does: Adds transient-error retry with exponential backoff + jitter to an Effect HTTP client.
Implementation:
export const withTransientReadRetry = <E, R>(client: HttpClient.HttpClient.With<E, R>) =>
client.pipe(
HttpClient.retryTransient({
retryOn: "errors-and-responses",
times: 2,
schedule: Schedule.exponential(200).pipe(Schedule.jittered),
}),
)Used by:
auth/index.ts-- Retrying auth token fetches
When NOT to use: Only for Effect-based HTTP clients. The codebase also uses raw fetch() in some places (provider SDK calls) -- those use their own retry logic.
---
effect-zod.ts -- (removed in the Effect Schema migration)
This Effect-Schema-to-Zod bridge was removed. The codebase no longer round-trips through Zod. Where a non-Effect representation is needed (tool/provider definitions), JSON Schema is generated directly from the Effect Schema via tool/json-schema.ts (JsonSchema.fromSchema / JsonSchema.fromTool). Do not reach for an Effect→Zod converter.
---
error.ts -- Error formatting and message extraction
What it does: Three functions for error handling: errorFormat (full representation), errorMessage (human-readable message), errorData (structured data for logging).
Implementation:
export function errorFormat(error: unknown): string {
if (error instanceof Error) return error.stack ?? `${error.name}: ${error.message}`
if (typeof error === "object" && error !== null) {
try { return JSON.stringify(error, null, 2) } catch { return "Unexpected error (unserializable)" }
}
return String(error)
}
export function errorMessage(error: unknown): string {
if (error instanceof Error) {
if (error.message) return error.message
if (error.name) return error.name
}
if (isRecord(error) && typeof error.message === "string" && error.message) return error.message
const text = String(error)
if (text && text !== "[object Object]") return text
const formatted = errorFormat(error)
if (formatted && formatted !== "{}") return formatted
return "unknown error"
}
export function errorData(error: unknown) { /* ... structured extraction ... */ }Used by: 19 files including:
session/message-v2.ts-- Formatting LLM errors for displayserver/server.ts-- Error response formattingserver/routes/*.ts-- Route error handling (session, question, pty, provider, project, permission, mcp, config, experimental, workspace, tui)plugin/index.ts-- Plugin error displaycli/error.ts-- CLI error formattingcli/cmd/tui/thread.ts-- TUI error displayprocess.ts(internal) --errorMessageused inProcess.runcatch
Real call pattern:
try { /* ... */ } catch (e) {
log.error("operation failed", errorData(e))
return { error: errorMessage(e) }
}When NOT to use: For Effect-based code, use Effect's own error handling (Effect.catchTag, Effect.catch). The errorMessage/errorFormat helpers are for vanilla JS try/catch boundaries.
---
filesystem.ts -- File system operations with auto-mkdir and cross-platform normalization
What it does: Namespace with 20+ file operations: exists, isDir, stat, size, readText, readJson, readBytes, readArrayBuffer, write (auto-mkdir), writeJson, writeStream, mimeType, normalizePath, resolve, windowsPath, overlaps, contains, findUp, up, globUp.
Implementation: ~200 lines. Key features:
write()auto-creates parent directories on ENOENTresolve()handles Git Bash / Cygwin / WSL path translation on WindowsnormalizePath()canonicalizes Windows case-insensitive paths viarealpathSync.nativefindUp()/up()/globUp()walk up directory trees searching for files
Used by: 14+ files including:
tool/bash.ts-- File existence checksskill/index.ts-- Reading skill filesshell/shell.ts-- Shell detectionserver/instance.ts-- Lock file managementproject/instance.ts-- Project root detectionplugin/shared.ts,plugin/meta.ts,plugin/install.ts-- Plugin file I/Oconfig/config.ts,config/paths.ts-- Config file readingstorage/storage.ts-- JSON storage backendcli/cmd/tui/util/editor.ts-- Temp file writingcli/cmd/tui/thread.ts-- File reading for thread context
Real call pattern:
// Auto-mkdir write
await Filesystem.write(path.join(dir, "config.json"), JSON.stringify(data))
// Read JSON with type
const config = await Filesystem.readJson<ConfigType>(filePath)
// Walk up directory tree
const files = await Filesystem.findUp(".opencode", startDir)
// Check path containment
if (Filesystem.contains(projectRoot, filePath)) { /* safe */ }When NOT to use: For Effect-based file operations, use @effect/platform-node/NodeFileSystem. This namespace is for vanilla async code only. Also: exists() and isDir() are misleadingly async (they use sync implementations internally) -- this is fine for current usage but be aware they don't truly yield.
---
flock.ts -- Cross-process file-based locking with heartbeat
What it does: Directory-based file locks with stale detection, heartbeat, exponential backoff retry, and token-based ownership verification. For cross-PROCESS synchronization (different Node.js processes).
Implementation: ~330 lines. Uses mkdir atomicity for lock acquisition. Features:
- Stale lock detection via heartbeat file mtime
- Breaker pattern for safe stale lock cleanup (prevents two processes from both cleaning up simultaneously)
- Heartbeat interval (default: staleMs/3) keeps long operations alive
- Token verification prevents releasing locks owned by other processes
- Configurable: staleMs, timeoutMs, baseDelayMs, maxDelayMs
Used by:
config/config.ts-- Config file writesplugin/meta.ts-- Plugin metadata updatesplugin/install.ts-- Plugin installationsnapshot/index.ts-- Snapshot operations
Real call pattern:
// Simple usage
await using _ = await Flock.acquire("my-operation-key")
// ... critical section ...
// With options
await Flock.withLock("config-write", async () => {
await writeConfig(data)
}, { staleMs: 30_000, timeoutMs: 10_000 })When NOT to use: For in-process synchronization (same Node.js process), use Lock instead -- it is dramatically faster (no filesystem I/O). Flock is ONLY needed when multiple processes compete for the same resource (e.g., config file writes from multiple opencode instances).
---
fn.ts -- Schema-validated function wrapper
What it does: Wraps a function with Effect Schema input validation. Provides .schema accessor and .force() bypass.
Implementation:
export function fn<T extends Schema.Top, Result>(schema: T, cb: (input: Schema.Schema.Type<T>) => Result) {
const decode = Schema.decodeUnknownSync(schema)
const result = (input: Schema.Schema.Type<T>) => cb(decode(input))
result.force = (input: Schema.Schema.Type<T>) => cb(input)
result.schema = schema
return result
}Used by: 6 files:
session/prompt.ts--prompt = fn(PromptInput, async (input) => { ... }),loop = fn(LoopInput, async (input) => { ... })session/summary.ts-- Summary generation input validationsession/message-v2.ts-- Message creation validationsession/index.ts-- Session CRUD operationssession/compaction.ts-- Compaction input validationcontrol-plane/workspace.ts-- Workspace operations
Real call pattern:
export const prompt = fn(PromptInput, async (input) => {
const session = await Session.get(input.sessionID)
// ... input is guaranteed to match PromptInput schema
})
// Call normally (validates)
await prompt({ sessionID: "ses_..." })
// Bypass validation (internal/trusted callers)
await prompt.force({ sessionID: "ses_..." })
// Access schema for documentation/routes
const schema = prompt.schemaWhen NOT to use: For Effect-based code, use Effect's own Schema.decode. For server routes, the routes use fn.schema to extract the Zod schema and validate separately. Do not use fn() for trivially typed functions that do not need runtime validation.
---
format.ts -- Human-readable duration formatting (seconds input)
What it does: Formats a number of seconds into a human-readable string ("5s", "3m 20s", "2h 15m", "~3 days", "~2 weeks").
Implementation:
export function formatDuration(secs: number) {
if (secs <= 0) return ""
if (secs < 60) return `${secs}s`
if (secs < 3600) {
const mins = Math.floor(secs / 60)
const remaining = secs % 60
return remaining > 0 ? `${mins}m ${remaining}s` : `${mins}m`
}
// ... continues for hours, days, weeks
}Used by:
server/instance.ts-- Displaying server uptimeproject/bootstrap.ts-- Project initialization timingformat/index.ts-- Format tool outputtool/write.ts,tool/edit.ts,tool/apply_patch.ts-- Tool execution timing
IMPORTANT -- Overlap with `Locale.duration`: There are TWO duration formatters:
format.tsformatDuration(secs)-- takes SECONDS as inputlocale.tsLocale.duration(ms)-- takes MILLISECONDS as input
These are NOT interchangeable. Check the input unit.
---
git.ts -- Git command runner
What it does: Runs git commands via Process.run with stdin ignored (avoids pipe inheritance issues).
Implementation:
export async function git(args: string[], opts: { cwd: string; env?: Record<string, string> }): Promise<GitResult> {
return Process.run(["git", ...args], {
cwd: opts.cwd,
env: opts.env,
stdin: "ignore",
nothrow: true,
})
.then((result) => ({
exitCode: result.code,
text: () => result.stdout.toString(),
stdout: result.stdout,
stderr: result.stderr,
}))
.catch((error) => ({
exitCode: 1,
text: () => "",
stdout: Buffer.alloc(0),
stderr: Buffer.from(error instanceof Error ? error.message : String(error)),
}))
}Used by: 5 files:
storage/storage.ts-- Git root detection for project ID migrationfile/watcher.ts-- Git status checksfile/index.ts-- File tracking via gitcli/cmd/pr.ts-- PR operationscli/cmd/github.ts-- GitHub operations
Real call pattern:
const result = await git(["rev-list", "--max-parents=0", "--all"], { cwd: worktree })
const [id] = result.text().split("\n").filter(Boolean).map(x => x.trim()).toSorted()When NOT to use: For Effect-based child processes, use the cross-spawn-spawner.ts layer. The git() helper is for vanilla async code only. Also note: it always uses nothrow: true and returns exitCode -- callers must check result.exitCode themselves.
---
glob.ts -- File globbing wrapper
What it does: Wraps the glob and minimatch packages with a simplified API.
Implementation:
export async function scan(pattern: string, options: Options = {}): Promise<string[]> {
return glob(pattern, toGlobOptions(options)) as Promise<string[]>
}
export function scanSync(pattern: string, options: Options = {}): string[] {
return globSync(pattern, toGlobOptions(options)) as string[]
}
export function match(pattern: string, filepath: string): boolean {
return minimatch(filepath, pattern, { dot: true })
}
// Self-barrel at file bottom -- the barrel IS the `Glob` namespace.
export * as Glob from "."Used by: 31 files (one of the most used utilities). Internal users include filesystem.ts, log.ts. External users span nearly every module: tool, storage, snapshot, skill, session, server, provider, mcp, lsp, file, config, cli, bus, bun, auth, worktree, index.ts.
Real call pattern:
// Find files
const matches = await Glob.scan("**/*.ts", { cwd: projectDir, include: "file", dot: true })
// Check if a path matches a pattern
if (Glob.match("*.test.ts", filepath)) { /* ... */ }
// Sync version for startup/config
const files = Glob.scanSync("*.json", { cwd: configDir })When NOT to use: For simple filename extension checks, use path.extname(). For directory listing without patterns, use fs.readdir. Glob is for actual pattern matching scenarios.
---
hash.ts -- Fast SHA-1 hashing
What it does: SHA-1 hash of string or Buffer input, returned as hex.
Implementation:
export function fast(input: string | Buffer): string {
return createHash("sha1").update(input).digest("hex")
}
// Self-barrel at file bottom -- the barrel IS the `Hash` namespace, so
// consumers still write `Hash.fast(...)`.
export * as Hash from "."Used by:
util/flock.ts(internal) -- Hashing lock keys to filesystem-safe filenamessnapshot/index.ts-- Content-addressable snapshot storage
ANTI-PATTERN: server/instance.ts uses createHash("sha256") directly instead of Hash.fast(). This is CORRECT because it needs SHA-256 (not SHA-1) and base64 output (not hex). Hash.fast is intentionally SHA-1 for speed in non-security contexts.
---
iife.ts -- Immediately invoked function expression helper
What it does: Executes a function immediately and returns its result. A type-safe way to use complex expressions where a simple value is expected.
Implementation:
export function iife<T>(fn: () => T) {
return fn()
}Used by: 13 files:
storage/db.ts-- ComputingDatabase.Pathwith conditional logictool/task.ts,tool/skill.ts-- Complex initialization expressionssession/retry.ts,session/prompt.ts,session/message-v2.ts,session/index.ts-- Inline computed valuesprovider/transform.ts,provider/provider.ts,provider/error.ts-- Provider configurationproject/instance.ts-- Instance path computationplugin/copilot.ts-- Copilot configurationconfig/config.ts-- Config path resolution
Real call pattern:
export const Path = iife(() => {
if (Flag.OPENCODE_DB) {
if (Flag.OPENCODE_DB === ":memory:" || path.isAbsolute(Flag.OPENCODE_DB)) return Flag.OPENCODE_DB
return path.join(Global.Path.data, Flag.OPENCODE_DB)
}
return getChannelPath()
})When NOT to use: For truly trivial expressions, just use the expression directly. iife adds readability for multi-line conditional initialization -- do not use it for single-line assignments.
---
keybind.ts -- Keyboard binding parsing and matching
What it does: Parses keybinding strings (like "ctrl+shift+p", "\<leader\> a"), matches key events against bindings, and converts back to display strings.
Implementation: ~100 lines. Handles ctrl, alt/meta/option, shift, super, leader prefix, special keys (esc->escape, del->delete, space).
Used by:
cli/cmd/tui/component/textarea-keybindings.ts-- TUI keybinding configuration
When NOT to use: TUI-specific. Not needed for non-TUI code.
---
lazy.ts -- Lazy evaluation with reset
What it does: Creates a lazily-evaluated value that caches on first call. Supports reset() to invalidate the cache. Does NOT cache on error.
Implementation:
export function lazy<T>(fn: () => T) {
let value: T | undefined
let loaded = false
const result = (): T => {
if (loaded) return value as T
try {
value = fn()
loaded = true
return value as T
} catch (e) {
throw e // Don't mark as loaded if initialization failed
}
}
result.reset = () => {
loaded = false
value = undefined
}
return result
}Used by: 6 files:
storage/db.ts--Database.Client = lazy(() => { ... })withClient.reset()used inclose()storage/storage.ts--state = lazy(async () => { ... })tool/bash.ts-- Lazy tree-sitter language loadingshell/shell.ts--Shell.preferred = lazy(() => ...),Shell.acceptable = lazy(() => ...)provider/models.ts-- Lazy model list initializationfile/watcher.ts-- Lazy watcher initialization
Real call pattern:
// Define lazy value
export const Client = lazy(() => {
const db = init(Path)
db.run("PRAGMA journal_mode = WAL")
return db
})
// Use (initializes on first call)
Client().run("SELECT 1")
// Reset (for cleanup/testing)
Client.reset()Key behavior: The reset() method is critical for Database.close() -- it allows re-initialization after closing. Also: lazy does NOT cache failures -- if the factory throws, next call will retry.
When NOT to use: For Effect-based code, use Effect's ScopedCache (as used in InstanceState). For values that need async initialization, lazy works but the returned function becomes () => Promise<T> -- which is fine (see storage.ts).
---
locale.ts -- Locale-aware formatting utilities
What it does: String formatting: titlecase, time/datetime display, number abbreviation (K/M), duration (milliseconds), truncation (start/middle), pluralization.
Implementation: ~80 lines covering:
titlecase(str)-- Word-initial capitalizationtime(ms)-- Short time string from epoch msdatetime(ms)-- Full date+time from epoch mstodayTimeOrDateTime(ms)-- Show time only if today, else full datetimenumber(num)-- "1.2K", "3.4M"duration(ms)-- "150ms", "3.2s", "5m 20s" (INPUT IS MILLISECONDS)truncate(str, len)-- Truncate with ellipsis at endtruncateMiddle(str, max)-- Truncate with ellipsis in middlepluralize(count, singular, plural)-- Template-based pluralization with{}placeholder
Used by:
cli/cmd/tui/util/transcript.ts-- Session transcript formatting
IMPORTANT -- Overlap with `format.ts`: Locale.duration(ms) takes MILLISECONDS. formatDuration(secs) takes SECONDS. Do not confuse them.
When NOT to use: For server/API responses, use raw values and let the client format. These helpers are for TUI/CLI display.
---
lock.ts -- In-process read/write lock
What it does: In-memory reader-writer lock with writer priority. Returns Disposable for using syntax.
Implementation:
export async function read(key: string): Promise<Disposable> {
const lock = get(key)
return new Promise((resolve) => {
if (!lock.writer && lock.waitingWriters.length === 0) {
lock.readers++
resolve({ [Symbol.dispose]: () => { lock.readers--; process(key) } })
} else {
lock.waitingReaders.push(() => { /* ... */ })
}
})
}
export async function write(key: string): Promise<Disposable> {
// Similar, but exclusive access
}
// Self-barrel at file bottom -- the barrel IS the `Lock` namespace, so
// consumers still write `Lock.read(...)` / `Lock.write(...)`.
export * as Lock from "."Used by:
storage/storage.ts-- All read/write operations useLock.read(target)/Lock.write(target)
Real call pattern:
// Read lock (multiple readers allowed)
using _ = await Lock.read(target)
const result = await Filesystem.readJson<T>(target)
// Write lock (exclusive)
using _ = await Lock.write(target)
await Filesystem.writeJson(target, content)Key behavior: Writer priority prevents reader starvation. Locks auto-cleanup when no waiters remain.
When NOT to use: This is in-PROCESS only. For cross-process locking, use Flock. Also: key is a string, so callers must choose consistent keys (Storage uses the file path as key).
---
log.ts -- Structured file-based logging
What it does: Creates tagged loggers that write to a log file (or stderr in print mode). Supports levels (DEBUG/INFO/WARN/ERROR), tags, cloning, and timed operations.
Implementation: ~180 lines. Features:
- Service-keyed logger caching (same service name returns same logger)
- Auto-cleanup of old log files (keeps last 10)
- Delta timing between log entries
log.time(msg)returns disposable timer for measuring operations
Used by: 70 files (the single most-used utility). Every module in the codebase uses logging.
Import path: Now lives in @opencode-ai/core -- import * as Log from "@opencode-ai/core/util/log" (moved out of @/util/log). The Log.create API is unchanged.
Real call pattern:
const log = Log.create({ service: "session" })
log.info("starting prompt", { sessionID })
log.error("prompt failed", { error: errorData(e) })
// Timed operation
using timer = log.time("compaction")
await doCompaction()
// timer auto-stops on scope exit, logs durationWhen NOT to use: Always use Log.create -- never use console.log in production code. The only exception is fn.ts which uses console.trace and console.error for schema validation failures (these are developer-facing debugging aids).
---
network.ts -- Network status checks
What it does: Two functions: online() checks navigator.onLine, proxied() checks HTTP proxy env vars.
Implementation:
export function online() {
const nav = globalThis.navigator
if (!nav || typeof nav.onLine !== "boolean") return true
return nav.onLine
}
export function proxied() {
return !!(process.env.HTTP_PROXY || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.https_proxy)
}Used by: 6 files:
config/config.ts-- Proxy-aware HTTP configurationcli/cmd/web.ts,cli/cmd/serve.ts,cli/cmd/acp.ts-- Network-dependent featuresbun/registry.ts,bun/index.ts-- Bun-specific network checks
---
process.ts -- Cross-platform child process management
What it does: Spawns child processes with cross-platform support (uses cross-spawn). Provides spawn() (raw), run() (capture output), text() (string output), lines() (split output), and stop() (Windows taskkill support).
Implementation: ~170 lines. Key features:
cross-spawnfor reliable Windows command resolution- Abort signal support with SIGTERM -> SIGKILL escalation
nothrowoption to return exit code instead of throwingwindowsHidefor invisible Windows processesRunFailedErrorwith cmd, code, stdout, stderr
Used by: 10 files:
util/git.ts(internal) -- Git command executionutil/archive.ts(internal) -- Zip extractionsession/prompt.ts,session/compaction.ts-- Process spawning for tool executionide/index.ts-- IDE integrationconfig/config.ts-- Config tool executioncli/cmd/tui/util/editor.ts-- Editor launchingcli/cmd/tui/plugin/runtime.ts-- Plugin process managementcli/cmd/pr.ts,cli/cmd/github.ts-- Git/GitHub CLI commands
Real call pattern:
// Run and capture output
const result = await Process.run(["git", "status"], { cwd: projectDir, nothrow: true })
if (result.code === 0) {
const output = result.stdout.toString()
}
// Get text directly
const { text } = await Process.text(["git", "branch", "--show-current"], { cwd })
// Get lines
const branches = await Process.lines(["git", "branch", "-a"], { cwd })
// Spawn with abort
const child = Process.spawn(["node", "server.js"], { abort: controller.signal })
await child.exitedWhen NOT to use: For Effect-based process spawning, use the cross-spawn-spawner.ts layer which integrates with Effect's resource management. Process is for vanilla async code.
---
queue.ts -- Async queue and concurrent work
What it does: Two utilities: AsyncQueue<T> (push/pull async queue implementing AsyncIterable) and work() (bounded-concurrency worker pool).
Implementation:
export class AsyncQueue<T> implements AsyncIterable<T> {
private queue: T[] = []
private resolvers: ((value: T) => void)[] = []
push(item: T) {
const resolve = this.resolvers.shift()
if (resolve) resolve(item)
else this.queue.push(item)
}
async *[Symbol.asyncIterator]() {
while (true) yield await this.next()
}
}
export async function work<T>(concurrency: number, items: T[], fn: (item: T) => Promise<void>) {
const pending = [...items]
await Promise.all(
Array.from({ length: concurrency }, async () => {
while (true) {
const item = pending.pop()
if (item === undefined) return
await fn(item)
}
}),
)
}Used by:
server/routes/global.ts-- Async queue for server events
When NOT to use: For Effect-based concurrency, use Effect's Stream, PubSub, or Queue. The AsyncQueue is for vanilla JS async iteration patterns.
---
record.ts -- Type guard for plain objects
What it does: Checks if a value is a non-null, non-array object.
Implementation:
export function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === "object" && !Array.isArray(value)
}Used by:
util/error.ts(internal) -- Used inerrorMessageanderrorDataplugin/shared.ts-- Plugin data validationconfig/tui.ts-- TUI config parsingconfig/config.ts-- Config value validationcli/cmd/tui/plugin/runtime.ts-- Plugin message parsing
---
rpc.ts -- Worker thread RPC protocol
What it does: Simple JSON-based RPC for communicating between main thread and workers. Provides listen() (worker side), emit() (worker events), and client() (main thread side).
Implementation: ~65 lines. Uses postMessage/onmessage with JSON serialization. Supports request/response (rpc.request/rpc.result) and events (rpc.event).
Used by:
cli/cmd/tui/worker.ts-- TUI background workercli/cmd/tui/thread.ts-- TUI main thread client
Real call pattern:
// Worker side
Rpc.listen({
async doSomething(input) { return result }
})
Rpc.emit("progress", { percent: 50 })
// Main thread side
const client = Rpc.client<WorkerDef>(worker)
const result = await client.call("doSomething", { data })
client.on("progress", (data) => { /* ... */ })---
schema.ts -- Effect Schema helpers (withStatics, Newtype)
Import path: These schema helpers now live in @opencode-ai/core -- import { Newtype } from "@opencode-ai/core/schema" (also NonNegativeInt and friends). They moved out of @/util/schema.
What it does: Two helpers for Effect Schema: 1. withStatics -- Attaches static methods to a schema via .pipe() 2. Newtype -- Creates nominal/branded scalar types that are also valid schemas
Implementation:
export const withStatics =
<S extends object, M extends Record<string, unknown>>(methods: (schema: S) => M) =>
(schema: S): S & M =>
Object.assign(schema, methods(schema))
export function Newtype<Self>() {
return <const Tag extends string, S extends Schema.Top>(tag: Tag, schema: S) => {
abstract class Base {
static make(value: Schema.Schema.Type<S>): Self { return value as unknown as Self }
}
Object.setPrototypeOf(Base, schema)
return Base as unknown as /* ... branded type ... */
}
}Used by (`withStatics`): 9 files -- every schema file that defines branded IDs:
session/schema.ts-- SessionID, MessageID, PartIDsync/schema.ts-- EventIDtool/schema.ts-- ToolCallIDpty/schema.ts-- PtyIDprovider/schema.ts-- ProviderIDproject/schema.ts-- ProjectIDcontrol-plane/schema.ts-- WorkspaceIDaccount/schema.ts-- AccountID
Used by (`Newtype`): 3 files:
question/schema.ts-- QuestionIDpermission/schema.ts-- PermissionID@opencode-ai/core/schema(definition)
Real call pattern:
// withStatics: attach factory methods to a branded schema (no .zod bridge)
export const SessionID = Schema.String.check(Schema.isStartsWith("ses")).pipe(
Schema.brand("SessionID"),
withStatics((s) => ({
descending: (id?: string) => s.make(Identifier.descending("session", id)),
})),
)
// Newtype: create a nominal type that IS a schema
class QuestionID extends Newtype<QuestionID>()("QuestionID", Schema.String.check(Schema.isStartsWith("que"))) {
static ascending(id?: string): QuestionID { return this.make(Identifier.ascending("question", id)) }
}---
scrap.ts -- Scratch/dummy file (NOT a real utility)
What it does: Contains dummy exports (foo, bar, dummyFunction, randomHelper). This is a scratch file.
Used by:
cli/cmd/debug/index.ts-- Debug command imports for testing
When NOT to use: NEVER use in production code. This file exists only for development/debugging.
---
signal.ts -- One-shot signal/trigger
What it does: Creates a promise-based signal that can be triggered once.
Implementation:
export function signal() {
let resolve: any
const promise = new Promise((r) => (resolve = r))
return {
trigger() { return resolve() },
wait() { return promise },
}
}Used by: No direct imports found.
When NOT to use: For Effect-based signals, use Deferred. For recurring events, use EventEmitter or PubSub.
---
timeout.ts -- Promise timeout wrapper
What it does: Races a promise against a timeout, rejecting with a descriptive error on timeout.
Implementation:
export function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
let timeout: NodeJS.Timeout
return Promise.race([
promise.then((result) => { clearTimeout(timeout); return result }),
new Promise<never>((_, reject) => {
timeout = setTimeout(() => reject(new Error(`Operation timed out after ${ms}ms`)), ms)
}),
])
}Used by:
mcp/index.ts-- MCP server connection timeoutcli/cmd/tui/thread.ts-- TUI operation timeout
When NOT to use: For abort-based timeouts (where you need to cancel the underlying operation, not just the wait), use abortAfter. withTimeout only races -- it does NOT cancel the original promise.
---
token.ts -- LLM token estimation
What it does: Estimates token count using the 4-chars-per-token heuristic.
Implementation:
const CHARS_PER_TOKEN = 4
export function estimate(input: string) {
return Math.max(0, Math.round((input || "").length / CHARS_PER_TOKEN))
}
// Self-barrel at file bottom -- the barrel IS the `Token` namespace, so
// consumers still write `Token.estimate(...)`.
export * as Token from "."Used by:
session/compaction.ts-- Estimating message token costs for compaction decisions
---
update-schema.ts -- (legacy Zod helper, removed)
A Zod-era helper that made every object field optional(nullable(T)) for PATCH/update payloads. Removed with the Effect Schema migration — partial update event shapes are now written directly as a Schema.Struct of Schema.optional(...) fields (see the SyncEvent "Updated" event in schemas-and-state.md).
---
which.ts -- Cross-platform command path resolution
What it does: Finds the full path of a command, checking PATH plus the global bin directory.
Implementation:
export function which(cmd: string, env?: NodeJS.ProcessEnv) {
const base = env?.PATH ?? env?.Path ?? process.env.PATH ?? process.env.Path ?? ""
const full = base ? base + path.delimiter + Global.Path.bin : Global.Path.bin
const result = whichPkg.sync(cmd, {
nothrow: true,
path: full,
pathExt: env?.PATHEXT ?? env?.PathExt ?? process.env.PATHEXT ?? process.env.PathExt,
})
return typeof result === "string" ? result : null
}Used by:
shell/shell.ts-- Finding shell executables
---
wildcard.ts -- Wildcard pattern matching for permissions
What it does: Matches strings against wildcard patterns (* = any, ? = single char). Includes all() for finding the best-matching pattern from a sorted set, and allStructured() for matching command head + args.
Implementation: ~60 lines. Features:
- Case-insensitive on Windows
"ls *"pattern matches both"ls"and"ls -la"(trailing wildcard is optional)- Patterns sorted by length (shortest first), last match wins (most specific)
Import path: Now lives in @opencode-ai/core -- import { Wildcard } from "@opencode-ai/core/util/wildcard" (moved out of @/util/wildcard). The Wildcard.* API is unchanged.
Used by:
permission/index.ts-- Command permission matchingpermission/evaluate.ts-- Permission evaluation
Real call pattern:
// Simple match
Wildcard.match("git commit *", "git commit -m 'fix'") // true
// Find matching permission from ruleset
const permission = Wildcard.all("git push origin main", {
"git *": "allow",
"git push *": "deny",
}) // returns "deny" (longer/more specific pattern wins)---
Non-util Helpers
effect/instance-state.ts -- Per-project-instance state cache
What it does: Creates a ScopedCache keyed by project directory. Each project instance gets its own lazily-initialized state. Automatically invalidates when an instance is disposed.
Implementation:
// effect/instance-state.ts -- flat module, closed by the self-barrel
export const make = <A, E = never, R = never>(
init: (ctx: InstanceContext) => Effect.Effect<A, E, R | Scope.Scope>,
): Effect.Effect<InstanceState<A, E, Exclude<R, Scope.Scope>>, never, R | Scope.Scope> =>
Effect.gen(function* () {
const cache = yield* ScopedCache.make<string, A, E, R>({
capacity: Number.POSITIVE_INFINITY,
lookup: () => init(Instance.current),
})
const off = registerDisposer((directory) => Effect.runPromise(ScopedCache.invalidate(cache, directory)))
yield* Effect.addFinalizer(() => Effect.sync(off))
return { [TypeId]: TypeId, cache }
})
export const get = <A, E, R>(self: InstanceState<A, E, R>) =>
Effect.suspend(() => ScopedCache.get(self.cache, Instance.directory))
export const use = <A, E, R, B>(self: InstanceState<A, E, R>, select: (value: A) => B) =>
Effect.map(get(self), select)
export const useEffect = <A, E, R, B, E2, R2>(
self: InstanceState<A, E, R>,
select: (value: A) => Effect.Effect<B, E2, R2>,
) => Effect.flatMap(get(self), select)
export const has = /* ... */
export const invalidate = /* ... */
export * as InstanceState from "./instance-state"Used by: 20 files (core infrastructure):
tool/registry.ts-- Tool state per projectsnapshot/index.ts-- Snapshot state per projectskill/index.ts-- Skill state per projectsession/status.ts-- Session status per projectquestion/index.ts-- Question state per projectpty/index.ts-- PTY state per projectprovider/auth.ts-- Provider auth per projectproject/vcs.ts-- VCS state per projectplugin/index.ts-- Plugin state per projectpermission/index.ts-- Permission state per projectmcp/index.ts-- MCP state per projectlsp/index.ts-- LSP state per projectformat/index.ts-- Format state per projectfile/watcher.ts,file/time.ts,file/index.ts-- File state per projectconfig/config.ts-- Config state per projectcommand/index.ts-- Command state per projectbus/index.ts-- Bus state per projectagent/agent.ts-- Agent state per project
Real call pattern:
// In layer definition
const state = yield* InstanceState.make<MyState>(
Effect.fn("MyModule.state")(function* (ctx) {
// Initialize per-instance state
return { /* ... */ }
}),
)
// In service methods
const s = yield* InstanceState.get(state)---
effect/run-service.ts -- Service runtime factory
What it does: Creates a managed runtime for an Effect service, providing runSync, runPromise, runFork, and runCallback that automatically resolve the service from its layer.
Implementation:
export const memoMap = Layer.makeMemoMapUnsafe()
export function makeRuntime<I, S, E>(service: Context.Service<I, S>, layer: Layer.Layer<I, E>) {
let rt: ManagedRuntime.ManagedRuntime<I, E> | undefined
const getRuntime = () => (rt ??= ManagedRuntime.make(layer, { memoMap }))
return {
runSync: <A, Err>(fn: (svc: S) => Effect.Effect<A, Err, I>) => getRuntime().runSync(service.use(fn)),
runPromise: <A, Err>(fn: (svc: S) => Effect.Effect<A, Err, I>, options?: Effect.RunOptions) =>
getRuntime().runPromise(service.use(fn), options),
runFork: <A, Err>(fn: (svc: S) => Effect.Effect<A, Err, I>) => getRuntime().runFork(service.use(fn)),
runCallback: <A, Err>(fn: (svc: S) => Effect.Effect<A, Err, I>) => getRuntime().runCallback(service.use(fn)),
}
}Used by: 27 files -- every service module uses this at the bottom to export its public API:
session/status.ts,mcp/auth.ts,question/index.ts,format/index.ts,account/index.ts,tool/truncate.ts,auth/index.ts,tool/registry.ts,permission/index.ts,config/config.ts,mcp/index.ts,command/index.ts,bus/index.ts,project/project.ts,project/vcs.ts,agent/agent.ts,lsp/index.ts,pty/index.ts,skill/index.ts,provider/auth.ts,snapshot/index.ts,file/time.ts,file/index.ts,file/watcher.ts,worktree/index.ts,plugin/index.ts,installation/index.ts
Real call pattern:
// At bottom of service module
const { runPromise } = makeRuntime(Service, defaultLayer)
// Exported as public API
export async function get(id: string) {
return runPromise((svc) => svc.get(id))
}Key behavior: The shared memoMap ensures services are only initialized once across the entire application, even when multiple modules depend on the same service.
---
effect/instance-registry.ts -- Instance disposal coordination
What it does: Maintains a set of disposer callbacks that run when a project instance is disposed.
Implementation:
const disposers = new Set<(directory: string) => Promise<void>>()
export function registerDisposer(disposer: (directory: string) => Promise<void>) {
disposers.add(disposer)
return () => { disposers.delete(disposer) }
}
export async function disposeInstance(directory: string) {
await Promise.allSettled([...disposers].map((disposer) => disposer(directory)))
}Used by:
effect/instance-state.ts(internal) -- Registers cache invalidationproject/instance.ts-- CallsdisposeInstanceduring cleanup
---
effect/cross-spawn-spawner.ts -- Effect-native process spawner
What it does: Provides an Effect ChildProcessSpawner layer that uses cross-spawn for cross-platform child process management. Handles piped commands, stdio configuration, process groups, Windows-specific taskkill, and resource cleanup.
Implementation: ~480 lines. Full Effect-native replacement for Node.js child_process.spawn with:
- Proper resource management via
Effect.acquireRelease - Process group killing (negative PID on Unix, taskkill on Windows)
- Piped command chains
- Additional file descriptors
- Overlapped pipes on Windows
Used by: 6 files:
worktree/index.ts,snapshot/index.ts-- Git operations via Effectproject/vcs.ts,project/project.ts-- VCS operationsmcp/index.ts-- MCP server process managementinstallation/index.ts-- Self-update process
---
id/id.ts -- Monotonic ID generation
What it does: Generates sortable, prefixed IDs with time-based ordering (ascending or descending). Uses a prefix registry for type safety.
Implementation: ~85 lines. Features:
- Prefixes: evt, ses, msg, per, que, usr, prt, pty, tool, wrk
- Monotonic: same-millisecond IDs get incrementing counters
- Descending: bitwise NOT of timestamp for reverse-chronological sorting
- Random suffix: base62 for uniqueness
timestamp(id)extracts creation time from ascending IDs
Used by: 10 files -- every schema file that defines an entity ID:
session/schema.ts-- SessionID (descending), MessageID (ascending), PartID (ascending)sync/schema.ts-- EventID (ascending)tool/schema.ts-- ToolCallID (ascending)pty/schema.ts-- PtyID (ascending)provider/schema.ts-- (uses Identifier.schema for validation)project/schema.ts-- ProjectIDcontrol-plane/schema.ts-- WorkspaceID (ascending)permission/schema.ts-- PermissionID (ascending)question/schema.ts-- QuestionID (ascending)account/schema.ts-- AccountID
Real call pattern:
// In schema definition
export const SessionID = Schema.String.pipe(
Schema.brand("SessionID"),
withStatics((s) => ({
descending: (id?: string) => s.make(Identifier.descending("session", id)),
})),
)
// Generate a new ID
const id = SessionID.descending()
// Extract timestamp
const created = Identifier.timestamp(id)When NOT to use: SessionIDs are DESCENDING (newest first in sorted order). All other IDs are ASCENDING. Do not mix these up -- it determines database query ordering.
---
bus/bus-event.ts -- Event type definition registry
What it does: Defines typed event definitions for the pub/sub bus. Maintains a global registry for schema generation.
Implementation:
export type Definition<Type extends string = string, Properties extends Schema.Top = Schema.Top> = {
type: Type
properties: Properties
}
const registry = new Map<string, Definition>()
export function define<Type extends string, Properties extends Schema.Top>(type: Type, properties: Properties) {
const result = { type, properties }
registry.set(type, result)
return result
}
export function payloads() {
return Schema.Union(/* all registered events as a discriminated union on "type" */)
}
// Self-barrel at file bottom -- the barrel IS the `BusEvent` namespace, so
// consumers still write `BusEvent.define(...)` / `BusEvent.Definition`.
export * as BusEvent from "."Used by: 25+ files define events. Every module that publishes events uses BusEvent.define:
session/index.ts-- Diff, Error eventssession/message-v2.ts-- PartDeltasession/compaction.ts-- Compactedsession/status.ts-- Status, Idlesession/todo.ts-- Updatedquestion/index.ts-- Asked, Replied, Rejectedpermission/index.ts-- Asked, Repliedpty/index.ts-- Created, Updated, Exited, Deletedfile/index.ts-- Editedfile/watcher.ts-- Updatedlsp/index.ts-- Updated;lsp/client.ts-- Diagnosticsmcp/index.ts-- ToolsChanged, BrowserOpenFailedworktree/index.ts-- Ready, Failedinstallation/index.ts-- Updated, UpdateAvailableide/index.ts-- Installedcommand/index.ts-- Executedproject/vcs.ts-- BranchUpdatedproject/project.ts-- Updatedcontrol-plane/workspace.ts-- Ready, Failedserver/event.ts-- Connected, Disposedserver/routes/global.ts-- GlobalDisposedEventcli/cmd/tui/event.ts-- PromptAppend, CommandExecute, ToastShow, SessionSelectbus/index.ts-- InstanceDisposed
---
bus/index.ts -- Effect-based pub/sub event bus
What it does: Full publish/subscribe system built on Effect's PubSub. Per-instance state, typed subscriptions, wildcard subscriptions, and callback-based subscriptions.
Key API:
Bus.publish(EventDef, properties)
Bus.subscribe(EventDef, callback)
Bus.subscribeAll(callback)Used by: The same 25+ modules that define events also subscribe to events.
---
sync/index.ts -- Event sourcing / CQRS system
What it does: Defines versioned, aggregated events with projectors. Events are stored in SQLite, replayed for state reconstruction, and published to the bus.
Used by:
session/message-v2.ts-- Message events (Created, Updated, Deleted, etc.)session/index.ts-- Session events (Created, Updated, Deleted, etc.)
---
storage/db.ts -- SQLite database management
What it does: Manages the SQLite database: connection, migrations, transactions, context-based connection sharing.
Key features:
- Uses
lazy()for connection initialization withreset()for cleanup - Uses
LocalContext.create()for transaction propagation Database.use(cb)-- auto-wraps in context or creates new oneDatabase.transaction(cb)-- explicit transaction with context nestingDatabase.effect(fn)-- deferred side-effects that run after transaction commitsiife()for computing the database path
Used by: 25+ files -- everything that reads/writes persistent data.
---
Usage Matrix
Which helpers are used by which major modules. Check marks indicate direct imports.
Helper | session | tool | provider | project | permission | config | file | mcp | lsp | server | bus | snapshot | cli/tui | pty | question | skill | account | install | worktree | format | command
--------------------|---------|------|----------|---------|------------|--------|------|-----|-----|--------|----- |----------|---------|-----|----------|-------|---------|---------|----------|--------|--------
Log | X | X | X | X | X | X | X | X | X | X | X | X | X | X | X | X | | X | X | X | X
Glob | X | X | X | | | X | X | | X | X | | X | X | | | X | | | X | |
Filesystem | | X | | X | | X | | | | X | | | X | | | X | | | | X |
InstanceState | | X | X | X | X | X | X | X | X | | X | X | | X | X | X | | | X | X | X
makeRuntime | X | X | X | X | X | X | X | X | X | | X | X | | X | X | X | X | X | X | X | X
BusEvent.define | X | | | X | X | | X | X | X | X | X | | X | X | X | | | X | X | | X
Identifier | X | X | X | X | X | | | | | | | | | X | X | | X | | | |
withStatics/schema | X | X | X | X | X | | | | | | | | | X | X | | X | | X | |
fn() | X | | | | | | | | | | | | | | | | | | | |
iife() | X | X | X | X | | X | | | | | | | | | | | | | | |
lazy() | | X | X | | | | X | | | X | | | | | | | | | | |
Process | | | | X | | X | | | | | | | X | | | | | | | X |
git() | | | | X | | | X | | | | | | X | | | | | | | |
error | X | | | | | X | | | | X | | | X | | | | | | | |
Context | | | | | | | | | | | | | X | | | | | | | |
Lock | | | | | | | | | | | | | | | | | | | | |
Flock | | | | | | X | | | | | | X | | | | | | | | |
Wildcard | | | | | X | | | | | | | | | | | | | | | |
Hash | | | | | | | | | | | | X | | | | | | | | |
Rpc | | | | | | | | | | | | | X | | | | | | | |
defer() | | X | | | | | | | | | | | X | | | | | | | |
signal() | | | | | | | | | | | | | | | | | | | | |
AsyncQueue/work | | | | | | | | | | X | | | | | | | | | | |
withTimeout | | | | | | | | X | | | | | X | | | | | | | |
Token | X | | | | | | | | | | | | | | | | | | | |
data-url | X | | | | | | | | | | | | | | | | | | | |
Locale | | | | | | | | | | | | | X | | | | | | | |
formatDuration | | X | | X | | | | | | X | | | | | | | | | | X |
Color | | | | | | | | | | | | | | | | | | | | |
Keybind | | | | | | | | | | | | | X | | | | | | | |
network | | | | | | X | | | | | | | X | | | | | | | |
which | | | | | | | | | | | | | | | | | | | | |
isRecord | | | | | | X | | | | | | | X | | | | | | | |
effect-http-client | | | | | | | | | | | | | | | | | | | | |
effect-zod | | | | | | | | | | | | | | | | X | X | | | |
cross-spawn-spawner | | | | X | | | | X | | | | X | | | | | | X | X | |
Database | X | X | X | X | X | | | | X | X | | | | | X | | X | X | X | | X
SyncEvent | X | | | | | | | | | X | | | | | | | X | | X | |---
Anti-Patterns and Correct Avoidances
ANTI-PATTERN: Direct createHash instead of Hash.fast
Location: server/instance.ts:303
const hash = match ? createHash("sha256").update(match[2]).digest("base64") : ""Verdict: CORRECT avoidance. Hash.fast uses SHA-1 and hex output. This site needs SHA-256 and base64. The helper does not fit.
---
ANTI-PATTERN: Direct new AbortController() without abortAfter
Locations: session/prompt.ts (4 sites), provider/provider.ts, acp/agent.ts, cli/cmd/tui/worker.ts, control-plane/workspace.ts, cli/cmd/tui/plugin/runtime.ts
Verdict: CORRECT avoidance. These are lifecycle controllers that are manually aborted on cancellation, not timeout-based. abortAfter is specifically for time-bounded abort and would leak timers if used for lifecycle management.
One potential anti-pattern: provider/provider.ts creates an AbortController for chunk timeout:
const chunkAbortCtl = typeof chunkTimeout === "number" && chunkTimeout > 0 ? new AbortController() : undefinedThis IS timeout-based -- it could use abortAfter(chunkTimeout). However, the timeout is reset per-chunk (not a single timeout), so the manual approach is actually correct here.
---
ANTI-PATTERN: Direct existsSync instead of Filesystem.exists
Locations: storage/json-migration.ts, storage/db.ts, config/tui.ts, config/config.ts, cli/cmd/tui/attach.ts
Verdict: MIXED.
storage/db.tsandconfig/config.tsuseexistsSyncduring synchronous initialization (e.g., insidelazy()oriife()) whereFilesystem.exists(which is async-wrapped but sync internally) would require unnecessary awaiting. Correct avoidance.config/tui.tsandcli/cmd/tui/attach.tsuseexistsSyncin contexts where async is fine -- these COULD useFilesystem.existsbut the difference is negligible sinceFilesystem.existsinternally callsexistsSyncanyway.
---
ANTI-PATTERN: new AsyncLocalStorage instead of LocalContext.create
Verdict: No violation found. The only new AsyncLocalStorage is inside LocalContext.create itself.
---
ANTI-PATTERN: Manual lazy initialization instead of lazy()
Verdict: No violation found. The let loaded = false pattern only appears in lazy.ts itself.
---
ANTI-PATTERN: Promise.race for timeout instead of withTimeout
Verdict: No violation found. The only Promise.race is inside withTimeout itself.
---
ANTI-PATTERN: Manual JSON.parse(await readFile(...)) instead of Filesystem.readJson
Verdict: The only occurrence is inside `Filesystem.readJson` itself. All external code uses the helper.
---
ANTI-PATTERN: Lock.read/Lock.write are never imported directly
Lock is used ONLY by storage/storage.ts. This is correct -- Lock is the internal concurrency mechanism for the Storage layer. Other modules should use Storage's API (which handles locking internally) rather than Lock directly.
---
ANTI-PATTERN: Two duration formatters
`format.ts` `formatDuration(secs)` and `locale.ts` `Locale.duration(ms)`
These take different units (seconds vs milliseconds) and have different output formats. They are NOT redundant -- formatDuration is for tool/server display (takes seconds), Locale.duration is for TUI display (takes milliseconds). However, this IS a footgun -- always check the input unit.
---
ANTI-PATTERN: scrap.ts exists in production
scrap.ts contains dummy test functions. It is only imported by cli/cmd/debug/index.ts. This is acceptable as a development scratch file but should not be imported by any production code.
---
ANTI-PATTERN: signal(), abort.ts, archive.ts, update-schema.ts, color.ts -- Defined but barely/never used
These utilities exist but have zero or near-zero direct imports in src/. They represent either: 1. Infrastructure for features not yet built 2. Utilities used by external consumers (SDK users) 3. Deprecated but not yet removed
An LLM should still USE these rather than reinvent them if the functionality matches.
---
CORRECT PATTERN: Effect vs vanilla utility split
The codebase maintains a clean split:
- Effect-based modules use
InstanceState,makeRuntime,Context.Service,ScopedCache,PubSub,cross-spawn-spawner - Vanilla async modules use
LocalContext.create(the@/util/local-contextAsyncLocalStorage helper, distinct from Effect'sContext.Service),lazy,Lock,Flock,Process,Filesystem,git
Never cross these boundaries. Do not use the @/util/local-context LocalContext.create inside an Effect service. Do not use InstanceState outside of an Effect service.
Related skills
FAQ
What does opencode-ts do?
opencode-ts: A skill for development. This provides functionality for development workflows.
When should I use opencode-ts?
When you need to use opencode-ts for development tasks, or when opencode-ts: a skill for development. this provides functionality for development workflows.
What are the main capabilities?
opencode-ts.