Developersv1.0.0

SDK & CLI

madless is the official TypeScript client: every operation on this reference, typed both ways and generated from the same document you are reading, plus hand-written helpers for the four flows a script actually touches — keeping, asking, paging, and getting bytes in. It ships a two-command CLI in the same package.

Install

Not on the registry yet — the client publishes when the v1 surface freezes, and the name is held until then. Everything below is what it will do on the day it does; until then this whole API is a fetch away.

Three lines to keep

typescript
import { Madless } from 'madless';

const client = new Madless({ apiKey: process.env.MADLESS_API_KEY });

await client.keep.text('Retrieval, not review, is what strengthens memory.');

One request, one concept. keep.link, keep.image, keep.audio, keep.video, keep.document and keep.compose are the same door by the other kinds, and keep.file takes the bytes themselves — it uploads them, keeps them as the kind their mime names and waits, in one call. The door answers 202 with an intake receipt the moment the material is staged; the helper waits on that receipt and resolves with it admitted, carrying concept_id — or throws the refusal. Every keep carries an idempotency key you never have to think about, so a dropped socket cannot become two copies of the same thing.

Three lines to ask

typescript
const answer = await client.ask('what have I been circling on memory?');

console.log(answer.text);
if (!answer.abstained) console.log(`${answer.citations.length} sources, checked.`);

One question, one checked answer. Claims the evidence does not entail are removed whole, and if that empties the answer it abstains — which is a real answer and not an error, so it comes back as an ordinary result with abstained: true, an abstain_reason saying why, and a text that is the server’s own sentence in your language. When it found something but could not stand on it, the nearest things ride on related.

The stream is there when you want to watch it work. Status frames and the evidence as it is gathered, then one reveal — never token chunks.

typescript
for await (const event of client.ask.stream('what have I been circling on memory?')) {
  if (event.kind === 'status') console.log(event.data.phase);   // searching / reading / checking
  if (event.kind === 'evidence') console.log(event.data.items);  // the ledger, as it forms
  if (event.kind === 'answer') console.log(event.data.text);     // the one checked reveal
}

Everything else

The helpers are the flows worth hand-writing. Every operation is on client.api, generated from the published spec — so it is complete the day an endpoint ships, and a release fails if the two disagree.

typescript
const { data, error } = await client.api.GET('/api/v1/concepts', {
  params: { query: { q: 'pricing', limit: 20 } },
});

await client.api.POST('/api/v1/relations', {
  body: { source_id: 'con_a', target_id: 'con_b', caption: 'why this matters' },
});

client.api answers { data, error } and never throws for you. The named helpers throw a typed error instead, because that is what a helper is for. Everything is snake_case, exactly as the wire is: a body you build with this client is a body you can paste into curl.

When something does not happen

typescript
import { RefusalError } from 'madless';

try {
  await client.keep.link('https://example.com/an-essay');
} catch (error) {
  if (error instanceof RefusalError) {
    console.log(error.code);  // link_unreadable
    console.log(error.detail);  // the sentence Madless says, safe to show as-is
  }
}

A refusal is the promise working: nothing enters until Madless has read it, and when it cannot, no concept is created and the temporary bytes go. The other designed outcomes have their own classes — RateLimitError (carrying retryAfter), AllowanceError (a spent meter, or a plan holding everything it may), ValidationError (a pointer per field). All of them carry requestId. The full catalogue is on the Errors page.

Rate limits are absorbed for you. A key runs at half the session rates, so a script will meet them: reads, every operation that publishes an Idempotency-Key, and every 429 are retried — and a write the API cannot make safe to repeat is never sent twice.

From a terminal

Two commands, and deliberately no more.

bash
export MADLESS_API_KEY=mad_sk_…

madless keep "retrieval, not review, strengthens memory"
git log --oneline -20 | madless keep
madless keep --link https://example.com/an-essay
madless keep --file notes.pdf

madless ask "what have I been circling on attention?"

madless keep prints the concept id and nothing else, so it pipes. madless ask prints the answer on stdout and the phases on stderr, so madless ask "…" > answer.txt writes an answer rather than a progress log. Exit codes: 0 kept or answered — an abstention is an answer — 1 something went wrong, 2 refused.

Your key, and where it calls

typescript
new Madless({
  apiKey: 'mad_sk_…',   // or MADLESS_API_KEY
  space: 'your-space',  // optional — or MADLESS_SPACE
});

space is a convenience. Without it the client calls https://madless.com and your key names its own space, because a key resolves its space before any host is consulted. Name a space that is not the key’s own and the call throws a MisdirectedRequestError (421) whose baseUrl is the address the key belongs to. Keys are bound to the environment they were minted in — see Authentication.