Agentic Spreadsheet
БесплатноПоддерживаетсяSpreadsheets with formulas that people and agents edit together.
Описание
Spreadsheets with formulas that people and agents edit together.
README
Spreadsheets with formulas that people and agents edit together.
Status: early. The workbook model, formula engine, CLI, and MCP server work. The package is on npm. The HTTP API keeps data in D1 and R2. Keys are read through the
Authport ofagentic-identity.Warning: the HTTP API (the Worker) signs callers in with the
agentic-identitystore. With no setting, only a key thatagentic-identityissued works.SHEETS_UNSAFE_TRUST_HEADERS=1trusts any caller who sets two headers. Use it for tests only. See HTTP API.Warning: the Worker also refuses to serve until
SHEETS_REGIONsays where the data lives (euorus). Underwrangler dev, setSHEETS_REGION=unknownandSHEETS_UNSAFE_REGION=1.Warning: the CLI and the MCP server do not check who calls them.
--asand--kindare claims that label each edit. Do not give either one to a caller you do not trust.
Install
npm install agentic-spreadsheet
Node 22 or later.
Quick start
import { Engine, Workbook } from "agentic-spreadsheet";
const me = { id: "user-1", kind: "human" } as const;
const wb = new Workbook();
wb.addSheet("Sheet1");
wb.writeRange("Sheet1", "A1", [[1], [2], ["=SUM(A1:A2)"]], me);
console.log(new Engine(wb).values("Sheet1", "A1:A3")); // [[1], [2], [3]]
SDK
agentic-spreadsheet/client calls the Worker over HTTP. It uses only fetch, so it runs in browsers,
Workers, and Node 22. It has one method for each route.
import { createClient, SheetsApiError } from "agentic-spreadsheet/client";
const sheets = createClient({ baseUrl: "https://sheets.example.com", token: "<your-key>" });
await sheets.create({ book: "budget" });
await sheets.write({ book: "budget", anchor: "A1", values: [[10, 20, "=A1+B1"]] });
const { values } = await sheets.read({ book: "budget", range: "A1:C1" }); // [[10, 20, 30]]
try {
await sheets.read({ book: "missing", range: "A1" });
} catch (error) {
if (error instanceof SheetsApiError) console.log(error.status, error.code, error.message); // 404 "not_found" ...
}
Methods: list, create, read, write, setFormula, insertRows, deleteRows, insertCols,
deleteCols, sheets, addSheet, deleteSheet, exportCsv, importCsv, share, shares, unshare,
invite, invites, uninvite, link, links, unlink, remove, and health. Each takes the same fields as its route, plus an optional
{ signal } as the second argument to cancel the call. Pass fetch to createClient to use your own.
A failed call throws SheetsApiError with the HTTP status and a code: bad_request (400),
unauthorized (401), forbidden (403), not_found (404), conflict (409), too_large (413), or
server_error (5xx). Any other status, such as a 429 from a proxy, is unknown. The API sends no code of its own, so the client names the status. A network failure, an abort, or
a 2xx answer that is not JSON (for example from a wrong baseUrl) throws the error that fetch or json() throws. The client never retries. A write that fails may have landed:
read the book before you send it again.
ODS and XLSX
agentic-spreadsheet/formats maps a workbook to and from ODS and XLSX, through agentic-formats.
import { exportFile, importFile, formatOf, MIME } from "agentic-spreadsheet/formats";
const { bytes, warnings } = exportFile("xlsx", book); // MIME.xlsx
const { names } = importFile("ods", odsBytes, book, { id: "bot", kind: "agent_of_human" });
exportFile(format, book)returns the bytes and the notes of what the format could not hold. A formula cell keeps its formula and its shown value. The used ranges of all the sheets together can hold up to 1,000,000 cells, or the call throws aSheetError.importFile(format, bytes, book, by)adds the sheets of the file after the sheets that are there. A taken name gets a number, and a name is cut to 31 characters. A file that needs a rename, and has a formula that names a sheet (a!in the formula), throws aSheetError, because the formula would point at another sheet. A formula that names a sheet that is not in the file binds to the sheet of that name in the workbook, if there is one. Every imported cell has the authorby. All of the sheets are added, or none. A file over 25 MB, over 100 sheets, or over 1,000,000 cells in the grids of its sheets throws aSheetError. Bytes that are not a file of that format throw theFormatErrorofagentic-formats.formatOf(fileName)returns"ods","xlsx", ornull.toFileBook(book)andaddSheets(book, model, by)work with the neutral model ofagentic-formats.
Other exports
The root export, agentic-spreadsheet, also exports these names.
- A1 helpers.
parseCell("B2")returns{ col: 2, row: 2 }, andformatCellturns it back into"B2".parseRange("A1:C3")returns{ c1, r1, c2, r2 }.colIndex("AA")returns27, andcolLetters(27)returns"AA".parseCellandparseRangethrow on bad text. - CSV helpers.
parseCsv(text)returns rows of strings.formatCsv(rows)writes CRLF rows. It adds a leading'to text that starts with= + - @, a tab, or a carriage return.inferCell(text)turns one field into a number, a boolean, a formula, or text. Empty text givesnull. WithasTextset totrue, a leading=is kept as text, with a'in front. - Stores.
createMemoryStore()andcreateMemoryBlobs()keep data in memory, for tests.createFileStore(dir)andcreateFileBlobs(dir)keep data in files, as the CLI does. A store runs a transaction again when another writer changed what it read, and throwsStoreConflictErrorwhen the transaction keeps losing.createFileStorealso throws it when it keeps losing to another process, or when the file lock stays held. Read the book, then retry the write. ctxFromEnv(process.env)builds the context that the CLI and the MCP server use. It readsSHEET_DIR,SHEET_AS, andSHEET_KIND.OPSlists the operations that the CLI, the MCP server, and the HTTP API run. Each has aname, adescription, theargsnames, ashape(a Zod shape), and arunfunction. Parse the arguments withz.object(op.shape)before you callrun.OpErrorcarries the HTTP status (404, 409, or 413).SheetErroris the error that aWorkbookmethod throws for bad input, such as a bad sheet name, column, width, position, value, or principal. A bad A1 address throws a plainError.toBase64(bytes)andfromBase64(text)convert between bytes and base64.PRINCIPAL_KINDSlists the five principal kinds.isMachineMade(kind)returnstruefor every kind excepthuman.VERSIONis the package version.
Package export reference
The examples above show the main calls. The lists below include the TypeScript types. Import types with import type.
agentic-spreadsheet
Workbook holds sheets; Engine computes values. parseCell, formatCell, parseRange, colIndex, and colLetters handle A1 addresses. parseCsv, formatCsv, and inferCell handle CSV. toBase64 and fromBase64 convert bytes. isMachineMade checks a principal kind; PRINCIPAL_KINDS lists the kinds. VERSION, MAX_COL, MAX_ROW, MAX_TEXT, MAX_SHEET_NAME, and MAX_STRUCTURE_COUNT are constants. SheetError reports invalid workbook input.
createMemoryStore and createMemoryBlobs make in-memory adapters. createFileStore and createFileBlobs make file adapters. StoreConflictError reports a failed transaction. ctxFromEnv makes a CLI context; OPS and OpError expose operations and their errors.
Types: Author, Blobs, Cell, CellFormat, CellFormatPatch, CellInput, CellPos, CellValue, Change, Ctx, ErrorCode, ErrorValue, FileStoreOptions, Op, Principal, PrincipalKind, RangePos, SheetInfo, Store, StoreOps, and Value.
import { createMemoryStore, parseCell } from "agentic-spreadsheet";
const store = createMemoryStore();
const position = parseCell("B2");
agentic-spreadsheet/client
createClient makes the HTTP client and SheetsApiError reports HTTP failures. Types: CallOptions, Cell, CellAuthor, ClientOptions, ErrorCode, FormulaError, InviteSummary, LinkSummary, PrincipalKind, ReadResult, ShareRole, SheetTarget, SheetsClient, SubjectKind, and Value. See the SDK example above.
agentic-spreadsheet/formats
exportFile, importFile, formatOf, MIME, toFileBook, and addSheets handle ODS and XLSX. MAX_IMPORT_SHEETS is the sheet limit. SheetFormat is the format type. See the ODS and XLSX example above.
agentic-spreadsheet/ui
mount mounts a sheet and WorkbookView is its React component. Types: EmbedPrincipal, HostWorkbook, MountOptions, MountedSheet, PrincipalKind, RoomPresence, SheetCalls, and SheetsClient. See the mount example below.
agentic-spreadsheet/ui.css
Load the styles with the UI:
import "agentic-spreadsheet/ui.css";
agentic-spreadsheet/worker
The default export is the Worker handler. createApp builds the HTTP app with supplied ports; fromAuth adapts an identity Auth port. storageFor builds ports from Worker bindings. roomGrant checks a presence request, roomRole caps its role, and WS_PROTOCOL names its WebSocket protocol. AppOptions and WorkerEnv are types.
import worker, { roomRole } from "agentic-spreadsheet/worker";
const role = roomRole("editor", []);
export default worker;
agentic-spreadsheet/account
spreadsheetAccount implements account export and erase; SpreadsheetAccountError reports damaged account data. SpreadsheetAccount, SpreadsheetAccountDeps, and SpreadsheetCollection are types. See the account example below.
The CLI and MCP server are commands, not import subpaths. See the CLI and MCP example below. The storage adapters above are root exports, not an adapters subpath.
CLI and MCP server
export SHEET_DIR=./.sheets # where workbooks live. The default is ./.sheets
export SHEET_AS=ana SHEET_KIND=human # who writes. A write fails without both
npx agentic-spreadsheet new budget
npx agentic-spreadsheet write budget A1 '[[10,20,"=A1+B1"]]'
npx agentic-spreadsheet read budget A1:C1 # values, formulas, and authors
npx agentic-spreadsheet mcp # MCP server on stdio
The CLI and the MCP server share one list of operations: new, list, read, write,
formula, insert-rows, delete-rows, insert-cols, delete-cols, sheets, add-sheet, delete-sheet,
export (CSV), import. MCP tool names start with sheet_.
To share a workbook, use a Worker. A local store has no roles, so these commands need SHEET_URL and the key of the owner in SHEET_TOKEN. Use an https URL: the key goes in a header, and plain http works only for a loopback host.
export SHEET_URL=https://sheets.example.com SHEET_TOKEN=<key>
npx agentic-spreadsheet share budget --subject ana --subject-kind human --role editor # give a role (owner only)
npx agentic-spreadsheet shares budget # list the roles (owner only)
npx agentic-spreadsheet unshare budget --subject ana --subject-kind human # take it away
npx agentic-spreadsheet invite budget --role viewer --email [email protected] # invite a person (or --handle <h> for an agent)
npx agentic-spreadsheet invites budget # list the invites (no emails)
npx agentic-spreadsheet uninvite budget --invite <id> # revoke an invite that nobody accepted
npx agentic-spreadsheet link budget --role viewer --signed-in-only true # make a share link: the token shows once
npx agentic-spreadsheet links budget # list the links (no tokens)
npx agentic-spreadsheet unlink budget --link <id> # revoke a link
The role is viewer, commenter, or editor. --kind is the kind of a writer, so a share uses --subject-kind.
shares, invites, and links list 100 items a page: use --limit and --cursor (the nextCursor of the last page) to read on. The other commands and tools use the local store, so the workbook must exist on the Worker. shares shows each subject as a hash of its id, because the store never keeps the id. A refused call prints
error: <message> and exits 1. With the same
two variables, mcp also has the sheet_ tools in the table below. Without them, these tools do not exist.
Every sharing call needs the owner role:
| Action | SDK | CLI | MCP tool | HTTP |
|---|---|---|---|---|
| Give a role | share |
share |
sheet_share |
PUT /:book/share |
| List the roles | shares |
shares |
sheet_shares |
GET /:book/shares |
| Take a role | unshare |
unshare |
sheet_unshare |
DELETE /:book/share/:kind/:subject |
| Invite by email or handle | invite |
invite |
sheet_invite |
POST /:book/invites |
| List the invites | invites |
invites |
sheet_invites |
GET /:book/invites |
| Revoke an invite | uninvite |
uninvite |
sheet_uninvite |
DELETE /:book/invites/:invite |
| Make a share link | link |
link |
sheet_link |
POST /:book/links |
| List the links | links |
links |
sheet_links |
GET /:book/links |
| Revoke a share link | unlink |
unlink |
sheet_unlink |
DELETE /:book/links/:link |
An invite or a link gives viewer, commenter, or editor, never owner. An invite names an email (a person) or a
handle (an agent). --expires-in-ms sets how long an invite or link lives. A link token shows once, in the reply that
makes it. A list never shows the email, the token, or the token hash. Accepting an invite and
opening a link are part of agentic-identity: this package does not serve them yet.
A write needs --as <id> and --kind <kind> (or SHEET_AS and SHEET_KIND). There is no default
kind. An agent sets agent, agent_of_human, or agent_of_agent. A read needs neither.
import takes --as-text: a cell that starts with = stays text. Use it for CSV you do not trust.
export and import handle sheets of 150,000 rows and more. Export puts a ' before text that
starts with + - @ or a control character, and import removes it, so a round trip keeps the text.
Live demo
A demo Worker runs at https://agentic-spreadsheet-demo.hello-fd1.workers.dev. It is a staging build. Open the
address to see the web UI. GET /health answers 200 with no key. The API needs a key, and the demo does not
publish one, so run your own Worker to try writes (see HTTP API). The demo may be reset at any time.
The demo uses wrangler.demo.jsonc: a D1 database in weur, an R2 bucket with the eu jurisdiction, and
SHEETS_REGION=eu. To redeploy, a maintainer with a Cloudflare token runs:
CLOUDFLARE_API_TOKEN=<token> npm run deploy:demo
The API keys live in a Worker secret, never in the repo or in CI. Set them with
npx wrangler secret put SHEETS_API_KEYS --config wrangler.demo.jsonc. To make your own demo, copy
wrangler.demo.jsonc, change the name, and put your own database id and bucket name in it.
HTTP API
The Worker serves the same operations under /api/v1/sheets. Run npm run build first, because wrangler.jsonc
serves dist/ui and wrangler needs that directory. Then run WRANGLER_SEND_METRICS=false npx wrangler dev.
Workbooks live in a D1 database (binding DB) and an R2 bucket (binding BLOBS), both declared in
wrangler.jsonc. They survive a restart. wrangler dev keeps them in .wrangler/state. To deploy, create the
database and the bucket (commands are in wrangler.jsonc), put the database id in the file, and set
SHEETS_REGION to eu or us. The Worker answers 500 if a binding or the region is missing, and that includes
/health.
The shipped wrangler.jsonc is for the EU: a bucket with the eu jurisdiction, and a database made with
--location weur. For us, create the database with a US location, drop the jurisdiction from the bucket, and set
SHEETS_REGION=us. D1 reports where it served each query, and the store refuses a mismatch with the claim (the
Worker answers 500). R2 cannot report its jurisdiction, so nothing checks the bucket: keep the claim and the bucket in step.
# one key, at least 32 bytes, tied to one id and kind (a key may also carry "scopes" and, for an agent that acts for a person, "parent")
WRANGLER_SEND_METRICS=false npx wrangler dev --var SHEETS_REGION:unknown --var SHEETS_UNSAFE_REGION:1 --var 'SHEETS_API_KEYS:{"<your-key>":{"id":"bot-1","kind":"agent"}}'
curl -H "authorization: Bearer <your-key>" -X POST localhost:8787/api/v1/sheets -d '{"book":"budget"}'
curl -H "authorization: Bearer <your-key>" -X PUT localhost:8787/api/v1/sheets/budget/range \
-d '{"anchor":"A1","values":[[10,20,"=A1+B1"]]}'
curl -H "authorization: Bearer <your-key>" 'localhost:8787/api/v1/sheets/budget/range?range=A1:C1'
| Method and path | Operation |
|---|---|
GET / and POST / |
list and new |
GET /:book/range and PUT /:book/range |
read and write |
PUT /:book/formula |
formula |
POST /:book/rows and DELETE /:book/rows?at=&count= |
insert-rows and delete-rows |
POST /:book/cols and DELETE /:book/cols?at=&count= |
insert-cols and delete-cols (at is a column number, A is 1) |
GET /:book/sheets, POST /:book/sheets, and DELETE /:book/sheets/:name |
sheets, add-sheet, and delete-sheet |
GET /:book/export and POST /:book/import |
export (CSV) and import |
GET /:book/shares?limit=&cursor= |
List the roles on the workbook (owner only). A subject shows as a hash |
PUT /:book/share and DELETE /:book/share/:kind/:subject |
Give a person (human) or an agent (agent) a role (viewer, commenter, editor), and take it back |
GET and POST /:book/invites, DELETE /:book/invites/:invite, GET and POST /:book/links, DELETE /:book/links/:link |
List and make invites and share links, and revoke an invite or a link (owner only). See the sharing table above |
DELETE /:book |
The owner deletes the workbook, and its grants, invites, links, and audit entries go with it |
The key sets who writes, so a body cannot name another principal. The Worker reads each request through the
Auth port of agentic-identity, so a key that agentic-identity issued works with no setting. SHEETS_API_KEYS
is optional. If you set it, the Worker writes its keys into the store on start as principals and credentials. The store holds the hash of a key, never the key. A key has 32 bytes or more, with only letters,
digits, _ and -, at most 512 characters, and each needs an id and a kind. A kind that acts for another principal
(agent_of_human, agent_of_agent, user_token) needs a parent, the id of another entry. human and agent
take no parent. An entry can also set scopes, such as ["workbook:read"]: the key then cannot write, whatever its
role on a workbook.
A key that you remove from the env stops working in that Worker at once (its hash stays in the store until you
revoke it with agentic-identity). Use random keys, for example the output of openssl rand -hex 32. Each key has its own id.
A session cookie is not a credential. A missing or bad key gets 401.
A principal with the viewer role (see PUT /:book/share) reads and gets 403 on a write.
Errors are 400, 403, 404, 409, or 413.
A request body is at most 5 MB. GET /health needs no key.
A workbook has an owner: the principal that made it. The owner shares it with a kind (human or agent), an id, and a role.
A viewer or commenter reads. An editor reads and writes. Only the owner shares. agentic-identity keeps the
roles and writes an audit entry for each check. A principal with no role gets 404, as for a book that does not
exist. A principal with a role that is too low gets 403. A share follows the kind and the id: a person and an agent with one id hold separate roles.
Every kind but human counts as an agent.
The owner cannot lower its own role. Each check adds an audit entry to the store, and nothing limits how many.
If the Worker fails after it saves a new workbook and before it records the owner, the book has no owner.
Mount the API in your own Worker
createApp from agentic-spreadsheet/worker builds the Hono app that serves the API above. It takes:
storeandblobs: theStoreandBlobsports ofagentic-identity. UsecreateMemoryStore()andcreateMemoryBlobs()for tests. A Cloudflare Worker uses the D1 and R2 adapters ofagentic-identity/cloudflare. RunD1_SCHEMAon the database once first, or callstorageFor(env)fromagentic-spreadsheet/worker, which runs it and builds both adapters from theDBandBLOBSbindings.authenticate: an async function that reads a request and resolves to the caller, ornull. The caller is{ id, kind, scopes? }. Scopes, when set, limit what the caller may do. Without them, the role alone decides. Anullanswers 401 on the/api/v1/sheetsroutes.GET /healthneeds no key.fromAuth(auth)from the same entry adapts anAuthport ofagentic-identity.pack(optional): the name thatGET /healthreturns. The default isagentic-spreadsheet.
import type { Auth } from "agentic-identity";
import { createApp, fromAuth } from "agentic-spreadsheet/worker";
import { createMemoryBlobs, createMemoryStore } from "agentic-spreadsheet";
declare const auth: Auth; // your Auth port of agentic-identity
const app = createApp({
store: createMemoryStore(),
blobs: createMemoryBlobs(),
authenticate: fromAuth(auth),
pack: "my-app",
});
export default app; // a Hono app: app.fetch(request) answers the routes above
The app runs the same operations as the table above, under /api/v1/sheets. It limits each request body to 5 MB and sets Cache-Control: no-store. It reads no key or setting of its own, so the host decides who signs in. The example needs agentic-identity in your own package.json. The default export of agentic-spreadsheet/worker builds this app from the DB and BLOBS bindings and these settings: SHEETS_REGION, SHEETS_UNSAFE_REGION, SHEETS_API_KEYS (optional seed), and SHEETS_UNSAFE_TRUST_HEADERS.
| Setting | Meaning |
|---|---|
SHEETS_REGION |
eu or us: where the data lives. Required |
SHEETS_UNSAFE_REGION |
1 lets SHEETS_REGION=unknown run (local only) |
SHEETS_API_KEYS |
Optional. JSON of keys to seed into the identity store. Not needed for identity-issued keys. If you remove it, the keys it seeded stop working |
SHEETS_UNSAFE_TRUST_HEADERS |
1 trusts the x-principal-* headers. Tests only. Not with SHEETS_API_KEYS. Any other value is an error |
PACK_NAME |
Sets pack |
Mount the sheet in your page
agentic-spreadsheet/ui renders the sheet into an element. It needs no framework in your app.
You must install react and react-dom (version 19) yourself: ./ui needs them as peer dependencies.
Load the styles once with agentic-spreadsheet/ui.css. The file has the agentic-design tokens, the component styles, and the fonts
it needs. It has no rule for *, html, body, headings, or links, so it leaves your page alone. The grid classes start with as-.
The sheet puts one <h1> with the workbook name in your page. The sheet does not set the theme of
your page: agentic-design reads data-theme on <html>, and your page owns that attribute.
The sheet uses the typed client, so the page must reach the Worker API.
import { createClient } from "agentic-spreadsheet/client";
import { mount } from "agentic-spreadsheet/ui";
import "agentic-spreadsheet/ui.css";
const client = createClient({ baseUrl: "https://sheets.example.com", token });
const sheet = mount(document.getElementById("sheet")!, {
client,
docId: "budget",
principal: { kind: "human", name: "Ada" }, // kind is required: there is no default
readOnly: false, // true shows the cells and allows no edit
});
sheet.update({ client, docId: "forecast", principal: { kind: "human", name: "Ada" } }); // show another workbook
sheet.unmount(); // remove the sheet and stop its polling
principalneeds a knownkindand aname. Withhostit also needs anid. A missing or unknown kind throws aTypeError, and so does a badclientor an emptydocId. The kind is only checked. It does not change what the server allows: the server checks every write against the key of the client. The name shows in the presence room.readOnlyturns off cell edits, the formula bar, and the row, column, and sheet buttons.tokenis optional. With it, the sheet joins the presence room on the origin of the page, and it sends the key in a WebSocket subprotocol entry, not in the URL. Do not passtokenwhen the page and the Worker use different origins. Without it, the sheet shows no peers. See "Hosting" below for what presence needs.onUnauthorizedruns when the server rejects the key.- A React app can use the
WorkbookViewcomponent from the same entry instead. Put it inside the providers ofagentic-design/react. The web UI of this repo uses it. titlesets the text of the<h1>. The default is thedocId.
Use your own Yjs document
An app that keeps the workbook in its own Yjs room gives it as host. The sheet then reads and writes that document
as a Workbook (its sheets array, the same shape this package stores) and computes values in the page. It does not
call a client, poll, or open a presence room. It reads again as soon as the document changes. client is not needed,
and token together with host throws a TypeError.
const sheet = mount(element, {
docId: "budget",
title: "Budget",
principal: { kind: "human", name: "Ada", id: "ada" }, // id is required with host: it is stamped on each cell Ada writes
host: {
doc, // a Y.Doc
presence: sync, // optional: a SyncClient of agentic-realtime, or { peers, onPeers, setPresence }
status: () => status, // optional: "connecting" | "connected" | "disconnected", shown as Live, Connecting, or Offline
onStatus: (listener) => stop, // optional: tell the sheet when the status changes
},
});
- Use the same
yjscopy as the sheet. A document from another copy is refused with aTypeError. - Each cell the sheet writes is stamped with the principal's
idandkind. A cell from an agent kind is machine-made and shows the agent mark. The server does not check these writes: your app and its room decide who may write. - With
presence, the sheet shows the peers and their cells, and sends its own cursor withsetPresence. Onunmountit clears its cursor. It never closes your room or your document. Keep the samepresenceobject between renders: a new one makes the sheet join again, and peers see its cursor blink. - While a cell edit is open, the sheet waits to read the document again, so the edit stays on its sheet and its A1 address. If a peer renamed or deleted that sheet, the save fails with a message. If a peer inserted a row or a column above or left of it, the save goes to the same address, as it does on the client path.
- A document that the sheet cannot read as a workbook shows "Could not open the sheet" in place of the grid. A workbook in an old format converts when the sheet opens it. A read-only sheet does not convert it and shows why.
- A workbook with no sheet shows "This workbook has no sheet yet." Your app adds the first sheet.
- The sheet opens the document again only when
docchanges. It readsonStatusonce each time.
UI
A small web UI comes with the Worker in this repo. The UI source is in ui/, and src/ui-worker.ts serves it.
It uses the agentic-design package for tokens, controls, and dialogs, the typed client (./client) for
every call, and a presence room from agentic-realtime.
It has a list of workbooks, a sheet bar, a grid with A1 cells and a formula bar, edits for values
and formulas, add and remove for rows, columns, and sheets, and light and dark themes.
Run it from a checkout:
npm ci
npm run dev # makes .dev.vars with a random key on the first run, and prints the key
Open the address that Wrangler prints, and paste the key (see "Run locally"). The tab keeps the key until you close
it. .dev.vars stays out of git. Data lives in a local D1 and R2 under .wrangler/state, so it survives a restart.
- Keyboard. Arrow keys move. Enter or F2 edits the cell. Typing on a cell replaces its text. Enter saves and moves down. Tab saves and moves right while you edit. Escape cancels. Delete clears. Outside an edit, Tab walks to the next cell and leaves the grid at the last cell, so focus is never trapped.
- Agent marker. A cell an agent wrote has a robot mark and a left bar, and its name says who wrote it. Color is never the only sign.
- Presence. Each workbook has a room. The room shows who has the workbook open, and which cell each person selects. Cells do not travel through the room: the API stores them, and the grid reads again every 4 seconds. Two people who edit one cell at once get the last write.
- Tests.
npm run test:e2eruns Playwright againstwrangler dev. It signs in, makes a sheet, enters a value and a formula, checks the result and a reload, checks the keys, the structure changes, the agent marker, and presence, and runs an axe WCAG 2.2 AA check on each view in light and dark. Screenshots of each view in both themes are in docs/screenshots. RunUPDATE_SCREENSHOTS=1 npm run test:e2eto make them again. - Limits. The grid shows the used range plus some room, up to 52 columns and 200 rows. The browser
sends the key in the
agentic-spreadsheet.bearer.<key>subprotocol entry when it joins a room, because a browser cannot set a header on a WebSocket. The Worker does not read atokenquery parameter: a URL reaches logs and proxies. Use HTTPS, and keep Worker logs private. There is no sign-in page for a person yet: the key is the sign-in. - Hosting. The npm package ships the UI files in
dist/ui, but not the Worker that serves them. Read the files by path, fromnode_modules/agentic-spreadsheet/dist/ui.exportsdoes not list them. A host must serve them at the root of an origin, because the asset paths start at/, and the UI calls the API on its own origin. The host answers every path under/api/v1/sheets/, and/health, with the default export ofagentic-spreadsheet/worker. Pass anenvwithSHEETS_REGION, and theDBandBLOBSbindings, or the Worker answers 500. Presence does not work this way yet. Its socket needsagentic-realtimeand aSyncRoomDurable Object, androomGrantneeds the sameDBbinding. For presence, run the Worker from a checkout, assrc/ui-worker.tsdoes. Without presence, the UI still works and shows Offline.
Account export and erase
Import spreadsheetAccount from agentic-spreadsheet/account. Its typed id, exportAccount(subject, deps), and eraseAccount(subject, deps) match the identity registry contract.
import { spreadsheetAccount } from "agentic-spreadsheet/account";
const subject = { id: "ana", kind: "human" } as const;
const data = await spreadsheetAccount.exportAccount(subject, { store, blobs });
const counts = await spreadsheetAccount.eraseAccount(subject, { store, blobs });
The export has plain JSON collections: workbooks (owned Yjs states as base64), grants (the subject's roles), authored (cell content and format stamps on other workbooks), invites, links, and audit. Visible authored cells include A1 addresses and formulas. The last three collections include records on owned workbooks and records the subject made. Invites omit emails and handles. Links omit token hashes. States include sheets, formulas, formats, deleted row cells, and stored CRDT history. There is no separate version or attachment store.
Erase deletes sole-owned workbooks, blobs, and sharing records. Other owners, including parent folder owners, keep their workbooks. The subject's grants, invites, and audit entries go. On workbooks that stay, author IDs become (erased). Content and kinds stay, so machine labels stay. Changed states get fresh CRDT structs to remove replaced history. Counts use the export names. A repeat counts zero. A failed run can have removed some data. Retry if another erase changes the same workbook. A durable journal keeps blob keys until cleanup succeeds. Any erase resumes pending cleanup for all subjects. Finish it before export.
Stop writes and disconnect editing clients first. Old clients must reload after erase. Run this contributor before identity erase. Sharing hashes IDs and maps machine kinds to agent. Machine kinds with one ID share roles. Stamps and invite creators match the exact kind. Use distinct machine IDs. Call once per delegated principal; the package cannot infer its person. Links have no maker ID and leave only with sole-owned workbooks. Invite recipients cannot be mapped to subject IDs. Orphan blobs from prior failed saves have no owner index and cannot be discovered. Each call scans all workbook blobs, one at a time. Its cost grows with the stored data. Damaged states, stamps, grants, owner chains, child resources, or missing invite creator audit rows stop the call. A missing blob with no cleanup journal needs operator repair. Damaged state errors name workbooks. Backend errors throw and never return a success report.
Features
- A workbook is one Yjs document. Two copies merge. Content, each format field, and the editor stamp of a cell are separate keys, so two writers on one new cell keep both edits. If two writers set the same field, the later write wins.
- Many sheets in a set order. Add, rename, and delete sheets.
- Sparse cells. Each cell holds a value or a formula, plus a format. Text is at most 32,767 characters. A formula is at most 8,192.
- Every row and column has an id that never changes. A cell is keyed by the ids of its row and its
column, and a formula keeps its refs as ids. An A1 address is the position of those ids, so the SDK,
the CLI, the MCP server, and the HTTP API take and return A1. Insert, delete, and move change the
order of the ids and nothing else. Two writers who edit one sheet at the same time lose nothing,
and a write lands on the row it was meant for, whatever the other writer inserts, deletes, or moves.
moveRowsandmoveColsare in the SDK. - A workbook saved by an older version, with A1 keys, converts when it loads.
- Column widths and frozen rows.
- Insert and delete rows and columns. Formula refs follow the moved cells.
A ref to a deleted cell becomes
#REF!. A range shrinks or grows to match. An insert that would push a cell off the grid is refused. A count is at most 10,000. - A formula engine reads the workbook. It has
+ - * / ^ & %, comparisons, and cross-sheet refs. It keeps values between reads. A change recomputes only the cells that read it, and a chain of 100,000 cells works. Callengine.dispose()when the workbook outlives the engine. It returns#REF!,#DIV/0!,#NAME?,#VALUE!,#NUM!,#N/A,#CIRCULAR, and#DEPTH!. - Functions:
IF,IFS,IFERROR,SUM,AVERAGE,MIN,MAX,COUNT,COUNTA,COUNTIF(S),SUMIF(S),VLOOKUP,XLOOKUP,INDEX,MATCH,AND,OR,NOT,ROUND,ROUNDUP,ROUNDDOWN,ABS,CONCAT,TEXT,LEFT,RIGHT,MID,LEN,UPPER,LOWER,TRIM,DATE,TODAY,NOW. Other names come from@formulajs/formulajsand are untested here. Blank cells in a range are skipped, as in a spreadsheet app.MUNITandEXPANDreturn#NAME?. - CSV import and export. Export marks text that starts with
= + - @so it cannot run as a formula. - Read a range. Write a range of values or formulas.
- Every cell keeps the principal that last edited it. The kind is one of
human,agent_of_human,agent_of_agent,agent, oruser_token. Onlyhumanreads as human-made. All other kinds read as machine-made.
Limits
- The grid has 16,384 columns (A to XFD) and 1,048,576 rows. A sheet name is at most 256 UTF-16 code units.
It cannot be blank or contain
[ ] : * ? / \ ! '. The constantsMAX_COL,MAX_ROW, andMAX_SHEET_NAMEhold these values. - A cell holds at most 32,767 UTF-16 code units of text (
MAX_TEXT). One insert or delete changes at most 10,000 rows or columns (MAX_STRUCTURE_COUNT). - A formula nested more than 100 levels deep, or with more than 1,000 chained operators, gives
#DEPTH!in that cell. Other cells are not affected. A chain of cells has no depth limit. - A loop gives
#CIRCULARin every cell on the loop, whichever cell you read first. - A change to a cell finds the formulas that read it through a range by a scan of all ranges on that sheet. A sheet with very many range formulas pays for that scan on each edit.
- Ref rewriting does not handle whole-column refs such as
A:A. - A write to a row that another writer deletes goes with the row. Two writers who move one row at once get one place for it, the same on both copies.
- Two copies that each convert an old A1-keyed workbook, and then merge, are flagged:
conversionClash()is true and every read and write throws, because each copy made its own order for the sheets and the merge keeps one. Load the workbook again from one copy, or better, convert once, save it, and then share it. The first write of the CLI or the Worker converts a stored workbook and saves it in one committed write, so a stored workbook converts once. A read converts a copy in memory and saves nothing. A copy of one old file converted on two machines meets this case. An old workbook whose log shows row, column, or sheet-name edits made at the same time and not accepted does not convert: the load throws, because its cells may sit at the wrong address. Accept the merge in the version that saved it, then load it again.structureConflict()andacceptStructureMerge()are gone from the SDK, because inserts, deletes, and moves now merge. A clash that was saved cannot be repaired in place. Restore the workbook from a copy saved before it. Old A1-keyed state that a sync provider delivers after the workbook is made is refused with an error. Load it withWorkbook.fromStateinstead. A sheet that comes without an id after a conversion converts on the nextWorkbook.fromState. It is a clash only when two copies convert that same sheet. A sheet that has an id but holds an old A1 cell key is refused with an error, because that cell would read as empty. Restore the workbook from a copy saved before that key arrived. An old formula that holds U+0001 or U+0002 does not convert: the load throws. Remove the character in the version that saved it, then load the workbook again. - A single ref follows a moved row. A range keeps the rows that stay between its ends: a row that moves out of a range leaves it, and a row that moves in joins it.
- Two inserts made at once can push a cell past the last row. The workbook keeps the cell, but no read, export, or count sees it until rows are deleted.
- A formula that names a sheet that does not exist yet takes the sheet when it is added. A formula written by one writer while another adds that sheet is not bound until the next rename or add.
- Two clients that add a sheet with the same name while offline both succeed. The merged workbook then holds two sheets with one name.
- The Worker keeps workbooks in D1 and R2. A workbook stored before sharing has no owner,
and the Worker refuses it for every principal.
liststill names every workbook to every valid key. There is no rate limit yet. - Storage uses the
StoreandBlobsports ofagentic-identity. A workbook is a document in the store and its state is a blob. The CLI keeps both in files under the store directory (store.jsonandblobs/). A write takes a lock file, and a lock that is more than 3 seconds old is taken over. A workbook saved by 0.1.0 as one file per workbook moves intostore.jsonon the first call, a read included. The old files stay. A 0.1.0 file whose id starts and ends with two underscores, or is longer than 256 characters, stops the store with an error that names the file. Rename it first. - CSV export refuses a sheet whose extent passes 1,000,000 cells. The HTTP API refuses a write or import of more than 10,000 cells (413). A formula may read at most 1,000,000 range cells.
Run locally
bun install # or: npm ci
bun run dev # or: npm run dev
curl http://localhost:8787/health
The script builds the app, then runs wrangler dev. On the first run it writes .dev.vars with a random API key and prints the key. Paste the key in the page to sign in. .dev.vars stays out of git. .dev.vars.example shows the format.
The scripts call npm and node (Node 22 or newer), so install both, even if you use bun. They need a POSIX shell.
Run npm run dev:smoke to check this path. It starts the dev server on port 8799, waits for GET /health to answer 200, and stops the server. CI runs it as the "dev smoke" step. Set DEV_SMOKE_PORT to use another port.
License
MIT.
Установить Agentic Spreadsheet в Claude Desktop, Claude Code, Cursor
unyly install agentic-spreadsheetСтавит в Claude Desktop, Claude Code, Cursor и VS Code — сам разбирается с npx, uvx и сборкой из исходников.
Впервые? Поставь CLI: curl -fsSL https://unyly.org/install | sh
Или настроить вручную
Выполни в терминале:
claude mcp add agentic-spreadsheet --env CLOUDFLARE_API_TOKEN="" --env SHEET_AS="" --env SHEET_DIR="" --env SHEET_URL="" --env WRANGLER_SEND_METRICS="" -- npx -y agentic-spreadsheetПошаговые гайды: как установить Agentic Spreadsheet
FAQ
Agentic Spreadsheet MCP бесплатный?
Да, Agentic Spreadsheet MCP бесплатный — установка в пару кликов через Unyly без оплаты.
Нужен ли API-ключ для Agentic Spreadsheet?
Да, требуются переменные окружения: CLOUDFLARE_API_TOKEN, SHEET_AS, SHEET_DIR, SHEET_URL, WRANGLER_SEND_METRICS. Unyly подставит их в конфиг при установке.
Agentic Spreadsheet — hosted или self-hosted?
Self-hosted: сервер запускается локально на твоей машине командой из раздела установки.
Как установить Agentic Spreadsheet в Claude Desktop, Claude Code или Cursor?
Открой Agentic Spreadsheet на unyly.org, выбери вкладку своего клиента (Claude Desktop, Claude Code, Cursor) и нажми Install — конфиг сгенерируется автоматически, без правки JSON.
Похожие MCP
GitHub
PRs, issues, code search, CI status
автор: GitHubFilesystem
Secure file operations with configurable access controls.
Memory
Knowledge graph-based persistent memory system.
Template MCP Server
A CLI tool to create a new Model Context Protocol server project with TypeScript support, dual transport options, and an extensible structure
автор: mcpdotdirectServer Sequential Thinking
MCP server for sequential thinking and problem solving
автор: GitHub ActionsUnyly Deploy
Deploy sites and MCP servers to Unyly straight from your AI agent
автор: UnylyAmap Maps Mcp Server
MCP server for using the AMap Maps API
автор: duxiaohuiPayMCP
Lightweight payments layer for MCP servers: turn tools into paid endpoints with a two-line decorator. [PyPI](https://pypi.org/project/paymcp/) · [npm](https://w
автор: blustAIServer For Power BI
Enables exploring Microsoft Fabric / Power BI workspaces and semantic models, and executing ad-hoc DAX queries.
автор: Mitsubishi-FusoGpt Image 2 Mcp
RunAPI GPT Image 2 MCP server for image generation: create tasks, poll results, and check pricing across 1 model from Claude Code, Codex, Cursor, and VS Code.
автор: runapi-aiCompare Agentic Spreadsheet with
Не уверен что выбрать?
Найди свой стек за 60 секунд
Автор?
Embed-бейдж для README
Похожее
Все в категории development
