MCP tool reference
The server lives at https://pando.ink/mcp and speaks streamable HTTP. Clients authenticate with OAuth 2.1 using dynamic client registration: point the client at the endpoint and the handshake takes care of the rest. A key from Agents and API keys in the app works too, sent as a bearer token. Protocol versions spoken, newest first: 2025-06-18, 2025-03-26, 2024-11-05. The server names itself pando, version 2.0.0.
15 tools, each one call onto a /api/v1 door described in the REST reference, so permission is decided in one place however a request arrives. The descriptions below are the exact text a connected model reads. The connect page is the two-minute setup.
remember recall share_memory unshare_memory shared_with_me shared_by_me tree search create update move mirror delete changes whoami
remember
Write something into your own memory so you still have it in a later session, or change a memory you already wrote. Use it for facts about the user, decisions and their reasons, and anything you would otherwise re-derive. WITHOUT id it CREATES a new memory under your memory root and returns its id, which you can nest further memories under; it never overwrites and never merges. WITH id it CHANGES that one and creates nothing. appendNote, addTags and removeTags each change one part and leave every other byte alone, so use them rather than reading a note back and resending it — that is where a line gets lost. Adding a line to a long note is 83 bytes in one call against 13,018 bytes in two for reading it back and sending the whole thing. Sending text or note WHOLE over something already there answers with `replaced`, which explains itself; the add-only parameters never destroy and never carry it. AND THE ANSWER DOES NOT CARRY A NOTE YOU DID NOT SEND: a call that did not send `note` gets `noteChars` instead, 15 bytes against however long the note is — measured, one append answered 83 bytes up and 1,900 down before this. layout shapes the memory in the SAME call that writes it — a checklist, a heading, a table — rather than a remember followed by an update. A create may answer with `similar` — older memories that say close to the same thing — and that answer explains what to do about it. Your memory is not automatically private: the user can share a branch, and tree marks what is shared. Check before writing anything you would not want read. A protected bullet usually refuses this, and so does everything under it; `protectedWords: false` in its answer means the words may still be written. tree marks them, so read before you plan, and the refusal explains the rest.
| Parameter | Type | Required | What it does |
|---|---|---|---|
id | string | Change this memory instead of writing a new one. Leave it out and a new memory is created every time, even if you have said the same thing before. | |
text | string | The memory, in one line. Put detail in note. With id this REPLACES the whole line, so leave it out when you only meant to change the tags. | |
note | string | Longer detail, kept under the line. REPLACES the whole note; use appendNote to add to it. A note is stored and returned WHOLE at every read — nothing shortens one, however long it is — so you never have to wonder whether what you read back was all of it. | |
appendNote | string | Add this to the END of the note, joined to what is there with exactly one newline. What is already in the note is not resent, so it cannot be dropped. On a new memory it is simply the note. | |
prependNote | string | The same, at the START of the note. Use it when the newest line should be read first. | |
tags | array of string | Tags for a NEW memory, without the #. Each is lowercased and appended to the end of text as #name; one already written in text is not added twice. On an existing memory this is refused — use addTags. | |
addTags | array of string | Tags to add, without the #. Appended to the end of text as #name, in order, skipping any already on the line. The rest of the line is untouched. | |
removeTags | array of string | Tags to take off the line, leaving every other byte of it as it was. Write @name to remove an @mention rather than a #tag. Needs id. | |
parent | string | Id to nest under. Defaults to your memory root, which is the memoryRoot whoami returns. | |
layout | string | One of: bullet, todo, numbered, h1, h2, h3, quote, code, board, table, divider. How the bullet is drawn. Most shape one line: todo a checkbox, numbered an ordinal, h1/h2/h3 a heading, quote, code, divider. TWO ARE ARRANGEMENTS OF THE BULLETS UNDER IT, and are how you build a grid without leaving the outline: table draws the children as rows and THEIR children as cells, first row naming the columns; board draws the children as columns and their children as cards. It is one prop on one bullet and nothing moves in the graph, so set it back to bullet and the same content is an ordinary branch. A table is three calls: the bullet with layout table, then all the rows, then all the cells. |
recall
Search your own memory root and every branch other agents or people have shared with you — the same roots search reaches with no focus. Leave q empty to list recent memories. The answer echoes `scopes`, the roots it really walked, so a 0 says WHERE it found nothing; each hit carries `scope` (which of those roots), `mine` (is that root yours) and `path` (ancestors, root first, ending at the parent of THIS copy, so one entry means it sits directly under the root). A FIELD WITH NOTHING TO SAY IS NOT SENT: no `note` means no note, no `done` means not done, no `protected` means not protected. A ROW CARRIES NO LINK. The answer carries `origin` once and a link to any row is `origin` followed by its `id` — the url used to be on every row and was 26.7% of a full tree read, a constant prefix plus an id the row already had. A one-bullet answer still carries `url`. SEND scope:"mine" WHEN YOU MEAN YOUR OWN MEMORY, which is most of the time. Measured on a real account: a bare recall for one word answered 42 hits of which 41 were the user's outline, and the first page was 3,700 bytes to reach the one memory that was mine. It is also faster, because the user's account is not loaded at all. scope:"shared" is the other half, "all" the default and the old behaviour. AND mode:"outline" is 70% smaller — use it to SURVEY what you remember and then recall the one memory in full, because a memory's note comes back whole every time. A bare word matches any part of a word, ignoring case and ignoring accents, in a bullet's text or its note: port finds Sport, and bucher finds Bücher. It is a SUBSTRING and nothing more: no stemming and no translation, so knee will not find Knie and plural will not find a singular. If a word could be in the outline in another language, search the word that is IN the outline, or search a proper noun, which survives both. "Quoted words" the same, kept together. -word excludes; #tag also matches its sub-tags; OR in capitals; `a > b` finds b under an ancestor matching a. Also is:complete|incomplete|shared|mirrored|backlinks, has:note|file|child|date|link|tag, in:text|note, layout:<name>, text:bold|code|highlight. For what changed lately, in whole days: changed:today, changed:this week, changed:2026-08-01 (that day or after), changed:>2026-08-01, changed:<2026-08-01; date-after: and date-before: read the dates written inside a bullet instead. An operator this server cannot read refuses the whole query and names it, so a count of 0 is always a real 0.
| Parameter | Type | Required | What it does |
|---|---|---|---|
q | string | What to look for. The operators are in this tool's description. | |
scope | string | One of: mine, shared, all. Which roots to walk. mine: your own memory only — ask for this when you mean what YOU remember. shared: only what others shared with you. all (default): both, which is what a recall cost before this existed. | |
mode | string | One of: full, outline. full (default): the whole row, note included. outline: id, text, children, flags, layout, scope, mine, path — 37% smaller here, because a hit keeps its path either way, for surveying many memories. | |
limit | number | Maximum results per page, default 20. | |
cursor | string | Send back the `cursor` from a previous answer for the next page. `count` is how many exist, `returned` is how many you were given. |
share_memory
Give another agent or person access to one branch of your memory, by handle. The grant reaches that bullet and everything under it and nothing beside it, so share the branch you mean rather than your whole memory. Use view unless they need to write.
| Parameter | Type | Required | What it does |
|---|---|---|---|
node | string | yes | The bullet id at the top of the branch to share. |
to | string | yes | Their handle, for example researcher or @researcher. |
level | string | One of: view, edit. Default view. |
unshare_memory
Take back access you granted. Takes effect immediately, including for what they already reached.
| Parameter | Type | Required | What it does |
|---|---|---|---|
node | string | yes | |
to | string | yes | Their handle. |
shared_with_me
List the branches other agents and people have shared with you, and who shared each. Everything here is already searchable through recall.
No parameters.
shared_by_me
List what you have shared out and with whom, so you can check before sharing more.
No parameters.
tree
Read an outline branch as nested items. With no focus this reads your own memory root; use it on a shared branch id to read what somebody handed you. DEPTH is how many levels BELOW the focus you get, and you never get one more: depth=1 gives the focus's own children and nothing else, depth=3 - the default - gives three levels (children, grandchildren, great-grandchildren), depth=0 gives no items at all and is the cheapest way to ask whether a bullet is there and how many children it has. Max 12. The answer carries `levels`, how deep the WALK went. Like `count` it describes the whole branch and not this page, so on a continuation page it can read deeper than the bullets in front of you; it is the same number on every page. `levels` below `depth` means the branch ran out before your budget did, so nothing is deeper and asking again buys the same bullets twice. SIZE. A bullet costs about 175 bytes in full mode and about 72 in outline mode (measured on 125 bullets at depth 3: 21,920 against 8,942 bytes, 59% smaller). PAGING IS NOT A SAVING, because every page re-pays for the focus and the trail above items, so the overhead is per PAGE and grows with how many you take: 2.9% more bytes on a 260-bullet branch in two pages at the default cap, and 27.9% more with three extra round trips on a real 81-child branch bounded far smaller. Use it to bound ONE answer you could not otherwise hold; read a whole branch in one call when it fits. maxNodes caps the answer at 200 bullets by default and 2000 at most, so a default outline read is at most about 14KB and a full one about 34KB. You are in outline mode unless you ask; read the one branch you want in full once you have found it. WHAT CAME BACK. `count` is how many bullets the walk found, `returned` is how many are in this answer, `cursor` is where the next page starts or null at the end. YOU HAVE IT ALL WHEN count EQUALS returned AND cursor IS null - `truncated` is NOT that test: it is true whenever ANYTHING was left out for ANY reason, including bullets deeper than you asked for, so a read that handed you every child of a wide branch still says true. WHEN SOMETHING WAS LEFT OUT THE ANSWER EXPLAINS ITSELF: it carries `reading`, which says what `dropped`, `cut`, `placements` and `textChanged` mean, only when they are in it. A ROW CARRIES NO LINK. The answer carries `origin` once and a link to any row is `origin` followed by its `id` — the url used to be on every row and was 26.7% of a full tree read, a constant prefix plus an id the row already had. A one-bullet answer still carries `url`. OUTLINE MODE returns id, text and children, dropping note, mirrored, protectedBy, sharedBy and the done date. It KEEPS `layout` on any bullet that is not a plain one, so a table reads as a table without a full read. It keeps a short `flags` string on the bullets that need one - p protected, s shared, m mirrored, d done, n has a note - so you can still see what would refuse a MOVE or a DELETE before you plan one. Whether the WORDS are writable needs a full read, where protectedWords answers it. The NAMES behind p and s are not there: read that bullet in full mode when you need to know who. WHERE THE COPIES ARE. Focus a mirrored bullet and the answer carries `placements`, one entry per copy, and explains itself: 171 bytes when it is there and nothing when it is not, so its absence is a real claim that there is exactly one of it. SKIPPING. exclude names bullet ids to leave out of this call, with everything under them. A branch whose text or note carries the tag #noindex is left out of every call unless includeNoindex is true - that is for a date scaffold or anything else the user keeps and nobody needs to read. Both are counted in `dropped.excluded` rather than passed over quietly. WHEN. Full mode carries `updated` and, when the words moved at a different instant, `textChanged`; the answer explains which to order by. `updated` costs 37 bytes a bullet, 21% of one in full mode, and outline mode leaves both out. Navigate in outline, lint in full — and depth 0 answers in full by default, because a depth-0 read means "read me this one bullet", note included. WHICH COPY. `mirrored: true` says this bullet is in several places and there is one of it, so an edit reaches all of them. `edgeId` is the PLACEMENT you were handed, and move and delete take it as `edge`; both say what happens without it. It costs 38 bytes a bullet, 22% of one in full mode, and outline mode drops it for the m flag — so read in full mode BEFORE you move or delete.
| Parameter | Type | Required | What it does |
|---|---|---|---|
focus | string | Bullet id to read from. Defaults to your memory root. | |
depth | number | How many levels below the focus. 1 gives its children, 3 (default) gives three levels, 0 gives none. Max 12. | |
mode | string | One of: full, outline, markdown. DEFAULT OUTLINE: id, text, children, layout, flags — use it to FIND, because a wide full read runs out of page before it reaches what you were looking for. full: note text, timestamps, and WHO a bullet is shared with — use it on the ONE branch you have already found. markdown: the branch as a document, by the same writer the Export door uses — GFM tables, fenced code, notes as paragraphs, checkboxes, headings, numbered runs. Read that way to SUMMARISE or QUOTE, because a table in JSON is a bullet with rows for children and cells under those, which is true and is not what a table looks like. It carries no ids, so read outline first if you intend to write. | |
maxNodes | number | Hard cap on bullets in this answer. Default 200, max 2000. | |
cursor | string | Send back the `cursor` from a previous answer for the next page. | |
exclude | array of string | Bullet ids to leave out, with everything under them. | |
includeNoindex | boolean | Read #noindex branches too. Default false. | |
done | boolean | Include completed bullets. Default true. |
search
Search every outline you can reach. With no focus that is your own memory root AND every branch anybody shared with you — the user's notes included, if they granted you access — the same reach recall has; give focus to search one branch and nothing else. The answer echoes `scopes`, the roots it really walked, so a 0 says WHERE it found nothing; each hit carries `scope` (which of those roots), `mine` (is that root yours) and `path` (ancestors, root first, ending at the parent). Hits answer in full by default — note, `done`, `updated`, `textChanged` included — because a hit is usually the end of the hunt. A FIELD WITH NOTHING TO SAY IS NOT SENT, so read the ABSENCE as the answer: no `note` means no note, no `done` means not done, no `protected` means not protected. Nine such fields were 146 bytes on a measured hit. A ROW CARRIES NO LINK. The answer carries `origin` once and a link to any row is `origin` followed by its `id` — the url used to be on every row and was 26.7% of a full tree read, a constant prefix plus an id the row already had. A one-bullet answer still carries `url`. For survey queries with hundreds of hits, send mode:"outline", which is 37% smaller here rather than the 59% a tree read saves, because a hit keeps its path either way: each hit keeps `id`, `text`, `children`, `flags`, `layout`, `scope`, `mine` and `path` and drops the rest. PATH SAYS WHERE A HIT IS, AND OFTEN WILL NOT TELL TWO OF THEM APART: bullets with the same words are usually SIBLINGS, and siblings have the same path. Measured on a real outline, 4 of 6 near-twin pairs were identical on `path` alone. What separates them is already on the row: `children`, `updated`, and the exact `text`, which can differ only by spacing you will not see rendered. When they still tie, they are two bullets and `edgeId` is the only honest answer. A bullet in more than one place comes back ONCE PER PLACE: same id, different edgeId, and its OWN `path` on each row, so the three rows of one mirrored bullet name the three lists it is in. So `count` counts PLACES, not bullets: three rows with one id is one bullet in three places, and deleting it in one leaves the other two. It counts the places INSIDE `scopes`: a row saying `mirrored: true` where you can see fewer copies than that may have others in branches you did not search. tree with that bullet as focus lists them. A bare word matches any part of a word, ignoring case and ignoring accents, in a bullet's text or its note: port finds Sport, and bucher finds Bücher. It is a SUBSTRING and nothing more: no stemming and no translation, so knee will not find Knie and plural will not find a singular. If a word could be in the outline in another language, search the word that is IN the outline, or search a proper noun, which survives both. "Quoted words" the same, kept together. -word excludes; #tag also matches its sub-tags; OR in capitals; `a > b` finds b under an ancestor matching a. Also is:complete|incomplete|shared|mirrored|backlinks, has:note|file|child|date|link|tag, in:text|note, layout:<name>, text:bold|code|highlight. For what changed lately, in whole days: changed:today, changed:this week, changed:2026-08-01 (that day or after), changed:>2026-08-01, changed:<2026-08-01; date-after: and date-before: read the dates written inside a bullet instead. An operator this server cannot read refuses the whole query and names it, so a count of 0 is always a real 0.
| Parameter | Type | Required | What it does |
|---|---|---|---|
q | string | yes | What to look for. The operators are in this tool's description. |
focus | string | Search inside this bullet only. Leave it out to search every root you can reach. | |
limit | number | Maximum results per page, default 50. | |
mode | string | One of: full, outline. full (default): every hit carries the whole row, note and stamps included. outline: survey shape — id, text, children, flags, layout, scope, mine, path — 37% smaller, for queries with hundreds of hits. | |
cursor | string | Send back the `cursor` from a previous answer for the next page. `count` is how many exist, `returned` is how many you were given. |
create
Add bullets under a parent. This is how you write into the user's notes rather than your memory. ONE: send parent and text. MANY: send parent and items: [{text}, {text}, …], at most 200, which land in the order given. An item may carry its own items to build a whole subtree in one call; the cap counts every level. A batch is checked as a whole first: if any item is refused there, NOTHING is written and results says why for each one. ONE EXCEPTION, and it is the only way applied can come back partial: a bullet limit is counted at the moment of writing, after those checks, so a batch that crosses the limit part way writes the items that fit. applied says which happened and written says how many landed — read them rather than assuming. The answer is {applied: "all" | "partial" | "none", written, failed, results: [{i, id, ok}]} — read applied, never assume. What was refused is totalled as refused: {code: count}, biggest first, and the answer says what each code means. A batch answers with ids only. terse: false echoes every node instead, which is not a small ask (measured: 11 items, 2,313 bytes against 648 bytes) — read the branch back if you need it. A protected bullet usually refuses this, and so does everything under it; `protectedWords: false` in its answer means the words may still be written. tree marks them, so read before you plan, and the refusal explains the rest. tree marks those with protected, so read before you plan rather than finding out mid-way.
| Parameter | Type | Required | What it does |
|---|---|---|---|
parent | string | yes | Parent bullet id. Used for every item that does not name its own. |
text | string | The bullet, for the one-bullet form. | |
note | string | ||
layout | string | One of: bullet, todo, numbered, h1, h2, h3, quote, code, board, table, divider. How the bullet is drawn. Most shape one line: todo a checkbox, numbered an ordinal, h1/h2/h3 a heading, quote, code, divider. TWO ARE ARRANGEMENTS OF THE BULLETS UNDER IT, and are how you build a grid without leaving the outline: table draws the children as rows and THEIR children as cells, first row naming the columns; board draws the children as columns and their children as cards. It is one prop on one bullet and nothing moves in the graph, so set it back to bullet and the same content is an ordinary branch. A table is three calls: the bullet with layout table, then all the rows, then all the cells. | |
items | array of objects | Many bullets in one call, in order. Each may carry its own parent, note and layout. | |
terse | boolean | Default true for items, false for one bullet. False echoes the whole node back. | |
dryRun | boolean | Ask whether this write would be allowed, and write nothing. The answer carries refused: {code, message} if it would not, and that is the SAME refusal the real call gives - it runs the kernel checks and the free-tier cap rather than guessing at them. One-bullet form only. |
Each item of items takes:
| Field | Type | Required | What it does |
|---|---|---|---|
text | string | yes | |
note | string | ||
layout | string | One of: bullet, todo, numbered, h1, h2, h3, quote, code, board, table, divider. | |
parent | string |
update
Change bullets: text, note, kind, or whether they are done. ONE: send id. MANY: send items: [{id, …}, …], at most 200, checked as a whole — if any item is refused there NOTHING is written, and results says why for each. A bullet limit is the one refusal counted later, so it can write part of a batch. Read applied ("all" | "partial" | "none") and written. refused: {code: count} totals them, biggest first, and the answer says what each code means. DO NOT RETYPE A LINE TO CHANGE PART OF IT. text replaces the whole thing, and bullets here carry leading and trailing spaces the user meant to keep. Instead use exactly one of: prependText / appendText, which need no prior read and cannot touch the rest of the line; or replace: {old, new}, which is REFUSED unless old occurs exactly once, so it can never fire twice or silently do nothing. ifText: the exact text you expect to find, byte for byte. The write is refused with conflict if the user changed it while you were thinking. There is no ifSeq: seq here counts the whole outline, not one bullet, so it moves when anything anywhere changes. A write that sends text, note, layout or done WHOLE over something already there answers with `replaced`, which explains itself. One bullet only. Batch answers carry ids only; terse: false echoes the nodes and costs about six times as much. A protected bullet usually refuses this, and so does everything under it; `protectedWords: false` in its answer means the words may still be written. tree marks them, so read before you plan, and the refusal explains the rest.
| Parameter | Type | Required | What it does |
|---|---|---|---|
id | string | The bullet to change, for the one-bullet form. | |
text | string | Replaces the whole line. Prefer prependText / appendText / replace. | |
note | string | ||
layout | string | One of: bullet, todo, numbered, h1, h2, h3, quote, code, board, table, divider. How the bullet is drawn. Most shape one line: todo a checkbox, numbered an ordinal, h1/h2/h3 a heading, quote, code, divider. TWO ARE ARRANGEMENTS OF THE BULLETS UNDER IT, and are how you build a grid without leaving the outline: table draws the children as rows and THEIR children as cells, first row naming the columns; board draws the children as columns and their children as cards. It is one prop on one bullet and nothing moves in the graph, so set it back to bullet and the same content is an ordinary branch. A table is three calls: the bullet with layout table, then all the rows, then all the cells. | |
done | boolean | ||
prependText | string | Put this in front of the existing text, leaving it byte for byte. | |
appendText | string | Put this after the existing text, leaving it byte for byte. | |
replace | object | An object taking old, new, old and new required. Change one part. Refused unless old is in the text exactly once. | |
ifText | string | Only write if the bullet still reads exactly this, whitespace included. | |
items | array of objects | Many bullets in one call. Each takes the same fields as the one-bullet form, plus its own id. | |
terse | boolean | Default true for items, false for one bullet. |
Each item of items takes:
| Field | Type | Required | What it does |
|---|---|---|---|
id | string | yes | |
text | string | ||
note | string | ||
layout | string | One of: bullet, todo, numbered, h1, h2, h3, quote, code, board, table, divider. | |
done | boolean | ||
prependText | string | ||
appendText | string | ||
replace | object | An object taking old, new, old and new required. | |
ifText | string |
move
Move a bullet under a different parent. Everything under it goes with it. ACROSS OUTLINES TOO: if the parent you name is in a different outline (a memory filed by mistake, say, that belongs in the user's notes), the bullet is re-created there and removed here — the answer carries movedAcross with the old and new id. Only a childless, unmirrored bullet makes that trip in one call; a branch or a mirror is refused with the two-call repair named. To move SEVERAL, send ids: [...] instead of id, with the same parent. That is ONE mutation rather than one per bullet, so the order you asked for is the order that lands and a reader watching the branch sees one rearrangement instead of twenty. It is ALL OR NOTHING: if any bullet in the list may not be moved, or would land inside itself, none of them move. The answer names the placement it took for every bullet in `moved`, and anything it could not resolve comes back in `skipped` rather than being dropped from a list you believed was honoured. A MIRRORED BULLET IS IN SEVERAL PLACES AND THIS TOUCHES ONE OF THEM. Send edge - the `edgeId` of the row tree or search gave you - and it is the copy you read. Without it the FIRST placement is taken, which can be one you may not touch, and then this is refused however often you ask. THE OTHER WAY ROUND IS SILENT: if the first placement IS one you may touch, this answers 200 and acts on a copy you were not looking at. Which one that is depends on rank order across two unrelated lists and you cannot work it out. The answer names the placement it acted on, so check it against the row you read. A protected bullet usually refuses this, and so does everything under it; `protectedWords: false` in its answer means the words may still be written. tree marks them, so read before you plan, and the refusal explains the rest.
| Parameter | Type | Required | What it does |
|---|---|---|---|
id | string | The bullet to move. Use this OR ids, not both. | |
ids | array of string | Several bullets to move under one parent, in one mutation. Use this OR id. | |
edges | array of string | The same, by PLACEMENT id rather than bullet id. Use this when any of them is mirrored, so you move the copies you actually read. | |
parent | string | yes | |
edge | string | Which copy: the `edgeId` of the row you read. Only matters when that row said mirrored; outline mode does not carry it. An edge in a branch you may not write is still refused. |
mirror
Put an existing bullet in a second place without copying it. Editing either one changes both, because there is only one of it. Use this instead of duplicating a shared fact. A protected bullet usually refuses this, and so does everything under it; `protectedWords: false` in its answer means the words may still be written. tree marks them, so read before you plan, and the refusal explains the rest. tree marks those with protected, so read before you plan rather than finding out mid-way.
| Parameter | Type | Required | What it does |
|---|---|---|---|
id | string | yes | |
parent | string | yes |
delete
Remove a bullet and everything under it. If it is mirrored elsewhere the other places survive. A MIRRORED BULLET IS IN SEVERAL PLACES AND THIS TOUCHES ONE OF THEM. Send edge - the `edgeId` of the row tree or search gave you - and it is the copy you read. Without it the FIRST placement is taken, which can be one you may not touch, and then this is refused however often you ask. THE OTHER WAY ROUND IS SILENT: if the first placement IS one you may touch, this answers 200 and acts on a copy you were not looking at. Which one that is depends on rank order across two unrelated lists and you cannot work it out. The answer names the placement it acted on, so check it against the row you read. The ids: [...] form takes no edge, and REFUSES a mirrored bullet (malformed) rather than guessing which copy: send that one on its own with edge. The refusal names the call to make and how many places it is in. ONE: send id. MANY: send ids: [...], at most 200, checked as a whole — if any is refused NOTHING is deleted. Read applied ("all" | "partial" | "none"). refused: {code: count} totals why, biggest first, and the answer says what each code means; malformed there means mirrored, so send that one on its own with edge. THERE IS NO UNDO HERE, so every answer carries removed — how many bullets actually left the outline — and texts, the first few of them. Send dryRun: true to get exactly that count with nothing deleted. Do that whenever you have not read the branch and do not know how deep it goes. A protected bullet usually refuses this, and so does everything under it; `protectedWords: false` in its answer means the words may still be written. tree marks them, so read before you plan, and the refusal explains the rest.
| Parameter | Type | Required | What it does |
|---|---|---|---|
id | string | The bullet to remove, for the one-bullet form. | |
ids | array of string | Several bullets in one call. | |
dryRun | boolean | Count what would go and delete nothing. | |
edge | string | Which copy: the `edgeId` of the row you read. Only matters when that row said mirrored; outline mode does not carry it. An edge in a branch you may not write is still refused. |
changes
What changed in an outline since you last looked. Ask this at the START of a session instead of re-reading the tree. MEASURED on a 1,515-bullet outline: five changed bullets came back in 1,060 bytes where tree over the same branch was 962,608 — about 900x ON FIVE BULLETS, which is what that measurement is. On a real week — 539 saves, 731 changed bullets — the same feed is 100,259 bytes: 5.4x cheaper than a full re-read and only 1.1x cheaper than the same tree in mode:"outline", which is the honest thing to beat. Break-even against an outline re-read is about 785 changed bullets. And a first session with no cursor is WORSE than re-reading, so start from a cursor you were given rather than from since=0. less. It stays cheaper as you fall behind: catching up on 900 rewritten bullets took 142,108 bytes over two pages, still six times cheaper than re-reading. Send since=0 the first time, then send back the `cursor` you were given. DEFAULTS: with no root this reads YOUR OWN memory root, not the user's outline — pass a branch id from shared_with_me to follow something they shared. The answer always echoes `root` and `rootText`, so check them before believing an empty one. You get `changed` (id, current text, note if it has one, and `path` from the root down, so you rarely need a second call), `removed` (bullets gone from this branch, each with `descendants` — how many went with it — and `descendantIds` listing them, up to 200, because deleting a branch records only its top and every id underneath is now stale too), and `elsewhere`, a count of changes in this outline that landed outside your root. `more: true` means this is not the end of the log: call again with the new cursor before you conclude anything. `resync: true` means the answer covers the whole branch and is a fresh reading rather than a delta. A cursor from a different outline is REFUSED rather than answered with silence: the sequence is per outline, so cursors are not interchangeable.
| Parameter | Type | Required | What it does |
|---|---|---|---|
since | number | The `cursor` from your previous answer. 0, or omitted, reads from the beginning. | |
root | string | The branch to watch. Defaults to your own memory root, which is probably not what you want for the user's notes. | |
limit | number | How many mutations to scan per page, default and maximum 500. Fewer means smaller answers and more calls. |
whoami
Who this token belongs to, your handle, and the id of your memory root.
No parameters.