The fxbot object
What createFxbot resolves to: every call on bots, chats, providers, settings, the library, and add-ons.
Every call returns a promise, unless its row says it is synchronous. A call that fails throws an Error with a status, as in Errors. A bot id is 12 hex characters: another shape fails with 400, and an unknown bot with 404.
Bots
| Call | What it does | Returns |
|---|---|---|
profiles.list() |
Every bot: the pinned ones first, in the order they were pinned, then the rest by name | Profile[] |
profiles.get(id) |
One bot | Profile |
profiles.create(fields) |
A new bot. Only name is required. provider defaults to Settings' defaultProvider. The first bot starts pinned. |
Profile |
profiles.update(id, update) |
Changes any of the fields, and pinned |
Profile |
profiles.remove(id) |
Removes the bot, its messages, and its memories. Its library items stay, with null for the bot. libfx's session files stay in dataDir. |
nothing |
profiles.earlier(id) |
The bot's chats before its current one, which a provider switch leaves behind, as finished turns, oldest first: the newest 400 messages of them. Turn is the client's type. |
Turn[] |
profiles.skills(id) |
The skills the bot has, for a $ and / picker |
BotSkill[] |
The fields of a bot:
| Field | Rules | Default on create |
|---|---|---|
name |
Trimmed, 1 to 200 characters | Required |
instructions |
Up to 64 KiB | "" |
provider |
A provider id, or "" for none. An unknown id fails with 404. |
Settings' defaultProvider |
model |
Trimmed, up to 200 characters. "" follows the provider's default model. |
"" |
shape |
One of shapes |
circle |
color |
A key of colors |
brown |
Other keys are ignored. An update also takes pinned:
pinned: truefails with 400 whenmaxPinned(3) bots are pinned already.- A new
providerwithout a newmodelsetsmodelto"". - A new provider for a bot with a chat compacts the chat. The old provider summarizes it, or, when it can't, the new one summarizes the stored messages. The bot starts a new chat from that summary, its
handoff, and the old chat moves toearlier. This fails with 400 when the old provider can't summarize and the new one has no model, and with 502 when neither provider could summarize. provider: ""fails with 400 for a bot with a chat.
Memories
| Call | What it does | Returns |
|---|---|---|
profiles.memories.list(id, { query?, limit? }) |
The bot's memories, newest first. With query, the best matches, by words and, with an embedder, by meaning. limit is 1 to 500, default 200. |
Memory[] |
profiles.memories.add(id, text) |
Adds a memory, saved by the owner. The text is trimmed, 1 to 1,000 characters. The same text again, ignoring case and extra spaces, returns the memory already there. | Memory |
profiles.memories.update(id, memoryId, text) |
New text for a memory. If another memory already says it, the two merge: this one is removed, and the other is returned. | Memory |
profiles.memories.remove(id, memoryId) |
Removes a memory | nothing |
memoryId is a memory's id, or mem:<id>. An unknown one fails with 404. See Work with memories.
Chat
| Call | What it does | Returns |
|---|---|---|
send(id, message) |
Sends a message to the bot. It resolves once libfx has stored the prompt. The reply arrives on the stream. | { messageId, sessionId } |
stream(id, cursor = 0) |
Every line of the bot's chat from cursor on, as NDJSON, then each new line. Cancel the stream to stop it. |
ReadableStream<Uint8Array> |
send takes a Message:
textis required, unless the message has files.messageIdis 1 to 128 letters, digits,.,_, or-. The samemessageIdagain reaches the same turn and runs nothing new.filesis up to 8{ name, type, data }, withdatain base64. An image is PNG, JPEG, GIF, or WebP, checked by its bytes: at most 3.75 MiB each, and 6 MiB for a message's images together. Any other file must be UTF-8 text with no NUL. It goes to the model as text, under its name. Every file is saved to the library, tagged with the bot and its chat.$nameanywhere in the text, or/nameat its start, names one of the bot's skills. That skill's instructions go with the message.- It fails with 400 when the bot has no provider, or no model of its own and no default model on its provider.
- The first message starts the bot's chat and sets its
sessionId.
stream:
- A line is one of libfx's stream lines. Its fields are the client's
StreamLine. - A
turn_startfor a message with files or skills carries theinputandfilesthat fxbot saved, which libfx's line lacks. cursoris a whole number, or the call fails with 400. 0 replays the whole chat. Each line'scursoris where to start again to get the lines after it.- For a bot with no chat yet, or whose provider is gone, the stream ends at once.
See Stream replies.
Providers
The kinds are in Providers and the kinds table.
| Call | What it does | Returns |
|---|---|---|
providers.list() |
Every provider, in the order they were added. hasKey and signedIn say whether it holds a key or a sign-in, not what it is. |
Provider[] |
providers.add(input) |
A provider of a key kind or a server kind. A key is checked with the provider first. | Provider |
providers.update(id, input) |
Changes name, model, apiKey, baseUrl (server kinds), contextWindow (server kinds), or teamId (a Vercel account) |
Provider |
providers.remove(id) |
Removes the provider. It fails with 400 while bots use it. When it was Settings' defaultProvider or embeddingProvider, that becomes "". |
nothing |
providers.models(id) |
The provider's models, and what fxbot knows of each | Model[] |
providers.teams(id) |
A Vercel account's teams. Other kinds fail with 400. | Team[] |
providers.add takes a NewProvider:
kindis a key kind or a server kind.chatgpt,grok, andvercelare added by signing in, and fail here with 400.baseUrlis fixed for a key kind. A server kind takes one, or uses its default, andcustomneeds one. It ishttps, orhttponlocalhost,127.0.0.1, or[::1], with no credentials, query, or fragment.apiKeyis required for a key kind, optional forcustom, and up to 4,096 characters. The check lists the provider's models with it. A 401 or 403 fails with 400, "The provider rejected this key." A 404 passes.namedefaults to the kind's label.modelis the default model for the provider's bots.contextWindowis for server kinds only: a whole number of tokens, ornull. It overrides what the server reports.
providers.update fails with 400 for apiKey on a kind that signs in, and for baseUrl on a kind that isn't a server. A new key, address, or team drops the kept model list.
Sign-ins
A device code sign-in adds a ChatGPT plan, a Grok plan, or a Vercel account. See Connect providers.
| Call | What it does | Returns |
|---|---|---|
signIns.start({ kind, provider? }) |
Starts a sign-in for chatgpt, grok, or vercel. With provider, it signs that provider in again. One runs at a time: a new one replaces one that waits. It fails with 502 when the service gives no code. |
SignIn |
signIns.get(id) |
Synchronous. The sign-in's status. It fails with 404 for a sign-in that isn't the latest one. When it is done, provider is the provider's id. |
SignInStatus |
signIns.cancel(id) |
Synchronous. Stops a sign-in that waits. | nothing |
A new Vercel account with one team gets that team.
Settings
| Call | What it does | Returns |
|---|---|---|
settings.get() |
The settings | Settings |
settings.update(input) |
Changes any of the fields, and returns them all | Settings |
settings.embedding() |
How far memory search's vectors have got | { model, error, messages, withVectors, memories, memoriesWithVectors } |
| Field | Type | Default | What it is |
|---|---|---|---|
defaultProvider |
string |
"" |
The provider new bots start on, or "" for none |
embedder |
"local", "remote", or "off" |
"local" |
Where memory search's vectors come from: embedUrl, a provider's /embeddings, or none |
embeddingProvider |
string |
"" |
For remote: a provider whose kind is in embeddingKinds |
embeddingModel |
string |
"" |
For remote: the model's id, up to 200 characters |
messageVectors |
number |
8000 |
How many characters of each message get a vector, 0 to 100,000. 0 embeds no messages. |
remoteneeds bothembeddingProviderandembeddingModel, or the update fails with 400.- A new remote source embeds a test string first. If that fails, the update fails with 400, and the old settings stay.
- A change to memory search's fields starts filling in vectors again, in the background.
settings.embedding() gives the model in use, or null when vectors are off or the embedder doesn't answer, the fill-in's last error, and the counts. fxbot's server adds local, switching, and canSwitch, which make an EmbeddingStatus.
Library
| Call | What it does | Returns |
|---|---|---|
library.list(query) |
Items, newest saved first. q searches names and document text, and every word must match. profileId narrows to one bot, and kind to image or document. limit is 1 to 100, default 60. before is a page's next. Pass {} for all. |
LibraryPage |
library.get(id) |
One item | LibraryItem |
library.file(id, ifNoneMatch?) |
The item's bytes as data, with its name, kind, type, and sha256. data is null when ifNoneMatch names the SHA-256 in quotes, as an ETag. |
{ name, kind, type, sha256, data } |
library.upload(id, name, ifNoneMatch?) |
The same, for an image's address from before the library: a bot id, and <sha256>.<ext> with png, jpg, gif, or webp |
{ name, kind, type, sha256, data } |
library.remove(id) |
Removes the one copy, for every bot it is tagged with | nothing |
An item's url is /api/library/<id>/file, a route of fxbot's server.
Add-ons
Remote MCP servers and skills. See Give bots tools.
| Call | What it does | Returns |
|---|---|---|
addons.list() |
Every server and skill. A server from a registry carries that entry's info, and a mark when it has a newer version, or is deprecated or gone. A skill carries the mark of the hourly check. |
Addon[] |
addons.search(q, options) |
Up to 20 servers from fxbot's list and the mirror of GitHub's registry. { everything: true } adds the official registry's results, for 3 characters or more. { kind: "skill" } searches skills instead: fxbot's list, then skills.sh for 2 characters or more. options is required: pass {} for servers. |
AddonSearch or SkillSearch |
addons.preview(ref) |
An add-on's page, with a hash. For a server, what probing it found. For a skill, its files at its repository's current commit. |
AddonPreview or SkillPreview |
addons.add(ref, hash) |
Adds what the preview showed. It fails with 409 when that changed since, or the skill is already added. | Addon |
addons.update(id, hash) |
Moves a registry server or a skill to the newer definition its page showed. It fails with 409 when that changed since. | Addon |
addons.change(id, input) |
Sets enabled, bots ("all", or "each" for chosen bots), and for a server, allowed: the names of the tools bots get |
Addon |
addons.remove(id) |
Removes the add-on | nothing |
addons.setSecrets(id, secrets) |
A server's header values, or the owner's own OAuth clientId and clientSecret. Write-only. |
Addon |
addons.forBot(profileId, addonId, enabled) |
Turns an add-on on or off for one bot. It counts when the add-on's bots is "each". |
Addon |
addons.readSkill(id, file?) |
Synchronous. One of an installed skill's files, SKILL.md by default. |
string |
addons.check() |
Runs the hourly check of the skills' repositories now | nothing |
An AddonRef is one of:
| Ref | What it names |
|---|---|
{ url, transport?, headers?, allowLocal? } |
A server by its address. transport is streamable-http (default) or sse. headers names the headers it needs. The address is https. A local or private address needs allowLocal, and may then use http. |
{ registry, version?, allowLocal?, from? } |
A registry entry, from github (default), fxbot, or official |
{ skill, from? } |
A skill, as <owner>/<repo>/<skillId> |
A server's allowed starts as its read-only tools. A header value is one line, up to 8 KiB, for a header the server was added with.
OAuth
oauth signs in to a server whose auth is oauth.
| Call | What it does | Returns |
|---|---|---|
oauth.start(id, origin?) |
Starts a sign-in, which expires in 10 minutes. With mode: "redirect", the browser comes back to <origin>/api/oauth/callback, for an HTTPS origin. With mode: "paste", it comes back to <localUrl>/callback, and the person pastes that address when the page doesn't load. A new sign-in to the same server replaces one that waits. |
OAuthAttempt |
oauth.get(attemptId) |
Synchronous. The sign-in's status. | OAuthStatus |
oauth.paste(attemptId, address) |
Finishes a sign-in with the address the person pasted | OAuthStatus |
oauth.callback(query) |
Finishes a redirect, from its query string as URLSearchParams |
OAuthStatus |
origin is the page's origin, which the server has checked against its own host.
Release and close
| Call | What it does | Returns |
|---|---|---|
release() |
Synchronous. The running version, and latest, the newest stable release when it is newer, or null |
{ version, latest } |
close() |
Stops the timers and MCP connections, closes the agents, which waits for their running turns, then ends the Postgres connection. See Stopping. | nothing |
Tools every bot gets
Every agent has these tools. Each one acts for the bot whose chat runs it.
| Tool | What it does |
|---|---|
chat_history |
Searches the bot's messages in all of its chats, by words, and by meaning when message vectors are on. With around, a msg:<id>, it reads that message and up to 6 on each side, in full. |
memory_save |
Saves one fact, up to 1,000 characters. With replaces, a mem:<id>, it changes that memory instead. |
memory_search |
Searches the bot's memories by words and, with an embedder, by meaning: up to 20 |
memory_forget |
Removes a memory by its mem:<id> |
library_search |
Searches the items tagged with the bot, by name and document text. An empty query lists the newest. |
library_read |
Opens a library:<id>: a document's text, up to 60,000 bytes, or an image |
library_save |
Saves a document the bot wrote. The name ends in .md, .txt, .csv, or .json, and the text is up to 1 MiB. |
skill |
Present while a skill is on. Loads a skill by name, one of its files by file, in parts of 60 KB, or finds skills by search. |
<prefix>_<tool> |
Each enabled MCP server's allowed tools. The prefix comes from the server's name or host, such as linear for mcp.linear.app, and is unique among add-ons. For a server set to each, a call from a bot it isn't on for fails. |
A bot's instructions on each turn are its own, then its handoff, its newest memories in up to 2% of the model's context window, and the list of its skills in up to 1%.
Exports
From @fxbot/sdk, besides createFxbot:
| Export | Returns | What it does |
|---|---|---|
readKey(text) |
Buffer |
Decodes the key file's base64 text. It throws unless the key is 32 bytes. |
rotateSecrets({ databaseUrl, from, to }) |
number |
Seals every stored secret again, providers' and add-ons', from the key from to the key to, in one transaction, on a connection of its own. Run it with the server stopped. It returns how many it sealed. |
compareVersions(a, b) |
number |
Below zero when a is older than b. A prerelease comes before its release. It throws for a string that isn't a version. |
isVersion(version) |
boolean |
Whether it is x.y.z, with an optional v before it and an optional prerelease after a dash, such as 0.4.0-rc.1 |
isStable(version) |
boolean |
Whether it is a version with no prerelease |
releasesUrlFor(repository) |
string or undefined |
GitHub's releases/latest API address for a https://github.com/<owner>/<repo> URL |
| Table | What it holds |
|---|---|
colors |
A bot's colors, from name to hex: black, brown, red, orange, amber, green, teal, blue, purple, pink, gray |
shapes |
A bot's shapes: circle, cloud, squircle, star, clover, heart, flower, egg, bean, triangle, diamond, sparkle, hexagon, pumpkin |
kinds |
Each provider kind's label, connect (device, key, or url), and baseUrl where it is fixed or a default |
itemKinds |
image, document |
embeddingKinds |
The provider kinds that serve /embeddings: openai, openrouter, gateway, vercel, ollama, lmstudio, custom |
maxPinned |
3 |
Every type the calls take or return is exported too, with FxbotOptions and Fxbot, the type of this object. Turn and Part come from @fxbot/sdk/client.
Types
type Fields = { name: string; instructions: string; model: string; provider: string; shape: Shape; color: Color };
type Profile = Fields & {
id: string;
sessionId: string | null; // the current chat, from the first message
preview: string | null; // the start of the latest reply
handoff: string | null; // the summary a provider switch carried over
busy: boolean; // a turn runs in this process
pinned: boolean;
};
type Message = { text: string; messageId: string; files?: Upload[] };
type Upload = { name: string; type: string; url?: string; data?: string }; // data: base64 on the way in
type Provider = {
id: string;
kind: Kind;
name: string;
baseUrl: string | null;
model: string;
contextWindow: number | null;
teamId: string | null;
hasKey: boolean;
signedIn: boolean;
error: string | null; // why its agent didn't start
};
type Settings = { defaultProvider: string; embedder: "local" | "remote" | "off"; embeddingProvider: string; embeddingModel: string; messageVectors: number };
type EmbeddingStatus = {
model: string | null;
error: string | null;
messages: number;
withVectors: number;
memories: number;
memoriesWithVectors: number;
local: "cpu" | "gpu" | "off" | null; // from fxbot's server
switching: { to: "cpu" | "gpu" | "off"; error?: string } | null; // from fxbot's server
canSwitch: boolean; // from fxbot's server
};
type Memory = {
id: string;
text: string;
savedBy: "bot" | "owner";
messageId: string | null; // the msg:<id> that chat_history knows, of the turn that saved it
createdAt: string;
updatedAt: string;
};