Skip to content
BeeuniBeeuni
English
Esc
↑↓navigate↵open⌘Jpreview
On this page

SDK reference

The full @beeuni/sdk API surface — client methods, modules, turns, and distribution.

This page is the full @beeuni/sdk API surface: every client method, the framework-free modules, the turns helpers, and how the package ships. Use SDK to get started and Sessions for the session lifecycle in depth.

The client

createBeeuni(config) returns one client. Every method is a typed call to the platform API. The project(id) and session(pid, sid) handles bind ids so you never repeat them.

const beeuni = createBeeuni({ backendUrl, getToken });

beeuni.accounts; // account / team operations
beeuni.accountInvites; // invite accept/decline by token alone
beeuni.projects; // top-level project operations
beeuni.connectors; // Connector calls scoped by an agent-minted token
beeuni.connector(slug); // one connector in the token's scope → see p.connector(slug)
beeuni.project(id); // id-bound project handle
beeuni.session(pid, sid); // id-bound session handle → see Sessions
beeuni.github; // GitHub App install + repo linking (account-scoped)
beeuni.gitBackend; // the instance git backend ("Beeuni managed")
beeuni.billing; // credits, subscription, tier, transactions
beeuni.sandboxShares; // public share links for a sandbox port
beeuni.connectStatus; // easy-connect status
beeuni.marketplace; // public marketplace catalog
beeuni.validateToken; // pasted-API-key check
beeuni.config; // platform config in effect
beeuni.runtime(); // runtime REST compatibility client

Accounts — beeuni.accounts

method what
list() · get(accountId) accounts you belong to · one account
create({ name }) · updateName(accountId, name) create · rename an account
branding.get(accountId) · branding.update(accountId, { app_name }) · branding.uploadAsset(accountId, kind, file) · branding.removeAsset(accountId, kind) · branding.reset(accountId) organization branding (Enterprise): own logo / icon / favicon and product name; kind is logo · icon · favicon, or logo_dark · icon_dark · favicon_dark for the dark-scheme variant
members(accountId) · invite(accountId, input) list members · invite one
updateMemberRole(accountId, userId, role) · removeMember(accountId, userId) assign the account role · remove a member
invites(accountId) pending invites
cancelInvite(accountId, inviteId) · resendInvite(accountId, inviteId) cancel · resend a pending invite
leave(accountId) leave the account

accounts.tokens mints account-scoped API keys (beeuni_pat_...). See SDK auth for the full token model.

method what
tokens.list(accountId?, options?) list API keys — the whole account’s, or only your own with { mine: true }
tokens.create(input) mint one — { accountId?, name, expiresAt?, projectId? }
tokens.revoke(tokenId, accountId?) revoke one

accounts.audit is the enterprise reconstruction log. It combines authenticated API requests with semantic session, connector, approval, and computer events.

method what
audit.log(accountId, filters?) list events by project, session, actor, source, outcome, request, correlation, resource, action, or time
audit.export(accountId, filters?) export the same filtered event stream as CSV or JSONL
audit.webhooks.list/create/update/remove(...) manage signed SIEM webhooks for the centralized stream

Each event includes project_id, session_id, actor_type, source, outcome, request_id, trace_id, and correlation_id when the action supplies them. The API does not store request bodies, prompts, secrets, credentials, or raw connector arguments in the centralized event. Connector events can include a bounded argument preview that redacts credential-shaped fields and opaque data.

Access assignments

Beeuni has one grant record: an assignment. It binds one principal (user, group, service_account, or pending) to one role, at one scope (account, or one project), optionally narrowed to one object (agent, skill, secret, app, or trigger) and optionally carrying an expires_at. Group access, per-resource access, and custom-role bindings are all assignments. See Accounts & access for the model.

The canonical REST surface is:

Method + path Does
GET /v1/accounts/{accountId}/iam/assignments list assignments, filtered by principal, scope, object, or role
POST /v1/accounts/{accountId}/iam/assignments create one assignment
DELETE /v1/accounts/{accountId}/iam/assignments/{assignmentId} revoke one assignment
GET /v1/accounts/{accountId}/iam/permissions the permission catalog, as data
GET /v1/accounts/{accountId}/iam/roles · …/roles/{roleId}/permissions roles · one role’s permissions

The SDK exposes them as listAssignments, createAssignment, revokeAssignment, and listPermissions. A catalog row carries action, scope_type, resource_type, delegable, description, area, level, and implies — read it instead of hardcoding action strings.

Assigning a custom role needs the account’s rbac entitlement; the route answers 402 with code: "entitlement_required" without it.

Account invites — beeuni.accountInvites

Reached by invite token alone — the invitee may not be a member yet.

method what
listMine() every pending, unexpired invite addressed to your email, with the workspace and any projects it grants
describe(inviteId) · accept(inviteId) · decline(inviteId) preview · accept · decline an invite

Projects — beeuni.projects

method what
list() · listForAccount(accountId) your projects · projects in an account
get(id) · detail(id) summary · full detail
create(input) · createRepo(input) from an existing repo_url · new empty GitHub repo
provision(input) new project on a new Beeuni-managed repo, seeded with a starter template — { name, account_id?, seed_starter?, starter_template?, marketplace_items?, source_item_id?, idempotency_key? }
update(id, input) · archive(id) update settings · archive
llmCatalog(id) · modelPicker(id) full · compact model catalog for a selector
sandboxTemplates(id) · sandboxHealth(id) sandbox build templates · build health
sessions(id) · createSession(id, input?) list visible sessions · create a session

provision creates a new project; it does not start an existing project’s sandbox. Start a session instead — see Sessions.

Send idempotency_key when a retry is possible — a reload, a second tab, a timeout you retried. provision mints a brand-new managed repo per call, so without a key those all create real duplicate projects. Reuse one key for every attempt at a single logical create and the repeats return the project the first attempt made (201, same project_id, push_token: null). The key identifies the attempt, not the payload — reusing one with a different name returns the first project and ignores the new value, so mint a fresh key per distinct create. Creating a second project with the same name and no key still works.

A repeat that arrives while the first call is still provisioning gets 409 with code: 'provision_in_flight' rather than a project_id that call may still roll back. Retry with the same key.

project(id).sessions.list({ scope: 'project' }) is a lifecycle inventory for a caller with project-manager permissions. It adds accessible unavailable, warm, and soft-deleted sessions with ownership and runtime-state metadata. Both list scopes omit every session the caller cannot open.

sessions.list and sessions.listPage also take the hierarchy and attribution filters. Without them the list stays flat and unchanged.

option what
parent: 'root' Top-level sessions only. Each row carries child_count.
parent: '<sessionId>' That session’s children, newest first.
startedBy: 'me' | 'others' | 'automated' Filter by initiator, relative to the caller.
q Case-insensitive search over title, starter label, trigger slug, and session id prefix, across every session the caller may open. With parent: 'root', a root that matched through a child has search_match: 'child'.

Every session row has parent_session_id and initiator ({ type: 'member' | 'trigger' | 'channel' | 'api' | 'system', id, label }). initiator names who started the whole run; a child copies its parent’s. sessionParentId(session) reads parent_session_id, then the legacy metadata.spawned_by_session. A cursor is valid only for the filter set that issued it.

GitHub — beeuni.github

Account-scoped GitHub App install and repo linking, not project-scoped.

method what
getInstallation(accountId) · listInstallations(accountId) this account’s install · installs the user can reach
saveInstallation(input) · deleteInstallation(accountId, installationId?) record · unlink an install
listLinkableInstallations(input) installs the authorized GitHub user may link; each entry carries linked_to_other_accounts, a count of other Beeuni accounts holding it — never their names
listRepositories(accountId, installationId?) · listRepositoryBranches(...) repos the install can see · branches and the GitHub default
linkRepository(input) Import a repository as a project using an authorized GitHub installation.
replaceProjectRepository({ project_id, repo_url, expected_repo_url, github_token }) replace an existing project’s GitHub repository. Use either github_token with Contents read/write access, or installation_id plus temporary github_user_token to verify organization admin access and grant only this repository through the GitHub App. copy_shared_secrets_from_project_id plus copy_shared_secret_identifiers copies selected shared runtime secrets in the same transaction. A stale expected_repo_url returns 409. The old repository remains on GitHub. New sessions use the replacement repository; existing sessions retain their checkout.

This list holds real account connections only. “Beeuni managed” is not one of them — it is the instance backend below.

Instance git backend — beeuni.gitBackend

One deployment-wide owner plus credential, used for Beeuni-managed repositories. It is never an account connection and never appears in beeuni.github.listInstallations.

method what
get() { configured, kind: 'app' | 'pat' | null, owner } — readable by any authenticated user; it exposes only the owner login that every managed repo_url already carries

Billing — beeuni.billing

Reads for credits, subscription, tier, and transaction history. Use Beeuni checkout and billing management to manage payments and credit purchases.

method what
accountState(accountId?) · accountStateMinimal(accountId?) full · minimal billing state
transactions(params?) · transactionsSummary(params?) history · summarized totals
creditBreakdown(accountId?) · usageHistory(params?) credit balance by source · usage over time
sessionCosts.list(options?) · sessionCosts.get(sessionId, options?) paginated session-cost records · one detailed session ledger
tierConfigurations() available plan tiers
checkout.createSession(input) · checkout.confirmSession(sessionId, accountId?) Deprecated. Reject with ENDPOINT_RETIRED. Removed in the next major.
subscription.createPortalSession(...) · subscription.cancel(...) · subscription.reactivate(...) open the customer portal · cancel · reactivate
subscription.cancelScheduledChange(...) · prorationPreview(...) cancel · preview a plan change
subscription.scheduleDowngrade(...) Deprecated. Rejects with ENDPOINT_RETIRED. Change the plan in the customer portal. Removed in the next major.
credits.purchase(input) · credits.autoTopupSettings(...) · credits.configureAutoTopup(...) one-off purchase · read · configure auto-topup

sessionCosts.list() accepts accountId, projectId, limit, and offset. Each row combines finalized LLM cost and billed sandbox compute cost. sessionCosts.get() adds model usage and the discriminated LLM/compute ledger. The list includes a reconciliation total for cost without a session.

Sandbox shares — beeuni.sandboxShares

Public share links for one exposed sandbox port. Sandbox-scoped, not project-scoped.

method what
list(sandboxId) active share links
create(input) create one — { sandboxId, port, ttl?, label? }
revoke(sandboxId, token) revoke one

Marketplace catalog — beeuni.marketplace

Public catalog browsing, read-only — distinct from project(id).marketplace, which installs an item onto a project.

method what
items(options?) · item(id) · itemFile(id, path) browse · one item · a file inside an item
marketplaces() · featured() all · featured marketplaces
sources.list() · sources.add(input) · sources.remove(id) list · add · remove a source

Notifications — beeuni.notifications

The caller’s notifications, across every account and project. Each signed-in person has one inbox, one preference record, and Web Push subscriptions for their browsers. Every method needs a person’s credential: a service account gets 403.

const page = await beeuni.notifications.list({ limit: 20 });
await beeuni.notifications.markRead({ ids: [page.notifications[0].id] });
await beeuni.notifications.updatePreferences({ kinds: { turn_done: { push: false } } });
method what
list({ limit?, before? }, { signal? }) the newest page: { notifications, unread_count, next_before }. limit is 1 to 50 (default 20). Pass next_before as before for the next page. Rows the caller can no longer see are left out. A failure does not call the host error handler.
markRead({ ids }) · markRead({ all: true }) · markRead({ sessionId }) mark 1 to 100 rows, every row, or every row of one session read. Changes only the caller’s rows. Returns { updated, unread_count }.
preferences() · updatePreferences({ kinds }) read · change the push and email choice per kind. Returns { kinds, email_available }. A patch changes only the kinds and channels it names.
webPushPublicKey() { public_key }: the applicationServerKey for pushManager.subscribe
registerWebPushSubscription({ endpoint, keys }) deliver push to this browser. Needs a browser sign-in: a personal token gets 403. An endpoint on an unknown push service gets 400.
unregisterWebPushSubscription(endpoint, { signal? }) stop push to this browser. Returns { deleted }.
registerDeviceToken(input) · unregisterDeviceToken(token, { signal? }) register · remove a native app’s Expo push token

Kinds (InboxNotificationKind, listed in INBOX_NOTIFICATION_KINDS):

kind sent when push email (default)
turn_done a turn you prompted or watch ended on off
turn_error that turn ended with an error on on, in a digest
question the agent asks a question on on, in a digest
permission the agent asks for permission on off
shared someone shared a session with you on on, in a digest
automation_failed an automation you watch started to fail on on, at once
automation_recovered that automation works again on off

DEFAULT_NOTIFICATION_PREFERENCES holds these defaults. The inbox row is always written; the preferences decide only push and email. A digest email lists the rows still unread 15 minutes after they arrived, at most one digest per hour. Hide the email column when email_available is false.

Each row’s url opens its subject. Its ?notification=<id> query lets the page mark the row read.

The session handle carries the caller’s watch on one session:

method what
session(pid, sid).watch() { watching }: is the caller notified about this session
session(pid, sid).setWatch(watching) false mutes the session for the caller, true watches it again. Prompting a session watches it, but never undoes a mute.
session(pid, sid).presence({ tab_id, active, alerts? }, { keepalive? }) this tab’s presence lease. alerts: true says the tab shows its own notifications: with notification_center on, the server skips the phone and Web Push only while an alerting tab is in use. With it off, alerts is ignored and any present tab holds back the phone push. keepalive lets the request outlive a closing page. Default: on for an absent report, off for a present one. useSession sends keepalive only with presencePageExit.

The project handle — beeuni.project(id)

Binds the project id; every sub-resource hangs off it.

const p = beeuni.project(projectId);
await p.detail();
await p.update({ name });
await p.llmCatalog();
method what
get · detail · update · archive read · full detail · update · archive
llmCatalog · modelPicker · sandboxHealth model and sandbox-build reads
onboardingComplete mark project onboarding done
validateManifest(raw) validate a beeuni.yaml (or legacy beeuni.toml) manifest server-side
gitToken() mint a fresh scoped git push token (409 for a bring-your-own repo)
setAgentScope(agentName, scope) set an agent’s allowed secrets and connectors in the manifest — the second binding, not a role

p.tokens — project-scoped API keys

Auto-minted at session create as BEEUNI_TOKEN; can also be minted by hand.

method what
list() project API keys
create(input?) mint a new one
revoke(tokenId) revoke one

A link a person opens to enter a secret or connect an app, without full project access.

method what
requestSecret(input) · requestConnector(input) link to collect a secret · connect an app

p.apps — Apps (releases, machines, volumes)

Behind the apps project feature. Guide: SDK → Apps.

method what
list() · get(appId) · create(input) · update(appId, input) · remove(appId) inventory and lifecycle; create takes uses, update also takes labels
deployments.* · artifacts.* · access.* · rollback · start · stop deployments, artifacts, the access policy, traffic and runtime
machines.list/start/stop/restart/destroy · scale(appId, count) · exec(appId, input) machines per process group; exec needs project.app.admin
volumes.list/create/update/destroy/snapshots/snapshot/restore/fork persistent volumes, daily snapshots, restore, fork
token(appId) a 15-minute sign-in token; 409 app_capability_unsupported without member_tokens

p.secrets — project secrets

method what
list() · upsert(input) list metadata · create or update a write-only value and delivery policy
setStrategy(identifier, strategy, options?) change the exposure and its host list
broker(identifier, request) execute a session-authorized, policy-bound HTTPS request
remove(identifier) delete a secret
setPersonal(name, value) · removePersonal(name) set · remove a per-user override
setGitCredential(input) set a git auth credential

runtime with consumer sandbox is environment exposure — the default, and the only policy that puts a plaintext value in the session. egress with consumer network is egress-enforced exposure: the session holds a handle and Beeuni substitutes the real value outside the sandbox, for the exact HTTPS hosts the policy lists. Egress-enforced exposure is experimental; it needs the secrets_egress feature flag (Settings → Feature flags). With the flag off, setStrategy(identifier, 'egress', …) and upsert(...) with an egress policy return 403 feature_disabled. Every broker consumer has no session presence at all. See Secrets. The broker(...) method requires a session-scoped token and an active session handle.

p.access — project assignments, invites, requests

Every method here reads or writes an assignment scoped to this project. Project roles are manager and member.

method what
list() · invite(email, role) principals with access · invite a user
update(userId, role) · revoke(userId) assign a project role · revoke the assignment
pendingInvites() · requests() outstanding invites · pending access requests
resendInvite(inviteId) · revokeInvite(inviteId) · approveRequest(id) · rejectRequest(id) resend/revoke an invite · approve/reject a request
groupGrants() · attachGroupGrant(...) · updateGroupGrant(...) · detachGroupGrant(...) the same assignments, with a group principal

p.access.resourceGrants is the object assignment view: it narrows a principal to one object in the project instead of the whole project. Beeuni enforces object assignments on agents and skills today. An agent is closed by default — a member reaches it only when an assignment names them or one of their groups. An object assignment carries no permissions of its own, and it restricts a project manager as much as a member.

method what
resourceGrants.list() object assignments in this project
resourceGrants.create(input) assign one object to a user or a group
resourceGrants.remove(grantId) revoke one object assignment

p.connector(slug) — one connector

method what
run(action, args, options?) the action’s output; throws ConnectorCallError or ConnectorApprovalPendingError — see Errors
call(action, args, options?) the raw call result; 202 resolves with ok: false
describe() · describe(action) the connector with input and output schemas · one action
accounts() the accounts this caller may run it as, default first
paginate(action, args, { next, maxPages? }) async iterator over each page’s output; throws ConnectorPageLimitError with nextArgs when more than maxPages (default 100) pages exist — see Pages
uploadAttachment(content, input) p.connectors.uploadAttachment with connector set to this slug

options: account, approvalContext, timeoutMs (default 90 000), signal. With the file from beeuni connectors types, args and output are typed.

p.connectors — tool connectors

method what
catalog() · tools() callable Connector catalog · flattened <connector>.<action> tools
search(query, options?) · describe(tool) find · inspect one callable tool
call(tool, args?, options?) call one <connector>.<action> tool through the server-side gateway. options.account names which account to run as — see below.
accounts(slug) the accounts this caller may call slug as, default first — what options.account matches against
uploadAttachment(content, input) stage bytes for input.connector; returns ref ({ $beeuni_attachment }) to place in a later call’s args — see Attach files
list() · config(connectorId) configured connectors · one connector’s config
create(input) add a connector
auth.discover(input) preview auth from an OpenAPI spec, Postman collection, or endpoint
remove(connectorId) · sync() delete a connector · re-sync connectors
setName(connectorId, name) · setSensitive(connectorId, sensitive) rename · mark it sensitive (extra approval gating)
setAuthorizationStrategy(slug, strategy) @deprecated. Still calls the route; the route is a no-op that answers {ok:true, deprecated:true} and changes nothing. owner_type on each connection is the whole access rule now.
setCredentialMode(connectorId, mode) · setCredential(connectorId, input) switch source · set the credential value
policies.get(connectorId) · policies.set(connectorId, policies) read · replace its tool policies
connections.list() · connections.reconcile(input) list · create/update connected accounts
connections.updateCredential(connectionId, input) rotate a connection credential
connections.revoke(connectionId) · connections.activate(connectionId) deny · restore a connection
connections.setDefault(connectionId) make it the default for its owner scope
connections.rename(connectionId, label) change the label only; the account stays authorized. Each connection carries connected_as, the identity it was authorized as.

p.connectors.discover browses the direct-connector catalog. It is experimental and off by default — enable it per project under Settings → Feature flags → “Connectors API Discover”. Easy Connect remains the default connector marketplace.

method what
discover.list(query?, cursor?) · discover.detail(id) search OpenAPI/MCP/GraphQL/CLI entries · one entry’s detail

p.connectors.pipedream is the legacy managed-OAuth compatibility path; listApps returns OAuth apps only. Use Easy Connect for new managed integrations. Connect API-key apps directly.

method what
pipedream.listApps(params?) · pipedream.connect(input) · pipedream.finalize(input) browse the app catalog · start a connect flow · finalize it

A connector defines the tool, provider app, and policies. connections stores its connected accounts — a connector can hold several side by side. A session can pin one for every call with connector_bindings: { alias: { connection_id } }, or a single call can name one with call(tool, args, { account }). Credentials stay encrypted and resolve per request.

connections is the only active authorization facade. The retired authorizations and profiles names are not part of the current SDK surface.

Accounts and the call-time gate

Each connection has an owner_type: project (shared — reachable by anyone the connector is granted to, human or service account) or member (private — reachable only by its own owner, and only in a private session; never by a service account). A shared account can be narrowed to groups and people: connections.list() returns its grants in shared_with, and connectionSharedWithEveryone(connection) answers whether everyone in the project may use it. A narrowed account follows the private rules for the people it names, and usable: false marks one the caller sees only to manage it. Grant or revoke with createAssignment / revokeAssignment (object: { type: 'connection', id }) — see Choose who can use a shared account. p.connectors.accounts(slug) lists every account this caller may reach, default first:

const accounts = await beeuni.project(projectId).connectors.accounts('stripe-read');
// [{ connection_id, label, owner_type, is_default }, ...]

await beeuni.project(projectId).connectors.call(
  'stripe-read.get_balance',
  {},
  { account: 'My Stripe' }, // a connection id, a label, or the words `me` / `project`
);

A call that names no account resolves to the caller’s own default private account first, then the project’s default shared account. A named account that does not resolve is denied with connector_not_connected — never silently substituted — and the denial lists requested_account and available_accounts. Every successful call echoes which account it ran as: result.account = { connection_id, label, owner_type }.

There is no session-create-time connector gate any more. A session used to be refused 409 CONNECTOR_CONNECTION_REQUIRED when a required connector had no usable connection; that refusal is gone (require_connectors is inert — see p.sessions). The gate moved to the call: a denied connector_not_connected carries connect_url, a hosted authorization link, whenever one can be minted for the caller — the whole remedy is one click, surfaced by the transcript’s connect button.

p.policies — project policies

method what
list() · set(policies) the project’s policies · replace the set

p.triggers — cron, webhook, monitor and event automations

A trigger starts an agent action on a schedule, an inbound webhook, a monitored command, or an app event on a connector. See Triggers for session strategy and payload templating.

method what
list() all triggers
eventTypes({ connector }) the app events a connector can trigger on ({ provider, app, event_types }); 404 for an unknown connector, 409 event_source_unavailable when its provider has no event source
create(input) create one
update(triggerId, input) edit a trigger
remove(triggerId) delete a trigger
fire(triggerId) run it now
setActivation(paused) pause or resume every trigger on the project

create(input) takes { name, type: 'cron' | 'webhook' | 'monitor' | 'event', prompt_template, slug?, agent?, model?, enabled?, session_mode?, session_id?, cron?, run_at?, timezone?, secret_env?, connector?, event?, event_source?, event_account?, event_config?, session_access? }. name and prompt_template are required. cron/run_at are mutually exclusive (type: 'cron'); secret_env (the webhook HMAC secret) applies to type: 'webhook'.

An event trigger needs connector (a connector slug) and event (a type id from eventTypes). event_account is optional: the label of one shared account of that connector; null or omitted uses the connector’s default shared account. event_source is optional: the event source adapter id, such as composio. null or omitted uses the connector’s provider. The type id belongs to that adapter. event_config is shaped by that type’s config_schema. eventTypes and eventApps report the adapter as source; provider is a deprecated alias. Each listed trigger carries event: { connector, account, connected_as, type, config, source, provider, app, status: 'active' | 'needs_connection' | 'error' | 'pending', error, last_event_at } | null. Prompt templates read the event as {{ event.data.<field> }} plus event.id, event.type, event.app, event.connector, and event.occurred_at.

session_access controls who can open sessions the trigger creates. It is { mode: 'private' | 'members' | 'project', memberIds: string[], groupIds: string[] } and defaults to private. This policy is account-local runtime state. It does not enter the portable beeuni.yaml manifest. Updating only session_access creates no Git commit. A pinned session keeps its own sharing settings. A project manager can always open trigger-created sessions, including sessions that use private or selected-member access.

session_access is a per-resource visibility setting on top of the role model, not a role. It decides who can open one trigger’s sessions. It grants no permission the role verdict denies. See Accounts & access.

p.marketplace / p.registry — installed items

Installs a catalog item’s files onto the project’s default branch. registry.* is an identical alias of marketplace.*.

method what
marketplace.list() · marketplace.install(id) installed items · install a catalog item
marketplace.updates() · marketplace.update(name) · marketplace.updateAll() available updates · update one · update all
marketplace.remove(name) uninstall an item

p.files — repo files (read)

Read-only access to the project’s git tree. To read and write files inside a running session, use the session’s file operations — see Files under Modules below.

method what
listDirectory({ ref?, path? }) one folder level: { entries: [{ path, type, size? }], truncated }. Use it to build a tree; every folder is complete up to 5,000 entries.
list(options?) · read(path, ref?) every file under a path, recursively, cut at 1,000 (the response carries X-Beeuni-Truncated: 1 when cut) · a file’s contents at a git ref
search(query, { content?, ref?, limit? }) search filenames over the whole repo, or contents with content: true
archive(options?) · history(path) download a tarball · a file’s git history

p.git — history

method what
commits() the commit log
commit(sha) · commitDiff(sha) one commit · its diff
branches() · versionDiff(from, to) branches · diff between two refs

p.changeRequests — lifecycle and merge

A change request (CR) is how a session’s work merges into the default branch.

method what
list() · get(crId) open CRs · one CR
diff(crId) · mergePreview(crId) its diff · preview the merge result
open(input) · merge(crId, input?) open · merge a CR
close(crId, input?) · reopen(crId, input?) close without merging · reopen a closed one
requestChanges(crId, input) record feedback, optionally delivered back to the originating session

p.sessions — and the session handle

method what
list() the project’s sessions
create(input?) create a session
session(sid) the session handle (same as beeuni.session(id, sid))

The session handle is the heart of the runtime — see Sessions.

create(input) accepts connector_bindings keyed by connector-connection slug. Each value names a connection_id. It also accepts secrets for backend-origin secret narrowing. require_connectors is deprecated and inert: the field is still accepted (an old client is never rejected for sending it), but nothing reads it — a session is never refused at create time for a connector with no usable account. See Accounts and the call-time gate.

p.approvals — the connector approval inbox

Pending connector-gated actions awaiting a decision — backs the permission-approval UX (APPROVE / ASK / BLOCK).

method what
list(options?) · sessionsNeedingInput(options?) pending approvals · sessions blocked on a decision
resolve(executionId, decision, scope?) approve or deny one — decision: 'approve' | 'deny', scope: 'once' | 'session' | 'session_all'

p.gateway — LLM observability

Request logs, cost/latency rollups, budgets, and gateway API keys for this project’s model traffic.

method what
logs(opts?) · log(logId) request log entries · one log entry
overview(days?) · series(days?) · breakdown(days?) · sessions(days?) · errors(days?) rollups, per-session cost, and errors over a window
budgets() · setBudget(input) · deleteBudget(budgetId) read · create/edit · remove a budget
keys() · createKey(name) · revokeKey(keyId) list · mint · revoke a gateway API key
playground(prompt, models) run one prompt against up to 6 models
routing.get() · routing.set(policy) · routing.reset() read · replace · inherit the routing policy
routing.preview(input) resolve a route without invoking a model

A routing policy holds a default model, a vision model, and an ordered fallback chain, each model attempted at most once; fallbackOn is transient or any-error.

The gateway moves to the next fallback model only when the current model failed before it returned any output:

  • transient: a network error, a timeout, any 5xx, or 402, 403, 429.
  • any-error: every failure, including a 400.

A failure after output started stays with that model. A request on your own provider key (BYOK) never falls back to a Beeuni-billed model. Each fallback model gets its own generation defaults.

p.channels — Slack / email / voice

Connector surfaces that let an agent act as a Slack app, an email address, or join a realtime voice call.

method what
slack.installation() · slack.mode() · slack.manifest() current install · mode · app manifest
slack.connect(input) · slack.disconnect() connect · disconnect
slack.getFile(url) · slack.uploadFile(input) download · upload a file via the server proxy
email.installation(connectorSlug?) · email.mode() current install · mode
email.connect(input) · email.disconnect(...) · email.updatePolicy(input) connect · disconnect · update the send/reply policy
voice.setBotName(name) rename the bot in a live call

p.modelDefaults — default model preferences

Account, agent, and project-scoped model defaults, resolved by the gateway.

method what
get() · set(input) · clear(params) read · set a default · clear an override

p.setDefaultAgent — project default agent

p.setDefaultAgent(agentName) checks that the agent is declared and enabled, then sets it as default_agent in the project’s beeuni.yaml. New sessions prefer this agent unless a user picks another one.

p.updateFeatureFlag — feature flags

p.updateFeatureFlag(feature, enabled) turns one feature flag on or off for the project. Pass enabled: null to clear the override and fall back to the platform default. It calls the canonical PATCH /v1/projects/:id/features.

feature is one of FEATURE_FLAG_KEYS (exported from @beeuni/sdk, typed as FeatureFlagKey). The caller needs the project’s project.settings.write permission; the route answers 403 otherwise.

Every flag-gated route rejects the same way while the flag is off: HTTP 403 with { error, code: "feature_disabled", feature }. Use isFeatureDisabledError(error) to branch on it and featureDisabledKey(error) to read the flag key — never match on the message text.

p.updateExperimentalFeature(feature, enabled) is the deprecated alias. It keeps calling the deprecated route alias PATCH /v1/projects/:id/experimental so consumers pinned to an older deployed API keep working. Use p.updateFeatureFlag in new code.

p.sandbox — templates and snapshot builds

Sandbox build config beyond sandboxHealth/sandboxTemplates on the project handle: Dockerfile/image/warm-pool templates and their snapshot builds.

method what
list() · snapshots() sandboxes for this project · built snapshots
rebuildSnapshot(slug?) · fixWithAgent() rebuild a snapshot · ask an agent to fix a broken build
createTemplate(input) · updateTemplate(...) · removeTemplate(...) · buildTemplate(...) add · edit · delete · build a template
setProvider(provider) request a provider switch. null (or the platform default / the already-active provider) applies immediately; switching to a different enabled provider starts a durable prepare→verify→activate transition — the current provider keeps serving while the target warm image is built and verified, then activated. The return is a tagged union: kind:'project' (immediate) or kind:'preparation' (poll getProjectSandboxProviderTransition() until activated/failed)

Platform admin hooks (@beeuni/sdk/react)

These hooks serve the admin console. The API answers them only for a platform admin. Each mutation invalidates the ['admin', 'accounts'] queries.

Hook What it does
useAdminRole(userId, options?) read the caller’s role from GET /user-roles → { isAdmin, role }. userId keys the cache. A missing userId disables the query. Refetches on mount and focus, so a revoked role stops gating within one navigation.
useAdminGrantCredits() mutation { accountId, amount, description, isExpiring }: add wallet credit. POST /admin/api/accounts/:id/credits. Moves money: confirm in the UI first.
useAdminDebitCredits() mutation { accountId, amount, description }: remove wallet credit. POST /admin/api/accounts/:id/credits/debit. Moves money: confirm in the UI first.
useAdminSetTier() mutation { accountId, tier }: set the plan tier.
useAdminGrantTrial() mutation { accountId, tierKey, seats, durationDays, creditGrant?, note? }: grant or replace a trial. The account behaves as tierKey until the window ends. creditGrant also adds wallet credit.
useAdminRevokeTrial() mutation { accountId }: end an active trial now. Answers 400 when none is active.

Escape hatch

beeuni.runtime() returns the typed runtime REST client for the active sandbox. On a client created by createScopedBeeuni (@beeuni/sdk/server), it throws — the process-global “active” runtime is another request’s sandbox in a multi-tenant server, a cross-tenant leak. Use the session-scoped beeuni.session(pid, sid).runtime (call ensureReady() first) instead, which resolves that session’s own sandbox.

Modules

The framework-free modules behind the client facade and the React hooks. Reach for them when you need one operation without the facade, a pure helper, or a Node-only isolation layer. Each module carries a stability tier so you know what to build on.

Tier Meaning
Canonical Import from the root @beeuni/sdk. Use this for all new code.
Supported A dedicated subpath (@beeuni/sdk/react, @beeuni/sdk/server). First-class, not deprecated.
Deprecated alias An old subpath that still works. It re-exports code the root already exports. Import from root instead.
Internal Outside semver. Do not import this in host code.

The root entry is canonical. Every framework-free name below is importable straight from @beeuni/sdk:

import { files, getSessionHealth, getClient, authenticatedFetch, backendApi } from '@beeuni/sdk';

Canonical modules

Module What it does
Files Workspace file operations: list, read, search, write
Session runtime Health probe and preview/proxy URL builders
Runtime client The typed runtime REST client and its full type surface
Auth authenticatedFetch and token accessors
Projects REST The raw REST functions the facade wraps
API client backendApi, the low-level typed HTTP client
Turns Message-to-turn grouping, cost, and status math — see Turns
Transcripts formatTranscript, a client-side Markdown export
Composer lists The session composer’s agent and model lists: which to offer, which runs

Files

import { files } from '@beeuni/sdk';

const tree = await files.list('/workspace/src');
const { content } = await files.read('/workspace/README.md');
const hits = await files.findText('TODO');
await files.upload(file, '/workspace/uploads', file.name, {
  onProgress: ({ loadedBytes, totalBytes }) => {
    console.log(`${loadedBytes}/${totalBytes}`);
  },
});

Uploads larger than 64 KiB cross the sandbox edge in verified chunks. The progress callback runs after each chunk reaches the session filesystem.

uploadNativeFile({ uri, name, type? }, targetPath, baseUrl) uploads a device file from React Native in one request. The runtime streams the bytes from uri, so they never enter the JS heap. It uses the same auth, deadline and 401 replay as files.upload. It does not chunk.

fetchProjectArchive, fetchCostExportCsv and the sandbox version calls send through the same transport. A failure throws an ApiError with status.

files.write(path, blob) replaces the file at path and fails if path is a folder. files.create(path) creates an empty file and never overwrites: when a file or folder already exists at path, it throws FileExistsError (status 409, code 'FILE_EXISTS').

import { FileExistsError, files } from '@beeuni/sdk';

try {
  await files.create('/workspace/notes.md');
} catch (error) {
  if (error instanceof FileExistsError) console.log(`${error.path} already exists`);
  else throw error;
}

files targets the globally active sandbox. If your host runs more than one session at a time, call s.files on the session handle instead. It always targets that session’s own sandbox. See Sessions.

Session runtime helpers

import { getSessionHealth, isRuntimeReady } from '@beeuni/sdk';

const result = await getSessionHealth();
if (result.ok && isRuntimeReady(result.health)) {
  // the sandbox daemon is ready
}

getSessionHealth never throws on a non-2xx status. It returns { status, ok, health, body } and lets you decide what a status means. The same module exports the URL helpers that rewrite an agent’s localhost output into a reachable proxy URL: detectLocalhostUrls, rewriteLocalhostUrl, proxyLocalhostUrl, parseLocalhostUrl, and buildWebProxyUrl.

Runtime client

import { getClient } from '@beeuni/sdk';

const client = getClient();
const { data } = await client.session.list({ limit: 100 });

getClient() returns the typed runtime compatibility client for the active sandbox, with auth already injected. Prefer beeuni.session(pid, sid).runtime, the same client scoped to one session, over the global getClient() when your host runs more than one session.

Auth helpers

import { authenticatedFetch, getAuthToken } from '@beeuni/sdk';

const res = await authenticatedFetch(`${runtimeUrl}/beeuni/health`);
const token = await getAuthToken();

The token comes from the getToken function you passed to createBeeuni. Most app code does not need this module — the file, session, and facade layers already authenticate for you.

API client

import { backendApi } from '@beeuni/sdk';

const data = await backendApi.get('/some/endpoint');
await backendApi.post('/some/endpoint', { name: 'x' });

backendApi is the typed HTTP client every REST function builds on. Use it only for an endpoint that has no typed wrapper yet.

Composer agent and model lists

Build a session composer’s agent picker and model picker with the same rules the Beeuni web app uses. Every function is pure: fetch the inputs with the client, then pass them in.

import {
  composerSelectableAgents,
  createModelVisibility,
  flattenModels,
  modelInDefaultView,
  pickerProviderList,
  projectConfigAgentsToRuntimeAgents,
  resolveComposerAgent,
  resolveComposerModel,
  resolveModelDefault,
} from '@beeuni/sdk';

// Agents: the roster, the picker list, and the agent the send carries.
const roster = projectConfigAgentsToRuntimeAgents(detail.config);
const pickerAgents = composerSelectableAgents(roster, { enableProjects: false });
const agent = resolveComposerAgent({
  agents: roster,
  boundAgent: session?.agent,
  defaultAgent: detail.config.default_agent,
  selectedAgent: pick,
});
if (agent.disabled) {
  // no accessible agent: refuse the send
}

// Models: gateway projects read /model-picker, native projects merge the
// catalog synthesis with the running sandbox's provider list.
const gatewayEnabled = detail.project.experimental?.llm_gateway === true;
const providers = pickerProviderList({
  gatewayEnabled,
  modelPicker,
  runtimeProviders,
  llmCatalogProviders,
  secretNames,
});
const models = flattenModels(providers, { providerMode: gatewayEnabled ? 'gateway' : 'native' });
const isVisible = createModelVisibility({ catalogModels: models });
const defaultView = models.filter(
  (m) => m.enabled !== false && modelInDefaultView(m, { search: '', isStoreVisible: isVisible, selected }),
);
const { model, explicit } = resolveComposerModel({
  models,
  picks: [sessionPick],
  serverDefault: resolveModelDefault(modelDefaults, agent.selected ?? undefined),
  providers,
});

resolveComposerModel returns explicit: undefined when the composer is on its default. model is the key to display and send. Each flattened model’s id is the ref a pick stores; modelRefToKey(ref, gatewayEnabled) reads a stored ref (a session pin, a channel binding, a trigger’s model) back into a key.

Supported subpaths

Subpath What it does
@beeuni/sdk/react React hooks — see React hooks
@beeuni/sdk/server Request-scoped config for multi-tenant backends
@beeuni/sdk/wire-message-id The runtime wire message-id clock alone (one module, no imports). The root exports the same names

Wire message ids

A prompt’s messageId is its position in the transcript. The runtime orders messages by msg_ + a 48-bit clock (Date.now() * 0x1000, low 48 bits) + 14 base62 characters, and the clock wraps every 2^36 ms (about 2.2 years). Never encode it by hand.

import { mintWireMessageId } from '@beeuni/sdk';

// `after`: the ids already in this session's transcript.
const messageId = mintWireMessageId({ after: transcriptIds });

The mint is dated two minutes back, then lifted one tick above the newest id in after that is at most one hour ahead. A caller that holds the newest position as a clock instead of ids uses mintWireMessageIdAbove({ nowMs, newestKnownTime }), which returns { id, time }. newestWireIdClock(ids) finds that clock. Every comparison is on the ring, so ids on both sides of a wrap keep their true order.

The 48-bit clock wraps every ~2.2 years, so never compare two clocks with > or <=: an id minted just after a wrap is numerically smaller than one minted just before it. Use wireIdClockDelta(clock, reference), the signed distance on the ring (positive when clock is ahead), and maxWireIdClock(clocks) for the newest of several:

import { maxWireIdClock, wireIdClockDelta } from '@beeuni/sdk/wire-message-id';

const floor = maxWireIdClock([transcriptNewest, deliveredNewest]);
const stale = floor !== null && wireIdClockDelta(clientClock, floor) <= 0n;

Server-side isolation

createBeeuni stores its config, including the token function, in one process-wide variable. That is fine for a browser tab, a CLI, or a single-tenant server. It is unsafe for a Node server that handles concurrent requests for different users, because the last createBeeuni call wins for every in-flight request. @beeuni/sdk/server fixes this with per-request isolation:

import { createScopedBeeuni } from '@beeuni/sdk/server';

export async function handler(req: Request) {
  const beeuni = createScopedBeeuni({ backendUrl, getToken: () => tokenFor(req) });
  return beeuni.projects.list();
}

createScopedBeeuni and runWithBeeuni isolate config per request with Node’s AsyncLocalStorage. Never import @beeuni/sdk/server from a browser bundle — it statically pulls in node:async_hooks.

A scoped client’s top-level runtime() throws (it would resolve another tenant’s sandbox). Reach a specific session’s runtime via beeuni.session(pid, sid).runtime after await s.ensureReady().

Deprecated aliases

About twenty old subpaths still work: /files, /turns, /session, /auth, /projects-client, /api-client, /config, /event-stream, /opencode-client, /platform-client, and more. Each one re-exports code the root @beeuni/sdk entry already exports. They stay working so no existing import breaks, but new code should import from the root.

Internal modules

@beeuni/sdk/internal/* holds the zustand stores apps/web’s own runtime uses internally — session sync state, active-runtime tracking, and reconnect bookkeeping. This subpath is explicitly outside semver. Do not import it in host code; the React hooks already expose the state you need.

Turns

Plain functions that group session messages into turns and classify each message part. Use them to build a custom chat renderer instead of the reference one in apps/web. No React, no DOM — every export is a plain function or type, safe to call from any host.

import { classifyPart, classifyTurn, toolInfo, toolViewModel } from '@beeuni/sdk';

Import every function from the root @beeuni/sdk entry. The @beeuni/sdk/turns subpath still works, but it is a deprecated alias. New code must use the root entry — see Distribution.

Classify a part

classifyPart(part) normalizes one of the 12 part types of the Beeuni transcript format (beeuni.transcript.v1, BEEUNI_TRANSCRIPT_SCHEMA) into a ClassifiedPart — a union keyed by kind. Each variant already resolves the fields a renderer needs: tool status, parsed JSON output, image detection.

import { classifyPart, type ClassifiedPart } from '@beeuni/sdk';

for (const part of message.parts) {
  const classified: ClassifiedPart = classifyPart(part);
  switch (classified.kind) {
    case 'text':
      render(classified.text);
      break;
    case 'tool':
      render(classified.tool.title, classified.tool.status);
      break;
  }
}
kind shape
text { id, text, synthetic } — skip synthetic parts; they mark shell mode’s synthetic prompt
reasoning { id, text }
tool { id, tool: ToolView }
file { id, filename?, mime, url, isImage, isPdf }
subtask { id, description, agent, prompt, model? }
patch { id, hash, files, fileCount }
snapshot { id, snapshot }
agent { id, name }
retry { id, attempt, message, createdAt }
compaction { id, auto, overflow, tailStartId? }
step { id, phase: 'start' | 'finish', snapshot?, reason?, cost?, tokens? }
unknown { raw } — a part type this SDK version does not know

An unrecognized wire part degrades to unknown at runtime instead of throwing. This lets an older client talk to a newer server.

A tool part classifies into ToolView:

interface ToolView {
  name: string;
  title: string;
  status: 'pending' | 'running' | 'done' | 'error';
  input?: Record<string, unknown>;
  output?: string;
  error?: string;
  outputParsed?: unknown; // JSON.parse(output) when it parses, capped at 256KB
  outputText?: string;    // the raw output text, always present
  kind?: ToolKind;        // toolKind(name): the action, whatever the harness calls the tool
  files?: ToolFile[];     // the files a file tool read, wrote or changed: path, patch, additions/deletions
  diff?: string;          // a unified diff of the change
  diagnostics?: Record<string, unknown[]>; // language-server diagnostics, keyed by file path
  answers?: string[][];   // the answers to a `question`, one list per question
  childSessionId?: string; // the child session a delegating call (`task`) runs
}

toToolView(part) builds one straight from a tool part. Read these fields instead of a harness’s own state.metadata: the same card then renders a runtime edit (filePath, filediff) and a pi edit (path, edits, a unified patch).

Some tools (web_search, image_search, connector calls) report state.status: 'completed' even when their JSON body carries success: false. classifyPart detects this and sets ToolView.status to 'error' in that case too.

classifyTurn(message) classifies every part of one assistant message and returns a ClassifiedTurn with three fields:

  • parts — each part, classified
  • error — from message.info.error, if any
  • isEmpty — true when the turn has no error and no part with visible content

Tool metadata

toolKind(name) is the one tool taxonomy: it maps any harness’s tool name (pi’s write, runtime’s oc-apply_patch, a plugin’s pty_spawn) to a ToolKind — read, edit, write, bash, web_search, task, question, … or other. Narration families, view models, toolInfo categories and the context-tool group all derive from it. inputPath(input) reads a file tool’s path from filePath, file_path or path.

toolKind('oc-apply_patch'); // 'apply_patch'
toolKind('pty_spawn');      // 'pty'
inputPath({ path: 'app.py' }); // 'app.py'

Two more lookups both describe a tool. Do not confuse them.

  • toolInfo(name) — icon-free, returns { label, category }. classifyPart uses this internally. ToolCategory is 'shell' | 'files' | 'search' | 'edit' | 'web' | 'task' | 'other'.
  • getToolInfo(name, input) — icon-aware, returns { icon, title, subtitle } for the reference tool-card UI. The subtitle comes from the tool’s input, for example a file path or a search query.
toolInfo('bash'); // { label: 'Shell', category: 'shell' }
getToolInfo('write', { filePath: '/workspace/main.go' });
// { icon: 'file-pen', title: 'Write', subtitle: 'main.go /workspace' }

Both functions match tool-name prefixes, so Beeuni’s plugin tool families (agent_*, session_*, task_*, trigger_*, project_*, pty_*) resolve without a registry update. An unknown tool name never throws — it falls back to humanizeToolName(name) with category 'other'.

Tool view models

toolViewModel(classifiedTool) maps a classified tool part to a shape built for one tool family. A UI can then render it specially instead of as a generic JSON blob.

const vm = toolViewModel(classifiedTool);
if (vm.kind === 'shell') {
  render(vm.command, vm.stdout, vm.exitCode);
}
kind shape tools
web-search { query, results?, answer?, error? } web_search, image_search
shell { command, stdout?, exitCode? } bash
file-read { path, preview? } read
file-write { path, preview? } write
file-edit { path, diff?: DiffLine[] } edit, morph_edit
search { pattern, matches?: SearchMatch[] } grep, glob
task { description, agent? } task
todo { items: TodoItem[] } todowrite
question { questions: QuestionItem[], answers? } question, ask
generic { label, inputPretty?, outputPretty? } everything else, always safe to render

DiffLine is { type: 'added' | 'removed' | 'unchanged', text }. String fields are capped so one large tool output never breaks a render: 4000 characters for pretty-printed JSON, 256KB before a diff runs.

Group messages into turns

A turn pairs one user message with the assistant messages that answered it. It is the unit a chat UI renders as one exchange.

import { groupMessagesIntoTurns, collectTurnParts, type TurnLike } from '@beeuni/sdk';

const turns: TurnLike[] = groupMessagesIntoTurns(messages);
for (const turn of turns) {
  const parts = collectTurnParts(turn);
}

groupMessagesIntoTurns links each assistant message to its parent user message. It falls back to message order when a parent link is missing. It also attaches an orphan assistant message — one with no parent that precedes every user message — to the first turn, not the last.

Related helpers:

  • findLastTextPart(parts) — the turn’s final response text
  • turnHasSteps(parts) — true if a tool, compaction, snapshot, or patch part exists
  • isShellMode(turn) / getShellModePart(turn) — a turn that is one synthetic prompt driving one bash call

Type guards

Narrow a part by type:

import { isTextPart, isToolPart, getPartText } from '@beeuni/sdk';

if (isTextPart(part)) {
  // part.type narrowed to 'text'
}
const text = getPartText(part); // works for 'text' and 'reasoning' parts

Also available: isReasoningPart, isFilePart, isAgentPart, isCompactionPart, isSnapshotPart, isPatchPart, isStepPart (step-start and step-finish).

Status and errors

import { getWorkingState, getTurnStatus, formatDuration } from '@beeuni/sdk';

const status = getTurnStatus(parts, childMessages); // "Running commands..."
formatDuration(4300); // "4s" — durations under 1s return ''

getTurnStatus scans a turn’s parts for the last status line. When the last part is a running task delegation, pass childMessages so the status shows the sub-agent’s real activity instead of a generic “Delegating…” line.

import { getTurnError, getChildSessionError, unwrapError } from '@beeuni/sdk';

getTurnError(turn);                    // the first assistant error, unwrapped
getChildSessionError(childMessages);   // newest error in a sub-agent's messages
unwrapError(rawError);                 // normalizes double-JSON and mixed error shapes

Cost and token totals

import { getTurnCost, getSessionCost, formatCost, formatTokens } from '@beeuni/sdk';

const info = getTurnCost(partsWithMessage, modelPricingLookup);
const sessionCost = getSessionCost(messages, modelPricingLookup);

formatCost(0.0032);  // "$0.003"
formatTokens(12345); // "12k"

Both functions read cost and token totals from step-finish parts. When a part reports zero, they estimate cost from token counts using a ModelPricingLookup.

ChatGPT/Codex subscription messages contribute $0.00, including historical messages with nonzero runtime costs. Token and context usage remain visible. Mixed sessions retain charges for paid API messages. OpenAI API models remain billable, including API models whose names contain codex.

Child sessions and pending requests

A project session’s conversation tree (the root and the subagent conversations it spawned) comes from its runtime_sessions list. These helpers read it, preferring the current names over the pre-W4 ones:

import { directSubsessions, projectSessionForRuntimeId, rootRuntimeSession, runtimeSessionsOf } from '@beeuni/sdk';

const root = rootRuntimeSession(projectSession);            // the root conversation id
const children = directSubsessions(projectSession);         // its subagent conversations
const owner = projectSessionForRuntimeId(projectSessions, childId); // the session a conversation belongs to

A task tool call delegates to a child session. These helpers connect a parent turn to that child’s own messages.

import { getChildSessionId, getChildSessionToolParts } from '@beeuni/sdk';

const childId = getChildSessionId(taskToolPart);
const steps = getChildSessionToolParts(childMessages);

Match a pending permission or question request to its tool call, and find which tool parts to hide while one is active:

import { getPermissionForTool, getHiddenToolParts, isToolPartHidden } from '@beeuni/sdk';

const permission = getPermissionForTool(permissions, callID);
const hidden = getHiddenToolParts(activePermission, activeQuestion);

Formatting and lists

import { getFilename, getDirectory, relativizePath, stripAnsi } from '@beeuni/sdk';

getFilename('/workspace/src/main.go');               // "main.go"
getDirectory('/workspace/src/main.go');               // "/workspace/src"
relativizePath('/workspace/src/main.go', '/workspace'); // "src/main.go"

Session-list helpers operate on the same session data:

import { sortSessions, childMapByParent, allDescendantIds } from '@beeuni/sdk';

sessions.sort(sortSessions(Date.now())); // pins sessions updated in the last 60s
const childMap = childMapByParent(sessions);
const descendants = allDescendantIds(childMap, sessionId);

Retry state: getRetryInfo(sessionStatus) returns { attempt, message, next, details? } when status.type === 'retry', with message capped to 60 characters. details preserves a structured LLM-gateway envelope when present: the final provider, gateway code, request ID, suggestion, upstream status, and ordered candidate failures. Each candidate failure contains attempt, provider, routeModel, resolvedModel, stage, optional status, code, and message. Legacy plain-text retries return details: undefined. getRetryMessage(sessionStatus) returns the full unwrapped message. When the runtime retains only that message, the gateway composite still includes the request ID and each candidate’s provider, resolved model, HTTP status, code, and bounded message.

Structural types

The grouping and status functions accept minimal structural types — PartLike, MessageInfoLike, TurnLike, ToolStateLike, SessionStatusLike — instead of the concrete transcript types. Your own message and part shapes flow through unchanged as long as they match the required fields. classifyPart and classifyTurn are the exception. They type against the Beeuni Part union (beeuni.transcript.v1), so their exhaustiveness check catches a new part type at build time.

Distribution

npm install @beeuni/sdk

The package ships as compiled ESM with full TypeScript type declarations. react and @tanstack/react-query are optional peer dependencies. If you use @beeuni/sdk/react, install both.

import { createBeeuni } from '@beeuni/sdk'; // framework-free core
import { useSession } from '@beeuni/sdk/react'; // optional React layer
import { createScopedBeeuni } from '@beeuni/sdk/server'; // Node and Bun servers

Entry points and stability

The root entry, @beeuni/sdk, is canonical. It exports the full framework-free surface and runs in browsers, Node 18+, Bun, and edge runtimes.

Entry Tier Contract
@beeuni/sdk Canonical Framework-free. Never imports React or node:*.
@beeuni/sdk/react Supported React hooks. The only entry that imports React.
@beeuni/sdk/server Supported Per-request config isolation for Node and Bun servers, via node:async_hooks. Never bundle it into a browser.
Legacy subpaths (/projects-client, /turns, /files, and more) Deprecated Still work. Each re-exports from the root. Import from the root instead.
@beeuni/sdk/internal/* Internal Used by the Beeuni web app only. Not a supported API.

CDN bundles

The package also ships two browser bundles, built by tsup: an ESM bundle (dist/beeuni.esm.min.js) and an IIFE global (dist/beeuni.global.js) that defines window.Beeuni.

<script src="https://unpkg.com/@beeuni/sdk/dist/beeuni.global.js"></script>
<script>
  const beeuni = Beeuni.createBeeuni({
    backendUrl: 'https://api.beeuni.ai/v1',
    getToken: async () => KEY,
  });
</script>

window.Beeuni exposes the same root entry as the npm import: Beeuni.createBeeuni, Beeuni.classifyTurn, Beeuni.ApiError.

See also

  • SDK — install and your first session.
  • Sessions — lifecycle, streaming, and error handling.
  • React hooks — the reactive layer built on these modules.

Agent repository access

Agent configuration accepts repository_access?: boolean (default true). Set false to run new sessions without the project repository or repository API access. Git, secret, connector, and tool permissions remain separate. Existing sessions retain their saved policy. AgentConfigBlock.workspace is deprecated. The SDK maps legacy branch/runtime to the boolean field. A legacy read write requires an explicit repository_access choice; it does not enable read-only repository access.

Provider and model access

Read beeuni.projects.modelAccess(projectId). Change one target with beeuni.projects.setModelAccess(projectId, { target: 'provider' | 'model', id, enabled }).

Use provider ID beeuni for Beeuni Managed Models, or a catalog provider ID for BYOK, Codex, and custom providers. Use the full gateway wire ID for a model. Provider disable covers future models and preserves individual model preferences and saved credentials.

The current default model and its provider require a replacement before disabling (409 cannot_disable_default). Direct gateway requests to disabled targets fail with model_disabled or provider_disabled. Existing in-flight upstream requests finish normally. The response’s enforced flag is false when the project uses a native runtime model connection instead of the gateway.

React consumers use useModelAccess(projectId) from @beeuni/sdk/react. Its setEnabled(change) mutation refreshes the management catalog and session picker.

Was this page helpful?