@abloatai/ablo 0.37.0 → 0.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/cli.md CHANGED
@@ -11,7 +11,7 @@ SQL — so what you test is what ships.
11
11
  ```bash
12
12
  npx ablo init # scaffold ablo/schema.ts + client
13
13
  npx ablo login # authorize in the browser
14
- npx ablo dev # push schema to the test sandbox + watch
14
+ npx ablo dev # prepare an isolated Git branch + push/watch
15
15
  ```
16
16
 
17
17
  **Two setup styles, and they pick your commands.** If your app database is the
@@ -19,42 +19,43 @@ source of truth, expose a [Data Source endpoint](./data-sources.md) and keep DB
19
19
  credentials in your app. If you explicitly want Ablo to open a Postgres
20
20
  connection, use the **Direct Postgres connector** commands: `ablo migrate`
21
21
  applies changes to your own `DATABASE_URL`, and `ablo check` / `ablo pull`
22
- adopt tables you already have. Hosted sandbox commands are tagged **Hosted**;
22
+ adopt tables you already have. Hosted branch commands are tagged **Hosted**;
23
23
  direct-connector commands are tagged **Direct Postgres**.
24
24
 
25
25
  ## Authenticate
26
26
 
27
27
  `ablo login` runs the OAuth 2.0 device flow: it opens your browser, you choose
28
28
  **log in** or **create an account** and approve, and the CLI provisions a
29
- **test + live key pair** (90-day) and stores them locally. The test key is a
30
- sandbox `sk_test_` key; the live key is a restricted `rk_live_` key (read-only
31
- observation `logs`, `status`), so a stolen config can't write to production.
32
- This mirrors `stripe login`.
29
+ 90-day, project-scoped `mk_` management credential. It has no test/live mode
30
+ and cannot read or write application data. `ablo dev` uses it to create or
31
+ resume a branch and exchanges it for a temporary branch-bound runtime key.
33
32
 
34
33
  | Command | What it does |
35
34
  | ------------------------ | -------------------------------------------------------------------------- |
36
- | `ablo login` | Authorize in the browser; provisions + stores a test and a live key. |
37
- | `ablo login --project <slug>` | Same, but scope (and mint) the pair to a project, and make it active. |
38
- | `ablo logout` | Remove the stored keys. |
39
- | `ablo status` | Show the active org, mode, both keys (prefix, what each can do, expiry), and server health. |
40
- | `ablo mode [sandbox\|production]` | Switch the active environment. With no argument, prompts. |
41
-
42
- Keys live in `~/.config/ablo/credentials.json` (mode `0600`), keyed by project
43
- then environment; the non-secret `config.json` holds only the active mode and
44
- project. In **CI**, don't log in — set `ABLO_API_KEY`, which always overrides
45
- the stored key.
46
-
47
- ## Test vs live
48
-
49
- Like Stripe, every account has a **test** mode and a **live** mode, and a key
50
- belongs to one of them. Test keys are bound to an isolated sandbox: their reads
51
- and writes never touch production data. Switch with `ablo mode`; `ablo dev` is always
52
- the sandbox by design.
53
-
54
- The schema is **one definition serving both**: a sandbox reads the production
55
- schema until it is pushed one of its own, so your test and live keys see the same
56
- models and only the rows differ. Each plane keeps its own copy once pushed, so a
57
- schema change reaches production when you push it with a live key — see
35
+ | `ablo login` | Authorize in the browser; store one project management credential. |
36
+ | `ablo login --project <slug>` | Same, scoped to a project, which becomes active. |
37
+ | `ablo logout` | Remove the stored credentials. |
38
+ | `ablo status` | Show the active org/project, effective credential, branch target, and server health. |
39
+
40
+ Keys live in `~/.config/ablo/credentials.json` (mode `0600`), keyed by project.
41
+ The non-secret `config.json` holds the active project. In **CI**, don't log in —
42
+ set the project management credential as `ABLO_MANAGEMENT_KEY`; it overrides the
43
+ stored credential during branch bootstrap.
44
+
45
+ ## Development branches vs live
46
+
47
+ A branch is your project at full strength over its own rows: the same models,
48
+ the same schema artifacts, the same claims and the same rules production runs.
49
+
50
+ Production is the project root. `ablo dev` creates or reuses a child branch for
51
+ your Git branch, then mints a temporary `sk_test_` key bound to that child.
52
+ Reads, writes, schema artifacts, claims, and credentials stay isolated from
53
+ production and from other development branches, which is what makes a
54
+ schema-changing pull request as routine as a code-only one.
55
+
56
+ There is no local mode switch. Development selection comes from Git or
57
+ `--branch`; production authority comes only from an explicit live credential.
58
+ Production schema changes use the reviewed one-shot path in
58
59
  [Deployment](./deployment.md).
59
60
 
60
61
  ## Projects
@@ -70,7 +71,7 @@ selects which profile every command authenticates with.
70
71
  | `ablo projects list` | List the org's projects (marks the active one and the org-default). |
71
72
  | `ablo projects create <slug>` | Create a project (`--name "Display Name"`). Its keys/schema/data are isolated. |
72
73
  | `ablo projects use <slug>` | Switch the active project. `ablo projects use default` returns to the org-default. |
73
- | `ablo login --project <slug>` | Mint and store a key pair for a project, and make it active. |
74
+ | `ablo login --project <slug>` | Store management access for a project and make it active. |
74
75
 
75
76
  Because keys are fixed to a project, `projects use` only changes which profile
76
77
  is active — it never re-scopes an existing key. Switch to a project you haven't
@@ -81,14 +82,15 @@ npx ablo projects use war-room
81
82
  # ✓ now targeting project war-room (prj_…)
82
83
  # No key stored for this project yet — run `ablo login --project war-room` to mint one.
83
84
 
84
- npx ablo login --project war-room # mints + stores its key pair, keeps it active
85
+ npx ablo login --project war-room # stores its management credential, keeps it active
85
86
  ```
86
87
 
87
88
  If you run a project-scoped command (`push`, `dev`) while the active project has
88
89
  no key — but other projects do — the CLI **refuses** rather than silently
89
90
  deploying with the wrong project's credential, and names the fix
90
- (`ablo login --project <slug>`). In CI, an explicit `ABLO_API_KEY` bypasses
91
- profiles entirely: it acts in whatever project it was minted for.
91
+ (`ablo login --project <slug>`). In CI, an explicit `ABLO_MANAGEMENT_KEY`
92
+ bypasses profiles for project/branch administration; the runtime key remains
93
+ `ABLO_API_KEY`.
92
94
 
93
95
  ## Commands
94
96
 
@@ -96,10 +98,10 @@ profiles entirely: it acts in whatever project it was minted for.
96
98
  | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
97
99
  | `ablo init` | Scaffold `ablo/` (`schema.ts`, client, optional Data Source / agent / component), write `.env`, install the SDK. Offers to log in at the end. |: |
98
100
  | `ablo login` / `logout` / `status` | Authentication & status (above). |: |
99
- | `ablo mode [sandbox\|production]` | Switch active environment. |: |
100
101
  | `ablo projects list\|create\|use\|rename` | Manage projects and the active one (see [Projects](#projects)). Each project's keys/schema/data are isolated. | `--name "<display>"` (create/rename) |
101
- | `ablo dev` | **Hosted**: push the schema to your test sandbox, then watch `ablo/schema.ts` and re-push on save. | `--no-watch`, `--schema <path>`, `--export <name>`, `--url <url>` |
102
- | `ablo logs` | Tail your scope's commit activity (`stripe logs tail`). Follows by default. | `-n, --tail <N>`, `--since <dur\|ts>`, `--model`, `--op`, `--json`, `--no-follow`, `--mode sandbox\|production` |
102
+ | `ablo dev` | **Hosted**: ensure an isolated Git branch, wire its temporary key, push, then watch `ablo/schema.ts`. | `--branch <slug>`, `--branch-ttl-hours <1-168>`, `--no-watch`, `--schema`, `--export`, `--url` |
103
+ | `ablo branch list\|status\|check\|create\|ensure\|credential\|delete` | Manage and diagnose immutable branch planes and expiring credentials. | Run `ablo branch --help`; use `--json` for automation. |
104
+ | `ablo logs` | Tail the effective credential's branch activity. Follows by default. | `-n, --tail <N>`, `--since <dur\|ts>`, `--model`, `--op`, `--json`, `--no-follow` |
103
105
  | `ablo push` | **Hosted**: upload the schema to Ablo; the server diffs, migrates, and activates it. | `--force`, `--rename old:new`, `--backfill model.field=value`, `--schema`, `--export`, `--url` |
104
106
  | `ablo migrate` | **Direct Postgres**: provision just the synced models (plus the adapter's `ablo_outbox` / `ablo_idempotency`) in your own `DATABASE_URL`. Leaves your other tables alone. | `--dry-run`, `--output <file>`, `--schema`, `--export` |
105
107
  | `ablo pull` | **Direct Postgres**: generate `defineSchema(...)` from your existing tables (read-only, like `prisma db pull`). | `--out <path>`, `--app-schema <name>`, `--import <pkg>`, `--force` |
@@ -131,16 +133,22 @@ HTTP at `/api/docs/<slug>` and through the docs MCP server.
131
133
 
132
134
  ## `ablo dev`
133
135
 
134
- The development loop. It pushes `ablo/schema.ts` to your **test sandbox**,
135
- prints the env line your app needs, then watches the file and re-pushes on every
136
- save (300 ms debounce). It refuses live keys so a tight save loop can never
137
- churn production data.
136
+ The branch-first development loop. It discovers your Git/CI branch, ensures the
137
+ matching Ablo child branch, exchanges the stored `mk_` project credential for an
138
+ expiring branch-only key, writes that key to gitignored `.env.local`, pushes
139
+ `ablo/schema.ts`, and re-pushes on every save.
138
140
 
139
141
  ```bash
140
- npx ablo dev # push + watch
141
- npx ablo dev --no-watch # push once and exit
142
+ npx ablo dev # discover from Git, push + watch
143
+ npx ablo dev --branch preview-pr-482 # explicit branch
144
+ npx ablo dev --no-watch # prepare, push once, exit
145
+ npx ablo dev --branch-ttl-hours 24 # change temporary-key lifetime
142
146
  ```
143
147
 
148
+ It does not start your app, run migrations, create a database-provider branch,
149
+ or copy production rows. Read [Branch-first development](./branch-development.md)
150
+ for the exact discovery order, CI flow, database boundary, and troubleshooting.
151
+
144
152
  ## `ablo logs`
145
153
 
146
154
  Tail commit activity, like `stripe logs tail`. Scope comes from the key — a test
@@ -212,7 +220,7 @@ reshaping it. `ablo check` is read-only; it never proposes a migration.
212
220
 
213
221
  Same engine, two setups. If you use the **Direct Postgres connector**, use
214
222
  `ablo migrate` — it provisions the synced models in your own `DATABASE_URL`. If
215
- Ablo manages the sandbox/hosted store, use `ablo push` and `ablo dev` — the
223
+ Ablo manages the hosted store, use `ablo push` and `ablo dev` — the
216
224
  server applies the change and version-gates connecting clients.
217
225
 
218
226
  ```bash
@@ -244,6 +244,17 @@ lifecycle. It needs no replication setup, which is exactly why it's the fallback
244
244
  reach for it only when logical replication isn't available, and prefer `ablo
245
245
  connect` everywhere else.
246
246
 
247
+ The current Prisma, Drizzle, and Kysely adapters are PostgreSQL bindings. Their
248
+ profiles record three independent facts: the database is PostgreSQL, the binding
249
+ is Prisma/Drizzle/Kysely, and observation is either the transactional outbox or
250
+ PostgreSQL WAL. An ORM name does not imply that the same adapter supports every
251
+ database that ORM can connect to.
252
+
253
+ The outbox automatically observes writes made through Ablo. A write made
254
+ directly by other application code is visible only if that code writes the same
255
+ outbox record in its transaction. Native WAL observation sees both Ablo and
256
+ external writes.
257
+
247
258
  ## Next steps
248
259
 
249
260
  - [Quickstart](./quickstart.md) — connect and write through `ablo.<model>`.
@@ -54,25 +54,25 @@ Everything below is those three in order.
54
54
  ### Planes: what a deployment targets
55
55
 
56
56
  A **plane** is the isolation unit a credential acts on. `production` is the root
57
- plane; every sandbox sits beside it. Three things are per-plane, and knowing
58
- which three is most of what production readiness means:
57
+ branch; development and preview branches are children. Three things are
58
+ per-plane, and knowing which three is most of what production readiness means:
59
59
 
60
- - **Rows:** a sandbox write is invisible to production and to every other sandbox.
60
+ - **Rows:** a child-branch write is invisible to production and every sibling.
61
61
  - **The registered database:** one per plane, so your production database and
62
62
  your dev database are separate registrations.
63
63
  - **The active schema artifact:** the model shapes the engine actually routes on.
64
64
 
65
- A key's plane is fixed at mint and spelled in its prefix: `sk_live_` acts on
66
- production, `sk_test_` on a sandbox. There is no runtime override — the
67
- credential *is* the environment selector, which is why application code never
68
- passes one.
65
+ A key's plane is fixed at mint: `sk_live_` acts on production, while `sk_test_`
66
+ is bound to one development branch. The immutable branch id,
67
+ not a user-supplied slug, is the selector. This is why application code never
68
+ passes an environment.
69
69
 
70
- One asymmetry is worth carrying into your deploy plan. A sandbox with no schema
71
- artifact of its own reads **production's**, so a schema pushed to production
72
- reaches your sandboxes automatically. The reverse does not hold: a push from a
73
- sandbox key creates a sandbox artifact that shadows production **for that
74
- sandbox's readers only**, and production keeps running the schema it was last
75
- pushed. Production gets its models when you push to production.
70
+ A child copies its parent's active schema when it is created, then owns its
71
+ schema history. A child push never changes production. Production gets new
72
+ models only when the reviewed deployment pushes them to the root.
73
+
74
+ There is no shared development plane. New credentials are branch-bound; follow
75
+ [Branch-first development](./branch-development.md).
76
76
 
77
77
  ## 1. The database production writes to
78
78
 
@@ -141,9 +141,10 @@ minting. Two things bite specifically at deploy time.
141
141
  observe-only `rk_live_` by design, so a stolen CLI config cannot write to
142
142
  production. A production deploy needs a **secret** `sk_live_` from the dashboard,
143
143
  supplied as `ABLO_API_KEY`. You do not have to discover this from a failed
144
- deploy: `ablo login`, `ablo mode production`, and `ablo status` each name what
145
- the key in hand does, and `ablo status --json` reports it as `effectiveKey.kind`
146
- for a pipeline to check before it pushes.
144
+ deploy: `ablo login` and `ablo status` name what the key in hand does, and
145
+ `ablo status --json` reports it as `effectiveKey.kind` for a pipeline to check
146
+ before it pushes. The deploy itself uses an explicit dashboard `sk_live_` in
147
+ `ABLO_API_KEY`; there is no local mode switch.
147
148
 
148
149
  **An explicit key always wins.** The CLI resolves `ABLO_API_KEY`, then
149
150
  `.env.local`, then `.env`, then the stored login — and `ablo status` prints which
@@ -204,9 +205,9 @@ answer to where a push would land.
204
205
 
205
206
  ## Webhooks point at the deployed URL
206
207
 
207
- `npx ablo dev` forwards commits to your machine while you build, the way
208
- `stripe listen` does. A deployed endpoint is registered once, and Ablo returns
209
- the signing secret a single time:
208
+ `npx ablo dev` prepares the schema branch; it does not forward webhooks. A
209
+ deployed HTTPS endpoint is registered once, and Ablo returns the signing secret
210
+ a single time:
210
211
 
211
212
  ```bash
212
213
  ABLO_API_KEY=sk_live_… npx ablo webhooks create https://yourapp.com/api/ablo/[...all]
@@ -1,17 +1,14 @@
1
- # AI SDK Tool
1
+ # AI SDK Tools
2
2
 
3
- > Put a claim-and-commit loop inside an AI SDK tool call.
3
+ > Give an AI SDK agent safe access to the same typed Ablo resources as your
4
+ > backend.
4
5
 
5
6
  Use AI SDK for the agent loop and Ablo for the state boundary inside the tool.
6
7
  When an agent updates a shared record from inside a tool call you have a
7
- concurrency problem: another agent may be editing the same row, and a naive write
8
- silently overwrites it. This is the safe pattern read the record, claim the row
9
- so anyone else waits their turn, write through a checked update, and release the
10
- claim automatically.
11
-
12
- Claims don't lock. If another writer holds the row, `claim` waits for them,
13
- re-reads the fresh row, then hands it to you — so two writers serialize instead
14
- of clobbering.
8
+ concurrency problem: another agent or a person may be editing the same row, and
9
+ a naive write can overwrite work the model never saw. Ablo's tool adapters put
10
+ the authoritative read, retry, claim, and confirmed-write behavior behind the
11
+ ordinary AI SDK tool contract.
15
12
 
16
13
  ```ts
17
14
  // app/api/chat/route.ts
@@ -20,11 +17,11 @@ import { defineSchema, model, z as schemaZ } from '@abloatai/ablo/schema';
20
17
  import { anthropic } from '@ai-sdk/anthropic';
21
18
  import {
22
19
  streamText,
23
- tool,
24
20
  convertToModelMessages,
25
21
  stepCountIs,
26
22
  type UIMessage,
27
23
  } from 'ai';
24
+ import { updateTool } from '@abloatai/ablo/ai-sdk';
28
25
  import { z } from 'zod';
29
26
 
30
27
  export const runtime = 'nodejs';
@@ -43,42 +40,19 @@ const ablo = Ablo({
43
40
  transport: 'http',
44
41
  });
45
42
 
46
- const updateTask = tool({
47
- description: 'Update a task in the product database.',
43
+ const updateTask = updateTool(ablo.tasks, {
44
+ title: 'Update task',
45
+ description: 'Update a task without overwriting concurrent work.',
48
46
  inputSchema: z.object({
49
47
  taskId: z.string(),
50
48
  status: z.enum(['todo', 'doing', 'done']).optional(),
51
49
  summary: z.string().optional(),
52
50
  }),
53
- execute: async ({ taskId, status, summary }) => {
54
- await ablo.ready();
55
-
56
- // retrieve hits the server for the latest row (async — await it).
57
- const task = await ablo.tasks.get({ id: taskId });
58
- if (!task) return { ok: false, reason: 'not_found' };
59
-
60
- // If another agent already holds this row, claim waits for them to finish,
61
- // re-reads the fresh row, then hands it back on `claim.data`. The claim is
62
- // released automatically when it goes out of scope.
63
- await using claim = await ablo.tasks.claim({
64
- id: taskId,
65
- description: 'editing',
66
- ttl: '2m',
67
- });
68
-
69
- // Because you hold the claim, this update is rejected if the row changed
70
- // underneath you, instead of silently overwriting it.
71
- const updated = await ablo.tasks.update({
72
- id: claim.data.id,
73
- data: {
74
- status: status ?? claim.data.status,
75
- summary: summary ?? claim.data.summary,
76
- },
77
- wait: 'confirmed',
78
- });
79
-
80
- return { ok: true, task: updated };
81
- },
51
+ id: ({ taskId }) => taskId,
52
+ apply: (current, { status, summary }) => ({
53
+ status: status ?? current.status,
54
+ summary: summary ?? current.summary,
55
+ }),
82
56
  });
83
57
 
84
58
  export async function POST(req: Request) {
@@ -100,13 +74,12 @@ export async function POST(req: Request) {
100
74
 
101
75
  The model provider is interchangeable — swap `anthropic(...)` for any
102
76
  server-bound provider instance. What matters is that the route binds the model on
103
- the server (never trusting one sent in the request body), converts the incoming
104
- `UIMessage[]` with `convertToModelMessages`, and that the tool:
105
-
106
- - reads the latest row with `retrieve` (a server read),
107
- - claims it for exclusive, ordered access if someone else holds it, the claim
108
- waits for them, then re-reads,
109
- - writes through the model resource, which is rejected if the row changed
110
- underneath you,
111
- - waits for confirmation with `wait: 'confirmed'`,
112
- - and auto-releases the claim when the tool returns.
77
+ the server (never trusting one sent in the request body) and converts the
78
+ incoming `UIMessage[]` with `convertToModelMessages`.
79
+
80
+ `updateTool` defaults to a functional update: Ablo re-reads and reapplies the
81
+ patch if another participant writes first. Use `strategy: 'claim'` when the
82
+ model should skip work already owned by someone else, or `strategy: 'queue'`
83
+ when it should wait in Ablo's server-owned FIFO claim queue. The same entrypoint
84
+ also exports `readTool`, `createTool`, and `deleteTool`; deletes require AI SDK
85
+ approval unless the application explicitly disables it.
@@ -69,8 +69,9 @@ database differs by environment — the code is identical.
69
69
  - **Production:** your Postgres. `ablo connect` sets up a scoped writer role and
70
70
  logical replication; your rows live in your database, and Ablo writes to them
71
71
  through that role.
72
- - **Sandbox and local dev:** a separate or local Postgres you can throw away. Same
73
- models, same code, a different database behind them.
72
+ - **Development branches and local dev:** a separate or local Postgres, or a
73
+ branch of the one you already run. Same models, same code, a different
74
+ database behind them.
74
75
  - **Before you connect one.** Ablo keeps state in its own log, so you can build the
75
76
  whole app today and point it at a real database when you're ready.
76
77
 
package/docs/index.md CHANGED
@@ -60,11 +60,14 @@ based on a row that has since changed is turned away rather than applied.
60
60
  stay in your own migrations.
61
61
 
62
62
  ```bash
63
- npx ablo init && npx ablo push
63
+ npx ablo init
64
+ npx ablo dev
64
65
  ```
65
66
 
66
- `push` is the step everything depends on: the server keeps its own copy of the schema, and
67
- until it has yours, a write to a new model fails with `server_execute_unknown_model`.
67
+ `dev` gives the current Git branch an isolated Ablo branch, wires its temporary key,
68
+ pushes the schema, and watches for changes. Until the server has your schema, a write to
69
+ a new model fails with `server_execute_unknown_model`. See
70
+ [Branch-first development](./branch-development.md).
68
71
  </Step>
69
72
 
70
73
  <Step title="Connect the database the rows live in">
@@ -161,6 +164,7 @@ default caller, not a special one.
161
164
 
162
165
  - [Quickstart](./quickstart.md) — make your first coordinated write.
163
166
  - [Integration Guide](./integration-guide.md) — the canonical end-to-end integration.
167
+ - [Integrations](./integrations.md) — long-running tasks, ingestion, and other application-edge runtimes.
164
168
  - [CLI & Migrations](./cli.md) — `init` / `connect` / `push` / `migrate` / `generate`.
165
169
  - [Connect Your Database](./data-sources.md) — where rows land when your own database is canonical.
166
170
  - [Deployment](./deployment.md) — the database, the keys, and the schema push that take an integration to production.
@@ -66,24 +66,17 @@ deterministic demo; it does not call your API key or mutate hosted Ablo data.
66
66
  It is also built for coding agents: copy the sandbox prompt into Claude Code or
67
67
  Codex and ask it to wire one real model through the schema model API.
68
68
 
69
- Use the authenticated org dashboard sandbox for real integration work. The
70
- default sandbox is the equivalent of Stripe test mode:
71
-
72
- - it is scoped to the organization,
73
- - it has an isolated sync group prefix,
74
- - it mints `sk_test_*` keys,
75
- - it can be reset without touching live state,
76
- - additional sandboxes can start blank or from copied live configuration.
77
-
78
- Live keys and sandbox keys are separate. Use `sk_test_*` while wiring your app,
79
- agents, and Data Source endpoint; move to `sk_live_*` only when the same schema
80
- and write path are ready for production.
69
+ Use `npx ablo dev` for real integration work. It derives an immutable branch
70
+ from Git, inherits the parent schema, and writes a temporary branch credential
71
+ to `.env.local`. Each developer or pull request gets independent schema, rows,
72
+ claims, and logs. Use an explicit `sk_live_*` root credential only in the
73
+ reviewed production deployment.
81
74
 
82
75
  When handing this to a coding agent, give it a concrete target:
83
76
 
84
77
  ```txt
85
78
  Add Ablo to this app for one model your agents edit.
86
- Use the org sandbox sk_test_* key. Declare schema, add the Ablo client, replace
79
+ Run npx ablo dev and use its branch-bound key. Declare schema, add the Ablo client, replace
87
80
  one write with ablo.<model>.update(..., { readAt, onStale: 'reject',
88
81
  wait: 'confirmed' }), and add a smoke test for two concurrent writers.
89
82
  ```