@openchamber/sdk 1.23.2-preview.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/API.md +529 -0
  2. package/DOCUMENTATION.md +81 -0
  3. package/GUEST_SERVICES.md +166 -0
  4. package/LICENSE +21 -0
  5. package/README.md +186 -0
  6. package/dist/api-version.d.ts +6 -0
  7. package/dist/api-version.js +5 -0
  8. package/dist/contract.d.ts +471 -0
  9. package/dist/contract.js +256 -0
  10. package/dist/host-version.d.ts +16 -0
  11. package/dist/host-version.js +50 -0
  12. package/dist/host.d.ts +101 -0
  13. package/dist/host.js +606 -0
  14. package/dist/index.d.ts +12 -0
  15. package/dist/index.js +7 -0
  16. package/dist/manifest.d.ts +298 -0
  17. package/dist/manifest.js +224 -0
  18. package/dist/parse.d.ts +279 -0
  19. package/dist/parse.js +379 -0
  20. package/dist/protocol.d.ts +1092 -0
  21. package/dist/protocol.js +450 -0
  22. package/dist/schemas.d.ts +4 -0
  23. package/dist/schemas.js +6 -0
  24. package/dist/ui/badge.d.ts +10 -0
  25. package/dist/ui/badge.js +26 -0
  26. package/dist/ui/banner.d.ts +13 -0
  27. package/dist/ui/banner.js +48 -0
  28. package/dist/ui/button.d.ts +14 -0
  29. package/dist/ui/button.js +49 -0
  30. package/dist/ui/checkbox.d.ts +12 -0
  31. package/dist/ui/checkbox.js +45 -0
  32. package/dist/ui/dom.d.ts +15 -0
  33. package/dist/ui/dom.js +60 -0
  34. package/dist/ui/empty.d.ts +11 -0
  35. package/dist/ui/empty.js +44 -0
  36. package/dist/ui/field.d.ts +18 -0
  37. package/dist/ui/field.js +49 -0
  38. package/dist/ui/icons.d.ts +10 -0
  39. package/dist/ui/icons.js +23 -0
  40. package/dist/ui/index.d.ts +35 -0
  41. package/dist/ui/index.js +17 -0
  42. package/dist/ui/list.d.ts +26 -0
  43. package/dist/ui/list.js +90 -0
  44. package/dist/ui/menu.d.ts +20 -0
  45. package/dist/ui/menu.js +111 -0
  46. package/dist/ui/navigation.d.ts +18 -0
  47. package/dist/ui/navigation.js +38 -0
  48. package/dist/ui/option.d.ts +16 -0
  49. package/dist/ui/option.js +35 -0
  50. package/dist/ui/popup.d.ts +5 -0
  51. package/dist/ui/popup.js +43 -0
  52. package/dist/ui/progress.d.ts +11 -0
  53. package/dist/ui/progress.js +44 -0
  54. package/dist/ui/search.d.ts +11 -0
  55. package/dist/ui/search.js +62 -0
  56. package/dist/ui/select.d.ts +22 -0
  57. package/dist/ui/select.js +164 -0
  58. package/dist/ui/separator.d.ts +6 -0
  59. package/dist/ui/separator.js +26 -0
  60. package/dist/ui/spinner.d.ts +8 -0
  61. package/dist/ui/spinner.js +29 -0
  62. package/dist/ui/style.d.ts +1 -0
  63. package/dist/ui/style.js +190 -0
  64. package/dist/ui/tabs.d.ts +15 -0
  65. package/dist/ui/tabs.js +61 -0
  66. package/dist/ui/text.d.ts +23 -0
  67. package/dist/ui/text.js +89 -0
  68. package/dist/ui/theme.d.ts +22 -0
  69. package/dist/ui/theme.js +69 -0
  70. package/dist/workspace-schemas.d.ts +136 -0
  71. package/dist/workspace-schemas.js +44 -0
  72. package/dist/workspace.d.ts +109 -0
  73. package/dist/workspace.js +4 -0
  74. package/package.json +55 -0
  75. package/scripts/bundle-guest.ts +44 -0
@@ -0,0 +1,81 @@
1
+ # OpenChamber SDK
2
+
3
+ Public author pages live in `packages/docs/content/docs/sdk.mdx`, `sdk/host.mdx`, `sdk/ui.mdx`, and `sdk/example.mdx`; the user page is `extensions.mdx`. Each has a copy under every locale folder.
4
+
5
+ ## Purpose
6
+
7
+ `packages/sdk` owns the guest contract. `@openchamber/sdk` is the guest-facing entry: types, limits, `connectHost`, and pure helpers, with no schema library, so a bundled panel stays a few kilobytes. `@openchamber/sdk/schemas` is the host-facing entry: the zod parse of an untrusted guest's manifest (`parseManifest`, `parseManifestJson`) and messages (`guestMessageSchema`, `hostMessageSchema`). Guest-only drawing lives under `@openchamber/sdk/ui`. Host and guest import from here. Do not copy these types into `packages/ui`.
8
+
9
+ ## Entrypoints
10
+
11
+ - `src/index.ts`: public exports. Host and guest protocol.
12
+ - `src/parse.ts`: `package.json` / bare-block parse. Fails closed.
13
+ - `src/host-version.ts`: `engines.openchamber` parse and compare. Install uses this against `OPENCHAMBER_VERSION`.
14
+ - `src/protocol.ts`: envelope parse for host and guest messages.
15
+ - `src/host.ts`: iframe client. Posts to `parent`. Ignores messages whose `source` is not `parent`. In-flight RPCs time out at `GUEST_REQUEST_TIMEOUT_MS` (20s) as `HOST_TIMEOUT`. `onDirectory` / `onSession` / `onSessionLifecycle` / `onItem` replay and stay current after later pushes, same as connection and settings. `onResolve` holds one handler for host `resolve` requests and answers each with `resolve-result` (the clamped chip, `null`, or the thrown error's message; no handler answers with an error). `setBadge` posts `badge` with the count clamped to 0..`GUEST_BADGE_MAX`.
16
+ - `scripts/bundle-guest.ts`: official IIFE build, exposed as the `openchamber-guest-bundle` bin. Run `bunx openchamber-guest-bundle panel/main.ts panel/main.js` from the guest folder; paths resolve against the current directory. Needs Bun. Any bundler with an IIFE target is an equivalent substitute.
17
+ - `dist/`: the published output (`bun run --cwd packages/sdk build`, `tsc -p tsconfig.build.json`). `package.json` `exports` point here so Node loads the SDK from `node_modules` (packaged Electron, `openchamber serve`). Inside the monorepo, Vite aliases and tsconfig `paths` resolve `@openchamber/sdk` to `src/` so builds and type-checks never depend on a stale `dist`. The server under Bun and Node uses `dist`; the root `postinstall` builds it. The package ships to npm from the release workflow with `bun publish`, right before `@openchamber/web`; `scripts/bump-version.mjs` bumps it with the app, so the SDK version always equals the OpenChamber version it was released with.
18
+ - `GUEST_SERVICES.md`: local service contract (`contributes.service`, `serviceRequest`, grant, loopback).
19
+ - `src/ui/`: guest drawing kit. Building blocks only (`mountButton`, `mountTextField`, `mountSearchField`, `mountSelect`, `mountCheckbox`, `mountSwitch`, `mountTabs`, `mountBadge`, `mountList`, `mountEmpty`, `mountSpinner`, `mountBanner`, `mountSeparator`, `mountProgress`, `mountMenu`, `mountText`); the guest composes screens. See `src/ui/DOCUMENTATION.md`. Import `@openchamber/sdk/ui`. Do not import this from `packages/ui`. Kit changes follow package semver, not `apiVersion`.
20
+
21
+ ## Invariants
22
+
23
+ - Extension activity uses `useConfigStore.isConnected` plus live status records and directory `sessionStatusReady` authority. `useUIStore.eventStreamStatus` is an unmaintained diagnostic field, not a connection gate; its default `idle` must not hide known running or idle sessions as `unknown`.
24
+
25
+ - `contributes.page` is `true` or `{ entry, title? }`, requires `panel.entry`, and adds a user-opened full-screen page. `resolvePageEntry` resolves shorthand; the server validates its HTML/scripts and exposes `pageEntry`/`pageTitle`. `PluginPane surface="page"` uses the same sandbox. `useUIStore.openGuestPageId` is transient and mutually exclusive with other main pages. The sidebar menu opens it; `close()` or explicit session navigation closes it. Pages are not context-rail surfaces.
26
+ - `workspace.ts` owns project/worktree/session snapshots and storage types; `workspace-schemas.ts` validates their wire contracts. The existing `sessions` grant covers lists, subscriptions, opening sessions, and targeted creation. `lib/guests/workspace.ts` projects shared project, topology, session, and live-status stores; reads perform no network or git discovery. A registered observer is shared per runtime/extension/query and stops with its last listener. `PluginPane` limits each frame to 32 subscriptions and clears them on unmount, grant withdrawal, and runtime switch. Initial snapshots and updates have explicit loading/error coverage. `HostClient.onProjects/onWorktrees/onSessions` return promises of unsubscribe functions, so approval failures reach the caller.
27
+ - `storage` is instance-owned JSON under the guest's namespace, without a new capability. `storage.js` serializes authorization, read-modify-write, and removal per namespace; writes use unique temporary files and rename. Missing keys differ from stored null; corrupt files and failed writes fail without clearing data. Limits and public result shapes live in `API.md`. For project data, extensions namespace keys with project IDs.
28
+
29
+ - Manifest `apiVersion` is `1`. Wire envelope `v` stays `1`. Any other value is `unsupported-api-version`. Optional `engines.openchamber` is `1.22.0` or `>=1.22.0`. Install refuses with `host-too-old` when this OpenChamber build is older. Package installs also need a semver `version` on `package.json` (`1.0.0`); the catalog exposes it and Settings → Extensions shows `v1.0.0` on the card. Already-installed packages without `version` still list; new installs without it fail as `invalid-manifest`.
30
+ - `panel.entry` is optional. `id`, `name`, and `icon` stay required: they identify the extension on the Extensions card and in the approval dialog. A package without `entry` is page-less (`hasGuestPage(contributes)` is `false`): it never mounts an iframe, so parse refuses `attach` (other than `false`), `actions`, `commands`, `service`, `integration`, a non-empty `capabilities`, and `filesystem` as `invalid-panel` (the message names the offending keys and says they need `panel.entry`). `tools`, `engines`, and `version` are all it may carry. Install skips the HTML and built-script checks, the catalog row has no `entry`, the served-file route refuses HTML and JS for that id (its SVG icons still serve), and the UI gives it no rail surface, attach row, action, command, or frame; the Extensions card shows "No panel" after the version. `examples/tools-only` is the reference.
31
+ - Panel `id` is kebab-case. `icon` is a Remixicon kebab-case name (`RiWindowLine` → `window`, `RiGitMergeLine` → `git-merge`) or a package `.svg` path (`icon.svg`). URLs and absolute paths fail parse. `entry` stays inside the package (no `..`, no absolute, no URL). The rail paints a Remixicon from the host sprite, or a `currentColor` mask for a package SVG. Package SVGs often fill the 24×24 viewBox; the rail scales that mask so it matches Remixicon optical size. A Remixicon name the sprite does not carry falls back to `window`.
32
+ - Extra manifest keys are dropped, not forwarded.
33
+ - `connectHost` rejects when `parent === self`. A silent host rejects in-flight RPCs as `HOST_TIMEOUT` after 20s. Dispose rejects the rest as `HOST_UNAVAILABLE`.
34
+ - Failed `result` may carry `code`. Known guest codes are `HOST_UNAVAILABLE`, `HOST_TIMEOUT`, `HOST_REJECTED`, `DISCONNECTED`, `DISABLED`, `BAD_PATH`, `NO_INTEGRATION`, `NO_SESSION`, `SESSION_BUSY`, `NO_SERVICE`, `SERVICE_FAILED`, `NOT_GRANTED`, `NO_DIRECTORY`, `NOT_FOUND`, `FILE_TOO_LARGE`, `DENIED`, `NO_MODEL`, `MODEL_FAILED`. Anything else becomes `HOST_REJECTED`. The `error` string stays.
35
+ - `parseManifest` takes a typed document. File or network bytes go through `parseManifestJson`. Both return a result and do not throw on junk.
36
+ - The host parses iframe `event.data` with `guestMessageSchema` (`@openchamber/sdk/schemas`). The guest reads host pushes with `readHostMessage` in `contract.ts`: envelope and discriminant only, because the sender is the verified parent frame. `protocol.ts` asserts the hand-written `HostMessage` / `GuestMessage` types against the zod schemas at compile time, so the two cannot drift.
37
+ - `ready.item` is `GuestItem | null`: the composer chip (`AttachIssueRequest`, unchanged, `kind` still `issue` / `pull`), or a `GuestMessageItem` / `GuestSessionItem` from a declared action, discriminated by `kind: 'message' | 'session'` and carrying the manifest `action` id. A message item's `text` is what `formatMessageRecordText` renders for the export, capped at `GUEST_ITEM_MESSAGE_TEXT_MAX` (200 000). A session item carries `messages` only when the action declared `payload: ["messages"]` and `conversation` is granted; `buildGuestSessionItem` in `packages/ui/src/lib/guests/actions.ts` drops the oldest messages until the serialized item is under `GUEST_ITEM_SESSION_MAX` (2 000 000) and sets `truncated`. The host schema refuses a larger item. `isGuestAttachItem` / `isGuestMessageItem` / `isGuestSessionItem` narrow it.
38
+ - `contributes.actions` is 1 to 8 entries (`GUEST_ACTIONS_MAX`), each a unique kebab-case `id`, `label` of 1 to `GUEST_ACTION_LABEL_MAX` (40) characters, optional `icon` with the `panel.icon` rules (falls back to the panel icon), and `where`. `roles` is only legal on `where: "message"` (default both roles); `payload` is only legal on `where: "session"` and only knows `"messages"`. Anything else is `invalid-actions`. Declaring a `messages` payload adds the `conversation` capability (`guestActionsNeedConversation`); actions without it need no capability. The public catalog row carries `actions` as declared.
39
+ - `contributes.commands` is 1 to 8 entries (`GUEST_COMMANDS_MAX`) with a unique `name` matching `GUEST_COMMAND_NAME` (`/^[a-z][a-z0-9-]{0,23}$/`) and an optional `description` of 1 to `GUEST_COMMAND_DESCRIPTION_MAX` (80) characters; else `invalid-commands`. The host owns collisions: a name the composer already has (built-in, OpenCode command, skill, or an earlier guest) is dropped with a console warning. The public catalog row carries `commands` as declared.
40
+ - `contributes.tools` is 1 to 16 entries (`GUEST_TOOLS_MAX`) that say how the extension's tool calls look in the chat, without code. `match` is the full tool name as OpenCode reports it, un-normalized (`mcp.jira.search`, `jira_search`), 1 to `GUEST_TOOL_MATCH_MAX` (128) characters of `[A-Za-z0-9_.:-]`, with one `*` allowed at the end as a suffix wildcard (`mcp.jira.*`; `*` alone is refused). Optional `name` (1 to `GUEST_TOOL_NAME_MAX`, 40) is the header title when `title` is absent or renders empty; `icon` is a Remixicon name or a package `.svg` path with the `panel.icon` rules (the chat draws the SVG as a `currentColor` mask at the glyph size through `resolveGuestToolIcon`, fetched as an authenticated asset under `/api/guests/<id>/<path>`; a Remixicon the sprite lacks leaves the host's own tool icon); `title` and `subtitle` are templates of up to `GUEST_TOOL_TEMPLATE_MAX` (200) characters with `{input.path}`, `{output.path}`, and `{metadata.path}` placeholders (dotted paths, array indexes allowed; a missing path is an empty string; a value is stringified and cut at `GUEST_TOOL_TEMPLATE_VALUE_MAX`, 200). `output` is `auto` (the host's own detection, the default), `text`, `json` (the JSON summary/tree/raw views), `markdown` (the assistant markdown renderer), `code` with `language` (1 to 32), or `table` with `columns` (1 to 16 dotted paths; rows are the output array or `output.items`). `language` outside `code` and `columns` outside `table` (or a `table` without `columns`) fail closed; anything else wrong is `invalid-tools`. The public catalog row carries `tools` as declared; the host applies them only for an active guest, an exact `match` beats a wildcard from any extension, and among equals the first extension in the catalog wins. The registry lives in `packages/ui/src/lib/guests/tool-presentation.ts` and is consulted by the existing chat tool renderers; VS Code and mobile keep the catalog empty, so nothing changes there.
41
+ - Host → guest `resolve` is `{ id, payload: { command, args } }`; the guest answers `resolve-result` with the same `id` and `{ item }` (a clamped `AttachIssueRequest` or `null`) or `{ error }` (1 to `GUEST_RESOLVE_ERROR_MAX` characters). The host waits `GUEST_REQUEST_TIMEOUT_MS`, then treats silence as a failure and never sends a `result` for a `resolve-result`. `badge` is `{ count }` with `count` an integer 0..999 or `null`; the host schema drops anything else.
42
+ - `ready` always carries `session` (`{ id, title, busy }` or `null`), `surface` (`panel` on the rail, `dialog` in the attach window), `connection` (`{ connected, account }`), and `settings` (declared keys only). Title falls back to `id` when the host record has no title. `busy` is live `session_status` `busy` or `retry`. Missing status is `busy: false`, not an empty success that clears a previous snapshot. `model` is `providerID/id` when the host session record has both. `agent` is the session agent name when present. `surface` is which host chrome mounted the iframe. It is not `openSurface`. `item` is the attached item the surface was opened for (the user clicked that item's chip on the composer; the same `AttachIssueRequest` shape the guest handed to `attach`, with `text` filled from the chip's context) or `null` from the rail icon and the + menu. The host also pushes `item` when it changes while the frame is up; `onItem` replays the latest value. `ready.theme.tokens` is the host colour bag: surfaces (`background`, `elevated`, `mutedSurface`, `subtle`), text (`foreground`, `elevatedForeground`, `muted`), interaction (`border`, `hover`, `active`, `selection`, `selectionForeground`, `focus`), `primary` / `primaryForeground`, the four status colours, `font`, `mono`, and `radius`. `applyHostReady` writes it onto the iframe. The host also pushes `connection` and `settings` the same way as `session`. Access tokens never appear in `ready` or a result.
43
+ - `writeClipboard` text is 1–32000 characters. `compose` text is 1–16000 after trim. Compose default mode is `append`. Compose writes the chat composer. It does not send.
44
+ - `contributes.capabilities` is a list of `prompt`, `sessions`, `files`, and/or `model` (`DECLARED_GUEST_CAPABILITIES`). `model` gates `generate`: `{ prompt, system?, maxOutputTokens? }` → `{ text }`, posted with a `GUEST_GENERATE_TIMEOUT_MS` (90s) timeout instead of the usual 20s because the model answers slowly; the host answers `NO_MODEL` when the small-model service resolves nothing (or only Claude Code) and `MODEL_FAILED` when the provider call fails. `requestedGuestCapabilities` adds `conversation`, `service`, `network`, and `filesystem` from a session action with `payload: ["messages"]`, `contributes.service`, `contributes.integration`, and `contributes.filesystem`; the user approves that whole list once at install (`PUT /api/guests/:id/capabilities`), and a package that later asks for more is re-approved. `hasGuestCapability` gates `prompt({ send })`, `startSession`, `request`, `serviceRequest`, and the four file calls; a refusal is `NOT_GRANTED` (`NO_SERVICE` for the service).
45
+ - `contributes.filesystem` is 1 to 16 globs, each 1 to 256 characters, starting with `/` or `~/`, with no `..`, empty segment, NUL, or backslash (`isGuestFilesystemPattern`; parse failure is `invalid-filesystem`). `**` spans directories, `*` and `?` stay inside one segment, and a pattern ending in `/**` also matches the directory itself. The public catalog row carries the list as `filesystem` so the approval dialog can show it.
46
+ - `readFile` / `writeFile` / `listDir` / `stat` are `file-read` / `file-write` / `file-list` / `file-stat` on the wire. `guestFileScope` decides the grant: a relative path is `project` (capability `files`, joined to the open project; none open is `NO_DIRECTORY`), `/…` or `~/…` is `filesystem` (must match a declared pattern). The server compares canonical paths, so a symlink out of either tree is `BAD_PATH`, as is any `..` segment. Results are `{ content }`, `{ written: true }`, `{ entries: { name, kind }[] }`, and `{ kind, size, mtime }` where `kind` may be `missing`. `GUEST_FILE_PATH_MAX` (1024), `GUEST_FILE_CONTENT_MAX` (2 000 000 characters, read and write), `GUEST_FILE_LIST_MAX` (2000 entries). The client refuses an empty, oversized, NUL, or backslash path as `BAD_PATH` and oversized content as `FILE_TOO_LARGE` before posting; the host schema accepts only what `isGuestFilePath` does, so a dropped message cannot turn into `HOST_TIMEOUT`. Nothing about the write is logged, not even the path.
47
+ - `contributes.attach` is `true`, `false`, `"panel"`, `"dialog"`, or `{ "mode": "panel" | "dialog", "entry"?: string }`. `true` and `"panel"` add a + menu row that opens the rail. `"dialog"` opens a host window that mounts the same iframe. The object form's `entry` is an HTML page inside the package (same path rules as `panel.entry`, install checks it exists and that its scripts are built) that the dialog loads instead of `panel.entry`; it is only accepted with `mode: "dialog"`, and `resolveAttachEntry(contributes)` returns it or `null`. The public catalog row carries it as `attachEntry`. Clicking the guest's chip on the composer reopens that guest with the chip as `ready.item`: a dialog guest in the attach window, a panel guest on the rail. New Worktree shows the same dialog-mode guests next to GitHub and Linear. Omitted or `false` stays off those menus. Public 1.0 is web and desktop. VS Code and mobile mark the guest catalog `unsupported`. They do not show the row.
48
+ - `contributes.integration` is optional. Panel stays required. Exactly one of `oauth`, `token`, or `host` is required. The public catalog slice is name, description, `auth`, `apiOrigin` (the one origin `request` may call; the approval dialog shows it, it is not a secret), token scheme, and setting field defs. OAuth URLs stay on the server. Client id, secret, and pasted tokens are not in the package. `host.provider: "linear"` binds the first-party Linear connection. `apiOrigin` is `https://api.linear.app` only.
49
+ - `attach` puts `{ providerId, id, title, url }` on the composer as a chip, exclusive with GitHub and Linear. `kind` is `issue` or `pull` and defaults to `issue`. `author` is a login. `branches` is `{ head, base }` on a pull. Avatar URLs stay off this payload. It does not invent a GitHub `number`. `text` is optional model context, 1–16000 after trim. `data` is optional plain JSON the guest chose (`JsonValue`); the host stores it on the chip, the queued context part, and the session snapshot, and hands it back byte-identical as `ready.item.data` / `onItem`. It never reaches the model or the chip text. Serialized size is capped at `GUEST_ATTACH_DATA_MAX` (16 000 characters): `clampAttachRequest` drops a larger value, the host schema refuses it. `connectHost.attach` clamps id/title/url/text/author/branches to the protocol max so the host schema does not drop the message. The rail and the attach window both write `pendingGuestIssue` on the composer store. Send writes `guest-issue` or `guest-pr` and a session snapshot. `close` dismisses the attach window and is a no-op on the rail.
50
+ - `startSession` extends the existing operation with `projectId`, root/existing/new worktree selection, optional new name/base, and `navigation` defaulting to `preserve`. It calls the owning session creation action with that navigation policy, so registration/message initialization/global cache updates still happen while selection stays unchanged. Worktree creation reuses `createWorktreeWithDefaults` and the configured bootstrap wait. It preserves a created worktree on bootstrap/session failure and returns that partial result. It captures send selection before async creation, rechecks runtime and grants before subsequent mutations, and distinguishes session creation from first-message and linked-item persistence outcomes. See `API.md` for the exact result. Web and desktop only.
51
+ - `prompt` is `{ text, send? }`. `send` omitted or `false` replace-composes on the current session. `send: true` sends on that session with the same model path as `startSession`. No session is `NO_SESSION`. A busy session on send is `SESSION_BUSY`. Compose still works while busy. The result is `{ sent }`. The guest does not pick a model or agent.
52
+ - `sessionLink` is the same attach fields on the current session. It does not create a session. No project or no session is a refusal.
53
+ - `onSessionLifecycle` is a host push. Live `session_status` `busy` and `retry` are `started`. `idle` is `completed`. An unknown status type is `failure`. That is not abort or crash. A late listener gets the last phase, from `ready.session.busy` when no dedicated push has arrived.
54
+ - `oauthStart` opens the provider authorize URL, or the first-party Linear authorize URL for a host Linear guest. `oauthDisconnect` drops that guest's tokens, or the Linear connection for a host Linear guest. Token guests have no authorize URL. `request` is `{ method, path, query?, body? }`. `path` must start with `/`, stay on the declared `apiOrigin`, and never include a scheme. The host attaches `Authorization`. Token guests send the pasted token as is (`token.scheme` omitted or `raw`), as `Bearer <token>` (`token.scheme: "bearer"`), or as `Basic base64(username:token)` (`token.scheme: "basic"`; the Integrations card asks for a username, `token.usernameLabel` names that field, and the server stores the encoded pair as the access token so requests never need the username again). OAuth and host Linear send Bearer. The result is `{ status, body }`. A 409 `DISCONNECTED` or 400 `BAD_PATH` / `NO_INTEGRATION` lands on the guest as `HostRequestError.code`. Silence for 20s is `HOST_TIMEOUT`. The guest parses JSON. A host Linear guest may also `GET /api/linear/issues/get` through that same `request` call. The host answers from the first-party Linear route, which already asks Linear for public file URLs. Guest request itself does not know Linear.
55
+ - `contributes.service` names a host-spawned entry on `apiVersion: 1`. `serviceRequest` / `serviceStatus` proxy to that process on loopback. See `GUEST_SERVICES.md`. Permissions are grant UI only in phase 1. No `host.docker`.
56
+
57
+ ## Host hole
58
+
59
+ `GET /api/guests` plus `PluginPane` on the desktop/web rail. The iframe URL carries a token minted for `guest:<id>` scope (`resolveGuestFrameUrl`), valid only for `GET /api/guests/<id>/*`; the session-wide URL token never enters a guest frame. Settings → Extensions installs a folder, a local `.zip`, or an https git / zip URL into that OpenChamber instance's data dir (`extensions.json`). Settings → Integrations shows a host card for each installed guest that declared `integration`. OAuth and pasted tokens live in `guest-auth.json`. Host Linear uses `linear-auth.json`. VS Code and mobile set the catalog to `unsupported`. That is not an empty success. A guest must ship a classic IIFE (`panel/main.js`). No runtime compiles guest TypeScript. The iframe cannot load ESM.
60
+
61
+ **Folder install** stores the realpath in `extensions.json` with `source: "path"`. Uninstall drops the catalog row and extension-owned storage. The user's folder stays.
62
+
63
+ **Zip / git install** copies into `{dataDir}/extensions/{id}` with `source: "zip"` or `"git"`. Only git installs update: Settings → Extensions compares the installed `package.json` `version` with the origin's and offers Update when the origin is newer, so bump `version` to ship one; a `#tag` or `#branch` on the install URL pins that channel. The archive is not kept open. The host reads it (local file or https download, 20MB cap), extracts with a small allowlist unzip (store/deflate only; no zip64, encryption, or `..` paths; 500 files / 40MB uncompressed), unwraps a single root folder when `package.json` sits inside it, inspects the manifest, then renames staging to `{dataDir}/extensions/{id}`. Runtime serves and spawns from that copy. Uninstall deletes the copy. The original `.zip` on disk is untouched.
64
+
65
+ The iframe is `sandbox="allow-scripts"` with no `allow-same-origin`, so its origin is opaque. Host posts to that frame's `contentWindow` with target `*`. The string `null` is not a legal `postMessage` targetOrigin. Receive still accepts only `event.source === contentWindow`. Guest `connectHost` posts `hello` to `parent` with `*`. The host answers `hello` and also pushes `ready` on iframe `load`, because a cached panel can fire `hello` while React is swapping the listener.
66
+
67
+ File calls go through `POST /api/guests/:id/files` with the `x-opencode-directory` header of the open project (`lib/guests/files.ts`). `PluginPane` answers `NOT_GRANTED` and `NO_DIRECTORY` itself, mirroring the server, so a refused call costs no round trip; the server is still the authority.
68
+
69
+ Clipboard writes run in the host (`copyTextToClipboard`). Compose uses `useInputStore.setPendingInputText`. Attach writes the composer chip. The iframe has no clipboard, no composer, and no chip. The + menu and the attach window chrome are host. Guest JS is not in that React tree.
70
+
71
+ Actions, commands, and the badge are host chrome too. `useGuestActions` (`packages/ui/src/hooks/useGuestSurfaces.ts`) lists the actions of active guests (empty on VS Code and mobile); the message hover row and touch sheet, the sidebar session menu, and the header session menus add them after the built-ins. A click builds the item (`buildGuestMessageItem` / `buildGuestSessionItem`; the session conversation is loaded the same way Export as Markdown loads it, and a failed load is a toast, not an empty conversation) and `openGuestWithItem` routes it exactly like a chip click: the attach window for `attach: "dialog"` (through `useGuestDialogStore`, rendered by `GuestHosts` in `MainLayout`), otherwise the rail with `useGuestItemStore`. `useGuestCommands` lists commands minus reserved names; the composer routes `/name args` (`submit/guestCommands.ts`) to `runGuestCommand`, which asks the registered resolver of a mounted pane (`lib/guests/resolve.ts`) or mounts a hidden headless `PluginPane` through `GuestHosts` for the call. `PluginPane` registers its resolver once the guest said `hello` or the iframe loaded, posts `resolve`, and settles on `resolve-result` or the 20s deadline. A returned chip goes through `useInputStore.setPendingGuestIssue`, the same slot a panel `attach` uses. `badge` writes `useGuestBadgeStore` (memory only); the rail paints it like the git count and the pane clears it when the panel opens. A paused or not fully approved guest contributes no actions, commands, tool presentations, or badge.
72
+
73
+ Add a type when the host grows that hole, not before. Any new `connectHost` method ships with a `PluginPane` / `answerGuestMessage` branch in the same change.
74
+
75
+ Rebuild every example bundle after an SDK change (`bunx openchamber-guest-bundle` per entry, see `examples/README.md`); the built files are checked in.
76
+
77
+ ## Not here yet
78
+
79
+ `issues.search` / `issues.get` on the host, a public OAuth broker, a second `host.provider`, file watching or globbing from the guest (it gets `listDir` and `stat`, not a search). `attach` is the chip. `startSession` creates a session and writes that snapshot. `sessionLink` writes that snapshot on the current session. `prompt` writes or sends on that session. `onSessionLifecycle` is the live phase. Search and the task list live in the guest. `@openchamber/sdk/ui` is the drawing kit: buttons, fields, select, list, tabs, badges, banners, menu, and `mountText`. No issue page, card, attach picker, or PR window; the guest composes those from the blocks. `mountText` lifts `![alt](https://…)` and `[label](https://…)` and hands link clicks to `onOpenUrl`. `ready.theme.tokens` carries the host surface and interactive colors. Call `applyHostReady` before mount.
80
+
81
+ Local services ship on `apiVersion: 1` (`contributes.service`, `serviceRequest`, grant UI, platform socket bindings + override). Contract detail: `GUEST_SERVICES.md`. Declared `sockets` / `exec` are shown on the grant card and do not confine the process; there is no sandbox for services. VS Code and mobile do not load guests or services yet.
@@ -0,0 +1,166 @@
1
+ # Guest local services
2
+
3
+ Implemented on manifest `apiVersion: 1`. Wire envelope `OPENCHAMBER_SDK_API_VERSION` stays `1`.
4
+
5
+ This is the contract for guests that need a local process (Docker CLI, Engine sockets, kubectl, DB sockets). The iframe stays sandboxed. The host owns spawn and the loopback proxy.
6
+
7
+ ## Why services exist
8
+
9
+ HTML guests in a sandboxed iframe reach the network only through `connectHost.request` onto a declared HTTPS `apiOrigin`. That fits cloud trackers. It cannot open `/var/run/docker.sock`, run `docker`, or hold a long-lived local daemon.
10
+
11
+ Services keep the iframe. They add a host-owned child process from the same package. The panel never sees the socket. The service does.
12
+
13
+ Do not put `docker` (or any product name) on `connectHost`. The SDK knows panel, service, and a loopback proxy. The package owns the integration.
14
+
15
+ Declare the OpenChamber floor with `engines.openchamber` (`1.22.0` or `>=1.22.0`). Install refuses when this host is older. Put a semver `version` on `package.json` (`1.0.0`); install requires it and Settings → Extensions shows `v1.0.0` on the card.
16
+
17
+ ## Model
18
+
19
+ ```
20
+ panel (iframe) --serviceRequest--> host --HTTP 127.0.0.1:port--> service process --> socket / CLI
21
+ ^
22
+ spawn / kill / grant
23
+ ```
24
+
25
+ 1. Package still ships `panel/index.html` and a classic IIFE `panel/main.js`.
26
+ 2. Optional `contributes.service` names a built entry the host can spawn.
27
+ 3. After the user allows the service in Settings → Extensions, the first `serviceRequest` starts that entry with the app runtime (`process.execPath` + `ELECTRON_RUN_AS_NODE` on desktop). A system `node` on PATH is not required.
28
+ 4. The host binds `127.0.0.1` on an ephemeral port and passes `OPENCHAMBER_SERVICE_PORT` and `OPENCHAMBER_SERVICE_TOKEN` in the service env. The service does not inherit the host environment: only PATH, HOME, temp, locale, and the Windows system variables are copied. API keys, the UI password, and other host secrets never reach it.
29
+ 5. The panel calls `serviceRequest({ method, path, query?, body? })`. The host proxies only to that guest's loopback listener. Same stay-on-origin rule as `request`, but the origin is the service the host started.
30
+ 6. The service talks to Docker, kubectl, or anything else. That logic stays in the package.
31
+
32
+ ## Permissions: `exec` vs `sockets`
33
+
34
+ | Declare | Means | Service does |
35
+ |---|---|---|
36
+ | `exec` | Named binaries on PATH | `child_process` / CLI |
37
+ | `sockets` | Unix socket or named pipe | Dial Engine API (or similar) itself |
38
+
39
+ Use `exec` when the integration shells out (like a modern Docker CLI panel). Use `sockets` when the service opens the daemon endpoint. Do not list a socket path in Needs for a CLI-only service.
40
+
41
+ ## Manifest
42
+
43
+ ```json
44
+ {
45
+ "apiVersion": 1,
46
+ "engines": {
47
+ "openchamber": ">=1.22.0"
48
+ },
49
+ "contributes": {
50
+ "panel": {
51
+ "id": "docker-sock",
52
+ "name": "Docker (socket)",
53
+ "icon": "icon.svg",
54
+ "entry": "panel/index.html"
55
+ },
56
+ "service": {
57
+ "entry": "service/main.js",
58
+ "runtime": "host",
59
+ "permissions": {
60
+ "sockets": [{
61
+ "id": "docker",
62
+ "candidates": {
63
+ "linux": ["/var/run/docker.sock", "/run/user/1000/docker.sock"],
64
+ "darwin": ["~/.docker/run/docker.sock", "~/.colima/default/docker.sock"],
65
+ "win32": ["//./pipe/docker_engine"]
66
+ }
67
+ }]
68
+ }
69
+ },
70
+ "attach": false
71
+ }
72
+ }
73
+ ```
74
+
75
+ A CLI service looks the same except `permissions.exec: ["docker"]` and no `sockets`.
76
+
77
+ Socket entry shapes:
78
+
79
+ - `string` — legacy one path for every platform; public id is that string
80
+ - `{ id, path }` — same path on linux / darwin / win32
81
+ - `{ id, candidates: { linux?, darwin?, win32? } }` — per-OS lists; `~` expands
82
+
83
+ Parse rules:
84
+
85
+ - `apiVersion` is `1`. Wire `v` on postMessage stays `1`.
86
+ - `engines.openchamber` is optional. Values are `1.22.0` or `>=1.22.0` only. Install returns `host-too-old` when this OpenChamber build is older.
87
+ - `contributes.panel` stays required. Same id / name / icon / entry rules as any guest.
88
+ - `contributes.service` is optional. A guest without `service` is HTML-only.
89
+ - `service.entry` is a relative path inside the package. Ship compiled JS; the host never compiles TypeScript.
90
+ - `service.runtime` phase 1 accepts only `"host"`.
91
+ - `service.permissions.sockets` and `service.permissions.exec` are shown to the user in the approval dialog. They describe intent and do not confine the process: a service runs with the user's full access. Declaring `contributes.service` adds the `service` capability to the package's request list; the user approves the whole list once when the package is installed (Settings → Extensions), and the first `serviceRequest` is refused with `NO_SERVICE` until then.
92
+ - The catalog adds `service.socketBindings`: `{ id, candidates, resolved, override }` for this host. The user can override a path in Extensions. Override empty clears it and the next spawn re-resolves.
93
+ - `contributes.integration` remains valid next to `service`. Cloud `request` and `serviceRequest` may both exist on one guest.
94
+
95
+ Extra keys still drop, not forward.
96
+
97
+ ## Host hole
98
+
99
+ | Method | Role |
100
+ |---|---|
101
+ | `serviceRequest` | `{ method, path, query?, body? }` → `{ status, body }`. Proxy to this guest's service loopback only. `path` starts with `/`, no scheme. |
102
+ | `serviceStatus` | `stopped` \| `starting` \| `ready` \| `failed`. |
103
+
104
+ Do not add: raw unix socket from the panel, arbitrary `spawn`, arbitrary filesystem, `host.docker`.
105
+
106
+ Error codes:
107
+
108
+ - `NO_SERVICE` — no service declared, not granted, not started, or already torn down
109
+ - `DISABLED` — extension paused in Settings → Extensions (service stopped; tokens/grants stay)
110
+ - `SERVICE_FAILED` — process crashed or never became ready
111
+ - Existing: `HOST_TIMEOUT`, `HOST_REJECTED`, `HOST_UNAVAILABLE`, `BAD_PATH`
112
+
113
+ Server routes (authenticated UI session):
114
+
115
+ - `POST /api/guests/:id/service/request`
116
+ - `GET /api/guests/:id/service/status`
117
+ - `PUT /api/guests/:id/capabilities` — `{ granted }`, the full requested list or `[]` to withdraw
118
+ - `PUT /api/guests/:id/service/sockets` — `{ id, path }` (`path` empty or null clears the override). Stops a running service so the next request respawns with the new env.
119
+
120
+ The panel never receives `OPENCHAMBER_SERVICE_TOKEN` and never dials the port itself. Opaque iframe origin stays. Only the host proxy talks to loopback.
121
+
122
+ VS Code and mobile stay `unsupported` for the guest catalog. They do not spawn services.
123
+
124
+ ## Service process contract
125
+
126
+ Env the host sets:
127
+
128
+ - `OPENCHAMBER_SERVICE_PORT` — port to bind on `127.0.0.1`
129
+ - `OPENCHAMBER_SERVICE_TOKEN` — shared secret
130
+ - `OPENCHAMBER_SERVICE_SOCKETS` — JSON map `{ [socketId]: absolutePath }` for every binding that resolved (override or first existing candidate)
131
+
132
+ Inbound auth: every request, including ready, must send:
133
+
134
+ ```
135
+ Authorization: Bearer <OPENCHAMBER_SERVICE_TOKEN>
136
+ ```
137
+
138
+ Ready signal: host polls `GET /health` until HTTP 200 (15s timeout), then marks `ready`.
139
+
140
+ Listen only on `127.0.0.1`. Do not bind `0.0.0.0`.
141
+
142
+ Ship `service/main.js` already built. Same packaging rule as `panel/main.js`.
143
+
144
+ ## Lifecycle
145
+
146
+ | Event | Host behavior |
147
+ |---|---|
148
+ | Install | Catalog row includes public `service` (`runtime`, `permissions`, `socketBindings`, `granted: false`) and `capabilities.requested` containing `service`. No spawn yet. |
149
+ | Approve | `PUT .../capabilities` writes `capabilityGrants[id]` in `extensions.json`; the list must equal what the package requests. |
150
+ | Socket override | `PUT .../service/sockets` writes `serviceSocketOverrides`. Running service for that guest stops. |
151
+ | First `serviceRequest` | Grant missing → `NO_SERVICE`. Else spawn with resolved sockets, wait for `/health`, proxy. |
152
+ | Panel open | Status via `serviceStatus`. Dead service restarts on the next `serviceRequest`. |
153
+ | Uninstall | SIGTERM, then kill after timeout. Clear grant and socket overrides. Path-install does not delete the user's folder. |
154
+ | Host quit | Kill every guest service. |
155
+ | Crash | Status `failed`. Panel sees `SERVICE_FAILED` / status. Manual retry, not silent loops. |
156
+
157
+ ## Security invariants
158
+
159
+ - Panel → host → service loopback only. No panel → socket.
160
+ - `serviceRequest` path must stay on that service (host-allocated port for that guest id).
161
+ - Every service requires an explicit grant before proxy.
162
+ - Permissions text is advisory. Phase 1 does not enforce an OS sandbox around those lists: an allowed service can run any command, use git, and read or write any file the user can. The approval dialog says so in plain words.
163
+
164
+ ## Example
165
+
166
+ `examples/service-echo` is a checked-in service extension: a Node HTTP server on loopback that the panel calls through `serviceRequest` and whose status it shows. Bundle the service with `--node`, install the folder from Settings → Extensions, allow the local service in the approval dialog, then open the rail panel. Streaming from a service to the panel (shell into a container, log tails) is deferred; it needs a streaming call on the SDK first.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Bohdan Triapitsyn
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,186 @@
1
+ # @openchamber/sdk
2
+
3
+ Build extensions for [OpenChamber](https://openchamber.dev). An extension is a small web page that OpenChamber shows on its right-hand rail. It can read the current project and session, show toasts, put text in the chat box, attach a task to a session, and, once the user approves it, start sessions and send prompts. This package is the contract between that page and the app.
4
+
5
+ Full guide: [Build an extension](https://openchamber.dev/docs/sdk/). Reference: [Host API](https://openchamber.dev/docs/sdk/host/) and [UI kit](https://openchamber.dev/docs/sdk/ui/). Extensions with a local process: [GUEST_SERVICES.md](./GUEST_SERVICES.md).
6
+
7
+ Extensions load in OpenChamber web and desktop. VS Code and mobile do not load them yet.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install @openchamber/sdk
13
+ ```
14
+
15
+ The package ships compiled JavaScript with type declarations, so any bundler works. Its version matches the OpenChamber release it shipped with, so `@openchamber/sdk@1.24.0` is the contract of OpenChamber 1.24.0.
16
+
17
+ ## Preview builds
18
+
19
+ Preview packages are for testing unreleased SDK changes against a matching development build of OpenChamber. They do not imply compatibility with the published app of the same base version.
20
+
21
+ ```bash
22
+ npm install @openchamber/sdk@preview
23
+ ```
24
+
25
+ Maintainers can run **Publish SDK preview** in GitHub Actions, select the source branch, and enter a positive preview number. For example, `1` publishes `1.23.2-preview.1` under the `preview` npm tag. Each publication needs an unused number. The workflow uses the existing `NPM_TOKEN` secret and publishes only the SDK. It changes the version in its temporary checkout, without creating commits or release tags or changing `latest`.
26
+
27
+ The workflow must exist on the default branch before GitHub exposes its manual trigger. Once it does, select `bohdan/dev` to publish that branch's SDK. Enable `dry_run` to validate without publishing. Validation installs the packed SDK in an isolated project and checks its imports, TypeScript declarations, and extension bundler before publishing that same archive.
28
+
29
+ ## What you ship
30
+
31
+ A folder with three files:
32
+
33
+ - `package.json` with an `openchamber` block (the manifest)
34
+ - `panel/index.html`, the page OpenChamber shows
35
+ - `panel/main.js`, your script built into one classic file (an IIFE; the page runs in a sandboxed iframe and cannot load ES modules)
36
+
37
+ OpenChamber never compiles your code. Build `panel/main.js` yourself. The package includes a bundler command that runs on Bun; esbuild with `--format=iife --platform=browser` does the same job.
38
+
39
+ ```bash
40
+ bunx openchamber-guest-bundle panel/main.ts panel/main.js
41
+ ```
42
+
43
+ Then install the folder from Settings → Extensions → Add. Folder installs run from your folder, so edit, rebuild, and reload. A `.zip` or an https git or zip link is copied into OpenChamber's data folder instead; ship the built files only. Git installs can update from Settings → Extensions when the repository's `version` is newer than the installed one, so bump `version` to ship an update; `https://…/panel.git#v1` pins a tag or branch.
44
+
45
+ A complete three-file example is on the [Build an extension](https://openchamber.dev/docs/sdk/) page. Five more are at [github.com/openchamber/openchamber/tree/main/packages/sdk/examples](https://github.com/openchamber/openchamber/tree/main/packages/sdk/examples).
46
+
47
+ ## Manifest
48
+
49
+ ```json
50
+ {
51
+ "name": "@acme/hello",
52
+ "version": "1.0.0",
53
+ "openchamber": {
54
+ "apiVersion": 1,
55
+ "engines": { "openchamber": ">=1.24.0" },
56
+ "contributes": {
57
+ "panel": {
58
+ "id": "acme-hello",
59
+ "name": "Hello",
60
+ "icon": "window",
61
+ "entry": "panel/index.html"
62
+ },
63
+ "attach": "dialog",
64
+ "capabilities": ["prompt", "sessions"],
65
+ "actions": [
66
+ { "id": "create-task", "label": "Create task from message", "where": "message", "roles": ["assistant"] },
67
+ { "id": "summarize", "label": "Summarize session", "where": "session", "payload": ["messages"] }
68
+ ],
69
+ "commands": [{ "name": "task", "description": "Attach a task by id" }],
70
+ "tools": [{ "match": "mcp.tasks.*", "name": "Tasks", "icon": "checkbox-circle", "title": "{input.id}", "output": "table", "columns": ["id", "title", "status"] }],
71
+ "integration": {
72
+ "name": "Acme",
73
+ "description": "Tasks from Acme",
74
+ "token": {
75
+ "apiOrigin": "https://api.acme.example",
76
+ "account": { "path": "/me", "name": "login" },
77
+ "scheme": "bearer"
78
+ },
79
+ "settings": [{ "id": "list-id", "label": "List ID" }]
80
+ }
81
+ }
82
+ }
83
+ }
84
+ ```
85
+
86
+ - `version` is required semver. Settings → Extensions shows it on the card.
87
+ - `apiVersion` is `1`. Anything else is refused.
88
+ - `engines.openchamber` is optional (`1.24.0` or `>=1.24.0`). Older OpenChamber builds refuse the install.
89
+ - `panel.id` is kebab-case and unique. `icon` is a Remixicon name (`RiWindowLine` becomes `window`) or an SVG inside the folder. `entry` is the HTML file inside the folder. Leave `entry` out for an extension that only declares `tools`: it gets no rail icon or page, just its tool rules in the chat (`examples/tools-only`).
90
+ - `attach` is optional. `"dialog"` opens the page in a window from the + menu next to the chat box; `true` or `"panel"` opens the rail panel instead. `ctx.surface` tells the page which one it is in. The object form `{ "mode": "dialog", "entry": "panel/attach.html" }` gives the window its own page. When the user clicks the attached chip, the page opens again with that item in `ctx.item` (`null` from the + menu), so it can show the item instead of the list.
91
+ - `actions` is optional: menu entries on messages (`where: "message"`, optionally only `roles: ["assistant"]`) and on sessions (`where: "session"`). Picking one opens your page with that message or session in `ctx.item` (`kind: "message"` with the text, or `kind: "session"`; add `payload: ["messages"]` to get the conversation too). Up to 8.
92
+ - `commands` is optional: slash commands for the chat box, up to 8. `/task DEMO-2` calls your `host.onResolve` handler instead of the model; return a chip to attach it, or `null` for nothing. A name the app already has is ignored.
93
+ - `tools` is optional: how your tool calls look in the chat, up to 16, no code. `match` is the tool name OpenCode reports (`mcp.tasks.*` matches every tool under that prefix); `name` and `icon` (a Remixicon name or an SVG inside the folder, like `panel.icon`) set the header, `title` and `subtitle` are templates like `{input.id}` or `{output.total} open`, and `output` picks the body: `text`, `json`, `markdown`, `code` (with `language`), or `table` (with `columns`, rows from the output array or `output.items`). Leave `output` out to keep the app's own detection.
94
+ - `capabilities` lists what needs the user's approval: `prompt` to send messages, `sessions` to create sessions and worktrees, `files` to read and write inside the open project, `model` for one-off text generation with the user's Small Model (`host.generate`, no session involved). An `integration` adds `network`, a `service` adds `service`, `filesystem` patterns (like `["~/.config/opencode/opencode.json"]`) add `filesystem`, which lets `readFile`, `writeFile`, `listDir`, and `stat` reach those paths outside the project, and a session action with `payload: ["messages"]` adds `conversation`. The user approves the whole list once at install. Calls outside it fail with `NOT_GRANTED`.
95
+ - `integration` is optional. It adds a card at Settings → Integrations. `token` takes a pasted API token (`scheme: "bearer"` for `Authorization: Bearer`, `"basic"` for a username and token pair as Jira Cloud wants), `oauth` runs an authorize flow with a pasted client id, and `host: { "provider": "linear" }` reuses the Linear account already connected in OpenChamber. The page never sees the token; OpenChamber makes the calls through `host.request`.
96
+ - `service` is optional. It declares a local process OpenChamber starts next to the extension. It runs with the user's full access and no sandbox, so declare one only when the page cannot do the job. See [GUEST_SERVICES.md](./GUEST_SERVICES.md).
97
+
98
+ ## In the page
99
+
100
+ For a full-screen board, add `"page": true` under `contributes`, or `"page": { "entry": "panel/page.html", "title": "Board" }` for separate HTML. Users open it from the Extension pages menu above the session list. `ctx.surface` is `"page"`. The extension cannot open the page itself.
101
+
102
+ With `sessions` approved, use `listProjects()`, `listWorktrees(projectId)`, and `listSessions(projectId)`. Subscribe through `await onProjects(listener)`, `await onWorktrees(projectId, listener)`, or `await onSessions(projectId, listener)` and retain the returned unsubscribe function. Snapshots distinguish loading, ready, and error; session activity and observed turn outcomes are separate from your task status.
103
+
104
+ `startSession` accepts `projectId` and `worktree: { kind: "new", name: "fix-login", baseBranch: "main" }` or `{ kind: "existing", directory }`. It preserves the current screen by default. `openSession(sessionId)` explicitly opens the chat. `host.storage.get/set/delete/keys` stores your own JSON on the connected server without a file-access grant. See [API.md](./API.md) for limits and partial results. The `tasks-demo` page exercises these methods together.
105
+
106
+ ```ts
107
+ import { connectHost, HostRequestError } from '@openchamber/sdk';
108
+
109
+ const host = connectHost();
110
+
111
+ host.onReady((ctx) => {
112
+ document.body.dataset.theme = ctx.theme.mode;
113
+ });
114
+
115
+ host.onSession((session) => {
116
+ document.querySelector('#session')!.textContent = session?.title ?? '';
117
+ });
118
+
119
+ try {
120
+ const user = await host.request({ method: 'GET', path: '/me' });
121
+ } catch (error) {
122
+ if (error instanceof HostRequestError && error.code === 'DISCONNECTED') {
123
+ await host.oauthStart();
124
+ }
125
+ }
126
+
127
+ await host.toast({ kind: 'info', message: 'Hello' });
128
+ await host.compose({ text: 'Ask about the latest diff' });
129
+ await host.attach({
130
+ providerId: 'acme-hello',
131
+ id: 'TICKET-1',
132
+ title: 'Login is broken',
133
+ url: 'https://example.com/TICKET-1',
134
+ });
135
+ await host.startSession({
136
+ providerId: 'acme-hello',
137
+ id: 'TICKET-1',
138
+ title: 'Login is broken',
139
+ url: 'https://example.com/TICKET-1',
140
+ worktree: true,
141
+ text: 'Optional first message',
142
+ });
143
+ await host.prompt({ text: 'Fix the login', send: true });
144
+ await host.setBadge(3); // number on the rail icon; null clears it
145
+ const { text } = await host.generate({ prompt: task.description, system: 'One-line summary only.' }); // capability model
146
+
147
+ host.onResolve(({ command, args }) => {
148
+ // the user typed /task DEMO-2
149
+ const task = findTask(args.trim());
150
+ return task ? { providerId: 'acme-hello', id: task.id, title: task.title, url: task.url } : null;
151
+ });
152
+
153
+ host.onItem((item) => {
154
+ if (item?.kind === 'message') showMessage(item.text); // "Create task from message"
155
+ if (item?.kind === 'session') showSummary(item.messages); // "Summarize session"
156
+ });
157
+ ```
158
+
159
+ Every method, its limits, and the error codes are on the [Host API](https://openchamber.dev/docs/sdk/host/) page.
160
+
161
+ ## UI kit
162
+
163
+ `@openchamber/sdk/ui` has buttons, fields, a searchable dropdown, checkboxes, tabs, badges, lists, empty states, spinners, banners, separators, progress bars, menus, and safe text, all drawn with the app's colours and fonts. Call `applyHostReady` from `onReady` first, then mount what you need. Every mount returns `{ update, dispose }`. Reach for the kit before writing your own controls: it follows the user's theme and keyboard habits, so the panel feels like part of the app.
164
+
165
+ ```ts
166
+ import { applyHostReady, mountList } from '@openchamber/sdk/ui';
167
+
168
+ host.onReady((ctx) => {
169
+ applyHostReady(ctx, document.documentElement);
170
+ mountList(document.querySelector('#root')!, {
171
+ items: tasks.map((task) => ({ id: task.id, leading: task.key, title: task.title })),
172
+ onSelect: (id) => {
173
+ const task = tasks.find((item) => item.id === id);
174
+ if (task) void host.attach({ providerId: 'acme-hello', id, title: task.title, url: task.url });
175
+ },
176
+ });
177
+ });
178
+ ```
179
+
180
+ ## Schemas
181
+
182
+ `@openchamber/sdk/schemas` exports the zod schemas for the manifest and the messages, for tools that validate extensions. The main entry has no zod dependency, so a page bundle stays small.
183
+
184
+ ## Scope
185
+
186
+ This package covers the page, the manifest, the messages, and the UI kit. The page gets no terminal, no git, no files outside what it declared, and no access to OpenChamber's React tree. A declared `service` is different: it is a real process with the user's rights, so it can do anything the user can, and the approval dialog says so. `apiVersion` 1 is frozen; new methods arrive with the app's releases and this package's version.
@@ -0,0 +1,6 @@
1
+ /** Wire envelope `v`. Guest postMessage and host pushes use this. */
2
+ export declare const OPENCHAMBER_SDK_CHANNEL = "openchamber.sdk";
3
+ export declare const OPENCHAMBER_SDK_API_VERSION = 1;
4
+ /** Manifest `apiVersion` values this host accepts. Service and sockets ship on `1`. */
5
+ export declare const OPENCHAMBER_SDK_MANIFEST_API_VERSIONS: readonly [1];
6
+ export type OpenChamberManifestApiVersion = (typeof OPENCHAMBER_SDK_MANIFEST_API_VERSIONS)[number];
@@ -0,0 +1,5 @@
1
+ /** Wire envelope `v`. Guest postMessage and host pushes use this. */
2
+ export const OPENCHAMBER_SDK_CHANNEL = 'openchamber.sdk';
3
+ export const OPENCHAMBER_SDK_API_VERSION = 1;
4
+ /** Manifest `apiVersion` values this host accepts. Service and sockets ship on `1`. */
5
+ export const OPENCHAMBER_SDK_MANIFEST_API_VERSIONS = [1];