@abloatai/ablo 0.10.1 → 0.11.1
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 +34 -0
- package/README.md +63 -23
- package/dist/BaseSyncedStore.d.ts +75 -0
- package/dist/BaseSyncedStore.js +193 -8
- package/dist/Database.d.ts +10 -2
- package/dist/Database.js +15 -1
- package/dist/SyncClient.d.ts +12 -1
- package/dist/SyncClient.js +110 -26
- package/dist/agent/Agent.d.ts +9 -9
- package/dist/agent/Agent.js +16 -16
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.js +2 -2
- package/dist/agent/types.d.ts +1 -1
- package/dist/agent/types.js +1 -1
- package/dist/ai-sdk/{intent-broadcast.d.ts → claim-broadcast.d.ts} +10 -10
- package/dist/ai-sdk/{intent-broadcast.js → claim-broadcast.js} +6 -6
- package/dist/ai-sdk/coordination-context.d.ts +9 -9
- package/dist/ai-sdk/coordination-context.js +8 -8
- package/dist/ai-sdk/index.d.ts +1 -1
- package/dist/ai-sdk/index.js +1 -1
- package/dist/ai-sdk/wrap.d.ts +4 -4
- package/dist/ai-sdk/wrap.js +4 -4
- package/dist/api/index.d.ts +2 -2
- package/dist/cli.cjs +369 -67
- package/dist/client/Ablo.d.ts +30 -63
- package/dist/client/Ablo.js +124 -103
- package/dist/client/ApiClient.d.ts +6 -5
- package/dist/client/ApiClient.js +86 -62
- package/dist/client/auth.d.ts +9 -4
- package/dist/client/auth.js +40 -5
- package/dist/client/createModelProxy.d.ts +41 -54
- package/dist/client/createModelProxy.js +123 -20
- package/dist/client/httpClient.d.ts +2 -0
- package/dist/client/httpClient.js +1 -1
- package/dist/client/index.d.ts +3 -3
- package/dist/client/writeOptionsSchema.d.ts +4 -4
- package/dist/client/writeOptionsSchema.js +4 -4
- package/dist/coordination/schema.d.ts +249 -38
- package/dist/coordination/schema.js +172 -39
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +4 -4
- package/dist/errorCodes.d.ts +9 -9
- package/dist/errorCodes.js +16 -16
- package/dist/errors.d.ts +51 -2
- package/dist/errors.js +94 -5
- package/dist/interfaces/index.d.ts +8 -4
- package/dist/policy/index.d.ts +1 -1
- package/dist/policy/types.d.ts +13 -13
- package/dist/policy/types.js +8 -8
- package/dist/react/AbloProvider.d.ts +51 -4
- package/dist/react/AbloProvider.js +95 -11
- package/dist/react/context.d.ts +26 -9
- package/dist/react/context.js +2 -2
- package/dist/react/index.d.ts +4 -4
- package/dist/react/index.js +4 -4
- package/dist/react/useAblo.js +5 -5
- package/dist/react/{useIntent.d.ts → useClaim.d.ts} +9 -9
- package/dist/react/useClaim.js +42 -0
- package/dist/schema/index.js +1 -1
- package/dist/schema/schema.d.ts +3 -3
- package/dist/schema/sugar.d.ts +3 -3
- package/dist/schema/sugar.js +3 -3
- package/dist/schema/sync-delta-wire.d.ts +8 -8
- package/dist/server/commit.d.ts +2 -2
- package/dist/sync/AreaOfInterestManager.d.ts +162 -0
- package/dist/sync/AreaOfInterestManager.js +233 -0
- package/dist/sync/BootstrapHelper.d.ts +9 -1
- package/dist/sync/BootstrapHelper.js +15 -5
- package/dist/sync/NetworkProbe.d.ts +1 -1
- package/dist/sync/NetworkProbe.js +1 -1
- package/dist/sync/SyncWebSocket.d.ts +59 -25
- package/dist/sync/SyncWebSocket.js +123 -26
- package/dist/sync/awaitClaimGrant.d.ts +40 -0
- package/dist/sync/awaitClaimGrant.js +86 -0
- package/dist/sync/createClaimStream.d.ts +34 -0
- package/dist/sync/{createIntentStream.js → createClaimStream.js} +92 -81
- package/dist/sync/createPresenceStream.js +3 -2
- package/dist/sync/participants.d.ts +10 -10
- package/dist/sync/participants.js +17 -10
- package/dist/sync/schemas.d.ts +8 -8
- package/dist/transactions/TransactionQueue.d.ts +23 -0
- package/dist/transactions/TransactionQueue.js +186 -12
- package/dist/types/global.d.ts +18 -13
- package/dist/types/global.js +11 -6
- package/dist/types/index.d.ts +9 -7
- package/dist/types/index.js +2 -2
- package/dist/types/streams.d.ts +114 -98
- package/dist/types/streams.js +1 -1
- package/dist/utils/asyncIterator.d.ts +1 -1
- package/dist/utils/asyncIterator.js +1 -1
- package/dist/wire/frames.d.ts +2 -2
- package/docs/api.md +3 -3
- package/docs/client-behavior.md +6 -3
- package/docs/coordination.md +13 -3
- package/docs/data-sources.md +29 -9
- package/docs/migration.md +40 -0
- package/docs/quickstart.md +61 -33
- package/docs/react.md +46 -0
- package/llms-full.txt +25 -8
- package/llms.txt +11 -9
- package/package.json +3 -2
- package/dist/react/useIntent.js +0 -42
- package/dist/sync/awaitIntentGrant.d.ts +0 -40
- package/dist/sync/awaitIntentGrant.js +0 -62
- package/dist/sync/createIntentStream.d.ts +0 -34
package/docs/migration.md
CHANGED
|
@@ -11,6 +11,7 @@ change when you upgrade.
|
|
|
11
11
|
|
|
12
12
|
| Version | What changed | What to do |
|
|
13
13
|
|---|---|---|
|
|
14
|
+
| **0.11.0** | `intent` → `claim` rename completed across the React hook, type namespace, and wire frames | `useIntent` → `useClaim`; `Register.Intents` → `Register.Claims`; `Ablo.Intent.*` → `Ablo.Claim.*`. Upgrade client **and** server together (wire frames moved `intent_*` → `claim_*`) |
|
|
14
15
|
| **0.10.0** | Environment enum renamed `test`/`live` → `sandbox`/`production` | Update code that branches on the environment (e.g. source `mode`): `'test'`→`'sandbox'`, `'live'`→`'production'`. Key prefixes `sk_test_`/`sk_live_` are unchanged |
|
|
15
16
|
| **0.9.2** | `turn` primitive + agent-work `tasks` resource removed | Coordinate with `claim`; mint a scoped session instead of `agent().run()` |
|
|
16
17
|
| **0.9.2** | `intents` deprecated in favor of `claim` | Use `ablo.<model>.claim`; `ablo.intents` is now `@internal` |
|
|
@@ -24,6 +25,45 @@ change when you upgrade.
|
|
|
24
25
|
|
|
25
26
|
---
|
|
26
27
|
|
|
28
|
+
## 0.11.0 — `intent` → `claim` rename completed
|
|
29
|
+
|
|
30
|
+
The coordination primitive has been `claim` since 0.9.2, but a few `intent`-named
|
|
31
|
+
surfaces lingered. 0.11.0 finishes the rename. There are three edits, all
|
|
32
|
+
mechanical:
|
|
33
|
+
|
|
34
|
+
**1. React hook.** `useIntent` is now `useClaim` (same signature):
|
|
35
|
+
|
|
36
|
+
```diff
|
|
37
|
+
- import { useIntent } from '@abloatai/ablo/react';
|
|
38
|
+
- const claimEditLayer = useIntent('editLayer');
|
|
39
|
+
+ import { useClaim } from '@abloatai/ablo/react';
|
|
40
|
+
+ const claimEditLayer = useClaim('editLayer');
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**2. Type registration.** The `Register` interface key is `Claims`, not `Intents`:
|
|
44
|
+
|
|
45
|
+
```diff
|
|
46
|
+
declare module '@abloatai/ablo' {
|
|
47
|
+
interface Register {
|
|
48
|
+
- Intents: { editLayer: { slideId: string; layerId: string } };
|
|
49
|
+
+ Claims: { editLayer: { slideId: string; layerId: string } };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**3. Type namespace.** The `Ablo.Intent.*` helper types moved to `Ablo.Claim.*`.
|
|
55
|
+
If you referenced them directly, rename the namespace; the shapes are unchanged.
|
|
56
|
+
|
|
57
|
+
> **Coordinated deploy required.** The on-the-wire frames moved from `intent_*`
|
|
58
|
+
> to `claim_*`. A `claim_*`-aware client cannot coordinate with an `intent_*`
|
|
59
|
+
> server (and vice-versa), so ship the client and server together. If you run a
|
|
60
|
+
> self-managed sync server, deploy it first.
|
|
61
|
+
|
|
62
|
+
Two non-breaking improvements ride along: claim-rejection errors now surface the
|
|
63
|
+
contending holders (`AbloClaimedError.claims` and a policy reason folded into the
|
|
64
|
+
message), and `participantKind` is the canonical `'user' | 'agent' | 'system'`
|
|
65
|
+
on presence and claim state.
|
|
66
|
+
|
|
27
67
|
## 0.10.0 — environment enum `sandbox` / `production`; stateless HTTP transport
|
|
28
68
|
|
|
29
69
|
### Environment enum rename (the only breaking change)
|
package/docs/quickstart.md
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
# Quickstart
|
|
2
2
|
|
|
3
|
-
Build with Ablo on **
|
|
4
|
-
models humans and agents edit together, hand the client your
|
|
5
|
-
`DATABASE_URL
|
|
6
|
-
is the system of record
|
|
7
|
-
layer on top: it registers your connection, commits every write there
|
|
8
|
-
row-level security, and fans the confirmed rows out to every connected
|
|
3
|
+
Build with Ablo on **the Postgres you already have**. You declare a small Ablo
|
|
4
|
+
schema for the models humans and agents edit together, hand the client your
|
|
5
|
+
Postgres `DATABASE_URL` (passed explicitly), and coordinate every write through
|
|
6
|
+
`ablo.<model>`. In production, your database is the system of record. Ablo is the
|
|
7
|
+
transaction layer on top: it registers your connection, commits every write there
|
|
8
|
+
behind row-level security, and fans the confirmed rows out to every connected
|
|
9
|
+
client.
|
|
10
|
+
|
|
11
|
+
> No database yet? The hosted **sandbox** can host rows in Ablo's test plane —
|
|
12
|
+
> pass an `apiKey` only and omit `databaseUrl`, like Stripe test mode — so you can
|
|
13
|
+
> try Ablo before pointing it at your Postgres.
|
|
9
14
|
|
|
10
15
|
## 1. Install and initialize
|
|
11
16
|
|
|
@@ -25,10 +30,12 @@ instead:
|
|
|
25
30
|
export ABLO_API_KEY=sk_test_...
|
|
26
31
|
```
|
|
27
32
|
|
|
28
|
-
Every SDK and CLI call needs a key. Test and live keys work like Stripe's
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
Every SDK and CLI call needs a key. Test and live keys work like Stripe's:
|
|
34
|
+
`sk_test_*` for the sandbox, `sk_live_*` for production. In production a key
|
|
35
|
+
points at the database *you* own; in the sandbox you can skip the database
|
|
36
|
+
entirely and let Ablo's test plane host the rows (apiKey only). There is no
|
|
37
|
+
keyless mode — a key is always required. (The public `/sandbox` page is a
|
|
38
|
+
separate hosted demo, not your app.)
|
|
32
39
|
|
|
33
40
|
## 2. Your Ablo schema (init scaffolded it)
|
|
34
41
|
|
|
@@ -51,18 +58,24 @@ export const schema = defineSchema({
|
|
|
51
58
|
```
|
|
52
59
|
|
|
53
60
|
|
|
54
|
-
|
|
55
|
-
is one parameter away — no `typeof schema` re-stating, anywhere:
|
|
61
|
+
The schema is registered once (init scaffolds `ablo/register.ts` for you), and
|
|
62
|
+
every type is one parameter away — no `typeof schema` re-stating, anywhere:
|
|
56
63
|
|
|
57
64
|
```ts
|
|
58
|
-
// ablo.
|
|
59
|
-
import type { schema } from './
|
|
65
|
+
// ablo/register.ts — scaffolded by `npx ablo init`, sits beside ablo/schema.ts
|
|
66
|
+
import type { schema } from './schema';
|
|
60
67
|
declare module '@abloatai/ablo' {
|
|
61
68
|
interface Register { Schema: typeof schema }
|
|
62
69
|
}
|
|
63
70
|
export {};
|
|
64
71
|
```
|
|
65
72
|
|
|
73
|
+
It's a regular `.ts` module, not a hand-authored `.d.ts`. The top-level
|
|
74
|
+
`import type { schema }` makes the `declare module` block *merge* into (augment)
|
|
75
|
+
the SDK's `Register` interface instead of colliding with it — the same shape
|
|
76
|
+
[TanStack Router uses in `src/router.tsx`](https://tanstack.com/router/latest/docs/framework/react/guide/type-safety). Any `.ts` file in your
|
|
77
|
+
`tsconfig` `include` works; it never needs to be imported.
|
|
78
|
+
|
|
66
79
|
```ts
|
|
67
80
|
import type { Model } from '@abloatai/ablo/schema';
|
|
68
81
|
|
|
@@ -73,6 +86,12 @@ type WeatherReport = Model<'weatherReports'>; // fully typed from YOUR schema
|
|
|
73
86
|
TanStack-Router pattern: declare the source of truth once, everything
|
|
74
87
|
infers from it.)
|
|
75
88
|
|
|
89
|
+
When you need to name the client type — to pass it to a function or store it in
|
|
90
|
+
a context — **infer it from the value**: `type Sync = typeof sync`. That's the
|
|
91
|
+
same idiom as tRPC's `typeof appRouter` and Drizzle's `typeof db`; it resolves
|
|
92
|
+
the typed overload at the call site. Avoid `ReturnType<typeof Ablo>`, which
|
|
93
|
+
collapses to the untyped client.
|
|
94
|
+
|
|
76
95
|
## 3. Point Ablo at your database
|
|
77
96
|
|
|
78
97
|
The client takes your schema, your key, and your `DATABASE_URL`. On first
|
|
@@ -93,10 +112,14 @@ import { schema } from './schema';
|
|
|
93
112
|
export const ablo = Ablo({
|
|
94
113
|
schema,
|
|
95
114
|
apiKey: process.env.ABLO_API_KEY,
|
|
96
|
-
databaseUrl: process.env.DATABASE_URL, // your Postgres — rows live here
|
|
115
|
+
databaseUrl: process.env.DATABASE_URL, // your Postgres, passed explicitly — rows live here
|
|
97
116
|
});
|
|
98
117
|
```
|
|
99
118
|
|
|
119
|
+
`databaseUrl` is not auto-read from the environment — you pass it explicitly
|
|
120
|
+
(as above). If a `DATABASE_URL` is set for another tool, `Ablo()` ignores it
|
|
121
|
+
unless you wire it in like this.
|
|
122
|
+
|
|
100
123
|
Use a dedicated **non-superuser role** for the connection — Ablo enforces
|
|
101
124
|
tenant isolation with row-level security, so the server rejects superuser or
|
|
102
125
|
`BYPASSRLS` roles outright (`database_role_cannot_enforce_rls`).
|
|
@@ -127,29 +150,34 @@ built from an ORM adapter instead — same product, same writes, see
|
|
|
127
150
|
[Connect Your Database](./data-sources.md). In that setup, omit `databaseUrl`
|
|
128
151
|
from `Ablo(...)`.
|
|
129
152
|
|
|
130
|
-
## 4. Push
|
|
153
|
+
## 4. Push the schema, then map it to tables
|
|
131
154
|
|
|
132
155
|
```bash
|
|
133
156
|
npx ablo push # checks your DATABASE_URL role, pushes the schema (sandbox),
|
|
134
|
-
#
|
|
135
|
-
#
|
|
136
|
-
# .env.local. Add --watch to re-push on every save.
|
|
157
|
+
# and writes ABLO_API_KEY to .env.local. Add --watch to
|
|
158
|
+
# re-push on every save.
|
|
137
159
|
```
|
|
138
160
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
161
|
+
`ablo push` uploads the schema *definition* — model names, fields, types. That
|
|
162
|
+
metadata is what tells Ablo which models to coordinate. Skipping it makes every
|
|
163
|
+
write to a new or changed model fail with `server_execute_unknown_model` — that
|
|
164
|
+
error literally means "run `npx ablo push`."
|
|
165
|
+
|
|
166
|
+
Now Ablo needs real Postgres tables behind those models. Two ways, depending on
|
|
167
|
+
who owns the tables:
|
|
168
|
+
|
|
169
|
+
- **Adopt existing tables (the common case).** Most teams already have the
|
|
170
|
+
tables — created by Prisma, Drizzle, or hand-written migrations. Run
|
|
171
|
+
`npx ablo pull` to import their shape into your schema, or `npx ablo check`
|
|
172
|
+
to verify your schema and the live tables agree. Keep managing the tables
|
|
173
|
+
with your own migration tool; Ablo just syncs the subset of models you
|
|
174
|
+
declared.
|
|
175
|
+
- **Let Ablo provision them.** If Ablo should own the tables, `npx ablo migrate`
|
|
176
|
+
creates your synced-model tables (with row-level security) in the registered
|
|
177
|
+
database. Your other tables are left untouched.
|
|
178
|
+
|
|
179
|
+
Nothing runs locally — there is no dev server to start. Your app talks to Ablo's
|
|
180
|
+
hosted API with the sandbox key; the rows land in your database.
|
|
153
181
|
|
|
154
182
|
## 5. Write through the model
|
|
155
183
|
|
package/docs/react.md
CHANGED
|
@@ -178,6 +178,52 @@ imperative work after an event or effect.
|
|
|
178
178
|
|
|
179
179
|
See [API reference](/docs/api) for the full options surface.
|
|
180
180
|
|
|
181
|
+
## useClaim — named-claim dispatcher
|
|
182
|
+
|
|
183
|
+
`useClaim` (renamed from `useIntent` in 0.11.0) is typed sugar for invoking a
|
|
184
|
+
*named* claim from your own coordination vocabulary — distinct from the
|
|
185
|
+
row-level `ablo.<model>.claim({ id })` resource claim. Use it when you want to
|
|
186
|
+
broadcast a semantic claim like "I'm editing this layer" or "the agent is
|
|
187
|
+
generating here" and let your transport turn it into a network effect.
|
|
188
|
+
|
|
189
|
+
Declare the vocabulary once via module augmentation on the `Register` interface
|
|
190
|
+
(the `Claims` key — previously `Intents`):
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
declare module '@abloatai/ablo' {
|
|
194
|
+
interface Register {
|
|
195
|
+
Claims: {
|
|
196
|
+
editLayer: { slideId: string; layerId: string };
|
|
197
|
+
generateWithAI: { entityId: string; tool: string };
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Then `useClaim('editLayer')` returns a function whose sole argument is the
|
|
204
|
+
`editLayer` shape — purely compile-time narrowing, no runtime checks:
|
|
205
|
+
|
|
206
|
+
```tsx
|
|
207
|
+
'use client';
|
|
208
|
+
|
|
209
|
+
import { useClaim } from '@abloatai/ablo/react';
|
|
210
|
+
|
|
211
|
+
export function LayerToolbar({ slideId, layerId }: { slideId: string; layerId: string }) {
|
|
212
|
+
const claimEditLayer = useClaim('editLayer');
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<button onClick={() => claimEditLayer({ slideId, layerId })}>
|
|
216
|
+
Edit layer
|
|
217
|
+
</button>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
The hook is pure sugar: the actual network effect lives in the `beginClaim`
|
|
223
|
+
function wired into the provider (bound to your transport). If no `beginClaim`
|
|
224
|
+
is wired, the returned invoker throws `AbloValidationError` with code
|
|
225
|
+
`claim_not_wired`.
|
|
226
|
+
|
|
181
227
|
## Next.js
|
|
182
228
|
|
|
183
229
|
The Next.js [App Router landing](/nextjs) walks through Server Components
|
package/llms-full.txt
CHANGED
|
@@ -19,7 +19,7 @@ Public imports:
|
|
|
19
19
|
- `@abloatai/ablo/react` — React provider and hooks.
|
|
20
20
|
- `@abloatai/ablo/testing` — test harnesses and mocks.
|
|
21
21
|
|
|
22
|
-
TYPES:
|
|
22
|
+
TYPES: the project registers its schema ONCE via declaration merging — `npx ablo init` scaffolds `ablo/register.ts` (a regular `.ts` module beside schema.ts, NOT a hand-authored `.d.ts`): `import type { schema } from './schema'; declare module '@abloatai/ablo' { interface Register { Schema: typeof schema } }`. The top-level `import type` makes `declare module` MERGE (augment) the SDK's Register interface rather than collide — same shape TanStack Router uses in src/router.tsx; any `.ts` file in tsconfig include works, never imported. Then model types are one parameter: `type Task = Model<'tasks'>` (import type { Model } from '@abloatai/ablo/schema'). Do NOT teach `InferModel` (deprecated) or the two-param `Model<typeof schema,'tasks'>` unless multiple schemas exist. Never hand-write model interfaces — derive from the schema. To NAME the client type (function param, context value), infer from the value: `type Sync = typeof sync` — same idiom as tRPC `typeof appRouter` / Drizzle `typeof db`; it resolves the typed overload at the call site. Do NOT use `ReturnType<typeof Ablo>` (collapses to the untyped last overload) and do NOT import a bespoke client-type generic — there is none.
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
Do not teach `/api`, `/agent`, `/ai-sdk`, `/core`, `/realtime`, or `internal/*` as public imports. The Data Source surface — `/source`, `/source/next`, `/source/drizzle`, `/source/kysely`, `/source/conformance` — IS public (it's how a customer-owned database is wired).
|
|
@@ -145,9 +145,21 @@ This applies to any API-backed app: Python, Rails, Go, or Node. The backend keep
|
|
|
145
145
|
its service layer and DB credentials. Ablo gets a Data Source endpoint and uses
|
|
146
146
|
`ABLO_API_KEY`, not a database URL.
|
|
147
147
|
|
|
148
|
-
|
|
149
|
-
`
|
|
150
|
-
|
|
148
|
+
The connection-string path is the PRIMARY one: pass `databaseUrl` explicitly to
|
|
149
|
+
`Ablo({ databaseUrl })`. It is NOT auto-read from the environment — a
|
|
150
|
+
`DATABASE_URL` set for another tool (Prisma, Drizzle, docker-compose) is ignored
|
|
151
|
+
unless you pass `databaseUrl`. The typical user ALREADY has a Postgres (often
|
|
152
|
+
Prisma-managed for auth/audit/log tables that are NOT in the Ablo schema); Ablo
|
|
153
|
+
syncs a SUBSET of models against it. Most users do NOT run `ablo migrate` — they
|
|
154
|
+
ADOPT existing tables with `npx ablo pull` / `npx ablo check`, or keep managing
|
|
155
|
+
tables with their own migration tool. Run `npx ablo migrate` only when Ablo
|
|
156
|
+
should OWN the tables. NOTE: Ablo's CLOUD connects to your Postgres over the
|
|
157
|
+
NETWORK, so a localhost / private-range DB is unreachable and rejected — for a
|
|
158
|
+
local dev DB, expose a signed Data Source endpoint (your app proxies to it) or
|
|
159
|
+
use the hosted sandbox (no DB needed).
|
|
160
|
+
|
|
161
|
+
When the user DOES use the connection-string path: the role must be NON-superuser
|
|
162
|
+
and NON-BYPASSRLS — Ablo enforces row-level security and rejects owner roles with
|
|
151
163
|
`database_role_cannot_enforce_rls`. Neon's and Supabase's default dashboard
|
|
152
164
|
strings use the database OWNER (e.g. `neondb_owner`) and ARE rejected. EASIEST: have the user run `npx ablo migrate` — it detects the unsafe role and creates the scoped one automatically from their machine (owner credential never reaches Ablo; new DATABASE_URL written to the env file). Manual alternative — create a scoped role first (`CREATE ROLE ablo_app LOGIN PASSWORD '...'
|
|
153
165
|
NOSUPERUSER NOBYPASSRLS; GRANT CREATE, CONNECT ON DATABASE <db> TO ablo_app;
|
|
@@ -162,9 +174,11 @@ The options that matter: `schema` and `apiKey`. Everything else
|
|
|
162
174
|
breaks the connection. It exists only for self-hosted/proxy setups the human
|
|
163
175
|
explicitly asks for.
|
|
164
176
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
177
|
+
`databaseUrl` is an OPTIONAL, server-only option on `Ablo(...)`. It is NOT
|
|
178
|
+
auto-read from the environment — pass it EXPLICITLY to register your Postgres
|
|
179
|
+
directly (the primary connection-string path). Omit it when you expose a signed
|
|
180
|
+
Data Source endpoint (so customer-owned app databases stay private), or when
|
|
181
|
+
trying Ablo against the hosted sandbox (apiKey only, no database).
|
|
168
182
|
|
|
169
183
|
Important per-write options: `wait`, `readAt`, `onStale`,
|
|
170
184
|
`idempotencyKey`, and `timeout`.
|
|
@@ -187,7 +201,10 @@ There are two sandbox surfaces:
|
|
|
187
201
|
be pasted into Claude Code or Codex to wire one real model through Ablo.
|
|
188
202
|
- Authenticated org sandboxes are real test environments. The default sandbox is
|
|
189
203
|
the Stripe-style sandbox for an org. It has an isolated sync group prefix,
|
|
190
|
-
can mint `sk_test_*` keys, and can be reset without touching live state.
|
|
204
|
+
can mint `sk_test_*` keys, and can be reset without touching live state. The
|
|
205
|
+
sandbox CAN host rows in Ablo's test plane, so you can try Ablo with NO
|
|
206
|
+
database — `apiKey` only, no `databaseUrl` — like Stripe test mode. (In
|
|
207
|
+
production, your own Postgres is the system of record.)
|
|
191
208
|
|
|
192
209
|
Additional org sandboxes can start blank or copy live configuration. Keep
|
|
193
210
|
customer production traffic on `sk_live_*`; use `sk_test_*` for app setup, Data
|
package/llms.txt
CHANGED
|
@@ -8,7 +8,7 @@ Use AI SDK for the agent loop. Use Ablo when agent reads and writes must persist
|
|
|
8
8
|
|
|
9
9
|
## Start here
|
|
10
10
|
|
|
11
|
-
First action when integrating into an app: run `npx ablo init --yes --framework <nextjs|vite|remix|vanilla
|
|
11
|
+
First action when integrating into an app: run `npx ablo init --yes --framework <nextjs|vite|remix|vanilla>`. Agents have no TTY — `--yes` is REQUIRED or it HANGS. The default `--storage direct` (connection string) scaffolds `ablo/schema.ts`, the client, and (for Next.js) the browser provider + session route; pass `--storage endpoint` instead when database credentials must stay inside the app — that variant scaffolds a signed Data Source endpoint. All on the current API. Edit the generated files rather than hand-writing from this doc.
|
|
12
12
|
|
|
13
13
|
Second: make sure a key exists — WITHOUT printing it. The key is a secret; it must never appear in your output, your reasoning, or a file you echo (it would live in the conversation history forever). Check PRESENCE only: `[ -n "$ABLO_API_KEY" ] && echo set` and `grep -cq '^ABLO_API_KEY=' .env.local && echo wired` — never `cat .env.local`, never `echo $ABLO_API_KEY`. If neither check passes, ask the HUMAN to run `npx ablo login` once — it opens a browser and saves a `sk_test_` key locally; an agent must NOT run it. You never copy the key by hand: the next step writes it into `.env.local` (and gitignores it) for you.
|
|
14
14
|
|
|
@@ -24,7 +24,7 @@ Each app gets its own PROJECT inside the org — its own schema, its own sandbox
|
|
|
24
24
|
import Ablo from '@abloatai/ablo';
|
|
25
25
|
import { defineSchema, model, z } from '@abloatai/ablo/schema';
|
|
26
26
|
|
|
27
|
-
TYPES:
|
|
27
|
+
TYPES: the project registers its schema ONCE via declaration merging — `npx ablo init` scaffolds `ablo/register.ts` (a regular `.ts` module beside schema.ts, NOT a hand-authored `.d.ts`): `import type { schema } from './schema'; declare module '@abloatai/ablo' { interface Register { Schema: typeof schema } }`. The top-level `import type` makes `declare module` MERGE (augment) the SDK's Register interface rather than collide — same shape TanStack Router uses in src/router.tsx; any `.ts` file in tsconfig include works, never imported. Then model types are one parameter: `type Task = Model<'tasks'>` (import type { Model } from '@abloatai/ablo/schema'). Do NOT teach `InferModel` (deprecated) or the two-param `Model<typeof schema,'tasks'>` unless multiple schemas exist. Never hand-write model interfaces — derive from the schema. To NAME the client type (function param, context value), infer from the value: `type Sync = typeof sync` — same idiom as tRPC `typeof appRouter` / Drizzle `typeof db`; it resolves the typed overload at the call site. Do NOT use `ReturnType<typeof Ablo>` (collapses to the untyped last overload) and do NOT import a bespoke client-type generic — there is none.
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
const schema = defineSchema({
|
|
@@ -123,9 +123,9 @@ A schema is model fields and relations. Advanced schema helpers such as `mutable
|
|
|
123
123
|
|
|
124
124
|
## Storage Boundary
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
`databaseUrl` is an OPTIONAL, server-only constructor option on `Ablo(...)`. It is NOT auto-read from the environment — pass it EXPLICITLY to register your Postgres directly. Omit it when you expose a signed Data Source endpoint, or when trying Ablo against the hosted sandbox (apiKey only). A `DATABASE_URL` set for another tool (Prisma, Drizzle, docker-compose) is ignored unless you pass `databaseUrl` explicitly.
|
|
127
127
|
|
|
128
|
-
|
|
128
|
+
In production, every schema model is backed by YOUR OWN database. The PRIMARY path is the connection string: pass `databaseUrl` (most users already have a Postgres — often Prisma- or Drizzle-managed for auth/audit/log tables that are NOT in the Ablo schema; Ablo syncs a SUBSET of models against it). Most users do NOT run `ablo migrate` — they ADOPT existing tables with `npx ablo pull` / `npx ablo check`, or keep managing tables with their own migration tool. Run `npx ablo migrate` only when Ablo should OWN the tables (it provisions the synced-model tables in your DB). The alternative to the connection string is a signed Data Source endpoint that hands Ablo an ORM `adapter` (Drizzle is the default; Prisma and Kysely are also supported) — it owns the transaction, exactly-once idempotency, and outbox in ONE pass (no hand-written `commit`/`events`); use it when database credentials must never leave your infrastructure (or for a local/private-range DB, which Ablo's cloud cannot reach over the network).
|
|
129
129
|
|
|
130
130
|
GOTCHA the user WILL hit: `DATABASE_URL` must use a NON-superuser, NON-BYPASSRLS role (Ablo enforces row-level security; owner roles are rejected with `database_role_cannot_enforce_rls`). Neon's and Supabase's default dashboard connection strings use the database OWNER (e.g. `neondb_owner`) and are rejected. EASIEST: `npx ablo migrate` detects the unsafe role and creates the scoped one automatically from the user's machine (owner credential never reaches Ablo; new DATABASE_URL written to the env file). Manual alternative — create a scoped role first: `CREATE ROLE ablo_app LOGIN PASSWORD '...' NOSUPERUSER NOBYPASSRLS; GRANT CREATE, CONNECT ON DATABASE <db> TO ablo_app; GRANT CREATE, USAGE ON SCHEMA public TO ablo_app;` — then swap user/password into the same host/db string.
|
|
131
131
|
|
|
@@ -145,7 +145,7 @@ export const { POST } = dataSourceNext({
|
|
|
145
145
|
});
|
|
146
146
|
```
|
|
147
147
|
|
|
148
|
-
`npx ablo init` defaults to
|
|
148
|
+
`npx ablo init` defaults to `--storage direct` (the connection-string path — it scaffolds the client carrying `databaseUrl` and the `DATABASE_URL` env entry; see CLI below). Pass `--storage endpoint` to scaffold the signed Data Source endpoint above instead, when app database credentials must stay private — Ablo only calls the endpoint.
|
|
149
149
|
|
|
150
150
|
## Sandboxes
|
|
151
151
|
|
|
@@ -156,9 +156,11 @@ when an agent is asked to "make Ablo work" in an existing app.
|
|
|
156
156
|
|
|
157
157
|
Authenticated org sandboxes are real test environments. Treat the default
|
|
158
158
|
sandbox like Stripe test mode: it has an isolated sync group prefix and mints
|
|
159
|
-
`sk_test_*` keys.
|
|
160
|
-
|
|
161
|
-
|
|
159
|
+
`sk_test_*` keys. The sandbox CAN host rows in Ablo's test plane, so you can try
|
|
160
|
+
Ablo with NO database — `apiKey` only, no `databaseUrl`. (In production, your own
|
|
161
|
+
Postgres is the system of record.) Extra sandboxes can start blank or copy live
|
|
162
|
+
configuration. Resetting a sandbox creates a clean future stream without
|
|
163
|
+
touching live data. Use `sk_live_*` only for production.
|
|
162
164
|
|
|
163
165
|
For coding agents, the sandbox success path is: pick one shared model,
|
|
164
166
|
declare schema, create the Ablo client, replace one direct mutation with a typed
|
|
@@ -185,7 +187,7 @@ Do not teach `/api`, `/agent`, `/ai-sdk`, `/core`, `/realtime`, or internal subp
|
|
|
185
187
|
|
|
186
188
|
`ablo init` and other prompts need a TTY; an agent/CI run has none and will HANG. Always:
|
|
187
189
|
|
|
188
|
-
- `npx ablo init --yes` (flags: `--framework`, `--auth`, `--storage
|
|
190
|
+
- `npx ablo init --yes` (flags: `--framework`, `--auth`, `--storage direct|endpoint` (default `direct`), `--no-agent`, `--no-pull`, `--no-install`, `--no-login`). Generates `ablo/schema.ts` + the client; `--storage direct` (default) wires `databaseUrl`, `--storage endpoint` scaffolds the `ablo/data-source.ts` endpoint above instead.
|
|
189
191
|
- Key: see "Start here" — env → `.env.local` → ask the human to `npx ablo login`; never run `login` yourself, never copy keys by hand (`ablo push` writes `.env.local`).
|
|
190
192
|
- Adopt an existing DB: `npx ablo pull prisma [path]` / `npx ablo pull drizzle <module>`.
|
|
191
193
|
- `npx ablo push --no-watch` pushes the schema (sandbox) AND writes `ABLO_API_KEY` to `.env.local` (default watches forever); `npx ablo logs --no-follow` (default tails forever); `npx ablo mode sandbox|production` (always pass the arg). `npx ablo push`/`status`/`pull`/`check`/`generate` are one-shot.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abloatai/ablo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "The Collaboration Layer For AI Agents",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -221,6 +221,7 @@
|
|
|
221
221
|
"ts-morph": "^26.0.0",
|
|
222
222
|
"tsup": "^8.0.0",
|
|
223
223
|
"typescript": "^5.8.3",
|
|
224
|
-
"publint": "^0.3.21"
|
|
224
|
+
"publint": "^0.3.21",
|
|
225
|
+
"yjs": "^13.6.29"
|
|
225
226
|
}
|
|
226
227
|
}
|
package/dist/react/useIntent.js
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import { useCallback } from 'react';
|
|
3
|
-
import { useSyncContext } from './context.js';
|
|
4
|
-
import { AbloValidationError } from '../errors.js';
|
|
5
|
-
/**
|
|
6
|
-
* Named-intent invoker, typed via `ResolveIntents[IntentName]`.
|
|
7
|
-
*
|
|
8
|
-
* The consumer declares their intent vocabulary in the global:
|
|
9
|
-
*
|
|
10
|
-
* ```ts
|
|
11
|
-
* declare module '@abloatai/ablo' {
|
|
12
|
-
* interface Register {
|
|
13
|
-
* Intents: {
|
|
14
|
-
* editLayer: { slideId: string; layerId: string };
|
|
15
|
-
* generateWithAI: { entityId: string; tool: string };
|
|
16
|
-
* };
|
|
17
|
-
* }
|
|
18
|
-
* }
|
|
19
|
-
* ```
|
|
20
|
-
*
|
|
21
|
-
* Then `useIntent('editLayer')` returns a function whose sole argument
|
|
22
|
-
* is the `editLayer` claim shape — no runtime checks, purely compile-
|
|
23
|
-
* time narrowing.
|
|
24
|
-
*
|
|
25
|
-
* The SDK doesn't own what happens next: the `beginIntent` function on
|
|
26
|
-
* the React context (supplied via `SyncProvider`) is where the intent
|
|
27
|
-
* claim turns into a network effect. A Node-backed consumer wires it
|
|
28
|
-
* through `SyncAgent.beginIntent`; a browser-backed consumer may
|
|
29
|
-
* broadcast it through their own WebSocket. This hook is pure sugar
|
|
30
|
-
* that adds the typed name + claim narrowing.
|
|
31
|
-
*/
|
|
32
|
-
export function useIntent(intentName) {
|
|
33
|
-
const { beginIntent } = useSyncContext();
|
|
34
|
-
return useCallback((claim) => {
|
|
35
|
-
if (!beginIntent) {
|
|
36
|
-
throw new AbloValidationError(`useIntent: no \`beginIntent\` wired into SyncProvider. Pass ` +
|
|
37
|
-
`a \`beginIntent\` prop (typically bound to your transport) ` +
|
|
38
|
-
`to enable intent invocations.`, { code: 'intent_not_wired' });
|
|
39
|
-
}
|
|
40
|
-
return beginIntent(intentName, claim);
|
|
41
|
-
}, [beginIntent, intentName]);
|
|
42
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `awaitIntentGrant` — the client side of the fair-queue handover.
|
|
3
|
-
*
|
|
4
|
-
* When a `claim` is contended, the server enqueues it and replies `queued`
|
|
5
|
-
* (HTTP 202 on `/v1/intents`, or `intent_queued` over WS). The grant is then
|
|
6
|
-
* PUSHED later over the WS as `intent_granted` when the claim reaches the head.
|
|
7
|
-
* This resolves once that frame arrives for our `intentId` — so the caller's
|
|
8
|
-
* `claim` promise stays pending (event-driven; no poll, no race) until it's
|
|
9
|
-
* actually our turn. Rejects on `intent_lost` (surfaced as `claim_lost`: the claim was taken away — TTL
|
|
10
|
-
* lapse on disconnect, revoke) or an optional timeout.
|
|
11
|
-
*
|
|
12
|
-
* Takes only a minimal `{ subscribe }` transport so it unit-tests against a
|
|
13
|
-
* fake; `SyncWebSocket` satisfies it structurally.
|
|
14
|
-
*/
|
|
15
|
-
export interface GrantTransport {
|
|
16
|
-
subscribe(event: 'intent_acquired' | 'intent_granted' | 'intent_lost' | 'intent_queued', handler: (payload: Record<string, unknown>) => void): () => void;
|
|
17
|
-
}
|
|
18
|
-
export interface IntentGrantInfo {
|
|
19
|
-
/**
|
|
20
|
-
* True when the grant arrived as `intent_granted` — i.e. the target was
|
|
21
|
-
* HELD when we asked and we waited in the FIFO line behind the holder.
|
|
22
|
-
* False for the immediate `intent_acquired` (target was free).
|
|
23
|
-
*
|
|
24
|
-
* Callers use this to know the row may have changed while we queued:
|
|
25
|
-
* intent VISIBILITY is entity-scoped (org-wide subscriptions receive no
|
|
26
|
-
* presence/intent fan-out — see Hub.broadcastPresenceChange), so the
|
|
27
|
-
* local coordination snapshot cannot be trusted to detect "we waited".
|
|
28
|
-
* The grant frame itself is the authoritative signal.
|
|
29
|
-
*/
|
|
30
|
-
readonly waited: boolean;
|
|
31
|
-
}
|
|
32
|
-
export declare function awaitIntentGrant(transport: GrantTransport, intentId: string, options?: {
|
|
33
|
-
timeoutMs?: number;
|
|
34
|
-
/**
|
|
35
|
-
* Backpressure: reject instead of waiting if, when we join the line, the
|
|
36
|
-
* server reports `position >= maxQueueDepth` (i.e. that many claims are
|
|
37
|
-
* already ahead of us). Omit to wait however deep the queue is.
|
|
38
|
-
*/
|
|
39
|
-
maxQueueDepth?: number;
|
|
40
|
-
}): Promise<IntentGrantInfo>;
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `awaitIntentGrant` — the client side of the fair-queue handover.
|
|
3
|
-
*
|
|
4
|
-
* When a `claim` is contended, the server enqueues it and replies `queued`
|
|
5
|
-
* (HTTP 202 on `/v1/intents`, or `intent_queued` over WS). The grant is then
|
|
6
|
-
* PUSHED later over the WS as `intent_granted` when the claim reaches the head.
|
|
7
|
-
* This resolves once that frame arrives for our `intentId` — so the caller's
|
|
8
|
-
* `claim` promise stays pending (event-driven; no poll, no race) until it's
|
|
9
|
-
* actually our turn. Rejects on `intent_lost` (surfaced as `claim_lost`: the claim was taken away — TTL
|
|
10
|
-
* lapse on disconnect, revoke) or an optional timeout.
|
|
11
|
-
*
|
|
12
|
-
* Takes only a minimal `{ subscribe }` transport so it unit-tests against a
|
|
13
|
-
* fake; `SyncWebSocket` satisfies it structurally.
|
|
14
|
-
*/
|
|
15
|
-
import { AbloClaimedError } from '../errors.js';
|
|
16
|
-
export function awaitIntentGrant(transport, intentId, options) {
|
|
17
|
-
return new Promise((resolve, reject) => {
|
|
18
|
-
const unsubs = [];
|
|
19
|
-
let timer;
|
|
20
|
-
const settle = (fn) => {
|
|
21
|
-
if (timer)
|
|
22
|
-
clearTimeout(timer);
|
|
23
|
-
for (const u of unsubs)
|
|
24
|
-
u();
|
|
25
|
-
fn();
|
|
26
|
-
};
|
|
27
|
-
// The target was free → `intent_acquired` (immediate); it was contended,
|
|
28
|
-
// we waited in line, and reached the head → `intent_granted`. Either frame
|
|
29
|
-
// means the lease is now ours; `waited` records which path it was.
|
|
30
|
-
unsubs.push(transport.subscribe('intent_acquired', (p) => {
|
|
31
|
-
if (p?.intentId === intentId)
|
|
32
|
-
settle(() => resolve({ waited: false }));
|
|
33
|
-
}));
|
|
34
|
-
unsubs.push(transport.subscribe('intent_granted', (p) => {
|
|
35
|
-
if (p?.intentId === intentId)
|
|
36
|
-
settle(() => resolve({ waited: true }));
|
|
37
|
-
}));
|
|
38
|
-
if (options?.maxQueueDepth !== undefined) {
|
|
39
|
-
const max = options.maxQueueDepth;
|
|
40
|
-
unsubs.push(transport.subscribe('intent_queued', (p) => {
|
|
41
|
-
if (p?.intentId !== intentId)
|
|
42
|
-
return;
|
|
43
|
-
const position = typeof p.position === 'number' ? p.position : 0;
|
|
44
|
-
if (position >= max) {
|
|
45
|
-
settle(() => reject(new AbloClaimedError(`Claim queue for ${intentId} is ${position} deep (max ${max}).`, { code: 'queue_too_deep' })));
|
|
46
|
-
}
|
|
47
|
-
}));
|
|
48
|
-
}
|
|
49
|
-
unsubs.push(transport.subscribe('intent_lost', (p) => {
|
|
50
|
-
if (p?.intentId === intentId) {
|
|
51
|
-
settle(() => reject(new AbloClaimedError(`Claim lost while queued for ${intentId}.`, {
|
|
52
|
-
code: 'claim_lost',
|
|
53
|
-
})));
|
|
54
|
-
}
|
|
55
|
-
}));
|
|
56
|
-
if (options?.timeoutMs && options.timeoutMs > 0) {
|
|
57
|
-
timer = setTimeout(() => {
|
|
58
|
-
settle(() => reject(new AbloClaimedError(`Timed out waiting for the queue grant on claim ${intentId}.`, { code: 'grant_timeout' })));
|
|
59
|
-
}, options.timeoutMs);
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Transport-driven IntentStream factory.
|
|
3
|
-
*
|
|
4
|
-
* Mirrors `createPresenceStream` — built directly on `SyncWebSocket`,
|
|
5
|
-
* no SyncAgent wrapper. Intents derive their `others` view from the
|
|
6
|
-
* same `presence_update` frames the presence stream consumes (the
|
|
7
|
-
* Hub piggybacks `activeIntents` on every presence frame). Outbound
|
|
8
|
-
* announce/revoke ride the same socket via `intent_begin` /
|
|
9
|
-
* `intent_abandon` frames.
|
|
10
|
-
*
|
|
11
|
-
* Wire contract (apps/sync-server/src/hub/types.ts):
|
|
12
|
-
* • Outbound: `{ type: 'intent_begin', payload: { intentId,
|
|
13
|
-
* entityType, entityId, action, field?, estimatedMs? } }`
|
|
14
|
-
* • Outbound: `{ type: 'intent_abandon', payload: { intentId,
|
|
15
|
-
* entityType?, entityId? } }`
|
|
16
|
-
* • Inbound (via presence): `event.activeIntents: IntentClaim[]`
|
|
17
|
-
* stamped with `declaredAt`, `expiresAt`.
|
|
18
|
-
* • Inbound: `intent_rejected` event with conflict metadata.
|
|
19
|
-
*
|
|
20
|
-
* After the dual-engine collapse (step #36), this is the only
|
|
21
|
-
* IntentStream factory in the SDK; the older compatibility path
|
|
22
|
-
* deletes.
|
|
23
|
-
*/
|
|
24
|
-
import type { SyncWebSocket } from './SyncWebSocket.js';
|
|
25
|
-
import type { IntentStream } from '../types/streams.js';
|
|
26
|
-
export interface IntentStreamConfig {
|
|
27
|
-
/** Identity used to filter our own active intents out of `others`. */
|
|
28
|
-
participantId: string;
|
|
29
|
-
}
|
|
30
|
-
export interface AttachableIntentStream extends IntentStream {
|
|
31
|
-
attach(transport: SyncWebSocket): void;
|
|
32
|
-
dispose(): void;
|
|
33
|
-
}
|
|
34
|
-
export declare function createIntentStream(config: IntentStreamConfig, transport?: SyncWebSocket | null): AttachableIntentStream;
|