Aller au contenu
↑↓Naviguer ↵Ouvrir escFermer

API hôte

Utilisez cette page quand vous avez besoin des méthodes exactes de connectHost, des limites et des codes d’erreur. Pour la structure du dossier, le manifeste et l’installation, commencez par Créer une extension.

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

connectHost lève HOST_UNAVAILABLE quand il n’y a pas de window. En dehors d’OpenChamber, il renvoie un client dont chaque appel est rejeté avec HOST_UNAVAILABLE. Appelez dispose() quand vous démontez la page ; les appels encore en cours sont rejetés avec le même code.

Ce qu’OpenChamber vous envoie

onReady se déclenche avec le premier instantané, puis à chaque fois qu’OpenChamber le rafraîchit.

Ce n’est pas un callback de montage unique. Montez l’interface et inscrivez les abonnements une fois, puis actualisez le thème et le contexte sans remplacer les champs ni les brouillons. Consultez l’exemple protégé dans UI kit. Les listeners comme onConnection livrent immédiatement la valeur actuelle et peuvent la répéter avec un nouvel instantané. Comparez les champs utiles avant une nouvelle requête et ignorez les réponses des requêtes remplacées.

ChampCe que c’est
theme.modelight ou dark
theme.tokensles couleurs de l’application (surfaces, texte, états d’interaction, primary, success/warning/error/info), font, mono et radius. Passez-le à applyHostReady avant de monter l’UI.
localele tag de langue de l’application
directoryle dossier du projet en cours, ou null
session{ id, title, busy } ou null. Le titre retombe sur l’id de session. busy est le statut en direct. model et agent (l’agent OpenCode) apparaissent quand la session les a.
surfacepanel dans la barre latérale, dialog dans la fenêtre d’attache, page en plein écran, status dans le panneau Work Status, file comme éditeur de fichiers dans Files, block dans un bloc d’interface du chat
itemce pour quoi cette surface a été ouverte, ou null : l’élément attaché sur lequel l’utilisateur a cliqué (les mêmes champs que vous avez passés à attach, y compris data), un message (kind: "message") ou une session (kind: "session") venant d’une de vos actions déclarées. Voir Actions.
connection{ connected, account } pour votre intégration
settingsles valeurs des champs déclarés dans integration.settings

Les tokens d’accès n’apparaissent jamais ici ni dans un résultat de request.

onDirectory, onSession, onSessionLifecycle, onConnection, onSettings et onItem rejouent la dernière valeur quand vous vous abonnez tard, puis continuent à se déclencher à chaque changement.

theme.tokens contient primaryText, successText, warningText, errorText et infoText. Ces champs obligatoires contiennent les couleurs de texte calculées par l’hôte pour les fonds neutres et les contrôles légèrement teintés du kit UI, pas pour les aplats de couleur vive. applyHostReady les applique à chaque instantané. Consultez le kit UI pour les variables CSS.

Méthodes

AppelCe qu’il fait
toast({ kind, message })affiche un toast dans l’application. kind est info, success ou error.
openUrl(url)ouvre une URL dans le navigateur de l’utilisateur
openSurface(surfaceId)bascule l’application sur cet écran
writeClipboard(text)copie du texte. 1 à 32000 caractères.
compose({ text, mode? })insère du texte dans la zone de chat sans l’envoyer. mode est append (par défaut) ou replace. 1 à 16000 caractères après trim.
attach({ ... })pose une puce sur la zone de chat, au même endroit que les éléments GitHub et Linear. Une seule puce à la fois.
startSession({ ... })crée une session avec cet élément attaché. Renvoie { sessionId, sent }. Nécessite la capacité sessions.
prompt({ text, send? })écrit dans la session en cours, ou y envoie un message. Renvoie { sent }. L’envoi nécessite la capacité prompt.
sessionLink({ ... })attache un élément à la session en cours sans en créer une
close()ferme la fenêtre d’attache. Ne fait rien dans la barre latérale.
oauthStart()ouvre la page d’autorisation du fournisseur, ou celle de Linear pour une intégration host: { provider: "linear" }
oauthDisconnect()oublie le token stocké, ou la connexion Linear
request({ method, path, query?, body? })appelle l’apiOrigin de votre intégration avec le token de l’utilisateur attaché
serviceRequest({ method, path, query?, body? })appelle le service local de votre extension (voir GUEST_SERVICES.md)
serviceStatus()stopped, starting, ready ou failed
readFile(path)lit un fichier texte. Renvoie { content }.
writeFile(path, content)écrit un fichier texte de façon atomique, en créant les dossiers parents. Renvoie { written: true }.
listDir(path)liste un dossier. Renvoie { entries: [{ name, kind }] }, kind vaut file, directory ou other.
stat(path){ kind, size, mtime }, kind vaut file, directory, other ou missing.
generate({ prompt, system?, maxOutputTokens? })texte ponctuel du Small Model de l’utilisateur. Renvoie { text }. Nécessite la capacité model.
onResolve(handler)enregistre le gestionnaire de vos commandes slash. Il reçoit { command, args } et renvoie un élément à attacher ou null.
setBadge(count)marque votre icône de la barre latérale d’un point et affiche le nombre (0 à 999) dans son infobulle, ou null pour l’effacer
openCommit(sha)ouvrir la vue Diff sur ce commit du projet courant. sha fait de 7 à 64 caractères hexadécimaux ; OpenChamber lit le commit lui-même.
setHeight(px)régler la hauteur de votre section Work Status. OpenChamber la maintient entre 24 et 320 pixels ; un contenu plus haut défile dans votre page. Ailleurs, c’est ignoré.
onFileOpen(listener)éditeurs de fichiers : recevoir le fichier à modifier, { path, name, readOnly } plus encoding: "text" avec content, ou encoding: "binary" avec bytes. Permet aussi d’enregistrer avec Cmd/Ctrl+S dans votre page.
onFileSnapshot(handler)éditeurs de fichiers : renvoyer { content, version } (ou { bytes, version } pour un éditeur binaire), le fichier modifié complet. OpenChamber l’appelle pour enregistrer et avant de passer à la vue source ou en plein écran.
onFileSaved(listener)éditeurs de fichiers : l’instantané avec cette version est sur le disque. Répondez avec reportFileChange.
reportFileChange({ dirty, edited })éditeurs de fichiers : indiquer à OpenChamber s’il y a des modifications non enregistrées et si le document vient de changer
requestFileSave()éditeurs de fichiers : enregistrer maintenant, comme Cmd/Ctrl+S
reportFileUnsupported()éditeurs de fichiers : ce fichier ne peut pas être ouvert ici, afficher sa source à la place

Les trois prennent les mêmes champs d’élément :

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 est l’id de votre panneau ; OpenChamber l’écrase de toute façon avec votre id. id est votre propre identifiant pour l’élément. kind est issue (par défaut) ou pull. text est un contexte optionnel pour le modèle, 1 à 16000 caractères après trim. data est un JSON optionnel à vous (statut, commentaires, n’importe quoi), jusqu’à 16000 caractères une fois sérialisé. OpenChamber le stocke avec la puce et vous le rend tel quel dans ctx.item quand l’utilisateur clique sur la puce ; il n’atteint jamais le modèle.

startSession accepte projectId sans changer le projet actif. Omettez worktree pour le dossier cible, utilisez true pour un worktree généré, { kind: "existing", directory } pour un existant ou { kind: "new", name?, baseBranch? } pour un nouveau. Le nom désigne la branche et le worktree. navigation vaut "preserve" par défaut ; "open" ouvre le nouveau chat. Le modèle, l’agent et la variante du premier message sont capturés au démarrage.

Le résultat contient sessionId, directory, sent, linked et éventuellement worktree. sent vaut sent, no-model, skipped ou failed ; linked: false indique que l’élément n’a pas été enregistré. Si un worktree reste après un échec de préparation ou de création de session, sessionId vaut null et failure vaut bootstrap-failed ou session-create-failed. Vérifiez ce résultat avant de réessayer. Le délai maximal est de 180 secondes ; une expiration ne prouve pas une annulation.

sessionLink attache l’élément à la session ouverte en ce moment. Pas de projet ou pas de session donne une erreur NO_SESSION.

Limites que le client applique avant tout envoi :

ChampCaractères max
id128
title200
url2000
text16000
data (sérialisé)16000
author80
chaque nom de branche200

prompt et cycle de vie de la session

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

Sans send, prompt remplace le texte de la zone de chat. Avec send: true, il envoie le message avec le modèle et l’agent choisis par l’utilisateur. L’extension ne les choisit jamais. Pas de session ouverte donne NO_SESSION. Envoyer pendant que la session est occupée donne SESSION_BUSY ; écrire dans la zone fonctionne même quand elle est occupée. Le résultat est { sent } avec les mêmes valeurs que startSession.

onSessionLifecycle vous dit ce que fait la session. phase est started pendant que le modèle travaille, completed quand elle redevient inactive, et failure sur un statut inattendu. Un écouteur tardif reçoit tout de suite la phase courante.

Projets, sessions en direct et stockage

listProjects(), listWorktrees(projectId) et listSessions(projectId) utilisent la permission sessions et les stores partagés, sans parcours Git par appel ni contenu des conversations. Les projets exposent ID, nom et dossier ; les worktrees ajoutent branche et disponibilité. Les sessions incluent métadonnées, dates, parent, worktree et seulement les éléments attachés par cette extension. Les sessions archivées connues sont incluses.

await onProjects(listener), await onWorktrees(projectId, listener) et await onSessions(projectId, listener) renvoient une fonction de désabonnement. Interceptez les erreurs d’inscription. Un premier instantané précède les changements. Maximum 32 abonnements par cadre ; dispose(), fermeture, pause, suppression et changement de serveur les libèrent. state vaut loading, ready ou error, avec coverage par dossier pour les sessions. Une erreur conserve les données disponibles. Seul ready confirme une liste vide complète.

activity vaut unknown, idle, running, retrying, waiting-permission ou waiting-question. outcome vaut completed, failed ou null, uniquement d’après les événements observés, en mémoire pour 2 000 sessions au maximum. Idle après une erreur conserve cet échec jusqu’au prochain lancement. Cela ne termine pas votre tâche. openSession(sessionId) ouvre explicitement le chat.

host.storage.get(key), set(key, value), delete(key) et keys() conservent votre JSON sans permission supplémentaire. Une clé absente donne undefined, null enregistré reste null. Clés de 1 à 128 caractères, valeur de 64 KiB UTF-8 maximum, ensemble de 2 MiB et 2 000 clés maximum. Incluez l’ID du projet dans la clé pour des données propres à un projet. Les écritures sont sérialisées et atomiques ; les erreurs préservent les données. La désinstallation supprime ce stockage sur le serveur connecté.

request

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

path commence par / et n’a ni schéma ni hôte ; OpenChamber le joint à l’apiOrigin de votre manifeste et ajoute l’en-tête Authorization. Les intégrations par token envoient le token collé tel quel, sous la forme Bearer <token> quand le manifeste définit token.scheme: "bearer", ou sous la forme Basic base64(username:token) quand il définit "basic". Les intégrations OAuth et Linear envoient toujours Bearer. Le résultat est { status, body }. Le corps est du texte ; parsez le JSON vous-même.

Une intégration Linear peut aussi appeler GET /api/linear/issues/get, auquel OpenChamber répond depuis sa propre route Linear.

Pas de réponse en 20 secondes donne 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

Quand le manifeste déclare contributes.service, OpenChamber démarre ce processus et votre page lui parle via le même type d’appel. La page n’ouvre jamais le socket elle-même.

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

path suit les mêmes règles que request. Déclarer un service local ajoute service aux capacités que l’utilisateur approuve à l’installation ; tant que ce n’est pas fait, chaque serviceRequest donne NO_SERVICE. Contrat complet : le fichier GUEST_SERVICES.md du package.

Événements de session

Votre page ne connaît une session que pendant qu’elle est ouverte. Votre service peut en savoir plus : déclarez contributes.sessionEvents à côté de contributes.service, et OpenChamber envoie au service les événements que vous listez, depuis chaque projet, qu’une fenêtre soit ouverte ou non.

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

Les événements sont turn-started, session-created, session-archived, session-deleted, turn-finished (l’agent a terminé et n’attend rien : aucun sous-agent en cours, aucune permission ou question ouverte, aucune étape d’objectif), turn-failed, permission-asked et question-asked. Un tour arrêté par l’utilisateur n’envoie rien. Approuver l’extension vaut abonnement ; la boîte de dialogue liste les événements sous la ligne du service. Sans content, le service ne reçoit que des identifiants : la session, le répertoire de son projet, les éléments que l’utilisateur lui a liés. Avec "content": true, le manifeste demande aussi une permission que l’utilisateur voit comme une ligne à part, Lire ce que dit l’agent en arrière-plan, et chaque événement porte la réponse finale de l’agent, les champs de la question ou le détail de la permission.

Chaque événement arrive sous la forme d’un POST /session-events avec le bearer du service. Lisez-le avec readSessionEventRequest et répondez { "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 });
}

Les événements de chaque projet sont envoyés ; comparez event.project.directory avec vos propres réglages et répondez { "ok": true } pour les autres. La livraison se fait au plus une fois et dans l’ordre, une requête à la fois ; une réponse en échec abandonne vos événements pendant 30 secondes, et rien n’est rejoué après une indisponibilité d’OpenChamber ou de votre service. Le service n’a pas besoin de panneau pour cela. L’exemple session-events-log journalise chaque événement dans un fichier.

Appeler OpenChamber depuis votre service

Un service peut agir sans aucune fenêtre ouverte. createServiceHost est son chemin de retour vers 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 et sessions.stop nécessitent contributes.sessionControl, que l’utilisateur approuve sur une ligne à part. sessions.lastReply nécessite sessionControl: { content: true }. Un message envoyé par sessions.send ferme un formulaire ou une page que l’agent attend dans cette session, comme un message tapé dans le chat.

Pour porter les demandes de l’agent ailleurs, par exemple dans une app de messagerie, ajoutez answers: true. L’utilisateur l’approuve sur une ligne de plus, et le service peut alors répondre à sa place dans n’importe quelle session : sessions.answerPermission(sessionId, requestId, { decision: 'allow' }) (ou 'deny' avec un message que l’agent lit) pour un événement permission-asked, et sessions.answerForm(sessionId, formId, values) pour un événement question-asked dont event.question.kind vaut form. Le contenu de l’événement liste les champs du formulaire avec leur key et leurs choix. null à la place des valeurs ferme le formulaire sans réponse. Une autorisation ne couvre que cette demande.

setInstructions nécessite contributes.instructions et complète ce que lisent les agents, dans un dossier ou, avec null, partout.

emit atteint vos frames ouvertes via host.onServiceEvent, ce qui évite à un panneau d’interroger le service en boucle. Rien n’est rejoué, donc une frame ouverte plus tard lit d’abord l’état actuel. readSettings donne au service les réglages de l’intégration, champs secret compris, et needsConfiguration affiche un message sur la carte de l’extension.

Fonctionner sans que personne ne le demande

Un service démarre à la première requête, et le service d’un rôle s’arrête quand il est inactif. service.keepRunning: true le démarre avec OpenChamber et le relance quand il s’arrête. service.schedule le réveille selon un horaire :

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

Une exécution correspond à un POST /schedule. Lisez-la avec readServiceScheduleRequest et répondez 200. Une exécution manquée pendant qu’OpenChamber était arrêté est perdue. Un service qui a besoin d’événements extérieurs garde sa propre connexion ou interroge la source.

Lignes dans le menu @

contributes.mentions: { label } ajoute une section au menu @ de la zone de chat. Votre service répond à POST /mentions/search avec { items: [{ id, title, subtitle?, url? }] } en 2 secondes au plus, et à POST /mentions/resolve avec { text } pour la ligne choisie par l’utilisateur. Le texte part vers l’agent avec le message, et l’utilisateur voit une puce. Un service lent n’ajoute aucune ligne et ne retient jamais les fichiers ; une ligne illisible ne laisse aucune puce et le signale.

Blocs dans le chat

contributes.blocks ajoute des composants que les agents peuvent écrire dans un bloc d’interface. Votre propre page dessine chacun d’eux, dans le même bac à sable et avec les mêmes autorisations que votre panneau.

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

L’agent écrit JiraIssue("OPE-1"), et votre page lit l’appel :

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

Appelez host.setHeight(px) depuis un ResizeObserver sur votre élément racine. La frame remplit sa boîte sans padding, ajoutez donc le vôtre. Jusqu’à 4 composants par extension, car chaque signature est du texte que chaque session lit.

Un bloc peut répondre à l’agent, comme le fait un bouton dans un bloc. Appelez host.answerBlock(text) depuis un clic : l’agent reçoit le texte et démarre son tour suivant. Une seule réponse par bloc, et block.answered la conserve après un rechargement, donc dessinez alors le bloc comme déjà répondu.

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

Définissez "answers": true sur un composant qui fait cela. Les agents ont pour consigne de poser la question avec lui plutôt qu’avec le choix intégré quand sa description convient, et une page qui attend une réponse omet son bouton Envoyer. Sous un tel bloc, OpenChamber propose le même champ de note que dans ses propres formulaires : l’agent lit la note avec votre réponse, et une note seule est aussi une réponse.

Les arguments sont écrits par un modèle, donc du texte provenant d’une page web ou d’un ticket peut les orienter. Traitez-les comme non fiables, et ne faites rien qui ait des effets de bord avant que l’utilisateur clique.

Canaux de notification

Un service peut transmettre les notifications de l’utilisateur, à côté du bureau et du téléphone : déclarez provides: ["notifications"] et décrivez le canal dans 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"] }
}

À partir de cette description, OpenChamber dessine une carte de compte pour le canal dans Réglages → Intégrations et garde le compte : l’utilisateur le connecte là, le jeton reste dans OpenChamber, et votre service ne le voit jamais. Une fois un compte connecté, chaque notification qu’OpenChamber enverrait au téléphone va aussi à votre service, selon les réglages de notification de l’utilisateur, et seulement quand aucune fenêtre n’est visible, sauf si l’utilisateur a choisi d’être toujours notifié. Mettre l’extension en pause l’arrête.

OpenChamber transmet trois appels à POST /notifications : auth.probe (ce serveur est-il un de ceux vers lesquels vous livrez), auth.verify (à qui appartient ce jeton) et notify (une notification). Lisez chacun avec readNotificationsProviderRequest, et n’atteignez le serveur du canal que par le helper outbound, qui poste votre requête à OpenChamber avec le handle de l’appel ; OpenChamber ajoute le jeton et garde la requête sur le serveur du compte.

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

Le salon ou le canal où va un projet est le choix de votre extension : la notification porte directory et sessionName. En mode entreprise, elle ne porte que le titre générique, comme le téléphone. L’exemple notification-webhook est un canal complet avec un récepteur pour l’essayer.

Outils de suivi

Un service peut être un outil de suivi des tâches à côté de Linear : déclarez provides: ["tracker"] et décrivez l’outil dans contributes.tracker. OpenChamber le liste alors à côté de Linear partout où les outils de suivi apparaissent, à partir du seul manifeste, avant même que le service tourne.

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

L’utilisateur connecte des comptes sur la carte de l’outil sous Comptes d’extensions : un jeton, ou une connexion dans le navigateur quand vous déclarez auth.oauth (OpenChamber mène la connexion avec le client id saisi par l’utilisateur et garde les jetons). Votre service ne voit jamais d’identifiants. urls.item sont des modèles pour l’adresse d’un élément, avec {id} pour l’identifiant (aussi sous la forme ?name={id} quand il passe dans la query) ; OpenChamber les compile pour que les liens collés deviennent des éléments liés. identifier dit à quoi ressemble la clé : key-number (PROJ-42), number ou short-code.

OpenChamber transmet neuf appels à POST /tracker : auth.probe et auth.verify, les lectures items.list, items.get, items.liveSummaries, states.list et containers.list, et les écritures items.updateState et comments.create. Lisez chacun avec readTrackerProviderRequest, répondez dans les formes d’élément neutres et n’atteignez le serveur de l’outil que par le helper outbound, qui poste votre requête à OpenChamber avec le handle de l’appel ; OpenChamber ajoute les identifiants et garde la requête sur le serveur du compte. Le handle d’une lecture n’envoie que 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)) } });
}
}

Quand l’utilisateur active les commentaires de session, OpenChamber appelle comments.create avec “session started”, “completed” ou “failed” et un lien vers la session, une fois chacun ; le commentaire ne porte rien d’autre. L’exemple tracker-provider est un outil de suivi complet avec un faux serveur pour l’essayer.

Fournisseurs de consommation

Un service peut rapporter la consommation à côté des fournisseurs intégrés : déclarez provides: ["quota"] et listez dans contributes.quota.providers les fournisseurs OpenCode dont il a besoin des identifiants. OpenChamber nomme le fournisseur ext-<panel.id>, dessine sa carte avec le nom et l’icône de l’extension, et l’interroge comme il interroge un fournisseur intégré.

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

Le dialogue d’approbation liste ces fournisseurs, et une version qui change la liste redemande. À chaque actualisation, OpenChamber envoie un appel, usage.read, à POST /quota, avec dans input.credentials l’identifiant qu’OpenCode conserve pour chaque fournisseur listé pour lequel il en a un (une clé d’API, ou les jetons d’une connexion navigateur) et rien d’autre du magasin. Un fournisseur listé pour lequel OpenCode n’a pas d’identifiant est affiché comme non configuré et le service n’est pas appelé ; une liste vide est permise, pour un fournisseur dont la clé vit dans les réglages propres de l’extension.

Lisez le corps avec readQuotaProviderRequest et répondez { ok: true, data } avec le résultat qu’un fournisseur intégré calcule : ok, configured, error, planLabel, et usage.windows par nom de fenêtre, chacune avec usedPercent, windowSeconds, resetAt (époque en millisecondes) et un valueLabel ou value facultatif, plus usage.models pour les fenêtres par modèle. OpenChamber dérive la part restante, le décompte jusqu’à la réinitialisation et les chaînes formatées, pour que la carte corresponde à celle d’un fournisseur intégré.

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

Le service répond en 20 s et s’arrête après dix minutes sans appel ; la page Consommation s’actualise toutes les trois minutes tant qu’elle est ouverte. L’exemple quota-provider est un fournisseur complet avec une fausse API de consommation pour l’essayer.

Fichiers

Un chemin relatif se trouve dans le projet ouvert et nécessite la capacité files. Un chemin qui commence par / ou ~/ doit correspondre à un motif contributes.filesystem déclaré et nécessite la capacité filesystem. Voir Créer une extension pour les règles.

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

Limites : chemin jusqu’à 1024 caractères, contenu jusqu’à 2 000 000 caractères, listing jusqu’à 2000 entrées. Au-delà de la limite de contenu, c’est FILE_TOO_LARGE.

generate

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

prompt fait de 1 à 64 000 caractères après trim, system jusqu’à 8 000, maxOutputTokens de 1 à 4 000. Rien n’entre dans une session et aucun historique n’est conservé ; l’extension ne choisit jamais le modèle. L’appel attend jusqu’à 90 secondes. Aucun Small Model utilisable donne NO_MODEL ; une erreur du modèle donne MODEL_FAILED. Voir Générer du texte.

Codes d’erreur

Les appels en échec lèvent HostRequestError. code est l’un des suivants ; message en dit plus.

CodeQuand
HOST_UNAVAILABLEpas de window, pas à l’intérieur d’OpenChamber, ou dispose() a été appelé
HOST_TIMEOUTpas de réponse pendant 20 secondes
HOST_REJECTEDOpenChamber a refusé, ou a répondu avec un code que ce SDK ne connaît pas
DISCONNECTEDpas de token ni de connexion Linear pour votre intégration
BAD_PATHpath était mal formé ou tentait de sortir de l’origine autorisée
NO_INTEGRATIONle manifeste n’a pas d’integration
NO_SESSIONprompt ou sessionLink sans session ouverte
SESSION_BUSYprompt({ send: true }) pendant que la session était occupée
DISABLEDl’utilisateur a mis l’extension en pause dans Paramètres → Extensions
NO_SERVICEpas de service local déclaré, non approuvé, ou pas en cours d’exécution
NOT_GRANTEDl’utilisateur n’a pas approuvé la capacité dont cet appel a besoin
SERVICE_FAILEDle service local a planté ou n’est jamais devenu prêt
NO_DIRECTORYun chemin de fichier relatif a été utilisé sans projet ouvert, ou openCommit sans projet ouvert
NOT_FOUNDreadFile sur un fichier qui n’existe pas, ou openCommit sur un commit absent du projet
FILE_TOO_LARGEcontenu du fichier au-delà de la limite, en lecture ou en écriture
DENIEDle système d’exploitation a refusé l’opération sur le fichier
NO_MODELgenerate sans Small Model disponible
MODEL_FAILEDle Small Model a renvoyé une erreur
UNSUPPORTEDcet OpenChamber ne peut pas faire l’appel, par exemple openCommit là où il n’y a pas de vue Diff

Voir aussi

  • Créer une extension pour le dossier, le manifeste, l’installation et les capacités
  • Kit UI pour les boutons, champs, listes et les autres composants