Przejdź do głównej zawartości
↑↓Nawigacja ↵Otwórz escZamknij

API hosta

Użyj tej strony, gdy potrzebujesz dokładnych metod connectHost, limitów i kodów błędów. Układ folderu, manifest i instalację opisuje Tworzenie rozszerzenia.

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

connectHost rzuca HOST_UNAVAILABLE, gdy nie ma window. Poza OpenChamber zwraca klienta, którego każde wywołanie jest odrzucane z HOST_UNAVAILABLE. Wywołaj dispose(), gdy zamykasz stronę; wywołania jeszcze w toku są odrzucane z tym samym kodem.

Co OpenChamber do Ciebie wysyła

onReady odpala się z pierwszą migawką i ponownie za każdym razem, gdy OpenChamber ją odświeża.

To nie jest jednorazowy callback montowania. Zamontuj UI i zarejestruj subskrypcje raz, a potem aktualizuj motyw i kontekst bez zastępowania pól ani szkiców. Przykład z ochroną przed ponownym montowaniem znajdziesz w UI kit. Listenery takie jak onConnection od razu przekazują bieżącą wartość i mogą ją powtórzyć z nową migawką. Porównuj istotne pola przed nowym żądaniem i ignoruj odpowiedzi zastąpionych żądań.

PoleCo to jest
theme.modelight lub dark
theme.tokenskolory aplikacji (powierzchnie, tekst, stany interakcji, primary, success/warning/error/info), font, mono i radius. Przekaż to do applyHostReady przed zamontowaniem UI.
localetag języka aplikacji
directorykatalog bieżącego projektu albo null
session{ id, title, busy } albo null. Tytuł wraca do id sesji, gdy go brak. busy to status na żywo. model i agent (agent OpenCode) pojawiają się, gdy sesja je ma.
surfacepanel na pasku bocznym, dialog w oknie dołączania, page na pełnym ekranie, status w panelu Work Status, file jako edytor plików w Files, block w bloku UI na czacie
itemto, dla czego otwarto tę powierzchnię, albo null: dołączony element, który użytkownik kliknął (te same pola, które przekazałeś do attach, łącznie z data), wiadomość (kind: "message") albo sesja (kind: "session") z jednej z zadeklarowanych przez Ciebie akcji. Zobacz Akcje.
connection{ connected, account } dla Twojej integracji
settingswartości pól zadeklarowanych w integration.settings

Tokeny dostępu nigdy nie pojawiają się tutaj ani w wyniku request.

onDirectory, onSession, onSessionLifecycle, onConnection, onSettings i onItem odtwarzają ostatnią wartość, gdy subskrybujesz późno, a potem odpalają się dalej przy każdej zmianie.

theme.tokens zawiera primaryText, successText, warningText, errorText i infoText. Te wymagane pola zawierają kolory tekstu obliczone przez hosta dla neutralnych teł i delikatnie zabarwionych kontrolek zestawu UI, nie dla pełnych kolorowych wypełnień. applyHostReady stosuje je przy każdym nowym stanie. Zmienne CSS opisuje zestaw UI.

Metody

WywołanieCo robi
toast({ kind, message })pokazuje toast w aplikacji. kind to info, success lub error.
openUrl(url)otwiera adres URL w przeglądarce użytkownika
openSurface(surfaceId)przełącza aplikację na ten ekran
writeClipboard(text)kopiuje tekst. Od 1 do 32000 znaków.
compose({ text, mode? })wstawia tekst do pola czatu bez wysyłania. mode to append (domyślnie) lub replace. Od 1 do 16000 znaków po przycięciu.
attach({ ... })umieszcza chip na polu czatu, w tym samym miejscu, gdzie trafiają elementy z GitHub i Linear. Tylko jeden chip naraz.
startSession({ ... })tworzy sesję z dołączonym elementem. Zwraca { sessionId, sent }. Wymaga uprawnienia sessions.
prompt({ text, send? })wpisuje tekst do bieżącej sesji albo go w niej wysyła. Zwraca { sent }. Wysyłanie wymaga uprawnienia prompt.
sessionLink({ ... })dołącza element do bieżącej sesji bez tworzenia nowej
close()zamyka okno dołączania. Na pasku bocznym nic nie robi.
oauthStart()otwiera stronę autoryzacji dostawcy albo stronę Linear dla integracji host: { provider: "linear" }
oauthDisconnect()zapomina zapisany token albo połączenie z Linear
request({ method, path, query?, body? })wywołuje apiOrigin Twojej integracji z dołączonym tokenem użytkownika
serviceRequest({ method, path, query?, body? })wywołuje usługę lokalną Twojego rozszerzenia (zobacz GUEST_SERVICES.md)
serviceStatus()stopped, starting, ready lub failed
readFile(path)czyta plik tekstowy. Zwraca { content }.
writeFile(path, content)zapisuje plik tekstowy atomowo, tworząc foldery nadrzędne. Zwraca { written: true }.
listDir(path)wypisuje zawartość folderu. Zwraca { entries: [{ name, kind }] }, kind to file, directory lub other.
stat(path){ kind, size, mtime }, kind to file, directory, other lub missing.
generate({ prompt, system?, maxOutputTokens? })jednorazowy tekst od Small Model użytkownika. Zwraca { text }. Wymaga uprawnienia model.
onResolve(handler)rejestruje obsługę Twoich poleceń z ukośnikiem. Dostaje { command, args } i zwraca element do dołączenia albo null.
setBadge(count)oznacza Twoją ikonę na pasku bocznym kropką i pokazuje liczbę (0 do 999) w jej podpowiedzi, albo null, żeby ją usunąć
openCommit(sha)otworzyć widok Diff na tym commicie bieżącego projektu. sha ma od 7 do 64 znaków szesnastkowych; OpenChamber sam odczytuje commit.
setHeight(px)ustawić wysokość twojej sekcji Work Status. OpenChamber trzyma ją między 24 a 320 pikselami; wyższa treść przewija się wewnątrz strony. W innych miejscach jest ignorowane.
onFileOpen(listener)edytory plików: odebrać plik do edycji, { path, name, readOnly } plus encoding: "text" z content albo encoding: "binary" z bytes. Sprawia też, że Cmd/Ctrl+S w twojej stronie zapisuje.
onFileSnapshot(handler)edytory plików: zwrócić { content, version } (albo { bytes, version } w edytorze binarnym), cały zmieniony plik. OpenChamber wywołuje to przy zapisie oraz przed przełączeniem na widok źródła lub pełny ekran.
onFileSaved(listener)edytory plików: migawka z tą version jest na dysku. Odpowiedz przez reportFileChange.
reportFileChange({ dirty, edited })edytory plików: powiedzieć OpenChamber, czy są niezapisane zmiany i czy dokument właśnie się zmienił
requestFileSave()edytory plików: zapisać teraz, tak jak Cmd/Ctrl+S
reportFileUnsupported()edytory plików: tego pliku nie da się tu otworzyć, zamiast tego pokazać jego źródło

Wszystkie trzy przyjmują te same pola elementu:

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 to id Twojego panelu; OpenChamber i tak nadpisuje je Twoim id. id to Twój własny identyfikator elementu. kind to issue (domyślnie) lub pull. text to opcjonalny kontekst dla modelu, od 1 do 16000 znaków po przycięciu. data to opcjonalny JSON na Twój użytek (status, komentarze, cokolwiek), do 16000 znaków po serializacji. OpenChamber zapisuje go razem z chipem i oddaje bez zmian w ctx.item, gdy użytkownik kliknie chip; nigdy nie trafia do modelu.

startSession przyjmuje projectId bez przełączania bieżącego projektu. Pominięte worktree oznacza katalog docelowy, true tworzy worktree z wygenerowaną nazwą, { kind: "existing", directory } wskazuje istniejące, a { kind: "new", name?, baseBranch? } nowe. Nazwa dotyczy też gałęzi. navigation domyślnie wynosi "preserve"; "open" otwiera nowy czat. Model, agent i wariant pierwszej wiadomości są ustalane na początku.

Wynik zawiera sessionId, directory, sent, linked i opcjonalne worktree. sent to sent, no-model, skipped lub failed; linked: false oznacza błąd zapisu elementu. Worktree pozostawione po błędzie przygotowania lub tworzenia sesji zwraca sessionId: null i failure: "bootstrap-failed" | "session-create-failed". Sprawdź wynik przed ponowieniem. Limit oczekiwania wynosi 180 sekund; timeout nie potwierdza wycofania operacji.

sessionLink dołącza element do sesji otwartej w tej chwili. Brak projektu lub brak sesji to błąd NO_SESSION.

Limity, które klient stosuje, zanim cokolwiek wyśle:

PoleMaks. znaków
id128
title200
url2000
text16000
data (po serializacji)16000
author80
każda nazwa gałęzi200

prompt i cykl życia sesji

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;
});

Bez send prompt zastępuje tekst w polu czatu. Z send: true wysyła wiadomość z modelem i agentem wybranymi przez użytkownika. Rozszerzenie nigdy ich nie wybiera. Brak otwartej sesji to NO_SESSION. Wysyłanie, gdy sesja jest zajęta, to SESSION_BUSY; wpisywanie do pola działa także wtedy. Wynik to { sent } z tymi samymi wartościami co w startSession.

onSessionLifecycle mówi, co robi sesja. phase to started, gdy model pracuje, completed, gdy sesja przechodzi w bezczynność, i failure przy nieoczekiwanym statusie. Późny nasłuchiwacz od razu dostaje bieżącą fazę.

Projekty, stany na żywo i pamięć

listProjects(), listWorktrees(projectId) i listSessions(projectId) używają istniejącego uprawnienia sessions i wspólnych store. Nie wykonują skanu Git przy każdym wywołaniu ani nie udostępniają rozmów. Projekty zawierają ID, nazwę i katalog, worktree także gałąź i dostępność. Sesje zawierają daty, rodzica, worktree i tylko elementy dołączone przez to rozszerzenie. Znane sesje archiwalne są uwzględnione.

await onProjects(listener), await onWorktrees(projectId, listener) i await onSessions(projectId, listener) zwracają funkcję anulowania subskrypcji. Obsłuż błędy rejestracji. Najpierw dostajesz migawkę, potem zmiany. Maksymalnie 32 subskrypcje na ramkę; dispose(), zamknięcie, wstrzymanie, usunięcie i zmiana serwera zwalniają je. state to loading, ready lub error, a sesje mają coverage dla katalogów. Błąd zachowuje dane; tylko ready potwierdza pełną pustą listę.

activity to unknown, idle, running, retrying, waiting-permission lub waiting-question. outcome to zaobserwowane completed, failed lub null, przechowywane wyłącznie w pamięci dla maksymalnie 2 000 sesji. Idle po błędzie zachowuje błąd do następnego uruchomienia. Nie oznacza to ukończenia zadania. openSession(sessionId) jawnie otwiera czat.

host.storage.get(key), set(key, value), delete(key) i keys() zapisują własny JSON na połączonym serwerze bez dodatkowego uprawnienia. Brak klucza daje undefined, zapisane null pozostaje null. Klucze mają od 1 do 128 znaków, wartość do 64 KiB UTF-8, całość do 2 MiB i 2 000 kluczy. Dla danych projektu umieść jego ID w kluczu. Zapisy są serializowane i atomowe, a błędy zachowują poprzednie dane. Usunięcie rozszerzenia usuwa pamięć.

request

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

path zaczyna się od / i nie ma schematu ani hosta; OpenChamber łączy go z apiOrigin z Twojego manifestu i dodaje nagłówek Authorization. Integracje tokenowe wysyłają wklejony token bez zmian, jako Bearer <token>, gdy manifest ustawia token.scheme: "bearer", albo jako Basic base64(username:token), gdy ustawia "basic". Integracje OAuth i Linear zawsze wysyłają Bearer. Wynik to { status, body }. Treść jest tekstem; JSON parsujesz samodzielnie.

Integracja Linear może też wywołać GET /api/linear/issues/get, na które OpenChamber odpowiada z własnej trasy Linear.

Brak odpowiedzi w ciągu 20 sekund to 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

Gdy manifest deklaruje contributes.service, OpenChamber uruchamia ten proces, a Twoja strona rozmawia z nim przez ten sam rodzaj wywołania. Strona nigdy sama nie otwiera socketu.

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

path podlega tym samym zasadom co w request. Zadeklarowanie usługi lokalnej dodaje service do uprawnień, które użytkownik zatwierdza przy instalacji; do tego czasu każde serviceRequest to NO_SERVICE. Pełny kontrakt: plik GUEST_SERVICES.md w pakiecie.

Zdarzenia sesji

Twoja strona wie o sesji tylko wtedy, gdy ta jest otwarta. Twoja usługa może wiedzieć więcej: zadeklaruj contributes.sessionEvents obok contributes.service, a OpenChamber wysyła do usługi wymienione zdarzenia ze wszystkich projektów, niezależnie od tego, czy jakieś okno jest otwarte.

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

Zdarzenia to turn-started, session-created, session-archived, session-deleted, turn-finished (agent skończył i na nic nie czeka: nie działa żaden subagent, nie ma otwartego uprawnienia ani pytania, nie ma kroku celu), turn-failed, permission-asked i question-asked. Tura zatrzymana przez użytkownika nie wysyła nic. Zatwierdzenie rozszerzenia jest subskrypcją; okno zatwierdzania wymienia zdarzenia pod wierszem usługi. Bez content usługa dostaje same identyfikatory: sesję, folder jej projektu i elementy, które użytkownik do niej dołączył. Z "content": true manifest prosi też o uprawnienie, które użytkownik widzi jako osobny wiersz, Czytać, co agent mówi w tle, a każde zdarzenie niesie końcową odpowiedź agenta, pola pytania albo szczegóły uprawnienia.

Każde zdarzenie przychodzi jako jedno POST /session-events z tokenem bearer usługi. Odczytaj je przez readSessionEventRequest i odpowiedz { "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 });
}

Wysyłane są zdarzenia ze wszystkich projektów; porównaj event.project.directory z własnymi ustawieniami, a na resztę odpowiadaj { "ok": true }. Dostarczanie jest co najwyżej jednokrotne i w kolejności, po jednym żądaniu naraz; nieudana odpowiedź odrzuca Twoje zdarzenia na 30 sekund, a po przestoju OpenChamber lub Twojej usługi nic nie jest wysyłane ponownie. Usługa nie potrzebuje do tego panelu. Przykład session-events-log zapisuje każde zdarzenie do pliku.

Wywoływanie OpenChamber z usługi

Usługa może działać bez otwartego okna. createServiceHost to jej droga powrotna do 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 i sessions.stop wymagają contributes.sessionControl, które użytkownik zatwierdza jako osobny wiersz. sessions.lastReply wymaga sessionControl: { content: true }. Wiadomość z sessions.send zamyka formularz lub stronę, na które agent czeka w tej sesji, tak samo jak wiadomość wpisana w czacie.

Aby przenosić żądania agenta gdzie indziej, na przykład do komunikatora, dodaj answers: true. Użytkownik zatwierdza to jako kolejny wiersz, a usługa może wtedy odpowiadać w imieniu użytkownika w dowolnej sesji: sessions.answerPermission(sessionId, requestId, { decision: 'allow' }) (lub 'deny' z message, którą czyta agent) na zdarzenie permission-asked oraz sessions.answerForm(sessionId, formId, values) na zdarzenie question-asked, którego event.question.kind to form. Treść zdarzenia wymienia pola formularza z ich key i opcjami wyboru. null zamiast wartości odrzuca formularz. Zgoda obejmuje tylko to jedno żądanie.

setInstructions wymaga contributes.instructions i uzupełnia to, co czytają agenci, w jednym folderze albo, z null, wszędzie.

emit dociera do Twoich otwartych ramek przez host.onServiceEvent, więc panel nie musi już odpytywać usługi. Nic nie jest wysyłane ponownie, dlatego ramka otwarta później najpierw odczytuje bieżący stan. readSettings daje usłudze ustawienia integracji razem z polami secret, a needsConfiguration pokazuje komunikat na karcie rozszerzenia.

Działanie bez niczyjej prośby

Usługa startuje przy pierwszym żądaniu, a usługa z rolą zatrzymuje się, gdy jest bezczynna. service.keepRunning: true uruchamia ją razem z OpenChamber i przywraca, gdy się zatrzyma. service.schedule budzi ją według harmonogramu:

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

Jedno uruchomienie to jedno POST /schedule. Odczytaj je przez readServiceScheduleRequest i odpowiedz 200. Uruchomienie pominięte, gdy OpenChamber nie działał, przepada. Usługa, która potrzebuje zdarzeń z zewnątrz, utrzymuje własne połączenie albo odpytuje źródło.

Wiersze w menu @

contributes.mentions: { label } dodaje sekcję do menu @ pola czatu. Twoja usługa odpowiada na POST /mentions/search w ciągu 2 sekund, zwracając { items: [{ id, title, subtitle?, url? }] }, a na POST /mentions/resolve, zwracając { text } dla wiersza wybranego przez użytkownika. Tekst trafia do agenta razem z wiadomością, a użytkownik widzi chip. Wolna usługa nie dodaje wierszy i nigdy nie wstrzymuje plików; wiersz, którego nie da się odczytać, nie zostawia chipa i informuje o tym.

Bloki na czacie

contributes.blocks dodaje komponenty, które agenci mogą pisać w bloku UI. Każdy z nich rysuje Twoja własna strona, w tej samej piaskownicy i z tymi samymi uprawnieniami co Twój panel.

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

Agent pisze JiraIssue("OPE-1"), a Twoja strona odczytuje wywołanie:

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

Wywołaj host.setHeight(px) z ResizeObserver na elemencie głównym. Ramka wypełnia swoje pole bez paddingu, więc dodaj własny. Do 4 komponentów na rozszerzenie, ponieważ każda sygnatura to tekst, który czyta każda sesja.

Blok może odpowiedzieć agentowi, tak jak robi to przycisk w bloku. Wywołaj host.answerBlock(text) po kliknięciu: agent dostaje tekst i zaczyna następną turę. Na blok przypada jedna odpowiedź, a block.answered przechowuje ją po przeładowaniu, więc narysuj wtedy blok z udzieloną odpowiedzią.

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

Ustaw "answers": true w komponencie, który to robi. Agenci dostają polecenie, by pytać za jego pomocą zamiast wbudowanego wyboru, gdy pasuje jego opis, a strona czekająca na odpowiedź pomija swój przycisk Wyślij. Pod takim blokiem OpenChamber oferuje to samo pole notatki co w swoich formularzach: agent czyta notatkę razem z twoją odpowiedzią, a sama notatka też jest odpowiedzią.

Argumenty pisze model, więc tekst ze strony internetowej lub zgłoszenia może nimi sterować. Traktuj je jako niezaufane i nie rób niczego z efektami ubocznymi, dopóki użytkownik nie kliknie.

Kanały powiadomień

Usługa może dostarczać powiadomienia użytkownika obok komputera i telefonu: zadeklaruj provides: ["notifications"] i opisz kanał w 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"] }
}

Z tego opisu OpenChamber rysuje kartę konta kanału w Ustawienia → Integracje i przechowuje konto: użytkownik podłącza je tam, token zostaje w OpenChamber, a Twoja usługa nigdy go nie widzi. Gdy konto jest podłączone, każde powiadomienie, które OpenChamber wysłałby na telefon, trafia też do Twojej usługi, po przejściu przez przełączniki powiadomień użytkownika i tylko wtedy, gdy żadne okno nie jest widoczne, chyba że użytkownik wybrał powiadamianie zawsze. Wstrzymanie rozszerzenia to zatrzymuje.

OpenChamber przekazuje trzy wywołania do POST /notifications: auth.probe (czy to serwer, do którego dostarczasz), auth.verify (do kogo należy ten token) i notify (jedno powiadomienie). Odczytaj każde przez readNotificationsProviderRequest, a z serwerem kanału łącz się tylko przez pomocnika wychodzącego, który wysyła Twoje żądanie do OpenChamber z uchwytem wywołania; OpenChamber dodaje token i kieruje żądanie wyłącznie na serwer konta.

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 } } }
}

To, do którego pokoju lub kanału trafia projekt, jest wyborem Twojego rozszerzenia: powiadomienie niesie directory i sessionName. W trybie enterprise niesie tylko ogólny tytuł, tak jak telefon. Przykład notification-webhook to kompletny kanał z odbiornikiem, na którym można go wypróbować.

Trackery

Usługa może być trackerem elementów pracy obok Linear: zadeklaruj provides: ["tracker"] i opisz tracker w contributes.tracker. OpenChamber wymienia go wtedy obok Linear wszędzie, gdzie pojawiają się trackery, na podstawie samego manifestu, zanim usługa w ogóle się uruchomi.

"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}"] }
}

Użytkownik podłącza konta na karcie trackera w sekcji Konta rozszerzeń: tokenem albo logowaniem w przeglądarce, gdy zadeklarujesz auth.oauth (OpenChamber prowadzi logowanie z client id wpisanym przez użytkownika i przechowuje tokeny). Twoja usługa nigdy nie widzi danych logowania. urls.item to szablony adresu elementu, z {id} w miejscu identyfikatora (także jako ?name={id}, gdy podróżuje w query); OpenChamber je kompiluje, żeby wklejone linki stawały się powiązanymi elementami. identifier mówi, jak wygląda klucz: key-number (PROJ-42), number albo short-code.

OpenChamber przekazuje dziewięć wywołań do POST /tracker: auth.probe i auth.verify, odczyty items.list, items.get, items.liveSummaries, states.list i containers.list oraz zapisy items.updateState i comments.create. Odczytaj każde przez readTrackerProviderRequest, odpowiadaj w neutralnych kształtach elementów, a z serwerem trackera łącz się tylko przez pomocnika wychodzącego, który wysyła Twoje żądanie do OpenChamber z uchwytem wywołania; OpenChamber dodaje dane logowania i kieruje żądanie wyłącznie na serwer konta. Uchwyt odczytu wysyła tylko 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)) } });
}
}

Gdy użytkownik włączy komentarze sesji, OpenChamber wywołuje comments.create z “session started”, “completed” albo “failed” i linkiem do sesji, każde raz; komentarz nie niesie nic więcej. Przykład tracker-provider to kompletny tracker z udawanym serwerem, na którym można go wypróbować.

Dostawcy zużycia

Usługa może raportować zużycie obok wbudowanych dostawców: zadeklaruj provides: ["quota"] i wymień w contributes.quota.providers dostawców OpenCode, których danych logowania potrzebuje. OpenChamber nazywa dostawcę ext-<panel.id>, rysuje jego kartę z nazwą i ikoną rozszerzenia i pyta go tak, jak pyta wbudowanego dostawcę.

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

Okno zatwierdzania wymienia tych dostawców, a wersja, która zmienia listę, pyta ponownie. Przy każdym odświeżeniu OpenChamber wysyła jedno wywołanie, usage.read, na POST /quota, z input.credentials zawierającym dane logowania, które OpenCode przechowuje dla każdego wymienionego dostawcy, dla którego je ma (klucz API albo tokeny logowania w przeglądarce), i nic więcej z magazynu. Wymieniony dostawca, dla którego OpenCode nie ma danych logowania, jest pokazywany jako nieskonfigurowany, a usługa nie jest wywoływana; pusta lista jest dozwolona, dla dostawcy, którego klucz leży we własnych ustawieniach rozszerzenia.

Odczytaj treść przez readQuotaProviderRequest i odpowiedz { ok: true, data } z wynikiem, jaki oblicza wbudowany dostawca: ok, configured, error, planLabel oraz usage.windows po nazwie okna, każde z usedPercent, windowSeconds, resetAt (epoka w milisekundach) i opcjonalnym valueLabel lub value, plus usage.models dla okien per model. OpenChamber wyprowadza pozostałą część, odliczanie do resetu i sformatowane napisy, więc karta zgadza się z kartą wbudowanego dostawcy.

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()) });
}

Usługa odpowiada w 20 s i zatrzymuje się po dziesięciu minutach bez wywołania; strona zużycia odświeża się co trzy minuty, dopóki jest otwarta. Przykład quota-provider to kompletny dostawca z udawanym API zużycia do wypróbowania.

Pliki

Ścieżka względna leży w otwartym projekcie i wymaga uprawnienia files. Ścieżka zaczynająca się od / lub ~/ musi pasować do zadeklarowanego wzorca contributes.filesystem i wymaga uprawnienia filesystem. Zasady opisuje Tworzenie rozszerzenia.

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");

Limity: ścieżka do 1024 znaków, treść do 2 000 000 znaków, listing do 2000 wpisów. Przekroczenie limitu treści to FILE_TOO_LARGE.

generate

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

prompt ma od 1 do 64 000 znaków po przycięciu, system do 8000, maxOutputTokens od 1 do 4000. Nic nie trafia do sesji i historia nie jest zapisywana; rozszerzenie nigdy nie wybiera modelu. Wywołanie czeka maksymalnie 90 sekund. Brak dostępnego Small Model to NO_MODEL; błąd modelu to MODEL_FAILED. Zobacz Generowanie tekstu.

Kody błędów

Nieudane wywołania rzucają HostRequestError. code to jeden z poniższych; message mówi więcej.

KodKiedy
HOST_UNAVAILABLEbrak window, poza OpenChamber albo po wywołaniu dispose()
HOST_TIMEOUTbrak odpowiedzi przez 20 sekund
HOST_REJECTEDOpenChamber odmówił albo odpowiedział kodem, którego to SDK nie zna
DISCONNECTEDbrak tokena lub połączenia z Linear dla Twojej integracji
BAD_PATHpath był źle sformułowany albo próbował wyjść poza dozwolony origin
NO_INTEGRATIONmanifest nie ma integration
NO_SESSIONprompt lub sessionLink bez otwartej sesji
SESSION_BUSYprompt({ send: true }), gdy sesja była zajęta
DISABLEDużytkownik wstrzymał rozszerzenie w Ustawienia → Rozszerzenia
NO_SERVICEusługa lokalna niezadeklarowana, niezatwierdzona albo nieuruchomiona
NOT_GRANTEDużytkownik nie zatwierdził uprawnienia, którego wymaga to wywołanie
SERVICE_FAILEDusługa lokalna padła albo nigdy nie osiągnęła gotowości
NO_DIRECTORYwzględna ścieżka pliku użyta bez otwartego projektu albo openCommit bez otwartego projektu
NOT_FOUNDreadFile na pliku, który nie istnieje, albo openCommit na commicie, którego nie ma w projekcie
FILE_TOO_LARGEtreść pliku ponad limit, przy odczycie lub zapisie
DENIEDsystem operacyjny odmówił operacji na pliku
NO_MODELgenerate bez dostępnego Small Model
MODEL_FAILEDSmall Model zwrócił błąd
UNSUPPORTEDten OpenChamber nie potrafi wykonać wywołania, na przykład openCommit tam, gdzie nie ma widoku Diff

Zobacz też