@mnemahq/sdk
v0.5.0
Published
Typed client for the Mnema API — docs, tasks, projects, sessions, repos, search.
Readme
@mnemahq/sdk
Typed client for the Mnema API — docs, tasks, projects, sessions, repos, search, the knowledge graph and the findings engine.
npm install @mnemahq/sdkQuickstart
import { Mnema } from '@mnemahq/sdk';
const mnema = new Mnema({ apiKey: process.env.MNEMA_API_KEY });
const me = await mnema.me();
console.log(`workspace ${me.workspaceName} — ${me.credential} credential, scopes: ${me.scopes}`);
for await (const task of mnema.tasks.list({ status: 'in_progress' })) {
console.log(task.publicId, task.title);
}Get a key from Settings → Access in the app, or run mnema login if you have the CLI — the SDK accepts either credential.
All calls go to the versioned public API (/api/public/v1), which is scope-enforced and rate-limited at 60 req/min per credential.
Every
tsblock in this file is extracted and type-checked against the built package in CI, and the quickstart is executed end to end against a mock server. If an example here does not work, that is a bug in the SDK, not in the README. Seescripts/check-readme.mjs.
Ask before you call
me() answers three separate questions, and merging them is how integrations go wrong:
const me = await mnema.me();
me.scopes; // what this CREDENTIAL may do — narrow the key, this shrinks
me.features; // what this PLAN includes — upgrade, this grows
me.edition; // what this BUILD contains — neither; it is the binary you runA refused call on a self-hosted core build is not a billing problem and cannot be fixed by upgrading. A refused call on cloud free is not a scope problem and cannot be fixed by minting a broader key. Checking here is cheaper and more honest than calling and interpreting the failure.
Pagination is an async iterator
list() returns something you can iterate directly — it fetches pages as you consume them and stops the moment you stop.
for await (const doc of mnema.docs.list()) { void doc; } // every doc, page by page
const first10 = await mnema.tasks.list({ limit: 10 }).all(); // or collect
for await (const page of mnema.sessions.list().pages()) { void page; } // or batch at a timeBreaking out of the loop stops fetching. No cursor bookkeeping.
The knowledge graph
Gated: needs the graph feature and a build that contains it.
const answer = await mnema.graph.ask('why did we move off the flat exports map?');
if (answer.confidence < 0.4) {
console.log(`low confidence (${answer.confidenceReason}) — treat as a lead, not a fact`);
}
console.log(answer.answer, answer.sources);Read confidence before you render answer. The interpreter hedges rather than asserts below its threshold; a UI that prints the sentence and drops the number turns a hedge into a claim. confidenceReason says which kind of thin it is: no_data, stale, thin or contested.
const path = await mnema.graph.traverse('Billing', 'Onboarding');
console.log(path.connected ? `${path.hopCount} hops` : 'no route between them');
const critical = await mnema.graph.godNodes({ limit: 10 }).all();connected: false with an empty path is a real answer, not an error. Traversal ignores edge direction — "how are these connected" is a reachability question.
Findings
Ungated: the findings engine ships in every build. What varies is how much it can see.
const briefing = await mnema.findings.briefing();
if (briefing.coverage.degraded) {
console.log(briefing.coverage.notice); // a short list is not necessarily good news
}
if (briefing.neverComputed) {
console.log('the engine has never run here');
}A short list has three quite different causes and only one is good news:
| signal | meaning |
| --- | --- |
| coverage.degraded | this build cannot produce most finding types — six of seven need the knowledge graph |
| neverComputed | the engine has never run for this workspace |
| neither | genuinely quiet |
Repeated findings arrive pre-collapsed, so branch on grouped:
import { isFindingGroup } from '@mnemahq/sdk';
for (const item of (await mnema.findings.briefing()).findings) {
if (isFindingGroup(item)) console.log(`${item.count}x ${item.headline}`);
else console.log(item.headline);
}Errors tell you which wall you hit
Every failure is typed, so you never have to parse a message to find out whether you have a bug, a quota, a paywall, or a build that simply lacks the feature.
import { PlanRequiredError, FeatureUnavailableError, InsufficientCreditError, RateLimitError, AuthError } from '@mnemahq/sdk';
try {
await mnema.graph.ask('what changed in billing last week?');
} catch (err) {
if (err instanceof FeatureUnavailableError) {
console.log(`${err.feature} is not in this ${err.edition} build — nothing to buy here`);
} else if (err instanceof PlanRequiredError) {
console.log(`${err.feature} needs the ${err.required} plan — ${err.upgradeUrl}`);
} else if (err instanceof InsufficientCreditError) {
console.log(`top up ${err.topUpNeededCents}c — balance ${err.balanceCents}c`);
} else if (err instanceof RateLimitError) {
console.log(`slow down for ${err.retryAfterMs}ms`);
} else if (err instanceof AuthError) {
console.log(err.fix); // "Run `mnema login` to authenticate."
}
}| error | when |
| --- | --- |
| AuthError | no credentials, expired, or revoked — carries .fix |
| PlanRequiredError | your plan does not include it — .feature .plan .required .upgradeUrl |
| FeatureUnavailableError | this build does not contain it — .feature .edition |
| InsufficientCreditError | you have the feature, the prepaid balance ran out — .balanceCents .topUpNeededCents .topUpPacks |
| RateLimitError | 429 — .retryAfterMs .limit .remaining .resetAt |
| ValidationError | 400/422 — .fields |
| ApiError | any other non-2xx — .status |
| NetworkError | never reached the server |
PlanRequiredError and FeatureUnavailableError are deliberately different. You cannot buy your way out of the second: a self-hosted core build has no graph engine, and telling that user to upgrade points them at a plan that does not exist on their own server. Likewise InsufficientCreditError is not a plan problem — the caller is already paying and needs to top up, and "upgrade your plan" would send them somewhere that cannot help.
A gated call always throws. It never returns an empty array — you should never be able to build a dashboard on a paywall and watch it render zeros.
And so does a shape mismatch. If the API returns something this client does not recognise, you get an ApiError naming the keys it actually found — not an empty list. An earlier build read the wrong envelope key and returned [] against 593 real tasks, silently. That cannot happen now.
Rate limits, before you hit them
Limits are read from every response, not only rejections:
await mnema.tasks.list().all();
console.log(mnema.rateLimit); // { limit: 60, remaining: 41, resetAt: Date }
const paced = new Mnema({
apiKey: process.env.MNEMA_API_KEY,
onRateLimit: ({ remaining }) => { if ((remaining ?? 60) < 10) console.warn('slowing down'); },
});
void paced;The numbers describe the 60/min per-credential limit that governs /api/public/v1. 429s are retried automatically, honouring Retry-After.
Retries
Automatic on 5xx, 429 and network failures, with full-jitter exponential backoff.
Only for requests that are safe to repeat — GET and HEAD. A failed POST is never retried, because a duplicate task is worse than a visible error. The exception is 429: the request was refused, not attempted, so repeating it cannot duplicate anything.
Runtimes
Node 20+, Bun, Deno, and edge runtimes. ESM and CJS. No heavy dependencies, and fetch is pluggable:
const instrumented = new Mnema({ fetch: globalThis.fetch, timeoutMs: 10_000 });
void instrumented;License
Apache-2.0
