* refactor(settings): settings registry and intent-gated writes
Problem: every setting lived in a flat document with ten hand-maintained
key lists that had drifted (three keys the server silently dropped, five
it kept that nothing read), and three code paths wrote to the server
without a person changing anything: the theme persist effect on mount,
bootstrap seeding of server-missing keys, and the auto-save echoing
values just adopted from the server.
Approach: one registry (packages/ui/src/lib/settings/registry.ts) names
every key with its scope (instance / profile / device), a boundary parser
and its store binding; DesktopSettings, the sanitizer, the mirror, the
apply step and the auto-save derive from it. A generated JSON snapshot
carries the key list to the server and the VS Code bridge. Writes carry
intent: the theme context writes only from its user-facing setters, a
missing server key leaves the local store alone instead of resetting it,
updateDesktopSettings drops values the server already holds, and the
auto-savers treat values applied from the server as a new baseline.
Testing: bun test packages/ui (registry + persistence suites cover zero
writes on load, dedup, toggle-back cancellation, failed-save retry, and
snapshot freshness); tsc for every workspace.
* refactor(ui): read and write settings through the shared path only
Problem: fourteen pages and stores fetched /api/config/settings on their
own and re-parsed the raw document by hand, so the registry could not
guard them and two of them treated a failed load as an empty list.
Approach: loadDesktopSettings() and updateDesktopSettings() (which now
resolves { ok }) replace every direct call; SkillsCatalogPage and
AddCatalogDialog refuse to write the catalog list until it is known.
Testing: bun test packages/ui (403 files), eslint on the changed files.
* refactor(server): validate settings writes against the registry snapshot
Problem: the server whitelist was the only guard on PUT /api/config/settings
and had drifted from the client; dead keys were still persisted.
Approach: settings-helpers.js drops any key the generated registry
snapshot does not list as persistable and strips secret keys from
responses; the dead keys (markdownDisplayMode, toolCallExpansion,
typographySizes, expandedEditorToolbar, gitProviderId/gitModelId) are
gone; the profile keys that were client-only now round-trip. A drift
test requires a valid sample for every persistable registry key.
Testing: vitest run in packages/web (182 files), including the packed
tarball import.
* refactor(vscode): gate bridge settings writes by the registry
Problem: the extension host wrote any key the webview sent straight into
settings.json, and commit-message generation read the dead
gitProviderId/gitModelId pair instead of the small-model setting.
Approach: filterPersistableSettingsChanges applies the registry snapshot
before the file write; chooseBridgeGitGenerationModel honours
smallModelUseDefault/smallModelOverride ahead of the zen fallback.
Testing: bun test packages/vscode (37 files), tsc, build:extension.
* feat(settings): split the user's profile into preferences.json
Problem: one flat settings.json held instance facts, the user's
preferences and device state together, so device state travelled between
installs and the profile had no document of its own to sync from.
Approach: the server keeps one merged document for clients but routes
each key by registry scope on disk (settings-files.js): profile keys go to
preferences.json as { value, updatedAt } entries stamped when the value
changes, everything else stays in settings.json, device keys are dropped
from writes. A missing preferences.json is seeded once from settings.json,
which is left intact; an unreadable one is a failure that pauses profile
writes and never gets overwritten. Server modules that read a profile key
off the disk use the merged sync read. Electron main reads the theme mode
from both files and now owns the splash colours, handed over the
window-theme IPC instead of the settings document. Clients stop sending
device keys, seed them once from a pre-split document, and persist
inputBarOffset locally. The PWA manifest keys are instance facts.
Testing: vitest in packages/web (seed, split write, timestamp retention,
unreadable file), bun test in packages/ui and packages/electron, tsc for
every workspace.
* feat(vscode): write the profile to preferences.json from the extension host
Problem: the extension host writes the shared settings files directly and
had to follow the server's split, and its file writes reported success on
failure.
Approach: settings-files.ts mirrors the server's format and split rules
(seed once, unreadable preferences.json is a failure); persistSettings
routes profile keys to preferences.json and the rest to settings.json,
and the atomic writers now throw so a failed save reaches the webview.
Clearing a key now actually removes it from the owning file.
Testing: bun test packages/vscode (38 files), tsc, build:extension.
* feat(settings): store the per-surface profile fields by surface kind
Problem: theme, chat-layout switches and typography sizes are one value
for every client of an instance, so the phone and the desktop cannot
disagree without a hard-coded runtime branch.
Approach: every settings request carries the client's surface kind in the
x-openchamber-surface header (web, desktop, vscode, mobile — the phone app
and the hosted mobile shell are one kind). For the registry's perSurface
keys the store writes a changed value under fields[key].surfaces[kind] in
preferences.json and never touches the base from a surface; reads resolve
the kind's own value, then the base, then nothing. Writes without a
surface (migrations, the seed) set the base. The VS Code host is always
vscode; Electron main resolves desktop for the native window theme. The
Settings UI is unchanged.
Testing: vitest in packages/web (surface write/read, no base copy, unknown
surface falls back to base), bun test in packages/vscode and packages/ui,
tsc for every workspace, build:extension.
* fix(settings): keep a legacy copy of the profile in settings.json
The first write after the split rewrote settings.json with the instance
part only, and that write happens on startup (relay reconcile). A build
from before the split reads only settings.json, so rolling back would
have lost every preference: theme, default model, all of it.
Every write now stores the profile's base values in settings.json next
to the instance part (`legacySettingsDocumentOf`), on the server and in
the VS Code extension host alike. Current builds ignore the copy because
preferences.json wins in the merged read. When preferences.json is
unreadable the copy already on disk is kept rather than dropped.
Testing: settings-runtime tests updated for the copy; full web suite
(182 files), VS Code tests and extension build, tsc clean. Verified live
on a scratch OPENCHAMBER_DATA_DIR: all 136 keys survive startup, theme
changes land per surface, plain keys land in the base.
* feat(settings): make the UI password and tunnel preset tokens write-only
GET /api/config/settings returned desktopUiPassword and the managed
remote tunnel preset tokens to every authenticated client, including
paired phones and the VS Code webview that never need them.
Both keys are now `secret` in the registry: accepted on write, withheld
from reads. The server answers with a hasDesktopUiPassword flag; the
desktop network page shows "Password set" and sends a value only when
the user types a new one or presses "Remove password" (an empty string
clears it and turns LAN access off). The tunnel page already learned
token presence from the status endpoint. The VS Code bridge strips
secret keys from what it hands the webview while still merging them
from disk on write.
Testing: registry, i18n parity, server settings, VS Code gate tests and
tsc; workspace type-check. Verified against a scratch server: GET
carries the flag and no password, PUT with '' clears, PUT with a value
sets. The desktop-only page itself awaits the owner's run.
* fix(settings): send the surface kind as a query parameter, not a header
The packaged desktop shell (openchamber-ui://app) and the phone app are
cross-origin to the OpenChamber server, so the x-openchamber-surface
header turned every settings request into a CORS preflight the server
did not allow. Settings looked reset and every save reported "Save
failed" without reaching persistSettings. An older remote instance would
refuse the header the same way even with the allow-list fixed.
The client now sends ?surface=<kind>, which keeps the request
CORS-simple on every server version; the server reads the query
parameter and still honours the header. The header is also in the CORS
allow-list for completeness.
Testing: workspace type-check, persistence and registry tests, server
opencode tests. On a scratch server: PUT with ?surface=vscode lands
under surfaces.vscode, GET without or with an unknown surface serves the
base, the header fallback resolves. Confirmed in the owner's rebuilt
desktop and on the phone.
* refactor(settings): drop the show-password toggle from the desktop network page
With the password write-only, the field only ever holds a value the user
is typing right now; the reveal toggle and its strings are gone from
every locale.
* refactor(projects): serve project setup through the server, drop the legacy migration
The shared UI read and wrote ~/.config/openchamber/projects/<id>.json
itself: it resolved the home directory, composed the path, and used the
Files API, which only desktop and VS Code have natively and which cannot
see a remote instance's file at all. It also still carried the months-old
migration from <repo>/.openchamber/openchamber.json, which deleted files in
the folder the upcoming shared project config will use.
The client-owned keys (worktree setup commands, project actions, draft
starters) now live behind GET/PUT /api/projects/:projectId/config.
project-setup.js sanitizes and builds the view; the project-config runtime
merges a patch under the same cross-process lock the scheduled-task writers
hold, so unknown and server-owned keys survive. A wrongly shaped key is a
400, not a silent drop. openchamberConfig.ts keeps its exported functions
and is now an HTTP client. The VS Code webview handles the route locally
and bridges to the extension host, which owns the file with a TS mirror of
the sanitizers.
Testing: server tests for sanitizers, round trip, lock, and invalid patch;
client tests against a mocked route; VS Code sanitizer and bridge tests;
workspace type-check, both VS Code builds, UI isolated suite (409 files),
server projects and project-context suites. Live GET/PUT against a
running server with the owner's real project config.
* feat(projects): read the team's shared config and merge it with the personal one
A project can now carry <repo>/.openchamber/project.json (version 1:
setupWorktree, setupWorktreeWait, projectActions, draftStarters,
plansDir). The server finds the checkout from the path-derived project
id, parses the file, and answers GET /api/projects/:id/config with one
merged view: what runs at the top level, plus shared and personal blocks
so a page can edit the personal file without copying a teammate's entry
into it.
Merge rules: shared setup commands run first (a personal
setupWorktreeMode of "replace" uses the personal list only); the
personal wait flag wins when set; actions union by id with a personal
action replacing the shared one and personal hiddenSharedActionIds
dropping shared ones; starters union by type:name; the primary action is
personal only. A shared file that exists but cannot be parsed, or that
names a plansDir outside the repo, is reported as invalid with a reason
and never treated as "no shared setup". Nothing writes the repo file yet.
Client: getProjectSetup exposes the view; the existing helpers return
effective values, while the Projects page sections and the draft
starters hook edit the personal block only. Shared entries show a quiet
"shared" mark in the actions dropdown and read-only lists above the
editable ones on the Projects page; shared starter chips have no remove
handle. The VS Code extension host mirrors the parser and merge.
Testing: server tests for the parser, plansDir guard, merge table, id
round trip, and a runtime test against a temp checkout; client tests
against a mocked route; VS Code sanitizer, merge, and bridge tests; the
section test covers the shared row; locale parity; workspace type-check;
UI isolated suite (409 files). Live: GET against a temp repo with a
shared file and with a broken one.
* feat(projects): ask before the team's shared commands run, once per set of commands
Shared setup commands and shared actions come from a file a git pull can
change, and they run on the machine of whoever pulls. The first time one
would run, a dialog now shows exactly what would run and asks: "Trust and
run" or "Not this time". A "trust" answer is recorded in the personal
config against a SHA-256 of the executable parts (setup commands and each
action's id, command, and runIn; renames and icons do not count), so a
pull that changes a command brings the prompt back. Nothing asks when the
shared file has nothing that executes.
Worktree creation (session creator, new-worktree dialog, session store,
multi-run launcher, agent-manager empty state) resolves its commands
through the prompt; "not this time" runs only the user's own commands.
The actions dropdown asks before a shared action runs. The Projects page
shows "Trusted on this instance" with a "Reset trust" button next to the
shared actions. The dialog is mounted beside the app-link confirmation on
every shell. The VS Code extension host mirrors the hash and the record.
Testing: server tests for hash stability, ordering, and the trusted flag,
plus a runtime test that changes the shared file and sees trust drop;
client tests for the confirmation store (ask, trust, skip, replace mode,
newer request, failed record, reset); VS Code mirror tests; the actions
button, new-worktree dialog, and issue-2039 tests updated for the trust
path; locale parity; workspace type-check; UI isolated suite (410 files).
* feat(projects): share and unshare setup with the team from the Projects page
The repo file <repo>/.openchamber/project.json is now written by the app,
and only when the user shares something: nothing appears in a repository
until then. PUT /api/projects/:id/config/shared replaces the keys it
names over the current file, writes it pretty-printed with version first
and only the keys that carry something, removes the file (and an empty
.openchamber folder) when nothing is left, refuses a missing checkout or
a plansDir outside the repo, and records trust for the writer, who has
seen what they shared.
On the Projects page, actions and setup commands get "Share with team"
and "Make personal"; shared actions can be hidden for this user; a
checkbox switches to "Use only my setup commands". Project starter chips
get share and make-personal hover buttons. A new "Shared config" block
shows the file's path and status, the shared plans folder, and the trust
status with "Reset trust". A share is a repo write followed by a personal
write; a failure after the first leaves the item visible once, as
personal. The VS Code extension host mirrors the writer.
Testing: server tests for the patch, serialization, emptiness, the write
and removal round trip, the writer's trust record, and the refusals;
client test for the shared route; VS Code bridge test for write and
removal; locale parity; workspace type-check; UI isolated suite (410
files). Live on a scratch server: share, invalid plansDir (400), unshare
to removal of file and folder.
* feat(projects): list, edit, and move plans in the team's shared plans folder
When the shared config names a plansDir, every markdown file in that
folder is a plan on the Plans tab: listed after the user's own plans,
marked shared, addressed as shared:<file>, read and edited in place
(the raw document is written verbatim, so a plan another tool wrote
keeps its shape), and deletable. Share moves one of the user's plans
into the folder; make personal moves it back under a new id; a name
collision gets a numeric suffix. Sharing is refused, with a hint in the
panel, until a shared plans folder is set in Project settings. This
answers the request to read plans from an existing folder such as
docs/plans.
Server: the project-context runtime takes resolveSharedPlansDir from the
project-config runtime; readContext reports sharedPlansDir; POST
.../plans/:id/share and /unshare. Client: movePlan in the context store,
a shared badge and a share / make-personal button per plan row. Session
attachments reference plan ids, so an attached plan that moves has to be
attached again.
Testing: runtime tests for listing, foreign markdown titles, id
traversal, in-place update and delete, share and unshare with a
collision, and the refusal without a folder; HTTP route tests; store and
locale parity tests; workspace type-check; full web suite (183 files);
UI isolated suite (410 files). Live on a scratch server against a temp
repo: list, share, read, unshare.
* fix(server): make OPENCHAMBER_DATA_DIR move every folder, not just the flat files
The variable is documented as the OpenChamber data directory, but only
settings, preferences, auth, and push files followed it; projects,
themes, speech models, and the chats default stayed under
~/.config/openchamber. A second instance started with a custom
directory therefore read and wrote the default instance's project
configs.
Every folder now hangs off the one root. An instance that already used
a custom directory gets projects, themes, and speech-models copied in
once at startup; copied, not moved, so a second instance beside the
default one cannot strip it, and nothing is merged into a folder that
already exists. Existing managed chats are not copied, as with
OPENCHAMBER_CHATS_DIR.
Testing: migration tests for copy-once, no-merge, and same-root no-op;
full web suite; a scratch server with an empty data dir copied the real
project configs and kept its writes in the copy.
* fix(projects): keep a plan's id when it moves into or out of the repository folder
A plan moved into the repository plans folder used to be listed under a
new shared:<file> id, so a session that had attached it lost the
attachment. The manifest entry now stays with a `shared` flag that says
which folder holds the file; the id survives both directions. Only a
plan that never had an entry (one written by another tool) gets an id
when it is brought in. A personal file and a repository file may share
a name because they live in different folders.
Testing: runtime tests for share and unshare with a stable id, reading
and editing the moved plan, the suffix on a name collision, and the
adoption of a foreign file.
* feat(projects): default repository plans folder, "move to repository" wording, tooltips
Plans now have a repository folder without any setup: .openchamber/plans
by default. A custom plansDir replaces the default outright (only that
folder is read and written; moving files between the two is the user's
job), and the field's placeholder and hint say so. The move buttons on
plans are therefore always available.
The word "share" is gone from the UI: it read like publishing, while
the action stores an item in the repository so everyone who pulls it
gets it. Labels are "Move to repository" / "Move to my settings", the
badge is "In repo", the block is "Repository config", and every button
on the Projects page carries a tooltip that says what happens (the
"Move to repository" button explains that edits save first while the
form is dirty). The trust status with "reset trust" moved from the
repository block into the Worktree section next to the commands it
guards; the plan row's badge sits beside the title.
Testing: locale parity, section test, workspace type-check, UI isolated
suite (410 files), full web suite.
* fix(projects): leave the icon key out of the repository file when an action has none
Actions without an icon were written as "icon": null into
.openchamber/project.json. The key is now omitted; readers already fall
back to the play icon. Server and VS Code serializers, tests updated.
* docs: describe the repository config file and how items move into it
A new page in every locale: what stays personal and what can move into
the repository, the .openchamber/project.json format with an example
and every key explained (setup commands, actions with the supported icon
names, starters, plansDir), the merge rules, the trust prompt, and plans
in the repository. Linked from the sidebar and from Project Actions.
Translations written by hand.
445 lines
36 KiB
Markdown
445 lines
36 KiB
Markdown
# OpenCode Module Documentation
|
|
|
|
## Purpose
|
|
This module provides OpenCode server integration utilities for the web server runtime, including configuration management and provider authentication.
|
|
|
|
## Entrypoints and structure
|
|
- `packages/web/server/lib/opencode/index.js`: public entrypoint (currently baseline placeholder).
|
|
- `packages/web/server/lib/opencode/auth.js`: provider authentication file operations.
|
|
- `packages/web/server/lib/opencode/auth-state-runtime.js`: managed OpenCode server auth password/header runtime.
|
|
- `packages/web/server/lib/opencode/cli-options.js`: CLI/environment option parsing for server startup arguments.
|
|
- `packages/web/server/lib/opencode/cli-entry-runtime.js`: CLI entrypoint runtime that detects direct execution, parses CLI options, and starts server bootstrap.
|
|
- `packages/web/server/lib/opencode/routes.js`: OpenCode/provider settings and auth-related route registration.
|
|
- `packages/web/server/lib/opencode/lifecycle.js`: OpenCode process lifecycle runtime (startup, restart, readiness, health monitoring). After readiness it warms the most recently used directories (`getWarmupDirectories` dep, sequential and best-effort) because OpenCode initializes each directory lazily on first request and that cost would otherwise be paid by the user's first interactive session open.
|
|
- `packages/web/server/lib/opencode/provider-env-aliases.js`: mirrors known provider credential env aliases into the managed OpenCode process environment (for example `GEMINI_API_KEY` → `GOOGLE_GENERATIVE_AI_API_KEY`) so OpenCode connection detection and the upstream AI SDK agree on the same key names. Canonical implementation shared by web lifecycle and the VS Code managed spawn path (`packages/vscode/src/provider-env-aliases.ts` re-exports this module).
|
|
- `packages/web/server/lib/opencode/env-runtime.js`: OpenCode CLI/binary resolution and shell environment runtime.
|
|
- `packages/web/server/lib/opencode/env-config.js`: OpenCode-related environment variable parsing and validation (host/port/hostname).
|
|
- `packages/web/server/lib/opencode/hmr-state-runtime.js`: HMR-persistent runtime state initialization, auth-state bootstrap, and HMR sync helpers.
|
|
- `packages/web/server/lib/opencode/bootstrap-runtime.js`: base app bootstrap runtime for status/auth/tts/notification/OpenChamber route wiring.
|
|
- `packages/web/server/lib/opencode/network-runtime.js`: OpenCode URL construction, health-probe readiness checks, and API prefix runtime.
|
|
- `packages/web/server/lib/opencode/project-directory-runtime.js`: request-scoped and settings-backed project directory resolution/validation runtime.
|
|
- `packages/web/server/lib/opencode/config-entity-routes.js`: route registration for agent/command/MCP config orchestration with deferred-apply semantics (`restartDeferred` payloads; explicit apply via `POST /api/config/reload`).
|
|
- `packages/web/server/lib/opencode/config-mutation-response.js`: shared response builders for deferred OpenCode restarts and external manual-restart guidance.
|
|
- `packages/web/server/lib/opencode/snippets.js`: opencode-snippets-compatible snippet file CRUD, discovery, and hashtag expansion.
|
|
- `packages/web/server/lib/opencode/cli-options.js`: CLI/environment option parsing for server startup arguments.
|
|
- `packages/web/server/lib/opencode/core-routes.js`: server status/system routes, auth/access guard routes, and settings utility route registration.
|
|
- `packages/web/server/lib/opencode/shutdown-runtime.js`: graceful shutdown orchestration runtime for watcher/session/terminal/process/server teardown.
|
|
- `packages/web/server/lib/opencode/server-startup-runtime.js`: server listen/startup tunnel flow and process/signal handler orchestration runtime.
|
|
- `packages/web/server/lib/opencode/static-routes-runtime.js`: static asset/SPA fallback route registration and manifest route wiring.
|
|
- `packages/web/server/lib/opencode/feature-routes-runtime.js`: feature route composition runtime for dynamic import-backed config/skill/provider route registration.
|
|
- `packages/web/server/lib/opencode/opencode-resolution-runtime.js`: OpenCode binary resolution snapshot runtime for settings routes and diagnostics.
|
|
- `packages/web/server/lib/opencode/upgrade-capability.js`: authoritative upgrade ownership policy for the active OpenCode runtime. Bundled, external, and unresolved runtimes fail closed; only managed non-bundled runtimes delegate upgrades to OpenCode.
|
|
- `packages/web/server/lib/opencode/tunnel-wiring-runtime.js`: tunnel service/routes composition runtime and active-port wiring for main server startup.
|
|
- `packages/web/server/lib/opencode/startup-pipeline-runtime.js`: server startup tail orchestration runtime for terminal/proxy/static/start-listen flow.
|
|
- `packages/web/server/lib/opencode/startup-performance.js`: opt-in startup phase diagnostics with fixed labels and numeric metadata allowlists.
|
|
- `packages/web/server/lib/agent-tool/runtime.js`: managed OpenCode custom-tool materialization, environment injection, loopback authentication, and fixed CLI action dispatch.
|
|
- `packages/web/server/lib/system-prompt/runtime.js`: opt-in managed OpenCode system-prompt optimizer materialization and plugin injection.
|
|
- `packages/web/server/lib/mcp-reconnect/runtime.js`: always-on managed OpenCode plugin that reconnects MCP servers OpenCode marked `failed`, with per-server backoff.
|
|
- `packages/web/server/lib/opencode/managed-plugin-config.js`: the one `OPENCODE_CONFIG_CONTENT` merge every managed plugin (agent tools, system prompt optimizer, MCP reconnect) appends itself through.
|
|
- `packages/web/server/lib/opencode/server-utils-runtime.js`: shared server runtime utilities for OpenCode proxy wiring, OpenCode port/readiness helpers, and snapshot fetchers.
|
|
- `packages/web/server/lib/opencode/openchamber-routes.js`: OpenChamber update and models metadata route registration.
|
|
- `packages/web/server/lib/opencode/pwa-manifest-routes.js`: PWA manifest route registration with recent-session shortcut resolution and short-lived caching.
|
|
- `packages/web/server/lib/opencode/project-icon-routes.js`: project icon upload/read/discovery route registration and icon storage orchestration.
|
|
- `packages/web/server/lib/opencode/skill-routes.js`: route registration for skill config CRUD, supporting files, and skills catalog scan/install flows.
|
|
- `packages/web/server/lib/opencode/settings-runtime.js`: Settings persistence runtime (disk IO, migrations, normalization, project validation, and persisted update serialization).
|
|
- `packages/web/server/lib/opencode/settings-helpers.js`: Settings payload sanitization/format helpers runtime for response shaping and persisted merge prep.
|
|
- `packages/web/server/lib/opencode/settings-normalization-runtime.js`: path/settings/tunnel normalization and sanitization helpers runtime used by settings/routes/config wiring.
|
|
- `packages/web/server/lib/opencode/theme-runtime.js`: custom theme JSON validation and theme directory loading runtime for settings utility routes.
|
|
- `packages/web/server/lib/opencode/proxy.js`: OpenCode API/SSE forwarding and readiness-gate route registration.
|
|
- `packages/web/server/lib/opencode/session-runtime.js`: session status/attention/activity runtime for OpenCode SSE events.
|
|
- `packages/web/server/lib/opencode/watcher.js`: global SSE watcher runtime for push/session event fanout.
|
|
- `packages/web/server/lib/opencode/shared.js`: shared utilities for config, markdown, skills, and git helpers.
|
|
- `packages/web/server/lib/ui-auth/ui-auth.js`: UI session authentication runtime (outside OpenCode module).
|
|
- `packages/web/server/lib/ui-auth/ui-passkeys.js`: UI passkey storage and WebAuthn registration/authentication helpers (outside OpenCode module).
|
|
|
|
## Public exports (auth.js)
|
|
- `readAuthFile()`: Reads and parses `~/.local/share/opencode/auth.json`.
|
|
- `writeAuthFile(auth)`: Writes auth file with automatic backup.
|
|
- `removeProviderAuth(providerId)`: Removes a provider's auth entry.
|
|
- `getProviderAuth(providerId)`: Returns auth for a specific provider or null.
|
|
- `listProviderAuths()`: Returns list of provider IDs with configured auth.
|
|
- `AUTH_FILE`: Auth file path constant.
|
|
- `OPENCODE_DATA_DIR`: OpenCode data directory path constant.
|
|
|
|
## Public exports (providers.js)
|
|
- `getProviderSources(providerId, workingDirectory)`: Resolves which OpenCode config layers define a provider.
|
|
- `upsertProviderConfig(providerId, config, workingDirectory, scope?, options?)`: Validates and writes a custom provider block (`npm`, `name`, `options.baseURL`, `models`, optional `env`/`headers`) into the user/project/custom config layer. The adapter may be OpenAI Chat Completions, OpenAI Responses, or Anthropic Messages. Existing provider, option, and retained-model fields not managed by the form are preserved; omitted models, headers, and env credentials remain explicit removals. Updating a legacy `providers` entry migrates it to the canonical `provider` key. Does not store API keys. Requires `config.env` or `options.hasStoredAuth` (auth already written via OpenCode `auth.set`). Edit flows must pass the provider's effective existing layer (`custom` > `project` > `user`) so updates do not create a global user override.
|
|
- `validateCustomProviderConfig(providerId, config, options?)`: Structural validation for custom provider payloads (id format, adapter allowlist `@ai-sdk/openai-compatible`/`@ai-sdk/openai`/`@ai-sdk/anthropic`, http(s) base URL, models, credentials via `env` or `hasStoredAuth`).
|
|
- `removeProviderConfig(providerId, workingDirectory, scope?)`: Removes a provider block from the selected config layer.
|
|
|
|
## Public exports (shared.js)
|
|
- `OPENCODE_CONFIG_DIR`, `AGENT_DIR`, `COMMAND_DIR`, `SKILL_DIR`, `CONFIG_FILE`: Path constants rooted at `$XDG_CONFIG_HOME/opencode` when `XDG_CONFIG_HOME` is non-empty, otherwise `~/.config/opencode`. These constants are evaluated when the module loads; no files are migrated. `OPENCODE_CONFIG` remains a separate explicit config-file path and is resolved at call time for the custom config layer; it does not replace the global config directory.
|
|
- `AGENT_SCOPE`, `COMMAND_SCOPE`, `SKILL_SCOPE`: Scope constants with USER and PROJECT values.
|
|
- `ensureDirs()`: Creates required OpenCode directories.
|
|
- `parseMdFile(filePath)`, `writeMdFile(filePath, frontmatter, body)`: Markdown file operations with YAML frontmatter.
|
|
- `getConfigPaths(workingDirectory)`, `readConfigLayers(workingDirectory)`, `readConfig(workingDirectory)`: Config file operations with layer merging (user, project, custom). `readConfigLayers` isolates `INVALID_JSONC` per layer: a broken file is omitted from the merge (`{}` for that layer only), recorded on `layerErrors`, and does not block valid sibling layers. Writes still refuse to overwrite the broken file.
|
|
- `readConfigFile(filePath)`: Reads one config file. Missing, whitespace-only, and comment-only files return `{}`; a comment-only file is recognized by `ValueExpected` being the only parse error. A `jsonc-parser` error that produces a partial or non-object tree throws `INVALID_JSONC` — partial parse trees must never be treated as authoritative (avoids rewriting a `$schema`-only stub over a full config). Content that yields no JSON value for any other reason (YAML, plain text) also throws instead of reading as empty.
|
|
- `readConfigLayer(filePath)`: Same parse as `readConfigFile`, but isolates `INVALID_JSONC` to `{ config: {}, error }` so plugin/MCP/agent readers can skip one broken layer without aborting valid siblings. Writes still refuse to overwrite the broken file.
|
|
- `writeConfig(config, filePath)`: Writes config with automatic backup. Refuses to overwrite an existing non-empty file that fails the same JSONC parse check.
|
|
- `getJsonEntrySource(layers, sectionKey, entryName)`: Resolves which config layer provides an entry. A failed custom or user layer throws `INVALID_JSONC` instead of treating that file as empty. A failed project layer is skipped so a valid user/custom entry can still be found.
|
|
- `getJsonWriteTarget(layers, preferredScope)`: Determines write target for config updates. Throws `INVALID_JSONC` when the chosen target file is the unparseable layer.
|
|
- `getAncestors(startDir, stopDir)`, `findWorktreeRoot(startDir)`: Git worktree helpers.
|
|
- `isPromptFileReference(value)`, `resolvePromptFilePath(reference)`, `writePromptFile(filePath, content)`: Prompt file reference handling.
|
|
- `walkSkillMdFiles(rootDir)`: Recursively finds all SKILL.md files.
|
|
- `addSkillFromMdFile(skillsMap, skillMdPath, scope, source)`: Parses and indexes a skill file.
|
|
- `resolveSkillSearchDirectories(workingDirectory)`: Returns skill search path order (config, project, home, custom).
|
|
- `listSkillSupportingFiles(skillDir)`, `readSkillSupportingFile(skillDir, relativePath)`, `writeSkillSupportingFile(skillDir, relativePath, content)`, `deleteSkillSupportingFile(skillDir, relativePath)`: Skill supporting file management.
|
|
|
|
## Public exports (routes.js)
|
|
- `registerOpenCodeRoutes(app, dependencies)`: Registers OpenCode-owned HTTP routes and internal module runtime:
|
|
- `GET /api/config/settings`
|
|
- `PUT /api/config/settings`
|
|
- `GET /api/config/opencode-resolution`
|
|
- `POST /api/opencode/upgrade` (enforces the active runtime's upgrade capability, serializes supported OpenCode upgrades, then restarts managed OpenCode so the new binary is active)
|
|
- `GET /api/opencode/upgrade-status` (returns version availability plus the authoritative `upgrade.supported`, `upgrade.manager`, and `upgrade.reason` capability)
|
|
- `POST /api/opencode/directory` (validates and activates an existing project directory; `{ create: true }` explicitly creates the requested project directory before activation, including outside the previously active workspace)
|
|
- `GET /api/provider/:providerId/source`
|
|
- `PUT /api/provider` (create/update custom OpenAI-compatible provider config in OpenCode user/project/custom layers via `scope`; secrets stay in auth via the OpenCode auth API)
|
|
- `DELETE /api/provider/:providerId/auth`
|
|
- Owns lazy auth library loading for provider auth checks/removal.
|
|
- Keeps route behavior independent from composition root; `index.js` now supplies dependencies only.
|
|
|
|
## Public exports (session-runtime.js)
|
|
- `createSessionRuntime({ writeSseEvent, getNotificationClients, broadcastEvent? })`: creates runtime-owned state machine and APIs for session status.
|
|
- Returned API:
|
|
- `processOpenCodeSsePayload(payload)`
|
|
- `getSessionActivitySnapshot()`
|
|
- `getActiveSessionCount()`
|
|
- `getSessionStateSnapshot()`
|
|
- `getSessionAttentionSnapshot()`
|
|
- `getSessionState(sessionId)`
|
|
- `getSessionAttentionState(sessionId)`
|
|
- `markSessionViewed(sessionId, clientId)`
|
|
- `markSessionUnviewed(sessionId, clientId)`
|
|
- `markUserMessageSent(sessionId)`
|
|
- `resetAllSessionActivityToIdle()`
|
|
- `interruptBusySessionsAfterRestart()`: settles every session whose authoritative status is `busy`/`retry` or whose activity phase is still busy, broadcasts `openchamber:session-status` idle plus an OpenCode-shaped `session.error`, resets leftover activity/cooldowns, and returns the interrupted session IDs in stable order.
|
|
- `dispose()`
|
|
|
|
The runtime maintains active-session count incrementally from idempotent activity phase transitions. Upstream stall-timeout and lifecycle health checks read it in O(1); the hourly cleanup removes activity phases older than 24 hours without broadcasting synthetic state transitions. Snapshot generation remains reserved for the session-activity API.
|
|
|
|
## Public exports (lifecycle.js)
|
|
- `createOpenCodeLifecycleRuntime(dependencies)`: creates lifecycle runtime for managed/external OpenCode process orchestration. The optional `onOpenCodeRestarted` dependency (default `null`) is fired after a successful managed restart. `index.js` rebinds event-stream readers to the possibly-new port (#2638), then calls `interruptBusySessionsAfterRestart()` and broadcasts one `opencode-restart-interrupted` UI notification when interrupted turns exist (#2943).
|
|
- Returned API:
|
|
- `startOpenCode()`
|
|
- `restartOpenCode()`
|
|
- `waitForOpenCodeReady(timeoutMs?, intervalMs?)`
|
|
- `waitForAgentPresence(agentName, timeoutMs?, intervalMs?)`
|
|
- `refreshOpenCodeAfterConfigChange(reason, options?)`
|
|
- `bootstrapOpenCodeAtStartup()`
|
|
- `startHealthMonitoring(healthCheckIntervalMs)`
|
|
- `waitForPortRelease(port, timeoutMs, hostname?)`
|
|
- `killProcessOnPort(port)`
|
|
|
|
Managed OpenCode launch also merges the environment returned by the agent-tool
|
|
runtime, the opt-in system prompt optimizer, and the always-on MCP reconnect
|
|
plugin, each appending its `file://` entry to the previous one's config. PATH and `OPENCODE_SERVER_PASSWORD` remain lifecycle-owned and cannot
|
|
be replaced by injected values. External OpenCode processes receive no
|
|
OpenChamber tool injection. Managed launch env strips AppImage `ARGV0` before
|
|
spawn so zsh-backed OpenCode tools do not rewrite child argv[0] to the AppImage
|
|
path (#2588).
|
|
|
|
Before spawn, `applyProviderEnvAliases` fills unset Google credential aliases
|
|
from any present sibling (`GOOGLE_GENERATIVE_AI_API_KEY`, `GOOGLE_API_KEY`,
|
|
`GEMINI_API_KEY`) so a shell that only exports `GEMINI_API_KEY` still satisfies
|
|
the Generative AI SDK path used at chat time. Existing non-empty values are
|
|
never overwritten.
|
|
|
|
Set `OPENCHAMBER_STARTUP_PERF=1` to emit bounded startup phase records for server listen, managed OpenCode preparation/readiness, and proxy readiness holds. Every OpenCode bootstrap emits one terminal `opencode.bootstrap.ready` or `opencode.bootstrap.error` event, including reused and external server paths. Records contain controlled phase/outcome/route labels and timing values only; they never contain request URLs, runtime keys, directories, session IDs, credentials, or content.
|
|
|
|
macOS `say` voice enumeration starts concurrently with server composition. The server listener and managed OpenCode startup do not wait for it; `/api/tts/say/status` awaits the same authoritative capability promise when queried before enumeration completes.
|
|
|
|
Transport-triggered health checks share the periodic monitor's failure accounting interval. Rapid WS reconnect callbacks therefore cannot exhaust the managed-process restart threshold using one cached unhealthy result; an exited managed process still restarts immediately.
|
|
|
|
Managed health failures are classified as `timeout`, `connection_refused`, `connection_reset`, `invalid_response`, or `error`. The lifecycle retains the latest counted failure with a bounded detail string and source. Managed process wrappers continue capturing a sanitized, bounded stderr tail after readiness and retain exit code/signal. Before replacing a managed process, lifecycle snapshots the reason, latest health failure, process diagnostics/aliveness, busy-session count, and timestamp into `lastOpenCodeRestartDiagnostics`; successful startup does not clear this snapshot, and `/health` exposes it for post-restart diagnosis without process environment or credentials.
|
|
|
|
## Public exports (env-runtime.js)
|
|
- `createOpenCodeEnvRuntime(dependencies)`: creates runtime that owns OpenCode CLI environment and binary discovery state.
|
|
- OpenCode CLI resolution order is persisted settings, environment overrides, bundled Desktop CLI when available, PATH, known install locations, then platform shell discovery.
|
|
- Returned API:
|
|
- `applyLoginShellEnvSnapshot()`
|
|
- `getLoginShellEnvSnapshot()`
|
|
- `ensureOpencodeCliEnv()`
|
|
- `applyOpencodeBinaryFromSettings()`
|
|
- `resolveOpencodeCliPath()`
|
|
- `resolveManagedOpenCodeLaunchSpec(opencodePath)`: resolves the effective managed OpenCode launch target, unwrapping Windows package-manager shims to a direct native binary or explicit runtime+script when possible.
|
|
- `resolveGitBinaryForSpawn()`
|
|
- `resolveWslExecutablePath()`
|
|
- `buildWslExecArgs(execArgs, distroOverride?)`
|
|
- `isExecutable(filePath)`
|
|
- `searchPathFor(binaryName, searchPath?)`: resolves an executable from the supplied PATH value, defaulting to the process PATH.
|
|
- `clearResolvedOpenCodeBinary()`
|
|
|
|
## Public exports (env-config.js)
|
|
- `resolveOpenCodeEnvConfig(options?)`: resolves and validates OpenCode host/port/hostname environment configuration.
|
|
- Returned object fields:
|
|
- `configuredOpenCodePort`
|
|
- `configuredOpenCodeHost`
|
|
- `effectivePort`
|
|
- `configuredOpenCodeHostname`
|
|
|
|
## Public exports (hmr-state-runtime.js)
|
|
- `createHmrStateRuntime(dependencies)`: creates runtime for HMR state container initialization and runtime<->HMR state synchronization.
|
|
- Returned API:
|
|
- `getOrCreateHmrState()`
|
|
- `ensureUserProvidedOpenCodePassword(hmrState)`
|
|
- `getUserProvidedOpenCodePassword(hmrState)`
|
|
- `resolveOpenCodeAuthFromState({ hmrState, userProvidedOpenCodePassword })`
|
|
- `syncStateFromRuntime(hmrState, runtime)`
|
|
- `restoreRuntimeFromState({ hmrState, userProvidedOpenCodePassword })`
|
|
|
|
## Public exports (bootstrap-runtime.js)
|
|
- `createBootstrapRuntime(dependencies)`: creates runtime for base app route bootstrap and UI auth controller initialization.
|
|
- Returned API:
|
|
- `setupBaseRoutes(app, options)`
|
|
|
|
## Public exports (network-runtime.js)
|
|
- `createOpenCodeNetworkRuntime(dependencies)`: creates runtime for OpenCode network and URL concerns.
|
|
- Returned API:
|
|
- `waitForReady(url, timeoutMs?)`
|
|
- `normalizeApiPrefix(prefix)`
|
|
- `setDetectedOpenCodeApiPrefix()`
|
|
- `buildOpenCodeUrl(path, prefixOverride?)`
|
|
- `ensureOpenCodeApiPrefix()`
|
|
- `scheduleOpenCodeApiDetection()`
|
|
|
|
## Public exports (settings-runtime.js)
|
|
- `createSettingsRuntime(dependencies)`: creates settings lifecycle runtime for read/migrate/persist concerns.
|
|
- Returned API:
|
|
- `readSettingsFromDisk()`
|
|
- `readSettingsFromDiskMigrated()`
|
|
- `writeSettingsToDisk(settings)`
|
|
- `persistSettings(changes)`
|
|
- Persistent permission auto-accept policy is stored under `permissionAutoAccept`; execution ownership lives in `lib/permission-auto-accept/`.
|
|
- Queued follow-up messages live in `<data-dir>/message-queue.json`, not in settings; execution ownership lives in `lib/message-queue/`.
|
|
- Shared sidebar preferences are stored as validated top-level fields: `sidebarProjectDisplayMode`, `sidebarSessionGroupingMode`, `sidebarProjectSortOrder`, and `sidebarShowRecentSection`. Device-local picker selection and sticky-header state do not enter either settings file.
|
|
- Two files (`settings-files.js`): `settings.json` holds instance facts and any legacy or unknown keys; `preferences.json` beside it holds every key the generated registry snapshot (`settings-registry.json`) marks `profile`, as `{ version: 1, fields: { key: { value, updatedAt, surfaces? } } }`. Keys the snapshot marks `perSurface` are stored per surface kind: `GET`/`PUT /api/config/settings` read the client's kind from the `surface` query parameter (`settingsSurfaceOf`; the legacy `x-openchamber-surface` header is still honoured, but a header forces a CORS preflight that cross-origin shells and older instances refuse, so clients must not send one) (`web`, `desktop`, `vscode`, `mobile`; anything else means base), `persistSettings(changes, { surface })` writes a changed per-surface key under `surfaces[surface]` and never touches its base, and `readSettingsFromDisk({ surface })` resolves that kind's value first, the base otherwise. Callers without a surface (migrations, the seed, server-side feature writers) read and write the base. `readSettingsFromDisk()` returns the merged document and seeds `preferences.json` once from an existing `settings.json` (which it leaves intact). An existing `preferences.json` that fails to parse is a failure, not an empty profile: it is never seeded or overwritten, the merged read serves the instance part, and `persistSettings` drops profile keys with a warning until the file is fixed or removed. `writeSettingsToDisk(document)` splits by scope and writes `settings.json` as the instance part plus a copy of the profile's base values (`legacySettingsDocumentOf`): a build from before the split reads only that file, so a rollback keeps the user's preferences, while current builds ignore the copy because `preferences.json` wins in the merge; device keys are dropped from writes. Modules that read one profile key off the disk on a hot path use `readMergedSettingsSync`.
|
|
|
|
## Public exports (settings-files.js)
|
|
- `parsePreferencesDocument(raw)`, `serializePreferencesDocument(fields)`, `flattenPreferences(fields)`, `buildPreferencesFields(previousFields, document, now)`, `instancePartOf(document)`, `seedPreferencesFrom(document, now)`, `readMergedSettingsSync({ fs, path, settingsFilePath })`, `getSettingsScope(key)`, `isProfileSettingsKey(key)`, `isDeviceSettingsKey(key)`, `preferencesFilePathFor(settingsFilePath, path)`.
|
|
- The VS Code extension host writes the same two files with the same shape (`packages/vscode/src/settings-files.ts`); format changes go to both.
|
|
|
|
## Public exports (settings-helpers.js)
|
|
- `createSettingsHelpers(dependencies)`: creates settings helper runtime for settings request/response shaping.
|
|
- Returned API:
|
|
- `normalizePwaAppName(value, fallback?)`
|
|
- `sanitizeSettingsUpdate(payload)`
|
|
- `mergePersistedSettings(current, changes)`
|
|
- `formatSettingsResponse(settings)`
|
|
|
|
## Public exports (settings-normalization-runtime.js)
|
|
- `createSettingsNormalizationRuntime(dependencies)`: creates normalization/sanitization runtime for shared settings and tunnel helper logic.
|
|
- Returned API:
|
|
- `normalizeDirectoryPath(value)`
|
|
- `normalizePathForPersistence(value)`
|
|
- `normalizeSettingsPaths(input)`
|
|
- `normalizeTunnelBootstrapTtlMs(value)`
|
|
- `normalizeTunnelSessionTtlMs(value)`
|
|
- `normalizeManagedRemoteTunnelHostname(value)`
|
|
- `normalizeManagedRemoteTunnelPresets(value)`
|
|
- `normalizeManagedRemoteTunnelPresetTokens(value)`
|
|
- `isUnsafeSkillRelativePath(value)`
|
|
- `sanitizeTypographySizesPartial(input)`
|
|
- `normalizeStringArray(input)`
|
|
- `sanitizeModelRefs(input, limit)`
|
|
- `sanitizeSkillCatalogs(input)`
|
|
- `sanitizeProjects(input)`
|
|
|
|
## Public exports (theme-runtime.js)
|
|
- `createThemeRuntime(dependencies)`: creates custom theme runtime for on-disk theme discovery and JSON normalization/validation.
|
|
- Returned API:
|
|
- `normalizeThemeJson(raw)`
|
|
- `readCustomThemesFromDisk()`
|
|
|
|
## Public exports (project-directory-runtime.js)
|
|
- `createProjectDirectoryRuntime(dependencies)`: creates runtime for request/project directory candidate normalization and validation.
|
|
- Returned API:
|
|
- `resolveDirectoryCandidate(value)`
|
|
- `validateDirectoryPath(candidate)`
|
|
- `resolveProjectDirectory(req)`
|
|
- `resolveOptionalProjectDirectory(req)`
|
|
|
|
## Public exports (config-entity-routes.js)
|
|
- `registerConfigEntityRoutes(app, dependencies)`: registers configuration entity routes:
|
|
- Agents: `/api/config/agents/:name` and `/api/config/agents/:name/config`
|
|
- Commands: `/api/config/commands/:name`
|
|
- MCP servers: `/api/config/mcp` and `/api/config/mcp/:name`
|
|
- Snippets: `/api/config/snippets`, `/api/config/snippets/:name`, and `/api/config/snippets/expand`
|
|
- Agent/command/MCP write routes persist config to disk and return a deferred-restart payload (`requiresReload: false`, `requiresRestart: true`, `restartDeferred: true`) instead of restarting OpenCode immediately. The UI accumulates these changes and applies them with `POST /api/config/reload`.
|
|
|
|
## Public exports (config-mutation-response.js)
|
|
- `buildDeferredRestartResponse(message)`: success payload for config mutations that are saved on disk but waiting for an explicit Apply & Restart (`restartDeferred: true`).
|
|
- `buildExternalManualRestartResponse(message)`: success payload when OpenCode is an external process and the operator must restart it manually (`requiresManualRestart: true`).
|
|
|
|
## Public exports (auth-state-runtime.js)
|
|
- `createOpenCodeAuthStateRuntime(dependencies)`: creates runtime for managed OpenCode auth password state and request headers.
|
|
- Returned API:
|
|
- `getOpenCodeAuthHeaders()`
|
|
- `isOpenCodeConnectionSecure()`
|
|
- `ensureLocalOpenCodeServerPassword(options?)`
|
|
|
|
## Public exports (core-routes.js)
|
|
- `registerServerStatusRoutes(app, dependencies)`: registers status/system endpoints:
|
|
- `GET /health`
|
|
- `POST /api/system/shutdown`
|
|
- `GET /api/system/info`
|
|
- `registerAuthAndAccessRoutes(app, dependencies)`: registers browser auth/session exchange and API access middleware:
|
|
- `GET /auth/session`
|
|
- `POST /auth/session`
|
|
- `GET /auth/passkey/status`
|
|
- `POST /auth/passkey/authenticate/options`
|
|
- `POST /auth/passkey/authenticate/verify`
|
|
- `POST /auth/passkey/register/options`
|
|
- `POST /auth/passkey/register/verify`
|
|
- `GET /api/passkeys`
|
|
- `DELETE /api/passkeys/:id`
|
|
- `POST /api/auth/reset`
|
|
- `GET /connect`
|
|
- `POST /api/system/probe-url`
|
|
- `app.use('/api', ...)` auth/tunnel guard
|
|
- `registerSettingsUtilityRoutes(app, dependencies)`: registers small settings utility endpoints:
|
|
- `GET /api/config/themes`
|
|
- `POST /api/config/reload` — applies accumulated deferred OpenCode config changes. Managed OpenCode restarts and returns `requiresReload: true`. External OpenCode returns `requiresManualRestart: true` (changes are already on disk; the connected server must be restarted outside OpenChamber).
|
|
- `registerCommonRequestMiddleware(app, dependencies)`: registers shared request middleware stack:
|
|
- conditional JSON body parser behavior for `/api/*` vs non-API requests
|
|
- URL-encoded parser setup
|
|
- request logging middleware
|
|
|
|
## Public exports (cli-options.js)
|
|
- `parseServeCliOptions(options)`: parses serve CLI flags and environment-derived defaults:
|
|
- Port/host/ui-password
|
|
- Tunnel provider/mode/config/token/hostname
|
|
- Legacy `--tunnel` shorthand normalization
|
|
|
|
## Public exports (cli-entry-runtime.js)
|
|
- `runCliEntryIfMain(dependencies)`: detects direct CLI execution and runs server startup with parsed CLI options.
|
|
|
|
## Public exports (server-utils-runtime.js)
|
|
- `createServerUtilsRuntime(dependencies)`: creates server utility runtime for OpenCode orchestration helpers.
|
|
- Returned API:
|
|
- `setOpenCodePort(port)`
|
|
- `waitForOpenCodePort(timeoutMs?)`
|
|
- `buildAugmentedPath()`
|
|
- `parseSseDataPayload(block)`
|
|
- `fetchAgentsSnapshot()`
|
|
- `fetchProvidersSnapshot()`
|
|
- `fetchModelsSnapshot()`
|
|
- `setupProxy(app)`
|
|
|
|
## Public exports (shutdown-runtime.js)
|
|
- `createGracefulShutdownRuntime(dependencies)`: creates graceful shutdown runtime for managed OpenCode and web server teardown sequencing.
|
|
- Returned API:
|
|
- `gracefulShutdown(options?)`
|
|
|
|
## Public exports (server-startup-runtime.js)
|
|
- `createServerStartupRuntime(dependencies)`: creates runtime for server bind/startup tunnel and process handler wiring.
|
|
- Returned API:
|
|
- `resolveBindHost(host)`
|
|
- `startListeningAndMaybeTunnel(options)`
|
|
- `attachProcessHandlers(options)`
|
|
|
|
## Public exports (static-routes-runtime.js)
|
|
- `createStaticRoutesRuntime(dependencies)`: creates runtime for static dist resolution and static route registration.
|
|
- Returned API:
|
|
- `registerStaticRoutes(app)`
|
|
|
|
## Public exports (feature-routes-runtime.js)
|
|
- `createFeatureRoutesRuntime(dependencies)`: creates runtime for main feature route registration orchestration.
|
|
- Returned API:
|
|
- `registerRoutes(app, routeDependencies)`
|
|
|
|
## Public exports (opencode-resolution-runtime.js)
|
|
- `createOpenCodeResolutionRuntime(dependencies)`: creates runtime for OpenCode binary/source snapshot resolution.
|
|
- Returned API:
|
|
- `getOpenCodeResolutionSnapshot(settings)`: returns configured/resolved OpenCode binary details plus effective managed-launch fields (`launchBinary`, `launchArgs`, `launchWrapperType`) when applicable.
|
|
|
|
## Public exports (tunnel-wiring-runtime.js)
|
|
- `createTunnelWiringRuntime(dependencies)`: creates runtime for tunnel service construction and tunnel route registration.
|
|
- Returned API:
|
|
- `initialize(app, initialPort)`
|
|
|
|
## Public exports (startup-pipeline-runtime.js)
|
|
- `createStartupPipelineRuntime(dependencies)`: creates runtime for terminal wiring, proxy/bootstrap scheduling, static route registration, and server startup/listen flow.
|
|
- Returned API:
|
|
- `run(options)`
|
|
|
|
The pipeline binds the OpenChamber listener and publishes its active port
|
|
before starting managed OpenCode. The managed custom tool therefore receives
|
|
an authoritative loopback callback URL even when OpenChamber binds port `0`.
|
|
|
|
## Public exports (openchamber-routes.js)
|
|
- `registerOpenChamberRoutes(app, dependencies)`: registers OpenChamber endpoints:
|
|
- `GET /api/openchamber/update-check`
|
|
- `POST /api/openchamber/update-install`
|
|
- Foreground servers running under a systemd user unit queue installation in
|
|
a separate transient unit and restart the configured service afterwards.
|
|
`OPENCHAMBER_SYSTEMD_UNIT` overrides the default `openchamber.service`.
|
|
- `GET /api/openchamber/models-metadata`
|
|
- `GET /api/zen/models`
|
|
|
|
## Public exports (pwa-manifest-routes.js)
|
|
- `registerPwaManifestRoute(app, dependencies)`: registers PWA manifest endpoint with dynamic app-name resolution and recent-session shortcuts:
|
|
- `GET /manifest.webmanifest`
|
|
|
|
## Public exports (project-icon-routes.js)
|
|
- `registerProjectIconRoutes(app, dependencies)`: registers project icon routes and owns icon storage/discovery flow:
|
|
- `GET /api/projects/:projectId/icon`
|
|
- `PUT /api/projects/:projectId/icon`
|
|
- `DELETE /api/projects/:projectId/icon`
|
|
- `POST /api/projects/:projectId/icon/discover`
|
|
|
|
## Public exports (skill-routes.js)
|
|
- `registerSkillRoutes(app, dependencies)`: registers skills-related routes:
|
|
- Skills config CRUD and metadata under `/api/config/skills*`
|
|
- Skill rename via `PATCH /api/config/skills/:name` with `{ renameTo }` (directory rename preserves `SKILL.md` body and supporting files; restricted to managed skill roots under `.opencode/skills|skill`, `.claude/skills`, and `.agents/skills`)
|
|
- Skill list responses include authoritative `renamable` derived from the same managed-root policy used by rename
|
|
- Skills catalog listing/source pagination, scan, and install routes
|
|
- Supporting skill file read/write/delete routes
|
|
- Directory resolution prefers an explicit request directory, then soft-falls
|
|
back to the active project / `lastDirectory` so repository-local
|
|
`.agents/skills` and `.opencode/skills` remain discoverable when the client
|
|
omits `directory`. Requests without any project still list user-scoped skills.
|
|
|
|
## Public exports (proxy.js)
|
|
- `registerOpenCodeProxy(app, dependencies)`: registers OpenCode proxy routes and middleware.
|
|
- Owns:
|
|
- SSE forwarders: `GET /api/global/event`, `GET /api/event`
|
|
- Downstream heartbeats keep clients and intermediaries alive, while a separate upstream-only stall watchdog closes the downstream response when OpenCode stops producing bytes so clients reconnect instead of trusting synthetic heartbeats indefinitely. Each watchdog reset uses the current load-aware timeout, matching the shared event transport.
|
|
- Session message forwarder: `POST /api/session/:sessionId/message`
|
|
- Interactive OAuth forwarder: `POST /api/provider/:providerID/oauth/callback`
|
|
- Upstream blocks inside this call for the whole browser sign-in (device-code polling or a loopback redirect), so it is exempt from the ordinary request deadline and uses a 15-minute proxy timeout instead of `LONG_REQUEST_TIMEOUT_MS`. All other `/api/provider/*` routes, including `oauth/authorize`, keep the ordinary deadline.
|
|
- Generic `/api/*` forwarding with hop-by-hop header filtering
|
|
- Windows `/session` merge fallback path behavior
|
|
- OpenCode readiness gate for proxied `/api` requests
|
|
|
|
## Public exports (watcher.js)
|
|
- `createOpenCodeWatcherRuntime(dependencies)`: creates global event watcher runtime backed by the shared upstream SSE reader.
|
|
- Returned API:
|
|
- `start()`
|
|
- `stop()`
|
|
- Behavior:
|
|
- Waits for OpenCode readiness before attaching the watcher.
|
|
- In production wiring, subscribes to the shared global message-stream hub instead of opening its own `/global/event` connection.
|
|
- Can still create its own `/global/event` reader when no shared hub is provided, which keeps module tests and isolated reuse simple.
|
|
- Reuses event-stream parsing, `Last-Event-ID`, stall timeout, and reconnect behavior.
|
|
- Forwards unwrapped global event payloads into notification/session side effects.
|
|
|
|
## Storage and configuration
|
|
- Provider auth: `~/.local/share/opencode/auth.json`.
|
|
- User config: `$XDG_CONFIG_HOME/opencode/opencode.json`, falling back to `~/.config/opencode/opencode.json` when unset or blank.
|
|
- Project config: `<workingDirectory>/.opencode/opencode.json` or `opencode.json`.
|
|
- Custom config: `OPENCODE_CONFIG` env var path.
|
|
- Rate limit config: `OPENCHAMBER_RATE_LIMIT_MAX_ATTEMPTS`, `OPENCHAMBER_RATE_LIMIT_NO_IP_MAX_ATTEMPTS` env vars.
|
|
|
|
## Notes for contributors
|
|
- This module serves as foundation for OpenCode-related server utilities.
|
|
- Route ownership moved to module-level `routes.js`; `index.js` wires dependencies only.
|
|
- All file writes include automatic backup before modification.
|
|
- Config merging follows priority: custom > project > user.
|
|
- UI auth uses scrypt for password hashing with constant-time comparison.
|
|
- Tunnel auth treats `host.docker.internal` as local-only when the socket remote IP is private/loopback.
|
|
|
|
The behavior `GET /api/behavior/agents-md` response includes `path`, the effective
|
|
server-side filename, whether or not the file exists. Settings displays this
|
|
path without deriving a directory from the browser environment.
|