@abloatai/ablo 0.35.0 → 0.36.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/CHANGELOG.md +713 -629
- package/README.md +56 -519
- package/bin/ablo.cjs +39 -0
- package/dist/BaseSyncedStore.d.ts +24 -4
- package/dist/BaseSyncedStore.js +53 -37
- package/dist/Database.d.ts +8 -20
- package/dist/Database.js +61 -59
- package/dist/InstanceCache.d.ts +6 -2
- package/dist/InstanceCache.js +18 -16
- package/dist/Model.d.ts +17 -7
- package/dist/Model.js +17 -7
- package/dist/ModelRegistry.d.ts +4 -0
- package/dist/ModelRegistry.js +17 -15
- package/dist/NetworkMonitor.d.ts +3 -1
- package/dist/NetworkMonitor.js +7 -5
- package/dist/SyncClient.d.ts +5 -15
- package/dist/SyncClient.js +60 -57
- package/dist/client/Ablo.js +30 -19
- package/dist/client/createInternalComponents.d.ts +4 -0
- package/dist/client/createInternalComponents.js +8 -2
- package/dist/client/createModelProxy.d.ts +21 -1
- package/dist/client/createModelProxy.js +123 -57
- package/dist/client/humans.d.ts +30 -9
- package/dist/client/humans.js +45 -19
- package/dist/client/reactiveEngine.d.ts +16 -11
- package/dist/client/reactiveEngine.js +113 -335
- package/dist/client/storeCluster.d.ts +47 -0
- package/dist/client/storeCluster.js +118 -0
- package/dist/client/storeLifecycle.d.ts +61 -0
- package/dist/client/storeLifecycle.js +231 -0
- package/dist/context.d.ts +13 -0
- package/dist/context.js +23 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/docs/catalog.js +6 -3
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/query/client.d.ts +3 -0
- package/dist/query/client.js +6 -5
- package/dist/react/AbloProvider.d.ts +13 -1
- package/dist/react/AbloProvider.js +5 -2
- package/dist/react/context.d.ts +2 -2
- package/dist/react/createAbloReact.d.ts +56 -0
- package/dist/react/createAbloReact.js +51 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +3 -0
- package/dist/react/useAblo.d.ts +9 -2
- package/dist/react/useAblo.js +25 -7
- package/dist/schema/coordination.js +5 -1
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +4 -0
- package/dist/schema/select.js +3 -0
- package/dist/schema/serialize.js +3 -0
- package/dist/source/adapter.d.ts +7 -5
- package/dist/source/adapter.js +7 -5
- package/dist/source/index.d.ts +1 -1
- package/dist/source/index.js +1 -1
- package/dist/{core/storeContract.d.ts → storeContract.d.ts} +5 -5
- package/dist/{core → stores}/DatabaseManager.d.ts +3 -1
- package/dist/{core → stores}/DatabaseManager.js +13 -12
- package/dist/{core → stores}/StoreManager.d.ts +5 -3
- package/dist/{core → stores}/StoreManager.js +24 -22
- package/dist/stores/SyncActionStore.d.ts +3 -1
- package/dist/stores/SyncActionStore.js +9 -7
- package/dist/sync/BootstrapFetcher.d.ts +4 -0
- package/dist/sync/BootstrapFetcher.js +28 -26
- package/dist/sync/OnDemandLoader.d.ts +3 -0
- package/dist/sync/OnDemandLoader.js +1 -0
- package/dist/sync/bootstrapApply.d.ts +3 -0
- package/dist/sync/bootstrapApply.js +2 -2
- package/dist/sync/deltaPipeline.d.ts +13 -12
- package/dist/sync/deltaPipeline.js +21 -4
- package/dist/sync/groupChange.d.ts +3 -0
- package/dist/sync/groupChange.js +16 -14
- package/dist/sync/participants.d.ts +19 -2
- package/dist/sync/participants.js +3 -1
- package/dist/sync/schemas.d.ts +2 -1
- package/dist/sync/schemas.js +3 -3
- package/dist/syncLog/contract.d.ts +20 -0
- package/dist/syncLog/contract.js +19 -0
- package/dist/syncLog/index.d.ts +1 -0
- package/dist/syncLog/index.js +1 -0
- package/dist/transaction/auth/capability.d.ts +35 -0
- package/dist/transaction/auth/capability.js +25 -0
- package/dist/transaction/coordination/awaitClaimGrant.d.ts +7 -0
- package/dist/transaction/coordination/awaitClaimGrant.js +12 -0
- package/dist/transaction/coordination/claimHeartbeatLoop.d.ts +34 -0
- package/dist/transaction/coordination/claimHeartbeatLoop.js +20 -0
- package/dist/transaction/coordination/index.d.ts +4 -4
- package/dist/transaction/coordination/index.js +4 -3
- package/dist/transaction/coordination/locator.d.ts +23 -2
- package/dist/transaction/coordination/locator.js +22 -2
- package/dist/transaction/coordination/schema.d.ts +125 -62
- package/dist/transaction/coordination/schema.js +228 -64
- package/dist/transaction/coordination/targetConflict.js +32 -28
- package/dist/transaction/errorCodes.d.ts +2 -2
- package/dist/transaction/errorCodes.js +13 -9
- package/dist/transaction/plugin.d.ts +95 -2
- package/dist/transaction/plugin.js +21 -2
- package/dist/transaction/resources/httpResources.d.ts +57 -2
- package/dist/transaction/resources/modelOperations.d.ts +148 -40
- package/dist/transaction/resources/where.d.ts +16 -0
- package/dist/transaction/resources/where.js +45 -0
- package/dist/transaction/schema/field.d.ts +12 -18
- package/dist/transaction/schema/fieldRef.d.ts +38 -0
- package/dist/transaction/schema/fieldRef.js +11 -0
- package/dist/transaction/schema/openapi.d.ts +16 -15
- package/dist/transaction/schema/openapi.js +186 -25
- package/dist/transaction/schema/relation.d.ts +7 -2
- package/dist/transaction/schema/schema.d.ts +27 -0
- package/dist/transaction/schema/schema.js +20 -0
- package/dist/transaction/transactions/settlement/commitEnvelope.d.ts +1 -1
- package/dist/transaction/transactions/settlement/pendingWrite.d.ts +1 -1
- package/dist/transaction/transport/httpClient.d.ts +9 -1
- package/dist/transaction/transport/httpClient.js +1 -0
- package/dist/transaction/transport/httpTransport.js +156 -44
- package/dist/transaction/transport/wsTransport.d.ts +2 -4
- package/dist/transaction/transport/wsTransport.js +16 -10
- package/dist/transaction/types/streams.d.ts +10 -0
- package/dist/transaction/utils/duration.d.ts +25 -0
- package/dist/transaction/utils/duration.js +32 -0
- package/dist/transaction/wire/accountResponses.d.ts +69 -0
- package/dist/transaction/wire/accountResponses.js +36 -1
- package/dist/transaction/wire/auth.d.ts +9 -2
- package/dist/transaction/wire/auth.js +7 -1
- package/dist/transaction/wire/claims.d.ts +164 -97
- package/dist/transaction/wire/claims.js +126 -28
- package/dist/transaction/wire/commit.d.ts +1 -1
- package/dist/transaction/wire/feedEvent.d.ts +27 -0
- package/dist/transaction/wire/feedEvent.js +27 -1
- package/dist/transaction/wire/frames.d.ts +2 -2
- package/dist/transaction/wire/inboundFrames.d.ts +12 -2
- package/dist/transaction/wire/index.d.ts +10 -6
- package/dist/transaction/wire/index.js +12 -3
- package/dist/transaction/wire/modelMutations.d.ts +31 -0
- package/dist/transaction/wire/modelMutations.js +52 -0
- package/dist/transaction/wire/modelShape.d.ts +78 -0
- package/dist/transaction/wire/modelShape.js +74 -0
- package/dist/transactions/mutations/MutationQueue.d.ts +6 -0
- package/dist/transactions/mutations/MutationQueue.js +55 -45
- package/dist/transactions/mutations/commitPayload.d.ts +3 -2
- package/dist/transactions/mutations/commitPayload.js +5 -5
- package/dist/transactions/mutations/deltaConfirmation.d.ts +4 -0
- package/dist/transactions/mutations/deltaConfirmation.js +10 -8
- package/dist/transactions/mutations/replayValidation.d.ts +2 -1
- package/dist/transactions/mutations/replayValidation.js +3 -2
- package/dist/{core → views}/QueryView.d.ts +1 -1
- package/dist/{core → views}/QueryView.js +1 -1
- package/dist/{core → views}/ViewRegistry.d.ts +1 -1
- package/dist/{core/queryUtils.d.ts → views/incrementalView.d.ts} +6 -6
- package/dist/{core/queryUtils.js → views/incrementalView.js} +6 -6
- package/docs/agents.md +1 -1
- package/docs/api-keys.md +6 -6
- package/docs/api.md +5 -43
- package/docs/audit.md +4 -3
- package/docs/cli.md +11 -11
- package/docs/client-behavior.md +4 -4
- package/docs/concurrency-convention.md +28 -42
- package/docs/coordination.md +235 -83
- package/docs/data-sources.md +4 -4
- package/docs/debugging.md +34 -12
- package/docs/deployment.md +8 -8
- package/docs/examples/scoped-agent.md +3 -3
- package/docs/groups.md +57 -3
- package/docs/guarantees.md +37 -10
- package/docs/how-it-works.md +29 -5
- package/docs/idempotency.md +6 -6
- package/docs/identity.md +22 -23
- package/docs/index.md +8 -8
- package/docs/integration-guide.md +14 -3
- package/docs/mcp.md +7 -7
- package/docs/migration.md +34 -15
- package/docs/projects.md +1 -1
- package/docs/react.md +19 -8
- package/docs/sessions.md +1 -1
- package/docs/webhooks.md +9 -9
- package/llms.txt +4 -4
- package/package.json +13 -20
- package/dist/cli.cjs +0 -288600
- package/dist/testing/fixtures/bootstrap.d.ts +0 -49
- package/dist/testing/fixtures/bootstrap.js +0 -59
- package/dist/testing/fixtures/deltas.d.ts +0 -83
- package/dist/testing/fixtures/deltas.js +0 -136
- package/dist/testing/fixtures/httpResponses.d.ts +0 -70
- package/dist/testing/fixtures/httpResponses.js +0 -90
- package/dist/testing/fixtures/models.d.ts +0 -83
- package/dist/testing/fixtures/models.js +0 -272
- package/dist/testing/helpers/reactWrapper.d.ts +0 -69
- package/dist/testing/helpers/reactWrapper.js +0 -67
- package/dist/testing/helpers/syncEngineHarness.d.ts +0 -54
- package/dist/testing/helpers/syncEngineHarness.js +0 -73
- package/dist/testing/helpers/wait.d.ts +0 -30
- package/dist/testing/helpers/wait.js +0 -49
- package/dist/testing/index.d.ts +0 -23
- package/dist/testing/index.js +0 -33
- package/dist/testing/mocks/FakeDatabase.d.ts +0 -18
- package/dist/testing/mocks/FakeDatabase.js +0 -10
- package/dist/testing/mocks/MockMutationExecutor.d.ts +0 -87
- package/dist/testing/mocks/MockMutationExecutor.js +0 -186
- package/dist/testing/mocks/MockNetworkMonitor.d.ts +0 -20
- package/dist/testing/mocks/MockNetworkMonitor.js +0 -46
- package/dist/testing/mocks/MockSyncContext.d.ts +0 -51
- package/dist/testing/mocks/MockSyncContext.js +0 -72
- package/dist/testing/mocks/MockSyncStore.d.ts +0 -88
- package/dist/testing/mocks/MockSyncStore.js +0 -171
- package/dist/testing/mocks/MockWebSocket.d.ts +0 -71
- package/dist/testing/mocks/MockWebSocket.js +0 -118
- package/docs/interaction-model.md +0 -99
- /package/dist/{core → query}/QueryProcessor.d.ts +0 -0
- /package/dist/{core → query}/QueryProcessor.js +0 -0
- /package/dist/{core/storeContract.js → storeContract.js} +0 -0
- /package/dist/{core → stores}/openIDBWithTimeout.d.ts +0 -0
- /package/dist/{core → stores}/openIDBWithTimeout.js +0 -0
- /package/dist/{source → transaction}/footprint.d.ts +0 -0
- /package/dist/{source → transaction}/footprint.js +0 -0
- /package/dist/{core → views}/ViewRegistry.js +0 -0
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
-
<strong>
|
|
6
|
+
<strong>Collaboration infrastructure for AI agents.</strong>
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
@@ -23,552 +23,89 @@
|
|
|
23
23
|
|
|
24
24
|
---
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
one agent clobbers another, or acts on data that already moved. Ablo is the
|
|
29
|
-
infrastructure that lets the fleet work as one — the load-bearing coordination
|
|
30
|
-
layer the way operational-transform and presence sit invisibly under a shared
|
|
31
|
-
document. You build the agents; Ablo is the substrate that lets them run together
|
|
32
|
-
on shared state without stepping on each other, with the people and server
|
|
33
|
-
actions alongside them on the exact same path.
|
|
26
|
+
Every write to your data, whether it comes from a person, a server, or an agent,
|
|
27
|
+
arrives coordinated with the others and stays attributed afterward.
|
|
34
28
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
line, re-reads the fresh row, then hands it over. No stale overwrite, no separate
|
|
40
|
-
agent mutation path. People are exempt by policy: a human edit is never made to
|
|
41
|
-
queue behind an agent, and by declared conflict rules always wins.
|
|
29
|
+
We work on the same things by looking and talking. You see someone's cursor in
|
|
30
|
+
the paragraph, so you wait, or you say you'll take the first half. None of that
|
|
31
|
+
is in the software. It's just what people do, and the software only has to show
|
|
32
|
+
them enough to do it.
|
|
42
33
|
|
|
43
|
-
|
|
44
|
-
the
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
Under the hood, you define your data once with a Zod schema and get the same
|
|
50
|
-
typed model client for every actor — people, server actions, and agents:
|
|
34
|
+
Agents have neither. Two of them read the same row, think for thirty seconds,
|
|
35
|
+
and the second one writes over the first, and nobody finds out. The writes
|
|
36
|
+
aren't even the main part: an agent acts on what it read, so if any of it moved
|
|
37
|
+
while it was thinking, it does the wrong thing without colliding with anyone at
|
|
38
|
+
all. That probably doesn't get better as the models get better.
|
|
51
39
|
|
|
52
40
|
```ts
|
|
53
|
-
|
|
54
|
-
await ablo.
|
|
55
|
-
await ablo.weatherReports.update({ id, data }) // update
|
|
56
|
-
await using claim = await ablo.weatherReports.claim({ id }) // hold for slow agent work
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
The schema is the public contract. It gives you typed model methods, realtime
|
|
60
|
-
fanout, React selectors, agent writes, and the HTTP/Data Source shape for
|
|
61
|
-
non-JavaScript services. Every confirmed change shows up everywhere, and active
|
|
62
|
-
claims are visible while the work is still in progress.
|
|
63
|
-
|
|
64
|
-
**[Get started](#set-up)** · point your coding agent at the shipped
|
|
65
|
-
`llms.txt` · **upgrading?** see the
|
|
66
|
-
[Version History & Migration Guide](./docs/migration.md)
|
|
67
|
-
|
|
68
|
-
It works with the auth and database you already have. **In production, your
|
|
69
|
-
database is the system of record.** You write through Ablo, and Ablo writes to
|
|
70
|
-
your Postgres: the call enters Ablo's commit chokepoint — where claims, ordering,
|
|
71
|
-
and idempotency are enforced — and lands in your own tables through a scoped
|
|
72
|
-
writer role. The commit is accepted (`queued`) the moment Ablo takes it; when the
|
|
73
|
-
row surfaces in your write-ahead log, the receipt is promoted to `confirmed`.
|
|
74
|
-
**The WAL echo is how Ablo confirms, not how it writes** — your database, not
|
|
75
|
-
Ablo, is the source of truth for row state, and that same stream is what keeps
|
|
76
|
-
every connected client current, scoped to *sync groups* from your own identity.
|
|
77
|
-
|
|
78
|
-
The writer role is non-superuser and cannot bypass RLS. Before each write Ablo
|
|
79
|
-
sets your tenant context on the connection, so your own row-level security
|
|
80
|
-
policies enforce against Ablo exactly as they do against your app. Ablo runs no
|
|
81
|
-
DDL and owns no schema — your migration tool stays in charge of the shape of your
|
|
82
|
-
database, and Ablo holds only the ordered transaction log and the coordination
|
|
83
|
-
state, never your rows. (Trying Ablo with no database
|
|
84
|
-
yet? A **sandbox** `sk_test` key holds throwaway **test data** — like Stripe test
|
|
85
|
-
mode — so you can explore before pointing it at your Postgres. Test-mode only; in
|
|
86
|
-
production every row lives in your database.)
|
|
87
|
-
|
|
88
|
-
**Built for** fleets of agents working a shared backlog, AI agent workflows on
|
|
89
|
-
your own infrastructure, collaborative editors where agents and people co-edit,
|
|
90
|
-
and internal tools — anywhere agents (and the people alongside them) change
|
|
91
|
-
shared state and everyone has to see it live.
|
|
92
|
-
|
|
93
|
-
## Set up
|
|
94
|
-
|
|
95
|
-
The CLI takes you from nothing to a synced schema — it handles the account,
|
|
96
|
-
the key, and the env file. You bring one thing: a Postgres you already have —
|
|
97
|
-
the same `DATABASE_URL` (local, Neon, RDS — any will do) that backs your auth,
|
|
98
|
-
audit, and log tables. Ablo syncs a *subset* of models against it; **in
|
|
99
|
-
production, your database is the system of record**.
|
|
100
|
-
|
|
101
|
-
```bash
|
|
102
|
-
npm install @abloatai/ablo
|
|
103
|
-
npx ablo login # opens the browser: sign in (or sign up) → a sk_test_ key is saved locally
|
|
104
|
-
npx ablo init # scaffolds ablo/schema.ts (offers to log in if you skipped it)
|
|
105
|
-
npx ablo push # pushes your schema (sandbox), writes ABLO_API_KEY to .env.local
|
|
106
|
-
npx ablo dev # the same push, watching ablo/schema.ts and re-pushing on save
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
Then point Ablo at the tables for your synced models. Most teams **already
|
|
110
|
-
have those tables** (often Prisma- or Drizzle-managed) — adopt them with
|
|
111
|
-
`npx ablo pull` / `npx ablo check`, the common case. Let Ablo own its own
|
|
112
|
-
tables instead? `npx ablo migrate` provisions them in your Postgres (reads
|
|
113
|
-
`DATABASE_URL`). Either way your other tables are left untouched.
|
|
114
|
-
|
|
115
|
-
After `ablo push`, the [Quick Start](#quick-start) below runs as-is —
|
|
116
|
-
`ABLO_API_KEY` is already in `.env.local` (frameworks load it automatically;
|
|
117
|
-
plain Node: `node --env-file=.env.local app.ts`). `npx ablo status` shows
|
|
118
|
-
what's configured at any time.
|
|
119
|
-
|
|
120
|
-
**Keys & runtime.** Ablo needs Node 24+ and TypeScript 5+. Keys come in two of
|
|
121
|
-
*your* environments — `sk_test_` and `sk_live_`, like Stripe — and `ablo login`
|
|
122
|
-
mints both. Keep the key in trusted server runtimes only. In the browser,
|
|
123
|
-
`<AbloProvider>` authenticates with the signed-in user's session — never the raw
|
|
124
|
-
key. Your database is connected once, out of band, via `npx ablo connect`
|
|
125
|
-
(logical replication); if it can't grant a replication role, expose a signed
|
|
126
|
-
[Data Source endpoint](./docs/data-sources.md) instead.
|
|
127
|
-
|
|
128
|
-
For production (React, an existing backend, Data Source, agents), the
|
|
129
|
-
[Integration Guide](./docs/integration-guide.md) is the deeper map.
|
|
130
|
-
|
|
131
|
-
**Prefer to let an agent wire it?** The package ships an `llms.txt` — a precise
|
|
132
|
-
map of the API — so Claude Code or Cursor integrates from the real surface
|
|
133
|
-
instead of guessing:
|
|
41
|
+
// Two agents reprice the same order. One gets it at a time.
|
|
42
|
+
await using claim = await ablo.orders.claim({ id: orderId });
|
|
134
43
|
|
|
135
|
-
|
|
44
|
+
const order = claim.data; // the current row, not the one you read a minute ago
|
|
45
|
+
const priced = await pricingAgent(order); // slow: an LLM call, a vendor API
|
|
136
46
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
```ts
|
|
143
|
-
import Ablo from '@abloatai/ablo';
|
|
144
|
-
import { defineSchema, model, z } from '@abloatai/ablo/schema';
|
|
145
|
-
|
|
146
|
-
const schema = defineSchema({
|
|
147
|
-
// id, createdAt, updatedAt, organizationId, createdBy come free on every model
|
|
148
|
-
weatherReports: model({
|
|
149
|
-
location: z.string(),
|
|
150
|
-
status: z.enum(['pending', 'ready']),
|
|
151
|
-
forecast: z.string().optional(),
|
|
152
|
-
}),
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
156
|
-
await ablo.ready();
|
|
157
|
-
|
|
158
|
-
const created = await ablo.weatherReports.create({
|
|
159
|
-
data: { location: 'Stockholm', status: 'pending' },
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
// Claim the row before slow work — anyone else waits in line, then re-reads.
|
|
163
|
-
await using claim = await ablo.weatherReports.claim({ id: created.id });
|
|
164
|
-
const forecast = await fetchForecast(claim.data.location); // slow: API or LLM call
|
|
165
|
-
await ablo.weatherReports.update({
|
|
166
|
-
id: created.id,
|
|
167
|
-
data: { status: 'ready', forecast },
|
|
168
|
-
claim, // the write completes the claimed work and releases the lease
|
|
47
|
+
await ablo.orders.update({
|
|
48
|
+
id: order.id,
|
|
49
|
+
data: { total: priced.total, discount: priced.discount, status: 'repriced' },
|
|
50
|
+
claim,
|
|
169
51
|
});
|
|
170
|
-
|
|
171
|
-
const ready = ablo.weatherReports.local.retrieve(created.id);
|
|
172
|
-
console.log({ id: ready?.id, status: ready?.status });
|
|
173
|
-
|
|
174
|
-
await ablo.dispose();
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
Expected output:
|
|
178
|
-
|
|
179
|
-
```txt
|
|
180
|
-
{ id: '...', status: 'ready' }
|
|
181
52
|
```
|
|
182
53
|
|
|
183
|
-
|
|
54
|
+
The second agent waits its turn, then gets handed the order as it now stands. If
|
|
55
|
+
the pricing call throws, the row frees on the way out, unchanged. A person
|
|
56
|
+
editing that order in the UI is in the same line as the agents.
|
|
184
57
|
|
|
185
|
-
|
|
186
|
-
schema to the SDK's types once — the same declaration-merging shape
|
|
187
|
-
[TanStack Router uses](https://tanstack.com/router/latest/docs/framework/react/guide/type-safety) —
|
|
188
|
-
so every hook and client infers from it:
|
|
58
|
+
And the write it eventually makes is signed:
|
|
189
59
|
|
|
190
60
|
```ts
|
|
191
|
-
// ablo/register.ts — scaffolded by `npx ablo init`
|
|
192
|
-
import type { schema } from './schema';
|
|
193
|
-
declare module '@abloatai/ablo' {
|
|
194
|
-
interface Register { Schema: typeof schema }
|
|
195
|
-
}
|
|
196
|
-
export {};
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
```ts
|
|
200
|
-
import type { Model } from '@abloatai/ablo/schema';
|
|
201
|
-
|
|
202
|
-
type WeatherReport = Model<'weatherReports'>; // fully typed from your schema
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
To pass the client around, take the type from the value — the tRPC /
|
|
206
|
-
Drizzle idiom:
|
|
207
|
-
|
|
208
|
-
```ts
|
|
209
|
-
export const sync = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
210
|
-
export type Sync = typeof sync;
|
|
211
|
-
|
|
212
|
-
function persist(client: Sync) { /* ... */ }
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
## Reading
|
|
216
|
-
|
|
217
|
-
Two ways to read, depending on whether you can wait. `retrieve({ id })` /
|
|
218
|
-
`list({ where })` answer from what's local and go ask the server when they have
|
|
219
|
-
to, so they return a `Promise`. Put `local.` in front and the read is restricted
|
|
220
|
-
to what's already here — instant, reactive in render, and what your UI uses.
|
|
221
|
-
|
|
222
|
-
```ts
|
|
223
|
-
ablo.weatherReports.local.retrieve('report_stockholm');
|
|
224
|
-
|
|
225
|
-
const pending = ablo.weatherReports.local.list({
|
|
226
|
-
where: { status: 'pending' },
|
|
227
|
-
orderBy: { location: 'asc' },
|
|
228
|
-
limit: 20,
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
const ready = await ablo.weatherReports.list({
|
|
232
|
-
where: { status: 'ready' },
|
|
233
|
-
type: 'complete',
|
|
234
|
-
});
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
An array value in `where` means `IN`. On `list`, `type: 'complete'` waits for
|
|
238
|
-
the server; `'unknown'` returns what's local now and refreshes in the background.
|
|
239
|
-
|
|
240
|
-
## Writing
|
|
241
|
-
|
|
242
|
-
`create` / `update` apply optimistically and resolve to the row. Two options
|
|
243
|
-
matter day to day:
|
|
244
|
-
|
|
245
|
-
| Option | Values | What it does |
|
|
246
|
-
| --- | --- | --- |
|
|
247
|
-
| `wait` | `'queued'` \| `'confirmed'` | `'confirmed'` resolves only after the server acks the write; `'queued'` resolves as soon as it's locally queued (fire-and-forget). |
|
|
248
|
-
| `idempotencyKey` | `string` | Auto-generated per call. Override only when you own the retry boundary (e.g. a job id) so a re-run dedupes server-side. |
|
|
249
|
-
|
|
250
|
-
```ts
|
|
251
|
-
await ablo.weatherReports.update({ id, data: { status: 'ready' }, wait: 'confirmed' });
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
To guard a write against a row that changed under you, pass `readAt` + `onStale`
|
|
255
|
-
— see [Coordinating long agent work](#coordinating-long-agent-work).
|
|
256
|
-
|
|
257
|
-
## Coordinating long agent work
|
|
258
|
-
|
|
259
|
-
An agent reads a row, thinks for 30s, writes back — and clobbers whatever changed
|
|
260
|
-
meanwhile, or worse, acts on stale state. `claim` holds the row across that gap:
|
|
261
|
-
|
|
262
|
-
```ts
|
|
263
|
-
await using claim = await ablo.weatherReports.claim({ id: 'report_stockholm' });
|
|
264
|
-
const report = claim.data;
|
|
265
|
-
const forecast = await weatherAgent.getWeather(report.location);
|
|
266
|
-
await ablo.weatherReports.update({
|
|
267
|
-
id: report.id,
|
|
268
|
-
data: { forecast, status: 'ready' },
|
|
269
|
-
claim, // attribute the write to the held claim
|
|
270
|
-
});
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
If someone else holds the row, `claim()` waits in a fair queue, then re-reads —
|
|
274
|
-
so `report` is the current row, never a stale snapshot. Reads stay open by
|
|
275
|
-
default; only acting on the row serializes. The claim releases when the `await
|
|
276
|
-
using` scope exits — on return, and on a throw. That "on throw" is why `await
|
|
277
|
-
using` earns its keep: if the agent call fails before the write, the row frees
|
|
278
|
-
for the next in line and stays exactly as it was, with no cleanup of your own.
|
|
279
|
-
|
|
280
|
-
See who's mid-edit before you act — decide to wait, or skip:
|
|
281
|
-
|
|
282
|
-
```ts
|
|
283
|
-
ablo.weatherReports.claim.state({ id: 'report_stockholm' });
|
|
284
|
-
ablo.weatherReports.claim.queue({ id: 'report_stockholm' });
|
|
285
|
-
|
|
286
|
-
{
|
|
287
|
-
await using claim = await ablo.weatherReports.claim({ id, queue: false });
|
|
288
|
-
/* do the held work */
|
|
289
|
-
}
|
|
290
|
-
|
|
291
61
|
{
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
behind it. `queue: false` skips rather than waiting when the row is held;
|
|
299
|
-
`maxQueueDepth: 2` bails when two or more are already ahead.
|
|
300
|
-
|
|
301
|
-
Default reads keep working while a row is claimed. Server reads that need claimed
|
|
302
|
-
semantics can opt in with `ifClaimed: 'return' | 'fail'`.
|
|
303
|
-
|
|
304
|
-
Even an unclaimed write can't land on stale reasoning — the commit is guarded:
|
|
305
|
-
|
|
306
|
-
```ts
|
|
307
|
-
try {
|
|
308
|
-
await ablo.weatherReports.update({ id, data: { status: 'ready' }, readAt, onStale: 'reject' });
|
|
309
|
-
} catch (e) {
|
|
310
|
-
if (e instanceof AbloStaleContextError) { /* row moved under you — re-read, retry */ }
|
|
311
|
-
}
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
> Use `await using` for ordinary held work — the claim releases when the scope
|
|
315
|
-
> exits. Call `claim.release({ id })` only to give a manually held claim back
|
|
316
|
-
> early.
|
|
317
|
-
|
|
318
|
-
See [Coordination](./docs/coordination.md) for the full `claim` / `claim.state` /
|
|
319
|
-
`claim.queue` / `claim.release` reference.
|
|
320
|
-
|
|
321
|
-
## Background workers — jobs that run for minutes, not seconds
|
|
322
|
-
|
|
323
|
-
Your API route enqueues a job on your own queue (SQS, EventBridge, anything);
|
|
324
|
-
a worker on your own infrastructure does the slow part. **Keep that queue —
|
|
325
|
-
Ablo is the worker's data layer.** It covers the two things every queue leaves
|
|
326
|
-
to you: keeping the row safe, and showing progress live.
|
|
327
|
-
|
|
328
|
-
Long work holds its claim by **heartbeating** — the same pattern as an SQS
|
|
329
|
-
visibility heartbeat or a Temporal activity heartbeat:
|
|
330
|
-
|
|
331
|
-
```ts
|
|
332
|
-
// on your worker — stateless HTTP, no socket to hold
|
|
333
|
-
await using claim = await ablo.weatherReports.claim({
|
|
334
|
-
id: msg.reportId,
|
|
335
|
-
ttl: '10m',
|
|
336
|
-
heartbeat: true, // beats automatically until release
|
|
337
|
-
onHeartbeatLost: () => abort(), // the lease is gone → stop working
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
for (const step of steps) {
|
|
341
|
-
await runStep(step);
|
|
342
|
-
// write progress to the row — every subscribed UI updates live
|
|
343
|
-
await ablo.weatherReports.update({ id: msg.reportId, data: { progress: step.pct }, claim });
|
|
344
|
-
}
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
- A worker that **crashes** stops beating; the row frees within one beat and
|
|
348
|
-
the next worker takes over.
|
|
349
|
-
- A worker that **wakes up late** learns it on its next beat
|
|
350
|
-
(`AbloClaimedError`), and the write path rejects its stale writes anyway.
|
|
351
|
-
- **Retries stay on your queue** — the redelivered job claims the now-free
|
|
352
|
-
row, reads its current state, and resumes.
|
|
353
|
-
- A worker holding **many rows** extends them all in one call:
|
|
354
|
-
`ablo.claims.heartbeatAll({ ttl: '5m' })`.
|
|
355
|
-
|
|
356
|
-
SQS's heartbeat protects the *message* from redelivery; Ablo's protects the
|
|
357
|
-
*row* from concurrent and stale writes. SQS is at-least-once, so two workers
|
|
358
|
-
will eventually get the same job — the claim is what keeps that from
|
|
359
|
-
corrupting data.
|
|
360
|
-
|
|
361
|
-
## React
|
|
362
|
-
|
|
363
|
-
In a React app it's the **same `ablo.<model>` API** — just mounted through a
|
|
364
|
-
provider and read with hooks, from `@abloatai/ablo/react`. Wrap your tree once;
|
|
365
|
-
everything inside is live.
|
|
366
|
-
|
|
367
|
-
```tsx
|
|
368
|
-
import Ablo from '@abloatai/ablo';
|
|
369
|
-
import { AbloProvider, useAblo } from '@abloatai/ablo/react';
|
|
370
|
-
import { schema } from './ablo/schema';
|
|
371
|
-
|
|
372
|
-
// Build the client once — authEndpoint is your session route; no key in the browser.
|
|
373
|
-
const ablo = Ablo({
|
|
374
|
-
schema,
|
|
375
|
-
authEndpoint: '/api/ablo-session',
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
function App() {
|
|
379
|
-
return (
|
|
380
|
-
<AbloProvider client={ablo}>
|
|
381
|
-
<Report id="report_stockholm" />
|
|
382
|
-
</AbloProvider>
|
|
383
|
-
);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
function Report({ id }: { id: string }) {
|
|
387
|
-
const report = useAblo((ablo) => ablo.weatherReports.local.retrieve(id));
|
|
388
|
-
const ablo = useAblo();
|
|
389
|
-
|
|
390
|
-
if (!report) return null;
|
|
391
|
-
|
|
392
|
-
return (
|
|
393
|
-
<button onClick={() => ablo?.weatherReports.update({ id, data: { status: 'ready' } })}>
|
|
394
|
-
{report.status}
|
|
395
|
-
</button>
|
|
396
|
-
);
|
|
62
|
+
actorKind: 'agent', // 'user' | 'agent' | 'system'
|
|
63
|
+
actorId: 'agent_pricing',
|
|
64
|
+
onBehalfOfKind: 'user',
|
|
65
|
+
onBehalfOfId: 'user_amir', // the person the agent acted for
|
|
66
|
+
capabilityId: 'key_live_ops',
|
|
67
|
+
confirmationState: 'approved', // ran on its own, was previewed, or was signed off
|
|
397
68
|
}
|
|
398
69
|
```
|
|
399
70
|
|
|
400
|
-
|
|
401
|
-
a
|
|
402
|
-
method as the server example above.
|
|
403
|
-
|
|
404
|
-
`<AbloProvider>` owns the connection — no API key in the browser. That's the
|
|
405
|
-
whole loop: read with `useAblo(selector)`, write with `ablo.<model>`, and every
|
|
406
|
-
other client (agent or human) on that row sees it in real time. See
|
|
407
|
-
[React](./docs/react.md) for the `<AbloProvider>` prop surface (`client`,
|
|
408
|
-
`userId`, `fallback`, `onError`) — schema, scope, and team membership live on the
|
|
409
|
-
`Ablo({ … })` client you pass it — plus status hooks.
|
|
410
|
-
|
|
411
|
-
## Identity & Sync Groups
|
|
412
|
-
|
|
413
|
-
Ablo is **not** an auth provider — you keep your own (Clerk, Auth0, NextAuth,
|
|
414
|
-
whatever). Ablo's job starts after you've authenticated a request: you tell it
|
|
415
|
-
*who* is connecting, and it scopes their realtime data to the right **sync
|
|
416
|
-
groups** (named channels like `org:acme` or `workspace:abc123` that are both the unit
|
|
417
|
-
of fan-out and the unit of access).
|
|
418
|
-
|
|
419
|
-
The model is a proxy: your `ABLO_API_KEY` stays on your trusted server, your
|
|
420
|
-
server resolves the signed-in user (org / team / user) from your own auth, and
|
|
421
|
-
the browser connects as an already-scoped participant — it never holds the key
|
|
422
|
-
and can't widen its own scope. Your schema's `identityRoles` map that identity
|
|
423
|
-
to sync-group strings.
|
|
424
|
-
|
|
425
|
-
`userId` / `teamIds` come from your auth, resolved server-side:
|
|
426
|
-
|
|
427
|
-
```tsx
|
|
428
|
-
// team membership is asserted server-side when the session route mints the token.
|
|
429
|
-
const ablo = Ablo({
|
|
430
|
-
schema,
|
|
431
|
-
authEndpoint: '/api/ablo-session',
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
<AbloProvider client={ablo} userId={user.id}>
|
|
435
|
-
<App />
|
|
436
|
-
</AbloProvider>
|
|
437
|
-
```
|
|
438
|
-
|
|
439
|
-
If it isn't obvious where org / team / user come from in the Quick Start above,
|
|
440
|
-
that's because they come from *your* app — see
|
|
441
|
-
[Identity & Sync Groups](./docs/identity.md) for the full picture: what a sync
|
|
442
|
-
group is, the two halves of scoping (`identityRoles` + per-model `orgScoped` /
|
|
443
|
-
`syncGroupFormat`), and how identity reaches Ablo without an API key in the
|
|
444
|
-
browser.
|
|
445
|
-
|
|
446
|
-
## Multiplayer
|
|
447
|
-
|
|
448
|
-
There is no separate multiplayer mode. When agent workers, server actions, and
|
|
449
|
-
human UI share the same schema and write through `ablo.<model>`, they all see
|
|
450
|
-
each other's changes in real time — that's the default, not a feature you turn on.
|
|
451
|
-
|
|
452
|
-
- `ablo.<model>.create/update/delete` fan out confirmed deltas to subscribers.
|
|
453
|
-
- `useAblo(...)` gives React clients the live row, kept current automatically.
|
|
454
|
-
- `ablo.<model>.claim({ id })` / `claim.state({ id })` / `claim.queue({ id })` let agents and people coordinate (and observe) active work on a row — and the line waiting behind it — before a write lands.
|
|
455
|
-
|
|
456
|
-
The bare client is the coordination layer: commit, read, observe, claim. The live
|
|
457
|
-
plane people watch — presence, live queries, the local copy — is the `humans()`
|
|
458
|
-
plugin on top of it, installed by default on a socket client. There is no
|
|
459
|
-
`agents()` plugin, and the absence is the point: an agent is the default caller
|
|
460
|
-
here, not a special one.
|
|
71
|
+
Every committed change lands in an audit log carrying that attribution, chained
|
|
72
|
+
with a keyed hash so any later alteration shows up. You write none of it.
|
|
461
73
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
One surface for agents, servers, and people; one place coordination happens.
|
|
74
|
+
- **Coordination.** Claims, a fair queue, and stale-write rejection, so slow agent work can't land on a moved row.
|
|
75
|
+
- **Realtime.** Every confirmed change reaches every connected client, agent or human, with no separate multiplayer mode to enable.
|
|
76
|
+
- **Your database and your auth stay yours.** Rows live in your Postgres under your own security policies; Ablo runs no migrations and owns no schema. Bring Clerk, Auth0, NextAuth, whatever you have.
|
|
77
|
+
- **A history you can answer questions from.** Who changed what, on whose behalf, with which key, and whether a human approved it.
|
|
467
78
|
|
|
468
|
-
##
|
|
469
|
-
|
|
470
|
-
Use the SDK when you are in JavaScript and want typed models or realtime. Use the
|
|
471
|
-
HTTP endpoint when a server-to-server caller needs to write without opening a
|
|
472
|
-
WebSocket:
|
|
79
|
+
## Start
|
|
473
80
|
|
|
474
81
|
```bash
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
{ "action": "update", "model": "weatherReports", "id": "report_stockholm", "data": { "status": "ready" } }
|
|
480
|
-
] }'
|
|
481
|
-
```
|
|
482
|
-
|
|
483
|
-
```json
|
|
484
|
-
{ "object": "commit_receipt", "status": "confirmed", "serverTxId": "tx_…", "lastSyncId": 1042, "ops": 1 }
|
|
82
|
+
npm install @abloatai/ablo
|
|
83
|
+
npx ablo login # sign in; saves a test key
|
|
84
|
+
npx ablo init # scaffolds your schema
|
|
85
|
+
npx ablo push # writes ABLO_API_KEY to .env.local, and you're running
|
|
485
86
|
```
|
|
486
87
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
In production, every schema model is backed by **your own database**, and that's
|
|
490
|
-
where your rows live. You write through Ablo; it lands each change in your Postgres
|
|
491
|
-
through a scoped role, then tails the WAL to confirm it and fan it out. Ablo holds
|
|
492
|
-
the ordered transaction log and coordination — never your rows. Two ways it
|
|
493
|
-
connects:
|
|
494
|
-
|
|
495
|
-
| | How Ablo connects to your Postgres | Use when |
|
|
496
|
-
| --- | --- | --- |
|
|
497
|
-
| **`ablo connect`** (primary) | Sets up logical replication and a scoped writer role (`npx ablo connect apply` does it end to end). Ablo writes your rows through the writer role and reads them back over the WAL to confirm — it writes rows but runs no DDL and owns no schema. The role is non-superuser and cannot bypass RLS, so your own policies govern Ablo's writes. | Your database can grant a `REPLICATION` role (most can). |
|
|
498
|
-
| **Signed endpoint** (fallback) | Your app exposes one route built from an ORM adapter (`prismaDataSource` / `drizzleDataSource`); Ablo writes and confirms through it. Needs no replication setup. | Your database **can't** grant a replication role (a locked-down managed DB). |
|
|
499
|
-
|
|
500
|
-
Your database is the system of record. See
|
|
501
|
-
[Connect Your Database](./docs/data-sources.md).
|
|
502
|
-
|
|
503
|
-
## Configuration
|
|
504
|
-
|
|
505
|
-
`Ablo({ ... })` takes your schema and your key. Your database is connected
|
|
506
|
-
**out of band** — once, via `npx ablo connect` (logical replication) or a signed
|
|
507
|
-
[Data Source endpoint](./docs/data-sources.md) — not through the constructor.
|
|
508
|
-
Every other option has correct defaults:
|
|
88
|
+
Point it at your own Postgres when you're ready with `npx ablo connect`.
|
|
509
89
|
|
|
510
|
-
|
|
511
|
-
| --- | --- | --- | --- |
|
|
512
|
-
| `schema` | `Schema` | — (required) | Typed model proxies (`ablo.<model>.*`) |
|
|
513
|
-
| `apiKey` | `string \| ApiKeySetter \| null` | `process.env.ABLO_API_KEY` | Server key — a string, or an async function for rotation |
|
|
90
|
+
## Docs
|
|
514
91
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
(custom `fetch`, logging, observability, transport overrides) live in
|
|
519
|
-
[Client Behavior](./docs/client-behavior.md).
|
|
520
|
-
|
|
521
|
-
## Errors
|
|
522
|
-
|
|
523
|
-
Every SDK error extends `AbloError` and carries a `requestId` for support.
|
|
524
|
-
Discriminate with `instanceof` or the `type` string — the string form also
|
|
525
|
-
survives worker / `postMessage` boundaries, where `instanceof` does not:
|
|
526
|
-
|
|
527
|
-
```ts
|
|
528
|
-
try {
|
|
529
|
-
await ablo.weatherReports.update({ id, data: { status: 'ready' }, readAt, onStale: 'reject' });
|
|
530
|
-
} catch (e) {
|
|
531
|
-
if (e instanceof AbloStaleContextError) { /* row moved under you — re-read, retry */ }
|
|
532
|
-
if ((e as AbloError).type === 'AbloClaimedError') { /* another participant holds it */ }
|
|
533
|
-
}
|
|
92
|
+
```bash
|
|
93
|
+
npx ablo docs # every page, for the version you installed
|
|
94
|
+
npx ablo docs audit # or any one of them
|
|
534
95
|
```
|
|
535
96
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
| `AbloAuthenticationError` | Invalid / missing / expired credentials |
|
|
539
|
-
| `AbloPermissionError` / `CapabilityError` | Action forbidden by scope |
|
|
540
|
-
| `AbloRateLimitError` | Rate limited (carries `retryAfterSeconds`) |
|
|
541
|
-
| `AbloIdempotencyError` | Same `idempotencyKey` reused with a different body |
|
|
542
|
-
| `AbloValidationError` | Invalid request payload |
|
|
543
|
-
| `AbloStaleContextError` | Write carried `readAt`, but the row has newer changes (`conflicts`) |
|
|
544
|
-
| `AbloClaimedError` | Target is claimed by another participant (`claims`) |
|
|
545
|
-
| `AbloConnectionError` / `AbloServerError` | Transport failure / server 5xx |
|
|
546
|
-
| `SyncSessionError` | Session expired (prompts re-auth) |
|
|
547
|
-
|
|
548
|
-
## Reconnect & retries
|
|
549
|
-
|
|
550
|
-
The client owns reconnection so your code doesn't have to. A dropped WebSocket
|
|
551
|
-
reconnects automatically with exponential backoff (1s → 30s, ±15% jitter, up to
|
|
552
|
-
~7.5 minutes); session errors (401/403) suppress it so you re-authenticate
|
|
553
|
-
instead of looping. Commits are idempotent by client transaction id, and a
|
|
554
|
-
commit that times out is never silently rolled back — the client reconciles
|
|
555
|
-
against authoritative server state on reconnect. These defaults are the
|
|
556
|
-
contract; there are no retry or timeout knobs to tune.
|
|
97
|
+
Those pages ship inside the package, so they match your install and need no
|
|
98
|
+
network. The same pages are at [docs.abloatai.com](https://docs.abloatai.com).
|
|
557
99
|
|
|
558
|
-
|
|
100
|
+
Building with a coding agent? Point it at `node_modules/@abloatai/ablo/llms.txt`.
|
|
559
101
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
- [Client Behavior](./docs/client-behavior.md) — options, errors, retries, timeouts, and public imports.
|
|
568
|
-
- [Connect Your Database](./docs/data-sources.md) — connect your Postgres by logical replication (`npx ablo connect`) or, as a fallback, a signed endpoint; your database is the system of record either way.
|
|
569
|
-
- [Existing Python Backend](./docs/examples/existing-python-backend.md) — migrate existing Python endpoints to multiplayer and agent-safe writes gradually.
|
|
570
|
-
- [AI SDK Tool](./docs/examples/ai-sdk-tool.md) — use Ablo inside an AI SDK tool call.
|
|
571
|
-
- [Server Agent](./docs/examples/server-agent.md) — schema-backed worker.
|
|
102
|
+
Start with [Quickstart](./docs/quickstart.md) ·
|
|
103
|
+
[Integration Guide](./docs/integration-guide.md) ·
|
|
104
|
+
[Coordination](./docs/coordination.md) ·
|
|
105
|
+
[Audit log](./docs/audit.md) ·
|
|
106
|
+
[Connect your database](./docs/data-sources.md) ·
|
|
107
|
+
[Guarantees](./docs/guarantees.md) ·
|
|
108
|
+
[Migration](./docs/migration.md)
|
|
572
109
|
|
|
573
110
|
## License
|
|
574
111
|
|
package/bin/ablo.cjs
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `npx ablo` — the SDK's bin. The command itself lives in its own package so
|
|
3
|
+
// installing the SDK never downloads the CLI's tooling; this file only finds
|
|
4
|
+
// that package and hands over.
|
|
5
|
+
//
|
|
6
|
+
// Resolution order:
|
|
7
|
+
// 1. An installed CLI — the project's own, or one hoisted beside this SDK.
|
|
8
|
+
// Requiring it runs it: the CLI's entry executes on load and reads
|
|
9
|
+
// process.argv itself, which this shim leaves untouched.
|
|
10
|
+
// 2. `npm exec` — fetches the CLI on demand, so `npx ablo init` in a fresh
|
|
11
|
+
// project keeps working with nothing else installed.
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
// The published name first; the workspace name resolves inside the monorepo.
|
|
16
|
+
const CLI_PACKAGES = ['@abloatai/cli', '@ablo/cli'];
|
|
17
|
+
|
|
18
|
+
for (const name of CLI_PACKAGES) {
|
|
19
|
+
let resolved;
|
|
20
|
+
try {
|
|
21
|
+
resolved = require.resolve(name);
|
|
22
|
+
} catch {
|
|
23
|
+
continue; // not installed under this name — try the next
|
|
24
|
+
}
|
|
25
|
+
require(resolved);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { spawnSync } = require('child_process');
|
|
30
|
+
const result = spawnSync(
|
|
31
|
+
process.platform === 'win32' ? 'npm.cmd' : 'npm',
|
|
32
|
+
['exec', '--yes', '--package=@abloatai/cli', '--', 'ablo', ...process.argv.slice(2)],
|
|
33
|
+
{ stdio: 'inherit', shell: process.platform === 'win32' },
|
|
34
|
+
);
|
|
35
|
+
if (result.error) {
|
|
36
|
+
console.error('Could not run the ablo CLI. Install it with: npm i -D @abloatai/cli');
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
process.exit(result.status ?? 1);
|