@korpus-cloud/sdk
v0.22.0
Published
Korpus from code: shared memory for your programs, as an agent: you, narrowed to what its job needs.
Maintainers
Readme
@korpus-cloud/sdk
Korpus from code. Korpus is shared memory: bundles of markdown concepts that people, agents and programs read and write together. This package is the way in for a program — a CI job, a cron script, an app — as an agent.
An agent is you, narrowed: a name, a job, and the bundles it works in, each Read, Write or Custom (path patterns, each with create, update and delete chosen). It reaches no more than you do, and shrinks when you lose reach. Every write it makes is signed @you via <agent> in the bundle's log. The SDK calls the Korpus API, https://api.<host>/v1, with the agent's credential.
Set up
npm install @korpus-cloud/sdkIn Korpus, open Agents → New agent. Give it a name (e.g. nightly-sync) and the least its job needs: its bundles, each Read, Write or Custom paths. Then give it a way in, in this order of preference:
| Where it runs | How | Secret stored |
|---|---|---|
| GitHub Actions | The agent → Trust GitHub or AWS → your repository and environment. Give the job permissions: id-token: write and set KORPUS_AUDIENCE to the audience Korpus shows (https://api.<host>/<username>/<agent>). | none |
| AWS (Lambda, ECS, EC2) | The agent → Trust GitHub or AWS → your account and role ARNs; give the role the sts:GetWebIdentityToken policy Korpus shows. Set KORPUS_AUDIENCE, install @aws-sdk/client-sts. | none |
| A machine you sign in on | npx -p @korpus-cloud/sdk korpus login nightly-sync --user <username>, then allow the code in Korpus. | a refresh token in ~/.config/korpus/sign-ins.json |
| Anything else | The agent → Create key, kept as KORPUS_KEY. Works for anyone who has it: last resort. | the key |
new Korpus() finds whichever the environment holds, at its first call: KORPUS_KEY first, then a GitHub Actions job with KORPUS_AUDIENCE (traded at Korpus for a one-hour token, renewed as it ages), then a sign-in stored on this machine, then an AWS workload with KORPUS_AUDIENCE (traded the same way). With none of them the call throws no_credential. Or name one: new Korpus({ credentials: fromGitHubActions({ audience }) }), fromAwsWebIdentity, fromOAuth, fromStaticToken.
Node 20 or later, Bun or Deno. The library needs nothing else (AWS needs the optional @aws-sdk/client-sts); the korpus command reads Markdown frontmatter with yaml. Server side only: a key in a browser page is a key anyone can read.
Which Korpus
A key names its deployment: kp_fra_… is fra.korpus.cloud, kp_dev.fra_… is dev.fra.korpus.cloud. So a key needs no URL: it is sent to its deployment unless you give another URL. The SDK calls, in this order:
- the
baseUrloption, - else
KORPUS_URL, - else the deployment the key names,
- else the deployment
KORPUS_AUDIENCEnames, - else production,
https://api.fra.korpus.cloud.
Any URL of a deployment will do (the app's, the API's, an MCP address, or a bare host such as dev.fra.korpus.cloud, taken as https://): the tools are called at its API host. A platform token (GitHub, AWS) is traded only at the deployment its audience names, whatever baseUrl says.
The korpus command
npm install -g @korpus-cloud/sdk # or: npx -p @korpus-cloud/sdk korpus …
korpus login # you, in the browser
korpus login <agent> [--user <username>] [--url <url>] # an agent, for programs
korpus logout [<agent>]As you. korpus login alone opens Korpus in your browser; you allow Korpus CLI to use Korpus as you, and the browser comes back to the command on 127.0.0.1. Every other command then runs as you, so it can do what only you can: make bundles, share them through teams, and make agents with their keys and trusts. It never uses KORPUS_KEY, and a program's new Korpus() never picks this sign-in up (only new Korpus({ agent: 'me' }) would).
korpus bundle create notes --description "team notes"
korpus agent create nightly-sync --description "syncs the CRM" --bundle '<bundle id>=crm/**:cu' # read all, create + update under crm/
korpus key create nightly-sync --label "cron on box-3" --days 90 > key.txt # the key alone on stdout, shown once
korpus trust github nightly-sync --repo acme/site --environment prod
korpus bundle share <bundle id> @pia read # share one bundle with a person; outside your team they accept first
korpus connection list nightly-sync
korpus connection revoke nightly-sync <id>
korpus schema <bundle> # its collections and views; collection show <bundle> <name> for one
korpus call query '{"bundle":"<bundle>","from":"incidents"}' # any tool you may call, by nameConcepts go in and out as Markdown files, laid out like an export (<path>.md: YAML frontmatter, then the body):
korpus ls <bundle> notes -r
korpus read <bundle> notes/auth/token-expiry -o token-expiry.md
korpus update <bundle> token-expiry.md --path notes/auth/token-expiry -m "Raised expiry to 2 h"
korpus create <bundle> export/**/*.md --root export -m "Imported the export" # path = file under --root, minus .md
korpus delete <bundle> notes/old -m "Superseded"type, title, description, tags, relations, status, stale_after and date are the concept's fields; any other key is kept as written. read adds rev, the revision it read; update sends each file's rev back, so it fails with rev_mismatch if someone changed the concept since (--rev overrides it for one file). On update, the file's tags, relations and other keys replace the concept's. korpus help lists every command; each takes --url <deployment> and --json (the data instead of the text). The deployment is --url, else KORPUS_URL, else the one you signed in to, else production.
As an agent. korpus login <agent> signs this machine in as the agent by device code: it prints a short code and a link, you allow it in Korpus, and the sign-in is stored in ~/.config/korpus/sign-ins.json (owner only; $XDG_CONFIG_HOME if set), refreshed at the deployment's API as it ages. new Korpus() finds it; with several stored, name one with KORPUS_AGENT or the agent option. Every sign-in is listed on the Agents page, where you revoke it. korpus logout forgets it on this machine.
A sign-in made here serves the API, where the SDK calls. An MCP client such as Claude Code signs in at the agent's MCP address itself, or sends a key.
Use
import { Korpus } from '@korpus-cloud/sdk';
const korpus = new Korpus(); // reads KORPUS_KEY, a trusted workload or a stored sign-in
const notes = korpus.bundle('01JBQ3M7X9VK2NDCTF8ZPYW4RH');
// Find, read, change.
const entries = await notes.folder('runbooks');
const { concept, inbound } = await notes.read('runbooks/deploy');
await notes.update('runbooks/deploy', {
rev: concept.rev, // fails with rev_mismatch if someone changed it since
body: concept.body + '\n- Check the dashboard after the deploy.',
change_description: 'Added the post-deploy check',
});
// Many at once: each item answers on its own.
const items = await notes.createMany(
[
{ path: 'log/2026-10-22', type: 'Log', body: 'Nightly sync: 14 records.' },
{ path: 'log/2026-10-23', type: 'Log', body: 'Nightly sync: 9 records.' },
],
'Nightly sync results',
);
for (const item of items.items) if (item.status !== 'ok') console.error(item.path, item.error.message);
// Search, the log, and what this agent may do here.
const { results } = await notes.search({ regex: 'deploy', body: true });
const log = await notes.log({ since: '2026-10-01T00:00:00Z' });
const rights = await notes.rights('runbooks/deploy');Every method is one tool of the Korpus API; korpus.call(name, args) reaches any of them by name, typed, with the text a model would have read beside the data:
const { data, text } = await korpus.call('check_relations', { bundle: notes.id });| Method | Tool |
|---|---|
| korpus.listBundles() | list_bundles as an agent: its own bundles, each with what it holds there, no counts |
| korpus.listMyBundles() | list_bundles signed in as you: every bundle you reach, with your access and its counts |
| korpus.usage() | get_usage (signed in as you only; an agent is refused it) |
| korpus.self() | list_agents |
| korpus.activity() | list_activity |
| korpus.describe() | what this credential may call, and whose it is (GET /v1: {api, agent, tools}; agent is null for you) |
| korpus.wake() / awaitReady() | wake (public, no credential sent): {ready}; awaitReady asks until ready, within wakeWait |
| bundle.folder(folder?, {recursive?}) | list_folder |
| bundle.index(folder?) | read_index |
| bundle.log({path?, since?, cursor?}) / logPage(…) | read_log |
| bundle.search(query) | search_concepts |
| bundle.read(path) / readMany(paths) | read_concept / read_concepts |
| bundle.create(path, input) / createMany(concepts) | create_concept / create_concepts |
| bundle.update(path, input) / updateMany(updates) | update_concept / update_concepts |
| bundle.delete(path, why) / deleteMany(paths, why) | delete_concept / delete_concepts |
| bundle.move(from, to, opts) / moveMany(moves, opts) | move_concept / move_concepts |
| bundle.relate(from, type, to, why) / unrelate(…) | add_relation / remove_relation |
| bundle.relationTypes() / checkRelations() | list_relation_types / check_relations |
| bundle.schema() / collection(name) | get_schema / get_collection |
| bundle.query({view?} \| {from?, type?, where?, sort?, group_by?, aggregates?, fields?, limit?, cursor?}) | query |
| bundle.defineCollection(input) / updateCollection(name, update) / deleteCollection(name) | define_collection / update_collection / delete_collection (as me only) |
| bundle.defineView(name, input) / updateView(name, update) / deleteView(name) | define_view / update_view / delete_view (as me only) |
| bundle.rights(path?, {agent?}) / activity() | get_rights / list_activity |
| bundle.readAsset(path, {inline?}) / createAsset / updateAsset / deleteAsset(path, why) | read_asset / create_asset / update_asset / delete_asset |
| bundle.uploadAsset(path, bytes, input) | create_asset (or update_asset with rev), then the PUT, then read_asset |
| bundle.downloadAsset(path) | read_asset, then the GET: {asset, bytes} |
A file (an asset) is kept beside the concept that explains it, at one path; body says what is in it, so the next reader need not open the file. uploadAsset sends the bytes straight to storage, never through Korpus:
const pdf = await readFile('q3-board-pack.pdf');
await notes.uploadAsset('reports/q3-board-pack', pdf, {
filename: 'q3-board-pack.pdf',
content_type: 'application/pdf',
body: 'Q3 board pack. Revenue 1.2 M (+8 %), churn 2.1 %. Hiring freeze until Q1.',
change_description: 'Filed the Q3 board pack',
});
const { bytes } = await notes.downloadAsset('reports/q3-board-pack');A collection is a folder of items sharing one shape: a name (incidents), an item type (Incident), fields, required sections and the key each path is made from. The owner defines and changes it (defineCollection, updateCollection, as me), and every write of an item is checked against it (item_mismatch, wrong_path, wrong_type, not_allowed_here). A view is a saved query of one collection, shown as a table, list, board or tree. An agent reads both and queries the items; a type no collection declares is queried by its values as stored (type):
const { collections, views } = await notes.schema(); // each collection with its item count
const open = await notes.query({
from: ['incidents'],
where: [{ field: 'severity', op: 'in', value: ['high', 'critical'] }],
sort: [{ field: 'opened', dir: 'desc' }],
limit: 20,
});
if (open.mode === 'rows') for (const row of open.rows) console.log(row.path, row.fields);
const board = await notes.query({ view: 'open-incidents' });group_by and aggregates answer groups or totals instead of rows; mode says which (rows, groups, totals).
Every change takes a change_description: one line, what changed and why. It is the log; nothing else records why a concept moved.
Errors and limits
A refusal throws KorpusError with the server's code (restricted, not_found, conflict, rev_mismatch, rate_limited, …), the HTTP status and a message that says what to do. A key of another deployment is refused with 401; give the URL it belongs to, or let the key name it.
Writes and reads each have a short-term rate, the same as every other way into Korpus, there only to stop a runaway loop. Over one, the call is refused with rate_limited and retryAfter in seconds. new Korpus({ retries: 2 }) waits that long and sends it again (up to maxRetryWait, 60 s by default). Space is two quotas per team, concept space and file space: korpus.usage(), signed in as you, says how full each is, as a share used, and is never refused; an agent sees no usage and meets a full space as quota_exceeded.
The database pauses after a while idle and takes about 20 s to start again. A call that meets it starting is refused with database_starting (HTTP 503, Retry-After) before anything ran, so the client waits retryAfter and sends the same call again, by itself, for up to wakeWait seconds all told (30 by default; 0 throws at once). An upgrade's short gate (maintenance) is waited out alike. To warm it up ahead of a burst, await korpus.awaitReady().
Options
new Korpus({
token: 'kp_fra_…', // a key; default: found in the environment (KORPUS_KEY)
credentials: fromGitHubActions({ audience }), // or fromAwsWebIdentity, fromOAuth, fromStaticToken
agent: 'nightly-sync', // which stored sign-in, when there are several (KORPUS_AGENT)
baseUrl: 'https://api.fra.korpus.cloud', // any URL of the deployment; default: KORPUS_URL, else the key's deployment, else KORPUS_AUDIENCE's, else this
retries: 0, // resends on rate_limited
maxRetryWait: 60, // seconds
wakeWait: 30, // seconds to wait, all told, while the database starts (database_starting)
fetch, // your own fetch: a proxy, a test double
userAgent: 'nightly-sync/1.2', // added to korpus-sdk/<version>
});| Variable | What |
|---|---|
| KORPUS_KEY | An agent's key, sent as the bearer. Names its deployment. |
| KORPUS_AUDIENCE | A trusted workload's audience, https://api.<host>/<username>/<agent>. |
| KORPUS_AGENT | Which stored sign-in, when this machine holds several. |
| KORPUS_URL | Which Korpus, over what the key or the audience names. |
What an agent cannot do
It is held to its bundles and their access, and never reaches above you. Creating bundles, teams and sharing, defining collections and views and managing agents stay with you, in the web app, as me over MCP, or with the korpus command.
More
Every method, the error codes, the limits and the API underneath: korpus.cloud/docs/api.
License
MIT
