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.mode | light 또는 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 } |
settings | integration.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 항목이 놓이는 곳과 같은 위치입니다. 칩은 한 번에 하나만. |
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는 16진수 7~64자이며, 커밋은 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() | 파일 편집기: 이 파일은 여기서 열 수 없으니 대신 소스를 보여 줍니다 |
attach, startSession, sessionLink
세 가지 모두 같은 항목 필드를 받습니다.
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 }는 기존 worktree, { 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 오류입니다.
무엇이든 보내기 전에 클라이언트가 적용하는 제한:
| 필드 | 최대 글자 수 |
|---|---|
id | 128 |
title | 200 |
url | 2000 |
text | 16000 |
data(직렬화 기준) | 16000 |
author | 80 |
| 각 브랜치 이름 | 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의 브랜치, 사용 가능 상태를 제공합니다. 세션에는 날짜, 부모 ID, worktree, 해당 확장 프로그램의 첨부 항목이 포함됩니다. 알려진 보관 세션도 포함됩니다.
await onProjects(listener), await onWorktrees(projectId, listener), await onSessions(projectId, listener)는 구독 해제 함수를 반환합니다. 등록 오류를 처리하세요. 초기 스냅샷 다음에 변경 사항이 전달됩니다. 프레임당 최대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 토큰과 함께 한 번의 POST /session-events로 도착합니다. readSessionEventRequest로 읽고 { "ok": true }로 답하세요.
import { SESSION_EVENTS_PATH, readSessionEventRequest } from "@openchamber/sdk";
// inside your service's request handler, after the bearer checkif (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 }로 답하세요. 전달은 최대 한 번, 순서대로, 한 번에 요청 하나씩입니다. 응답에 실패하면 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를 추가합니다. 사용자는 이를 한 줄 더 승인하며, 그러면 서비스가 어느 세션에서든 사용자 대신 응답할 수 있습니다. permission-asked 이벤트에는 sessions.answerPermission(sessionId, requestId, { decision: 'allow' })(또는 에이전트가 읽는 message와 함께 'deny')를, event.question.kind가 form인 question-asked 이벤트에는 sessions.answerForm(sessionId, formId, values)를 씁니다. 이벤트 내용에는 폼의 필드가 key와 선택지와 함께 나열됩니다. 값 대신 null을 주면 폼을 닫습니다. 허용은 그 요청 하나에만 적용됩니다.
setInstructions에는 contributes.instructions가 필요하며, 에이전트가 읽는 내용을 폴더 하나에, 또는 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" }]}실행 한 번은 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)를 호출하면 에이전트가 텍스트를 받아 다음 턴을 시작합니다. 답은 블록당 하나이고 새로고침 후에는 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는 세 가지 호출을 POST /notifications로 전달합니다. auth.probe는 이 서버가 전달 대상인지, auth.verify는 이 토큰이 누구의 것인지, notify는 알림 하나입니다. 각각을 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 checkif (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는 아홉 가지 호출을 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 checkif (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를 각각 한 번씩 호출합니다. 댓글에는 그 외 아무것도 담기지 않습니다. 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 호출을 한 번 보내며, 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 checkif (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_UNAVAILABLE | window가 없거나, OpenChamber 안이 아니거나, dispose()가 실행됨 |
HOST_TIMEOUT | 20초 동안 응답 없음 |
HOST_REJECTED | OpenChamber가 거부했거나, 이 SDK가 모르는 코드로 응답함 |
DISCONNECTED | 통합에 대한 토큰이나 Linear 연결이 없음 |
BAD_PATH | path 형식이 잘못되었거나 허용된 origin을 벗어나려 함 |
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_FAILED | Small Model이 오류를 반환함 |
UNSUPPORTED | 이 OpenChamber에서는 할 수 없는 호출, 예를 들어 Diff 뷰가 없는 곳의 openCommit |