コンテンツにスキップ
↑↓移動 ↵開く esc閉じる

Host API

connectHost のメソッド、上限、エラーコードを正確に知りたいときにこのページを使ってください。フォルダ構成、マニフェスト、インストールについては 拡張機能を作る から始めてください。

import { connectHost, HostRequestError } from "@openchamber/sdk";
const host = connectHost();

connectHost は window がないと HOST_UNAVAILABLE を throw します。OpenChamber の外では、すべての呼び出しが HOST_UNAVAILABLE で reject されるクライアントを返します。ページを破棄するときは dispose() を呼んでください。進行中の呼び出しは同じコードで reject されます。

OpenChamber から届くもの

onReady は最初のスナップショットで発火し、OpenChamber がそれを更新するたびに再び発火します。

一度だけ実行されるマウント用コールバックではありません。UI のマウントと購読登録は一度だけ行い、以降は入力欄や下書きを置き換えずにテーマとコンテキストを更新します。UI kit のガード付き例を参照してください。onConnection などは現在値を即座に通知し、新しいスナップショットで同じ値を再通知する場合があります。次のリクエストを始める前に必要なフィールドを比較し、古いリクエストの応答は無視してください。

フィールド内容
theme.modelight または dark
theme.tokensアプリの色(サーフェス、テキスト、操作状態、primary、success/warning/error/info)、font、mono、radius。UI をマウントする前に applyHostReady に渡してください。
localeアプリの言語タグ
directory現在のプロジェクトディレクトリ、または null
session{ id, title, busy } または null。タイトルがなければセッション ID になります。busy はライブの状態です。セッションが持っていれば model と agent(OpenCode のエージェント)も含まれます。
surfaceレールは panel、添付ウィンドウは dialog、全画面は page、Work Status パネルは status、Files のファイルエディターは file、チャットの UI ブロック内は block
itemこの画面が何のために開かれたか、または null。ユーザーがクリックした添付済みの項目(attach に渡したのと同じフィールド、data を含む)、または宣言したアクションから来たメッセージ(kind: "message")かセッション(kind: "session")。アクション を参照。
connectionあなたの連携の { connected, account }
settingsintegration.settings で宣言した入力欄の値

アクセストークンはここにも request の結果にも決して現れません。

onDirectory、onSession、onSessionLifecycle、onConnection、onSettings、onItem は、遅れて購読しても最新の値を再送し、その後は変化のたびに発火し続けます。

theme.tokens には primaryText、successText、warningText、errorText、infoText が含まれます。これらは必須フィールドで、ホストが計算した文字色を持ちます。ニュートラルな背景や UI キットの淡い色付きコントロール用で、濃い単色の塗りつぶし用ではありません。applyHostReady はスナップショットごとにこれらを適用します。CSS 変数については UI キットを参照してください。

メソッド

呼び出し動作
toast({ kind, message })アプリにトーストを表示します。kind は info、success、error のいずれかです。
openUrl(url)ユーザーのブラウザで URL を開く
openSurface(surfaceId)アプリをその画面に切り替える
writeClipboard(text)テキストをコピーします。1 から 32000 文字。
compose({ text, mode? })送信せずにチャット欄にテキストを入れます。mode は append(デフォルト)または replace。トリム後 1 から 16000 文字。
attach({ ... })チャット欄にチップを付けます。GitHub や Linear の項目が置かれるのと同じ場所です。チップは同時に 1 つだけ。
startSession({ ... })その項目を添付したセッションを作ります。{ sessionId, sent } を返します。sessions 権限が必要です。
prompt({ text, send? })現在のセッションに書き込む、または送信します。{ sent } を返します。送信には prompt 権限が必要です。
sessionLink({ ... })セッションを作らずに、現在のセッションに項目を添付する
close()添付ウィンドウを閉じます。レール上では何もしません。
oauthStart()プロバイダーの認可ページを開く。host: { provider: "linear" } 連携なら Linear のページ
oauthDisconnect()保存されたトークン、または Linear 接続を忘れる
request({ method, path, query?, body? })ユーザーのトークンを付けて連携の apiOrigin を呼ぶ
serviceRequest({ method, path, query?, body? })拡張機能のローカルサービスを呼ぶ(GUEST_SERVICES.md を参照)
serviceStatus()stopped、starting、ready、failed のいずれか
readFile(path)テキストファイルを読む。{ content } を返す。
writeFile(path, content)テキストファイルをアトミックに書き、親フォルダも作成する。{ written: true } を返す。
listDir(path)フォルダを一覧する。{ entries: [{ name, kind }] } を返し、kind は file、directory、other のいずれか。
stat(path){ kind, size, mtime }。kind は file、directory、other、missing のいずれか。
generate({ prompt, system?, maxOutputTokens? })ユーザーの Small Model による一回限りのテキスト。{ text } を返す。model 権限が必要。
onResolve(handler)スラッシュコマンドのハンドラーを登録する。{ command, args } を受け取り、添付する項目または null を返す。
setBadge(count)レールのアイコンにドットを付け、数字(0 から 999)をツールチップに表示する。null で消す
openCommit(sha)現在のプロジェクトのそのコミットで Diff ビューを開く。sha は 7〜64 文字の 16 進数で、コミットは OpenChamber 自身が読み込みます。
setHeight(px)Work Status セクションの高さを設定する。OpenChamber は 24〜320 ピクセルに収め、それより高い内容はページ内でスクロールします。他の場所では無視されます。
onFileOpen(listener)ファイルエディター: 編集するファイルを受け取る。{ path, name, readOnly } に加えて、encoding: "text" なら content、encoding: "binary" なら bytes が入ります。ページ内の Cmd/Ctrl+S でも保存されるようになります。
onFileSnapshot(handler)ファイルエディター: 編集済みのファイル全体を { content, version }(バイナリエディターなら { bytes, version })で返す。OpenChamber は保存時と、ソース表示や全画面に切り替える前に呼び出します。
onFileSaved(listener)ファイルエディター: その version のスナップショットがディスクに書き込まれた。reportFileChange で応答してください。
reportFileChange({ dirty, edited })ファイルエディター: 未保存の変更があるか、ドキュメントが今変わったかを OpenChamber に伝える
requestFileSave()ファイルエディター: Cmd/Ctrl+S と同じように今すぐ保存する
reportFileUnsupported()ファイルエディター: このファイルはここでは開けないので、代わりにソースを表示する

3 つとも同じ項目フィールドを受け取ります。

await host.attach({
providerId: "acme-hello",
id: "TICKET-1",
title: "Login is broken",
url: "https://example.com/TICKET-1",
});
await host.attach({
providerId: "acme-hello",
id: "!12",
title: "Fix login",
url: "https://example.com/merge_requests/12",
kind: "pull",
author: "ada",
branches: { head: "feature", base: "main" },
text: "Optional notes for the model",
});
await host.startSession({
providerId: "acme-hello",
id: "!12",
title: "Fix login",
url: "https://example.com/merge_requests/12",
kind: "pull",
worktree: true,
text: "Optional first message",
});

providerId はパネル ID です。いずれにせよ OpenChamber があなたの ID で上書きします。id は項目に対するあなた自身の識別子です。kind は issue(デフォルト)または pull です。text はモデルに渡す任意のコンテキストで、トリム後 1 から 16000 文字です。data は任意の JSON で、内容は自由です(ステータス、コメントなど)。シリアライズ後 16000 文字までです。OpenChamber はこれをチップと一緒に保存し、ユーザーがチップをクリックしたときに ctx.item でそのまま返します。モデルには決して渡りません。

startSession は projectId を指定して、表示中のプロジェクトを切り替えずに作成できます。worktree 省略時は対象ディレクトリ、true は名前を生成する新規 worktree、{ kind: "existing", directory } は既存、{ kind: "new", name?, baseBranch? } は名前と基点を指定する新規 worktree です。名前はブランチにも使います。navigation は既定で "preserve"、"open" なら新しいチャットを開きます。最初のメッセージのモデル、エージェント、バリアントは開始時に確定します。

結果は sessionId、directory、sent、linked と任意の worktree を含みます。sent は sent、no-model、skipped、failed です。linked: false は項目の保存失敗です。準備やセッション作成の失敗後に worktree が残った場合は sessionId: null と failure: "bootstrap-failed" | "session-create-failed" を返します。再試行前に確認してください。待機は最大180秒で、タイムアウトは取り消しの証拠ではありません。

sessionLink は今開いているセッションに項目を添付します。プロジェクトやセッションがない場合は NO_SESSION エラーになります。

送信前にクライアント側で適用される上限:

フィールド最大文字数
id128
title200
url2000
text16000
data(シリアライズ後)16000
author80
各ブランチ名200

prompt とセッションのライフサイクル

await host.prompt({ text: "Fix the login" });
await host.prompt({ text: "Fix the login", send: true });
host.onSessionLifecycle((event) => {
document.body.dataset.phase = event.phase;
});

send なしの prompt はチャット欄のテキストを置き換えます。send: true を付けると、ユーザーが選択しているモデルとエージェントでメッセージを送ります。拡張機能がそれらを選ぶことはありません。開いているセッションがなければ NO_SESSION です。セッションがビジー中の送信は SESSION_BUSY になります。ビジー中でも欄への書き込みはできます。結果は { sent } で、値は startSession と同じです。

onSessionLifecycle はセッションが何をしているかを伝えます。phase は、モデルが作業中なら started、アイドルになったら completed、予期しない状態なら failure です。遅れて登録したリスナーにも現在の phase がすぐ届きます。

プロジェクト、ライブ状態、保存

listProjects()、listWorktrees(projectId)、listSessions(projectId) は既存の sessions 権限で共有ストアを読みます。呼び出しごとに Git を調べず、会話内容も公開しません。プロジェクトの ID・名前・ディレクトリ、worktree のブランチ・利用状態、セッションの日時・親・worktree・この拡張機能の添付項目を取得できます。既知のアーカイブ済みセッションも含みます。

await onProjects(listener)、await onWorktrees(projectId, listener)、await onSessions(projectId, listener) は購読解除関数を返します。登録時の権限エラーを処理してください。初期スナップショットの後に変更が届きます。1フレーム最大32件で、dispose()、閉じる操作、一時停止、削除、サーバー切り替えで解除されます。state は loading、ready、error で、セッションにはディレクトリ別の coverage もあります。失敗時はデータを保持し、完全な空一覧を保証するのは ready だけです。

activity は unknown、idle、running、retrying、waiting-permission、waiting-question です。outcome は観測済みの completed、failed、または null で、最大2,000セッション分をメモリだけに保持します。エラー後の idle は次の実行まで失敗を保持します。タスクの完了を意味しません。openSession(sessionId) でチャットを明示的に開けます。

host.storage.get(key)、set(key, value)、delete(key)、keys() は追加権限なしで独自 JSON を接続先サーバーに保存します。未保存は undefined、保存した null は null です。キーは1〜128文字、値は UTF-8 で64 KiB、全体は2 MiB・2,000キーまでです。プロジェクト別データにはキーにプロジェクト ID を含めます。書き込みは直列化されアトミックで、失敗時は既存データを保持します。アンインストールで削除されます。

request

const user = await host.request({ method: "GET", path: "/api/v2/user" });

path は / で始まり、スキームやホストを含みません。OpenChamber がマニフェストの apiOrigin と結合し、Authorization ヘッダーを付けます。トークン連携は貼り付けられたトークンをそのまま送るか、マニフェストで token.scheme: "bearer" を設定していれば Bearer <token> として、"basic" を設定していれば Basic base64(username:token) として送ります。OAuth と Linear の連携は常に Bearer を送ります。結果は { status, body } です。body はテキストなので、JSON は自分でパースしてください。

Linear 連携では GET /api/linear/issues/get も呼べます。これには OpenChamber が自身の Linear ルートから応答します。

20 秒以内に応答がなければ HOST_TIMEOUT です。

try {
await host.request({ method: "GET", path: "/api/v2/user" });
} catch (error) {
if (error instanceof HostRequestError && error.code === "DISCONNECTED") {
await host.oauthStart();
}
}

serviceRequest

マニフェストで contributes.service を宣言すると、OpenChamber がそのプロセスを起動し、ページは同じ種類の呼び出しでそれと話します。ページ自身がソケットを開くことはありません。

const status = await host.serviceStatus();
const result = await host.serviceRequest({ method: "GET", path: "/containers" });

path は request と同じ規則に従います。ローカルサービスを宣言すると、インストール時にユーザーが承認する権限に service が加わります。承認されるまで、すべての serviceRequest は NO_SERVICE です。完全な契約はパッケージ内のファイル GUEST_SERVICES.md にあります。

セッションイベント

ページがセッションについて知れるのは、そのセッションが開いている間だけです。サービスはそれ以上を知ることができます。contributes.service の隣に contributes.sessionEvents を宣言すると、OpenChamber は列挙したイベントを、すべてのプロジェクトから、ウィンドウが開いているかどうかに関係なくサービスに送ります。

"service": { "entry": "service/main.js", "runtime": "host" },
"sessionEvents": { "events": ["turn-finished", "question-asked"], "content": true }

イベントは turn-started、session-created、session-archived、session-deleted、turn-finished、turn-failed、permission-asked、question-asked です。turn-finished はエージェントが作業を終え、何も待っていない状態です。実行中のサブエージェントも、未応答の許可や質問も、ゴールのステップもありません。ユーザーが停止したターンは何も送りません。拡張機能を承認することが購読にあたり、ダイアログはサービスの行の下にイベントを列挙します。content がなければ、サービスは識別子だけを受け取ります。セッション、そのプロジェクトのディレクトリ、ユーザーがそのセッションに添付した項目です。"content": true にするとマニフェストは権限も求め、ユーザーには バックグラウンドでエージェントの発言を読む という独立した行として表示されます。このとき各イベントには、エージェントの最終回答、質問のフィールド、または許可の詳細が付きます。

各イベントは、サービスの bearer トークンを付けた 1 回の POST /session-events として届きます。readSessionEventRequest で読み取り、{ "ok": true } を返します。

import { SESSION_EVENTS_PATH, readSessionEventRequest } from "@openchamber/sdk";
// inside your service's request handler, after the bearer check
if (url.pathname === SESSION_EVENTS_PATH) {
const request = readSessionEventRequest(body);
if (!request) return json(res, 400, { ok: false, error: "not a session event" });
const { event, content } = request;
if (event.type === "turn-finished" && event.project.directory === myProject) {
await postComment(event.linked, content?.type === "turn-finished" ? content.reply : "");
}
return json(res, 200, { ok: true });
}

すべてのプロジェクトのイベントが送られます。event.project.directory を自分の設定と比較し、それ以外には { "ok": true } を返してください。配信は最大 1 回で順序どおり、同時に 1 リクエストずつです。応答に失敗すると 30 秒間イベントが破棄され、OpenChamber またはサービスが停止していた間のイベントは再送されません。この用途にサービスのパネルは不要です。session-events-log の例は、すべてのイベントをファイルに記録します。

サービスから OpenChamber を呼び出す

サービスはウィンドウが開いていなくても動作できます。createServiceHost は、サービスから OpenChamber へ戻る経路です。

import { createServiceHost } from '@openchamber/sdk';
const host = createServiceHost({ env: process.env });
const { sessionId } = await host.sessions.start({ prompt: 'Summarize the failed deploy', directory });
await host.sessions.send(sessionId, 'Now write the incident note');
await host.setInstructions(directory, 'Deploys run through the release branch.');
await host.emit('deploy-finished', { id: 42 });

sessions.start、sessions.send、sessions.retry、sessions.status、sessions.stop には contributes.sessionControl が必要で、ユーザーはこれを独立した行として承認します。sessions.lastReply には sessionControl: { content: true } が必要です。sessions.send で送ったメッセージは、チャットに入力したメッセージと同じように、そのセッションでエージェントが待っているフォームやページを閉じます。

エージェントのリクエストをチャットアプリなど別の場所へ運ぶには、answers: true を追加します。ユーザーはこれをもう 1 行として承認し、サービスはどのセッションでもユーザーの代わりに応答できるようになります。permission-asked イベントには sessions.answerPermission(sessionId, requestId, { decision: 'allow' })(またはエージェントが読む message を付けた 'deny')、event.question.kind が form の question-asked イベントには sessions.answerForm(sessionId, formId, values) を使います。イベントの内容には、フォームのフィールドが key と選択肢付きで並びます。値の代わりに null を渡すとフォームを閉じます。許可はその 1 件のリクエストだけに適用されます。

setInstructions には contributes.instructions が必要で、エージェントが読む内容に、1 つのフォルダー単位で、または null を渡してすべての場所に追加します。

emit は host.onServiceEvent を通じて開いているフレームに届くので、パネルはポーリングをやめられます。再送はないため、あとから開いたフレームは最初に現在の状態を読み取ります。readSettings はインテグレーションの設定を secret フィールドも含めてサービスに渡し、needsConfiguration は拡張機能のカードにメッセージを表示します。

誰からも呼ばれなくても動かす

サービスは最初のリクエストで起動し、ロールを持つサービスはアイドル状態になると停止します。service.keepRunning: true にすると OpenChamber と一緒に起動し、停止しても再起動します。service.schedule は時刻に合わせてサービスを起こします。

"service": {
"entry": "service/main.js",
"runtime": "host",
"keepRunning": true,
"schedule": [{ "id": "nightly", "cron": "0 3 * * *", "timezone": "Europe/Kyiv" }]
}

1 回の実行は 1 回の POST /schedule です。readServiceScheduleRequest で読み取り、200 を返します。OpenChamber が停止している間に逃した実行は失われます。外部のイベントが必要なサービスは、自分で接続を保持するかポーリングします。

@ メニューの行

contributes.mentions: { label } は、チャット欄の @ メニューにセクションを追加します。サービスは POST /mentions/search に 2 秒以内に { items: [{ id, title, subtitle?, url? }] } を返し、POST /mentions/resolve にはユーザーが選んだ行に対して { text } を返します。テキストはメッセージと一緒にエージェントに届き、ユーザーにはチップが表示されます。遅いサービスは行を追加せず、ファイルの表示を止めることもありません。読み取れない行はチップを残さず、その旨を表示します。

チャット内のブロック

contributes.blocks は、エージェントが UI ブロックに書けるコンポーネントを追加します。各コンポーネントは、パネルと同じサンドボックスと同じ権限で、あなたのページが描画します。

"blocks": [{ "name": "JiraIssue", "description": "A Jira ticket", "args": ["key"], "entry": "blocks/issue.html" }]

エージェントが JiraIssue("OPE-1") と書くと、ページはその呼び出しを読み取ります。

host.onBlock(({ args }) => draw(args.key));

ルート要素に付けた ResizeObserver から host.setHeight(px) を呼び出します。フレームはパディングなしでボックスを埋めるので、パディングは自分で追加してください。コンポーネントは拡張機能ごとに最大 4 つです。すべてのシグネチャが、各セッションの読むテキストになるためです。

ブロックは、ブロック内のボタンと同じようにエージェントに回答できます。クリックから host.answerBlock(text) を呼び出すと、エージェントがそのテキストを受け取り、次のターンを始めます。回答はブロックごとに 1 回だけで、再読み込み後は block.answered に保持されるので、そのときは回答済みとしてブロックを描画してください。

host.onBlock(({ args, answered }) => draw(args.key, answered));
button.onclick = () => host.answerBlock('Close this ticket');

このように動作するコンポーネントには "answers": true を設定します。説明が合うとき、エージェントは組み込みの選択肢の代わりにこのコンポーネントで質問するよう指示され、回答を待つページでは送信ボタンが省かれます。このようなブロックの下には、OpenChamber 自身のフォームと同じメモ欄が表示されます。エージェントはメモを回答と一緒に読み、メモだけでも回答になります。

引数はモデルが書くので、ウェブページやチケットのテキストに左右されることがあります。信頼できない入力として扱い、副作用のある処理はユーザーがクリックするまで行わないでください。

通知チャネル

サービスは、デスクトップとスマートフォンに加えてユーザーの通知を届けることができます。provides: ["notifications"] を宣言し、contributes.notifications でチャネルを記述します。

"service": { "entry": "service/main.js", "runtime": "host", "provides": ["notifications"] },
"notifications": {
"label": "Slack",
"instances": { "kind": "fixed", "origin": "https://slack.com" },
"token": { "header": "authorization", "scheme": "bearer", "scopes": ["chat:write"] }
}

OpenChamber はその記述から、設定 → 連携 にチャネルのアカウントカードを描き、アカウントを管理します。ユーザーはそこでアカウントを接続し、トークンは OpenChamber に保持され、サービスがそれを見ることはありません。アカウントが接続されると、OpenChamber がスマートフォンに送るすべての通知がサービスにも届きます。ユーザー自身の通知の設定に従い、ウィンドウが表示されていない間だけ届きますが、ユーザーが常に通知を受け取る設定を選んでいればそのときも届きます。拡張機能を一時停止すると止まります。

OpenChamber は 3 種類の呼び出しを POST /notifications に転送します。auth.probe はこのサーバーが配信先かどうか、auth.verify はこのトークンが誰のものか、notify は 1 件の通知です。それぞれ readNotificationsProviderRequest で読み取り、チャネルのサーバーには送信ヘルパー経由でのみアクセスします。ヘルパーは呼び出しのハンドルを付けてリクエストを OpenChamber に送り、OpenChamber がトークンを加え、リクエストをアカウントのサーバーに限定します。

import { NOTIFICATIONS_PROVIDER_PATH, createNotificationsOutbound, readNotificationsProviderRequest } from "@openchamber/sdk";
const outbound = createNotificationsOutbound({ env: process.env });
// inside your service's request handler, after the bearer check
if (url.pathname === NOTIFICATIONS_PROVIDER_PATH) {
const request = readNotificationsProviderRequest(body);
if (!request) return json(res, 400, { ok: false, error: { message: "not a notifications call" } });
if (request.method === "notify") {
const { notification } = request.input as { notification: GuestNotification };
const answer = await outbound.request(request.session.handle, {
method: "POST", path: "/api/chat.postMessage",
headers: { "content-type": "application/json" },
body: JSON.stringify({ channel: "#agents", text: `${notification.title}: ${notification.body}` }),
});
return json(res, 200, answer.status === 200 ? { ok: true, data: { delivered: true } } : { ok: false, error: { message: `Slack answered ${answer.status}`, status: answer.status } });
}
// auth.probe → { ok: true, data: { supported: true } }; auth.verify → { ok: true, data: { account: { id, login } } }
}

どのプロジェクトをどのルームやチャンネルに送るかは拡張機能が決めます。通知には directory と sessionName が含まれます。エンタープライズモードでは、スマートフォンと同じく汎用的なタイトルだけが含まれます。notification-webhook の例は完成したチャネルで、試すための受信側も付いています。

トラッカー

サービスは、Linear の隣に並ぶ作業アイテムのトラッカーになれます。provides: ["tracker"] を宣言し、contributes.tracker でトラッカーを記述します。OpenChamber は、サービスが一度も動く前に、マニフェストだけを基にトラッカーが現れるすべての場所で Linear の隣にそれを並べます。

"service": { "entry": "service/main.js", "runtime": "host", "provides": ["tracker"] },
"tracker": {
"wire": 1,
"label": "YouTrack",
"instances": { "kind": "user", "label": "Server address", "placeholder": "https://youtrack.example.com" },
"auth": { "token": { "header": "authorization", "scheme": "bearer", "createPath": "/users/me?tab=account-security" } },
"capabilities": { "stateUpdate": true, "labels": true, "priority": true, "projectMapping": true, "sessionComments": true },
"stateFilters": ["open", "all", "todo", "in-progress", "done"],
"identifier": "key-number",
"urls": { "item": ["/issue/{id}", "/{segments+}/issue/{id}"] }
}

ユーザーは、拡張機能のアカウント にあるトラッカーのカードでアカウントを接続します。トークンか、auth.oauth を宣言した場合はブラウザーでのサインインです(OpenChamber はユーザーが入力した client id でサインインを実行し、トークンを保持します)。サービスが認証情報を見ることはありません。urls.item はアイテムのアドレスのテンプレートで、{id} が識別子を表します(クエリで渡される場合は ?name={id} の形も使えます)。OpenChamber がこれをコンパイルするので、貼り付けたリンクはリンクされたアイテムになります。identifier はキーの形を示します。key-number(PROJ-42)、number、short-code のいずれかです。

OpenChamber は 9 種類の呼び出しを POST /tracker に転送します。auth.probe と auth.verify、読み取りの items.list、items.get、items.liveSummaries、states.list、containers.list、書き込みの items.updateState と comments.create です。それぞれ readTrackerProviderRequest で読み取り、中立的なアイテムの形で応答し、トラッカーのサーバーには送信ヘルパー経由でのみアクセスします。ヘルパーは呼び出しのハンドルを付けてリクエストを OpenChamber に送り、OpenChamber が認証情報を加え、リクエストをアカウントのサーバーに限定します。読み取りのハンドルは GET しか送れません。

import { TRACKER_PROVIDER_PATH, createTrackerOutbound, readTrackerProviderRequest } from "@openchamber/sdk";
const outbound = createTrackerOutbound({ env: process.env });
// inside your service's request handler, after the bearer check
if (url.pathname === TRACKER_PROVIDER_PATH) {
const request = readTrackerProviderRequest(body);
if (!request) return json(res, 400, { ok: false, error: { message: "not a tracker call" } });
if (request.method === "items.get") {
const answer = await outbound.request(request.session.handle, { method: "GET", path: `/api/issues/${encodeURIComponent(String(request.input.id))}` });
if (answer.status === 404) return json(res, 200, { ok: true, data: { item: null } });
if (answer.status !== 200) return json(res, 200, { ok: false, error: { message: `YouTrack answered ${answer.status}`, status: answer.status } });
return json(res, 200, { ok: true, data: { item: toTrackerItem(JSON.parse(answer.body)) } });
}
}

ユーザーがセッションコメントをオンにすると、OpenChamber は “session started”、“completed”、“failed” とセッションへのリンクを付けて comments.create をそれぞれ 1 回呼び出します。コメントにはそれ以外何も含まれません。tracker-provider の例は完成したトラッカーで、試すための偽のサーバーも付いています。

使用量プロバイダー

サービスは、組み込みのプロバイダーの隣で使用量を報告できます。provides: ["quota"] を宣言し、contributes.quota.providers に必要な認証情報を持つ OpenCode プロバイダーを列挙します。OpenChamber はそのプロバイダーを ext-<panel.id> と名付け、拡張機能の名前とアイコンでカードを描き、組み込みのプロバイダーと同じように問い合わせます。

"service": { "entry": "service/main.js", "runtime": "host", "provides": ["quota"] },
"quota": { "providers": ["openrouter"] }

承認ダイアログにはそれらのプロバイダーが列挙され、リストを変えたバージョンは改めて承認を求めます。更新のたびに OpenChamber は POST /quota に usage.read を 1 回送ります。input.credentials には、列挙されたプロバイダーのうち OpenCode が認証情報を持つものについて、その認証情報(API キー、またはブラウザーログインのトークン)だけが入り、ストアのそれ以外は入りません。OpenCode が認証情報を持たない列挙済みプロバイダーは未設定として表示され、サービスは呼ばれません。空のリストも許されます。拡張機能自身の設定にキーを持つプロバイダー向けです。

ボディは readQuotaProviderRequest で読み、組み込みのプロバイダーが算出する結果を { ok: true, data } で答えます。ok、configured、error、planLabel、そしてウィンドウ名をキーにした usage.windows(それぞれ usedPercent、windowSeconds、resetAt(エポックミリ秒)、任意の valueLabel または value)、加えてモデル別ウィンドウの usage.models です。残りの割合、リセットまでのカウントダウン、整形済み文字列は OpenChamber が導出するため、カードは組み込みのプロバイダーと一致します。

import { QUOTA_PROVIDER_PATH, readQuotaProviderRequest } from "@openchamber/sdk";
// inside your service's request handler, after the bearer check
if (url.pathname === QUOTA_PROVIDER_PATH) {
const request = readQuotaProviderRequest(body);
if (!request) return json(res, 400, { ok: false, error: { message: "not a usage call" } });
const credential = request.input.credentials.openrouter;
const bearer = credential.type === "api" ? credential.key : credential.access;
const answer = await fetch("https://openrouter.ai/api/v1/key", { headers: { authorization: `Bearer ${bearer}` } });
if (answer.status === 401) return json(res, 200, { ok: true, data: { ok: false, configured: true, error: "OpenRouter refused the key.", usage: null } });
return json(res, 200, { ok: true, data: toUsage(await answer.json()) });
}

サービスは 20 秒以内に答え、呼び出しが 10 分なければ停止します。使用量ページは開いている間 3 分ごとに更新します。quota-provider の例は完成したプロバイダーで、試すための偽の使用量 API も付いています。

ファイル

相対パスは開いているプロジェクトの中を指し、files 権限が必要です。/ または ~/ で始まるパスは、宣言済みの contributes.filesystem パターンに一致する必要があり、filesystem 権限が必要です。規則は 拡張機能を作る を参照してください。

const { content } = await host.readFile("package.json");
await host.writeFile("notes/today.md", "# Today\n");
const { entries } = await host.listDir("src");
const info = await host.stat("~/.config/opencode/opencode.json");

上限: パスは 1024 文字まで、内容は 2,000,000 文字まで、一覧は 2000 件まで。内容の上限を超えると FILE_TOO_LARGE です。

generate

const { text } = await host.generate({ prompt: notes, system: "Summarize in one sentence." });

prompt はトリム後 1 から 64,000 文字、system は 8,000 文字まで、maxOutputTokens は 1 から 4,000 です。何もセッションには入らず、履歴も残りません。拡張機能がモデルを選ぶことはありません。呼び出しは最大 90 秒待ちます。使える Small Model がない場合は NO_MODEL、モデルがエラーを返した場合は MODEL_FAILED です。テキストを生成する を参照してください。

エラーコード

失敗した呼び出しは HostRequestError を throw します。code は次のいずれかで、message に詳細が入ります。

コード発生条件
HOST_UNAVAILABLEwindow がない、OpenChamber の中ではない、または dispose() が実行された
HOST_TIMEOUT20 秒間応答がない
HOST_REJECTEDOpenChamber が拒否した、またはこの SDK が知らないコードで応答した
DISCONNECTED連携用のトークンや Linear 接続がない
BAD_PATHpath の形式が不正、または許可されたオリジンの外に出ようとした
NO_INTEGRATIONマニフェストに integration がない
NO_SESSION開いているセッションがない状態での prompt または sessionLink
SESSION_BUSYセッションがビジー中の prompt({ send: true })
DISABLEDユーザーが 設定 → 拡張機能 で拡張機能を一時停止した
NO_SERVICEローカルサービスが未宣言、未承認、または未起動
NOT_GRANTEDこの呼び出しに必要な権限をユーザーが承認していない
SERVICE_FAILEDローカルサービスがクラッシュした、または ready にならなかった
NO_DIRECTORY開いているプロジェクトがない状態で相対ファイルパスを使った、または開いているプロジェクトがない状態で openCommit を呼んだ
NOT_FOUND存在しないファイルに対する readFile、またはプロジェクトにないコミットへの openCommit
FILE_TOO_LARGE読み込みまたは書き込み時にファイル内容が上限を超えた
DENIEDオペレーティングシステムがファイル操作を拒否した
NO_MODEL利用できる Small Model がない状態での generate
MODEL_FAILEDSmall Model がエラーを返した
UNSUPPORTEDこの OpenChamber では実行できない呼び出し。例: Diff ビューがない場所での openCommit

関連