@abloatai/ablo 0.12.0 → 0.13.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/AGENTS.md +2 -2
- package/CHANGELOG.md +19 -0
- package/README.md +3 -3
- package/dist/cli.cjs +149 -40
- package/dist/schema/index.d.ts +2 -2
- package/dist/schema/index.js +2 -2
- package/dist/schema/model.d.ts +38 -84
- package/dist/schema/model.js +12 -12
- package/dist/schema/roles.d.ts +49 -0
- package/dist/schema/roles.js +21 -0
- package/dist/schema/schema.d.ts +1 -1
- package/dist/schema/schema.js +1 -1
- package/dist/schema/serialize.d.ts +4 -2
- package/dist/schema/serialize.js +4 -2
- package/dist/schema/sugar.d.ts +7 -28
- package/dist/schema/sugar.js +2 -7
- package/dist/schema/sync-delta-row.d.ts +2 -0
- package/dist/schema/sync-delta-row.js +2 -1
- package/dist/schema/tenancy.d.ts +67 -28
- package/dist/schema/tenancy.js +93 -23
- package/dist/server/commit.d.ts +8 -3
- package/docs/api.md +1 -1
- package/docs/cli.md +43 -4
- package/docs/client-behavior.md +2 -2
- package/docs/coordination.md +1 -1
- package/docs/examples/agent-human.md +6 -6
- package/docs/examples/ai-sdk-tool.md +1 -1
- package/docs/examples/existing-python-backend.md +0 -2
- package/docs/examples/nextjs.md +2 -2
- package/docs/examples/scoped-agent.md +3 -3
- package/docs/examples/server-agent.md +4 -4
- package/docs/identity.md +27 -20
- package/docs/index.md +0 -1
- package/docs/integration-guide.md +12 -9
- package/docs/interaction-model.md +1 -1
- package/docs/mcp.md +17 -5
- package/docs/quickstart.md +3 -3
- package/llms.txt +2 -3
- package/package.json +3 -2
- package/docs/mcp/claude-code.md +0 -35
- package/docs/mcp/cursor.md +0 -35
- package/docs/mcp/windsurf.md +0 -33
- package/docs/roadmap.md +0 -55
- package/docs/the-loop.md +0 -21
- package/llms-full.txt +0 -396
package/docs/cli.md
CHANGED
|
@@ -24,18 +24,23 @@ direct-connector commands are tagged **Direct Postgres**.
|
|
|
24
24
|
|
|
25
25
|
`ablo login` runs the OAuth 2.0 device flow: it opens your browser, you choose
|
|
26
26
|
**log in** or **create an account** and approve, and the CLI provisions a
|
|
27
|
-
**test + live key pair** (90-day
|
|
28
|
-
|
|
27
|
+
**test + live key pair** (90-day) and stores them locally. The test key is a
|
|
28
|
+
sandbox `sk_test_` key; the live key is a restricted `rk_live_` key (read-only
|
|
29
|
+
observation — `logs`, `status`), so a stolen config can't write to production.
|
|
30
|
+
This mirrors `stripe login`.
|
|
29
31
|
|
|
30
32
|
| Command | What it does |
|
|
31
33
|
| ------------------------ | -------------------------------------------------------------------------- |
|
|
32
34
|
| `ablo login` | Authorize in the browser; provisions + stores a test and a live key. |
|
|
35
|
+
| `ablo login --project <slug>` | Same, but scope (and mint) the pair to a project, and make it active. |
|
|
33
36
|
| `ablo logout` | Remove the stored keys. |
|
|
34
37
|
| `ablo status` | Show the active org, mode, both keys (prefix + expiry), and server health. |
|
|
35
38
|
| `ablo mode [sandbox\|production]` | Switch the active environment. With no argument, prompts. |
|
|
36
39
|
|
|
37
|
-
Keys
|
|
38
|
-
|
|
40
|
+
Keys live in `~/.config/ablo/credentials.json` (mode `0600`), keyed by project
|
|
41
|
+
then environment; the non-secret `config.json` holds only the active mode and
|
|
42
|
+
project. In **CI**, don't log in — set `ABLO_API_KEY`, which always overrides
|
|
43
|
+
the stored key.
|
|
39
44
|
|
|
40
45
|
## Test vs live
|
|
41
46
|
|
|
@@ -47,6 +52,39 @@ the sandbox by design.
|
|
|
47
52
|
The schema, however, is **shared** across the org — pushing a schema (from
|
|
48
53
|
either environment) defines the same models sandbox and production see; only the rows differ.
|
|
49
54
|
|
|
55
|
+
## Projects
|
|
56
|
+
|
|
57
|
+
An org can have multiple **projects**, each with its own isolated keys, schema,
|
|
58
|
+
and data. Keys are scoped to a project **at mint** and never re-scoped, so the
|
|
59
|
+
CLI keeps a separate credential profile per project — Stripe's
|
|
60
|
+
`login --project-name` model. The active project (set with `projects use`)
|
|
61
|
+
selects which profile every command authenticates with.
|
|
62
|
+
|
|
63
|
+
| Command | What it does |
|
|
64
|
+
| ----------------------------- | ---------------------------------------------------------------------------------- |
|
|
65
|
+
| `ablo projects list` | List the org's projects (marks the active one and the org-default). |
|
|
66
|
+
| `ablo projects create <slug>` | Create a project (`--name "Display Name"`). Its keys/schema/data are isolated. |
|
|
67
|
+
| `ablo projects use <slug>` | Switch the active project. `ablo projects use default` returns to the org-default. |
|
|
68
|
+
| `ablo login --project <slug>` | Mint and store a key pair for a project, and make it active. |
|
|
69
|
+
|
|
70
|
+
Because keys are fixed to a project, `projects use` only changes which profile
|
|
71
|
+
is active — it never re-scopes an existing key. Switch to a project you haven't
|
|
72
|
+
logged into yet and the CLI tells you to mint one:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npx ablo projects use war-room
|
|
76
|
+
# ✓ now targeting project war-room (prj_…)
|
|
77
|
+
# No key stored for this project yet — run `ablo login --project war-room` to mint one.
|
|
78
|
+
|
|
79
|
+
npx ablo login --project war-room # mints + stores its key pair, keeps it active
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
If you run a project-scoped command (`push`, `dev`) while the active project has
|
|
83
|
+
no key — but other projects do — the CLI **refuses** rather than silently
|
|
84
|
+
deploying with the wrong project's credential, and names the fix
|
|
85
|
+
(`ablo login --project <slug>`). In CI, an explicit `ABLO_API_KEY` bypasses
|
|
86
|
+
profiles entirely: it acts in whatever project it was minted for.
|
|
87
|
+
|
|
50
88
|
## Commands
|
|
51
89
|
|
|
52
90
|
| Command | What it does | Flags |
|
|
@@ -54,6 +92,7 @@ either environment) defines the same models sandbox and production see; only the
|
|
|
54
92
|
| `ablo init` | Scaffold `ablo/` (`schema.ts`, client, optional Data Source / agent / component), write `.env`, install the SDK. Offers to log in at the end. | — |
|
|
55
93
|
| `ablo login` / `logout` / `status` | Authentication & status (above). | — |
|
|
56
94
|
| `ablo mode [sandbox\|production]` | Switch active environment. | — |
|
|
95
|
+
| `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) |
|
|
57
96
|
| `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>` |
|
|
58
97
|
| `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` |
|
|
59
98
|
| `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` |
|
package/docs/client-behavior.md
CHANGED
|
@@ -146,7 +146,7 @@ the latest row, then hands you the fresh row — so you can't overwrite a change
|
|
|
146
146
|
see. Options on the claim:
|
|
147
147
|
|
|
148
148
|
- default `claim` waits in the fair queue and re-reads before handing you the row;
|
|
149
|
-
- `{
|
|
149
|
+
- `{ queue: false }` rejects with `AbloClaimedError` instead of queuing;
|
|
150
150
|
- `{ maxQueueDepth }` rejects if the wait line is already too deep.
|
|
151
151
|
|
|
152
152
|
While waiting, schema clients learn when the claim clears from the live claim
|
|
@@ -166,7 +166,7 @@ All SDK errors extend `AbloError` and carry a stable `type`.
|
|
|
166
166
|
| `AbloValidationError` | Invalid input or unsupported request shape. |
|
|
167
167
|
| `AbloServerError` | Server-side 5xx. Retry with backoff if the operation is idempotent. |
|
|
168
168
|
| `AbloStaleContextError` | Write was based on stale `readAt` state. Re-read and retry. |
|
|
169
|
-
| `AbloClaimedError` | An active claim conflicted with `{
|
|
169
|
+
| `AbloClaimedError` | An active claim conflicted with `{ queue: false }`, the queue was too deep, or a claim wait timed out. |
|
|
170
170
|
|
|
171
171
|
```ts
|
|
172
172
|
import { AbloClaimedError } from '@abloatai/ablo';
|
package/docs/coordination.md
CHANGED
|
@@ -280,7 +280,7 @@ Releasing **promotes the head of the queue**: the next waiter receives the claim
|
|
|
280
280
|
**Example**
|
|
281
281
|
|
|
282
282
|
```ts
|
|
283
|
-
const claim = await ablo.weatherReports.claim({ id: 'report_stockholm',
|
|
283
|
+
const claim = await ablo.weatherReports.claim({ id: 'report_stockholm', reason: 'reviewing' });
|
|
284
284
|
const report = claim.data;
|
|
285
285
|
try {
|
|
286
286
|
const ok = await reviewExternally(report);
|
|
@@ -47,14 +47,14 @@ export async function markReady(reportId: string) {
|
|
|
47
47
|
if (!report) return { status: 'not_found' };
|
|
48
48
|
|
|
49
49
|
try {
|
|
50
|
-
//
|
|
50
|
+
// queue: false → don't queue behind a current holder. If a human already
|
|
51
51
|
// holds the row, claim rejects with AbloClaimedError (caught below), so the
|
|
52
|
-
// agent yields instead of waiting. Omit it, or pass
|
|
53
|
-
// behind them.
|
|
52
|
+
// agent yields instead of waiting. Omit it, or pass queue: true, to queue
|
|
53
|
+
// behind them. reason → the label observers see while we work.
|
|
54
54
|
await using claim = await ablo.weatherReports.claim({
|
|
55
55
|
id: reportId,
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
queue: false,
|
|
57
|
+
reason: 'marking_ready',
|
|
58
58
|
});
|
|
59
59
|
const claimed = claim.data;
|
|
60
60
|
|
|
@@ -120,7 +120,7 @@ export function ReportRow({ report: serverReport }: Props) {
|
|
|
120
120
|
- The claim is visible to everyone: the UI reads it synchronously with
|
|
121
121
|
`claim.state({ id })`, and it also arrives over the live stream.
|
|
122
122
|
- `claim({ id })` makes writers take turns instead of racing — with
|
|
123
|
-
`
|
|
123
|
+
`queue: false`, the agent simply yields when a human already holds the row.
|
|
124
124
|
- The `update` made while the claim is held is stale-checked automatically, so a human's
|
|
125
125
|
edit landing mid-run rejects the agent's write with a typed
|
|
126
126
|
`AbloStaleContextError` instead of overwriting it.
|
|
@@ -43,7 +43,7 @@ const updateReport = tool({
|
|
|
43
43
|
// The claim is released automatically when it goes out of scope.
|
|
44
44
|
await using claim = await ablo.weatherReports.claim({
|
|
45
45
|
id: reportId,
|
|
46
|
-
|
|
46
|
+
reason: 'editing',
|
|
47
47
|
ttl: '2m',
|
|
48
48
|
});
|
|
49
49
|
const claimed = claim.data;
|
|
@@ -37,10 +37,8 @@ import { defineSchema, model, z } from '@abloatai/ablo/schema';
|
|
|
37
37
|
|
|
38
38
|
export const schema = defineSchema({
|
|
39
39
|
weatherReports: model({
|
|
40
|
-
id: z.string(),
|
|
41
40
|
location: z.string(),
|
|
42
41
|
status: z.enum(['pending', 'ready']),
|
|
43
|
-
updatedAt: z.string(),
|
|
44
42
|
}),
|
|
45
43
|
});
|
|
46
44
|
```
|
package/docs/examples/nextjs.md
CHANGED
|
@@ -57,8 +57,8 @@ import { ablo } from '@/lib/ablo';
|
|
|
57
57
|
export async function markReady(id: string) {
|
|
58
58
|
await using claim = await ablo.weatherReports.claim({
|
|
59
59
|
id,
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
queue: false,
|
|
61
|
+
reason: 'marking_ready',
|
|
62
62
|
});
|
|
63
63
|
const claimed = claim.data;
|
|
64
64
|
|
|
@@ -19,18 +19,18 @@ import { defineSchema, identityRole, model, relation, z } from '@abloatai/ablo/s
|
|
|
19
19
|
|
|
20
20
|
export const schema = defineSchema(
|
|
21
21
|
{
|
|
22
|
-
// A deck's rows form the group `deck:<id>` (the kind comes from `
|
|
22
|
+
// A deck's rows form the group `deck:<id>` (the kind comes from `groups.root`).
|
|
23
23
|
decks: model(
|
|
24
24
|
{ title: z.string() },
|
|
25
25
|
{},
|
|
26
|
-
{
|
|
26
|
+
{ groups: { root: 'deck' } },
|
|
27
27
|
),
|
|
28
28
|
// A slide has no group of its own. It inherits its deck's group via the
|
|
29
29
|
// `parent` edge, so a slide write reaches everyone viewing the deck.
|
|
30
30
|
slides: model(
|
|
31
31
|
{ deckId: z.string(), body: z.string() },
|
|
32
32
|
{ deck: relation.belongsTo('decks', 'deckId', { parent: true }) },
|
|
33
|
-
{
|
|
33
|
+
{},
|
|
34
34
|
),
|
|
35
35
|
},
|
|
36
36
|
{
|
|
@@ -38,8 +38,8 @@ export async function completeReport(reportId: string) {
|
|
|
38
38
|
|
|
39
39
|
await using claim = await ablo.weatherReports.claim({
|
|
40
40
|
id: reportId,
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
queue: false,
|
|
42
|
+
reason: 'completing',
|
|
43
43
|
});
|
|
44
44
|
const claimed = claim.data;
|
|
45
45
|
|
|
@@ -60,9 +60,9 @@ the server has accepted it.
|
|
|
60
60
|
|
|
61
61
|
The two options on the claim:
|
|
62
62
|
|
|
63
|
-
- `
|
|
63
|
+
- `queue: false` — skip this record if another claim is already in progress,
|
|
64
64
|
rather than queueing behind it. (The default queues.)
|
|
65
|
-
- `
|
|
65
|
+
- `reason: 'completing'` — a human-readable label for what your worker is doing,
|
|
66
66
|
visible to anyone reading `claim.state({ id })`.
|
|
67
67
|
|
|
68
68
|
Because the worker uses the same schema and `claim()` as the UI, its writes sync
|
package/docs/identity.md
CHANGED
|
@@ -47,18 +47,20 @@ import { defineSchema, identityRole, relation, model, z } from '@abloatai/ablo/s
|
|
|
47
47
|
|
|
48
48
|
export const schema = defineSchema(
|
|
49
49
|
{
|
|
50
|
-
// A scope root: its rows form the group `deck:<id>` (kind from `
|
|
50
|
+
// A scope root: its rows form the group `deck:<id>` (kind from `groups.root`).
|
|
51
|
+
// Tenant isolation defaults to a row-local `organization_id` column, so no
|
|
52
|
+
// `policy` is needed here.
|
|
51
53
|
decks: model(
|
|
52
54
|
{ title: z.string(), status: z.enum(['draft', 'published']) },
|
|
53
55
|
{},
|
|
54
|
-
{
|
|
56
|
+
{ groups: { root: 'deck' } },
|
|
55
57
|
),
|
|
56
58
|
// A child: it has no group of its own; it inherits its deck's group via the
|
|
57
59
|
// `parent` edge. A write to a slide reaches everyone viewing the deck.
|
|
58
60
|
slides: model(
|
|
59
61
|
{ deckId: z.string() },
|
|
60
62
|
{ deck: relation.belongsTo('decks', 'deckId', { parent: true }) },
|
|
61
|
-
{
|
|
63
|
+
{},
|
|
62
64
|
),
|
|
63
65
|
},
|
|
64
66
|
{
|
|
@@ -208,13 +210,13 @@ You never write a sync-group string for a row. You declare a model's *place* in
|
|
|
208
210
|
the entity graph and the engine derives the groups its rows fan out on. Three
|
|
209
211
|
declarations, in order of how often you reach for them:
|
|
210
212
|
|
|
211
|
-
**`
|
|
212
|
-
The kind comes from the model's `typename` by default, or pass a string to
|
|
213
|
-
it explicitly (use the string form when the wire kind differs from the
|
|
214
|
-
e.g. typename `SlideDeck` but group `deck:<id>`):
|
|
213
|
+
**`groups.root` — this model is a scope root.** Its rows form a group of their
|
|
214
|
+
own. The kind comes from the model's `typename` by default, or pass a string to
|
|
215
|
+
set it explicitly (use the string form when the wire kind differs from the
|
|
216
|
+
typename, e.g. typename `SlideDeck` but group `deck:<id>`):
|
|
215
217
|
|
|
216
218
|
```ts
|
|
217
|
-
decks: model({ title: z.string() }, {}, {
|
|
219
|
+
decks: model({ title: z.string() }, {}, { groups: { root: 'deck' } });
|
|
218
220
|
// a deck row → group `deck:<id>`
|
|
219
221
|
```
|
|
220
222
|
|
|
@@ -232,7 +234,7 @@ slides: model(
|
|
|
232
234
|
deck: relation.belongsTo('decks', 'deckId', { parent: true }), // ownership → inherit deck:<id>
|
|
233
235
|
sourceSlide: relation.belongsTo('slides', 'sourceSlideId'), // reference → NOT routed
|
|
234
236
|
},
|
|
235
|
-
{
|
|
237
|
+
{}, // default policy: row-local organization_id
|
|
236
238
|
);
|
|
237
239
|
```
|
|
238
240
|
|
|
@@ -241,8 +243,8 @@ slides: model(
|
|
|
241
243
|
> some required FKs are mere references. Containment is a fact only you know, so
|
|
242
244
|
> it's declared, exactly as it is in OpenFGA/Zanzibar.
|
|
243
245
|
|
|
244
|
-
**`grants` — a membership edge.** On a join model (e.g. `dataroomMember`),
|
|
245
|
-
says "this row grants a *subject* access to a *scope root*." Both are relation
|
|
246
|
+
**`groups.grants` — a membership edge.** On a join model (e.g. `dataroomMember`),
|
|
247
|
+
it says "this row grants a *subject* access to a *scope root*." Both are relation
|
|
246
248
|
names on the model. The server resolves it at connect time — for user `U`, it
|
|
247
249
|
finds the scope-root groups `U` is a member of and adds them to `U`'s allowed
|
|
248
250
|
set (Linear's `/sync/user_sync_groups`). Use this for sub-org sharing; plain
|
|
@@ -255,15 +257,19 @@ dataroomMember: model(
|
|
|
255
257
|
member: relation.belongsTo('users', 'userId'),
|
|
256
258
|
room: relation.belongsTo('datarooms', 'dataroomId'),
|
|
257
259
|
},
|
|
258
|
-
{
|
|
260
|
+
{ groups: { grants: { subject: 'member', scope: 'room' } } },
|
|
259
261
|
);
|
|
260
262
|
```
|
|
261
263
|
|
|
262
264
|
For the rare group keyed on a plain field rather than a relation (per-recipient
|
|
263
|
-
inbox fan-out, say), there's
|
|
265
|
+
inbox fan-out, say), there's a `groups: { roles: [entityRole({ kind, source })] }`
|
|
264
266
|
escape hatch. For rows that inherit *tenancy* (not a sync group) through a
|
|
265
|
-
foreign key without carrying `organization_id`, use `
|
|
266
|
-
|
|
267
|
+
foreign key without carrying `organization_id`, use `policy: { by: 'parent', fk,
|
|
268
|
+
parent }` rather than opting out of isolation. The old `orgScoped: false`
|
|
269
|
+
exposed the whole table cross-tenant, so `validate_schema` rejects the removed
|
|
270
|
+
options as `tenancy-option-removed` errors and steers you to `policy: { by:
|
|
271
|
+
'parent' }` (FK inheritance) or, for genuinely global reference data, the
|
|
272
|
+
explicit `policy: { by: 'none' }`. See
|
|
267
273
|
`packages/sync-engine/src/schema/model.ts` for the full option set.
|
|
268
274
|
|
|
269
275
|
## How identity reaches Ablo — the proxy model
|
|
@@ -393,8 +399,8 @@ an entity anchor on the models an agent operates on:
|
|
|
393
399
|
|
|
394
400
|
```ts
|
|
395
401
|
// each scope-root model an agent edits forms a per-entity group
|
|
396
|
-
documents: model({ /* … */ }, {}, {
|
|
397
|
-
decks: model({ /* … */ }, {}, {
|
|
402
|
+
documents: model({ /* … */ }, {}, { groups: { root: 'document' } }),
|
|
403
|
+
decks: model({ /* … */ }, {}, { groups: { root: 'deck' } }),
|
|
398
404
|
```
|
|
399
405
|
|
|
400
406
|
Then a run subscribes only to the entity groups for the rows it works on — a
|
|
@@ -484,9 +490,10 @@ an agent pointed at the entities it's working on. You **never hand-write**
|
|
|
484
490
|
(it returns a participant handle with `.peers`). See
|
|
485
491
|
[Coordination](./coordination.md).
|
|
486
492
|
|
|
487
|
-
> **`
|
|
488
|
-
> in `model(...)` declares a scope root
|
|
489
|
-
>
|
|
493
|
+
> **`groups.root` is the schema model option, not a client setting.**
|
|
494
|
+
> `groups: { root: 'deck' }` in `model(...)` declares a scope root
|
|
495
|
+
> ([Half 2](#half-2--per-model-scope-row--group)) — it names the group
|
|
496
|
+
> (`deck:<id>`) that the mechanisms above then subscribe to.
|
|
490
497
|
> There is no `Ablo({ scope })` constructor option. The lifecycle filter on
|
|
491
498
|
> [`list()`](./api.md#model-methods) is a separate axis named **`state`**
|
|
492
499
|
> (`'live' | 'archived' | 'all'`, GitHub's open/closed/all), precisely so it
|
package/docs/index.md
CHANGED
|
@@ -99,4 +99,3 @@ Three things stay true no matter how you use Ablo:
|
|
|
99
99
|
- [README](../README.md) — product overview and first example.
|
|
100
100
|
- [AGENTS.md](../AGENTS.md) — short installation guidance for coding assistants.
|
|
101
101
|
- [Changelog](../CHANGELOG.md) — what shipped recently.
|
|
102
|
-
- [Roadmap](./roadmap.md) — what's planned next.
|
|
@@ -142,20 +142,23 @@ model(
|
|
|
142
142
|
},
|
|
143
143
|
/* relations */ {},
|
|
144
144
|
{
|
|
145
|
-
//
|
|
146
|
-
|
|
145
|
+
// Axis 1 — `policy`: who may READ a row (tenant isolation / RLS). A
|
|
146
|
+
// row-local `organization_id` column is the default, so you omit this for
|
|
147
|
+
// normal tables; set it only for the exceptions (parent-inherited / global).
|
|
147
148
|
|
|
149
|
+
// Axis 2 — `groups`: which sync-group CHANNELS a row fans into.
|
|
148
150
|
// Scope root: rows form the group `matter:<id>`. Children point at it with
|
|
149
151
|
// `relation.belongsTo('matters', 'matterId', { parent: true })` to inherit.
|
|
150
|
-
|
|
152
|
+
groups: { root: 'matter' },
|
|
151
153
|
}
|
|
152
154
|
);
|
|
153
155
|
```
|
|
154
156
|
|
|
155
|
-
For rows that don't carry `organization_id` themselves but inherit
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
157
|
+
For rows that don't carry `organization_id` themselves but inherit tenancy via a
|
|
158
|
+
foreign key, set `policy: { by: 'parent', fk: '<fk>', parent: '<parentTable>' }`.
|
|
159
|
+
For genuinely global/reference data, `policy: { by: 'none' }`. ⚠ `by: 'none'`
|
|
160
|
+
exposes the whole table cross-tenant, so it's an explicit, named branch — never a
|
|
161
|
+
falsy flag. See `packages/sync-engine/src/schema/model.ts` for the full option set.
|
|
159
162
|
|
|
160
163
|
## 2. Create The Client
|
|
161
164
|
|
|
@@ -444,7 +447,7 @@ scope.
|
|
|
444
447
|
```ts
|
|
445
448
|
await using claim = await ablo.weatherReports.claim({
|
|
446
449
|
id: reportId,
|
|
447
|
-
|
|
450
|
+
reason: 'forecasting',
|
|
448
451
|
});
|
|
449
452
|
const claimed = claim.data;
|
|
450
453
|
if (!claimed) return;
|
|
@@ -510,7 +513,7 @@ them.
|
|
|
510
513
|
| `update({ id, data, ...opts })` | Update through the model client. |
|
|
511
514
|
| `delete({ id, ...opts })` | Delete through the model client. |
|
|
512
515
|
| `claim.state({ id })` | See who is currently working on a row (synchronous). |
|
|
513
|
-
| `claim({ id,
|
|
516
|
+
| `claim({ id, reason?, ttl? })` | Acquire a disposable handle: wait for your turn, re-read, and hold the row. |
|
|
514
517
|
|
|
515
518
|
Keep first integrations on the model methods above. Every mutation and
|
|
516
519
|
server-read verb takes one options object; only the synchronous `get(id)` stays
|
|
@@ -70,7 +70,7 @@ automatically when the scope exits:
|
|
|
70
70
|
```ts
|
|
71
71
|
await using claim = await ablo.weatherReports.claim({
|
|
72
72
|
id: 'report_stockholm',
|
|
73
|
-
|
|
73
|
+
reason: 'editing',
|
|
74
74
|
});
|
|
75
75
|
await ablo.weatherReports.update({ id: claim.data.id, data: { status: 'ready' } }); // rejected if the row changed under the claim
|
|
76
76
|
```
|
package/docs/mcp.md
CHANGED
|
@@ -67,11 +67,23 @@ Point your assistant at the hosted endpoint — no auth, no token:
|
|
|
67
67
|
claude mcp add --transport http ablo https://<your-app>/api/mcp
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
The endpoint is identical for every client — only the config surface differs:
|
|
71
71
|
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
-
|
|
72
|
+
- **Claude Code** — run the `claude mcp add` command above; verify with `/mcp list`, remove with `claude mcp remove ablo`.
|
|
73
|
+
- **Cursor** — add the server to `~/.cursor/mcp.json` (macOS / Linux), then restart.
|
|
74
|
+
- **Windsurf** — add the same JSON via Settings → Cascade → MCP, then restart.
|
|
75
|
+
|
|
76
|
+
Cursor and Windsurf use the same config shape:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"mcpServers": {
|
|
81
|
+
"ablo": { "transport": "http", "url": "https://<your-app>/api/mcp" }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Each client then lists the Ablo tools (`search_ablo_docs`, `get_recipe`, `get_api_surface`, `validate_schema`, `scaffold_app`) in its MCP panel.
|
|
75
87
|
|
|
76
88
|
### What it exposes
|
|
77
89
|
|
|
@@ -96,7 +108,7 @@ loading everything into context.
|
|
|
96
108
|
Reusable, parameterised templates that drive an end-to-end flow:
|
|
97
109
|
|
|
98
110
|
- `integrate-sync-engine` — wire the SDK into an existing project.
|
|
99
|
-
- `add-agent` — add an agent worker that coordinates via
|
|
111
|
+
- `add-agent` — add an agent worker that coordinates via claims and
|
|
100
112
|
conflict-safe writes.
|
|
101
113
|
- `define-schema` — design a Zod-first schema from a description, then run
|
|
102
114
|
`validate_schema` before committing.
|
package/docs/quickstart.md
CHANGED
|
@@ -222,7 +222,7 @@ Call `handle.release()` when your work is done.
|
|
|
222
222
|
// Claim the row so other participants serialize behind us while we work.
|
|
223
223
|
const handle = await ablo.weatherReports.claim({
|
|
224
224
|
id: 'weather_stockholm',
|
|
225
|
-
|
|
225
|
+
reason: 'checking_weather',
|
|
226
226
|
ttl: '2m',
|
|
227
227
|
});
|
|
228
228
|
|
|
@@ -260,7 +260,7 @@ write through the model.
|
|
|
260
260
|
```ts
|
|
261
261
|
const active = ablo.weatherReports.claim.state({ id: 'weather_stockholm' });
|
|
262
262
|
if (active) {
|
|
263
|
-
console.log(`${active.heldBy} is ${active.
|
|
263
|
+
console.log(`${active.heldBy} is ${active.reason}`);
|
|
264
264
|
}
|
|
265
265
|
|
|
266
266
|
const handle = await ablo.weatherReports.claim({ id: 'weather_stockholm' });
|
|
@@ -268,7 +268,7 @@ await ablo.weatherReports.update({ id: handle.data.id, data: { status: 'ready' }
|
|
|
268
268
|
await handle.release();
|
|
269
269
|
```
|
|
270
270
|
|
|
271
|
-
Use `{
|
|
271
|
+
Use `{ queue: false }` on `claim` when work should be skipped instead of queued
|
|
272
272
|
behind an active holder.
|
|
273
273
|
|
|
274
274
|
## Next steps
|
package/llms.txt
CHANGED
|
@@ -12,7 +12,7 @@ First action when integrating into an app: run `npx ablo init --yes --framework
|
|
|
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
|
|
|
15
|
-
Then PUSH — this is the step everything depends on. The server keeps its OWN copy of the schema. Run `npx ablo push
|
|
15
|
+
Then PUSH — this is the step everything depends on. The server keeps its OWN copy of the schema. Run `npx ablo push`: it pushes `ablo/schema.ts` (sandbox) AND writes `ABLO_API_KEY` into `.env.local` from the stored login. Until the schema is pushed, EVERY write to a new or changed model fails with `server_execute_unknown_model`. Re-run it after schema changes. `push` is one-shot; `dev` is the watcher, so use `npx ablo dev --no-watch` only when you intentionally want the dev command to push once and exit.
|
|
16
16
|
|
|
17
17
|
## Projects (one org, many apps)
|
|
18
18
|
|
|
@@ -29,7 +29,6 @@ TYPES: the project registers its schema ONCE via declaration merging — `npx ab
|
|
|
29
29
|
|
|
30
30
|
const schema = defineSchema({
|
|
31
31
|
weatherReports: model({
|
|
32
|
-
id: z.string(),
|
|
33
32
|
location: z.string(),
|
|
34
33
|
status: z.enum(['pending', 'ready']),
|
|
35
34
|
forecast: z.string().optional(),
|
|
@@ -190,6 +189,6 @@ Do not teach `/api`, `/agent`, `/ai-sdk`, `/core`, `/realtime`, or internal subp
|
|
|
190
189
|
- `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.
|
|
191
190
|
- 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`).
|
|
192
191
|
- Adopt an existing DB: `npx ablo pull prisma [path]` / `npx ablo pull drizzle <module>`.
|
|
193
|
-
- `npx ablo push
|
|
192
|
+
- `npx ablo push` pushes the schema (sandbox) AND writes `ABLO_API_KEY` to `.env.local`; `npx ablo dev --no-watch` is the push-once form of the watcher; `npx ablo logs --no-follow` exits instead of tailing forever; `npx ablo mode sandbox|production` always needs the argument. `npx ablo push`/`status`/`pull`/`check`/`generate` are one-shot.
|
|
194
193
|
|
|
195
194
|
Canonical docs to read before integrating: `quickstart`, `schema-contract`, `integration-guide`, `guarantees`, `client-behavior`, `data-sources`, `examples/existing-python-backend`, `api`, `examples/ai-sdk-tool`, and `examples/server-agent`. When upgrading an existing integration, read `migration` — every breaking change, what to change, and which version introduced it.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abloatai/ablo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "The Collaboration Layer For AI Agents",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -119,7 +119,6 @@
|
|
|
119
119
|
"examples",
|
|
120
120
|
"AGENTS.md",
|
|
121
121
|
"llms.txt",
|
|
122
|
-
"llms-full.txt",
|
|
123
122
|
"LICENSE",
|
|
124
123
|
"NOTICE",
|
|
125
124
|
"README.md",
|
|
@@ -131,9 +130,11 @@
|
|
|
131
130
|
"build:cli": "tsup --config tsup.cli.config.ts",
|
|
132
131
|
"typecheck:cli": "tsc -p tsconfig.cli.json",
|
|
133
132
|
"pack:check": "npm_config_cache=${TMPDIR:-/tmp}/ablo-npm-cache npm pack --dry-run",
|
|
133
|
+
"lint": "npm run lint:imports && npm run lint:errors && npm run lint:docs",
|
|
134
134
|
"lint:imports": "node scripts/check-js-extensions.mjs",
|
|
135
135
|
"generate:errors": "tsx scripts/generate-error-docs.mts",
|
|
136
136
|
"lint:errors": "tsx scripts/check-error-docs.mts",
|
|
137
|
+
"lint:docs": "node scripts/check-doc-drift.mjs",
|
|
137
138
|
"lint:pkg": "publint",
|
|
138
139
|
"prepublishOnly": "npm run build && npm run lint:pkg",
|
|
139
140
|
"check:dist": "node scripts/check-dist-fresh.mjs",
|
package/docs/mcp/claude-code.md
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
# Claude Code
|
|
2
|
-
|
|
3
|
-
## Install
|
|
4
|
-
|
|
5
|
-
```bash
|
|
6
|
-
claude mcp add --transport http ablo https://<your-app>/api/mcp
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
That's it — no token or header needed. The endpoint is public and serves
|
|
10
|
-
only docs, schema lint, and scaffolds. The next `/help` in Claude Code will
|
|
11
|
-
list the Ablo Sync tools.
|
|
12
|
-
|
|
13
|
-
## Verify
|
|
14
|
-
|
|
15
|
-
In Claude Code, run:
|
|
16
|
-
|
|
17
|
-
```
|
|
18
|
-
/mcp list
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
You should see `ablo` with the integration tools enumerated:
|
|
22
|
-
`search_ablo_docs`, `get_recipe`, `get_api_surface`, `validate_schema`,
|
|
23
|
-
`scaffold_app`.
|
|
24
|
-
|
|
25
|
-
## Removing
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
claude mcp remove ablo
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## More
|
|
32
|
-
|
|
33
|
-
- [MCP overview](/docs/mcp) — what the server exposes and how the transport works.
|
|
34
|
-
- [Cursor setup](/docs/mcp/cursor) — same URL, different UI.
|
|
35
|
-
- [Windsurf setup](/docs/mcp/windsurf) — same URL, different UI.
|
package/docs/mcp/cursor.md
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
# Cursor
|
|
2
|
-
|
|
3
|
-
## Install
|
|
4
|
-
|
|
5
|
-
Add the Ablo Sync MCP server to Cursor's `mcp.json`:
|
|
6
|
-
|
|
7
|
-
```json
|
|
8
|
-
{
|
|
9
|
-
"mcpServers": {
|
|
10
|
-
"ablo": {
|
|
11
|
-
"transport": "http",
|
|
12
|
-
"url": "https://<your-app>/api/mcp"
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
The file lives at `~/.cursor/mcp.json` on macOS / Linux. No auth header is
|
|
19
|
-
needed — the endpoint is public and serves only docs, schema lint, and
|
|
20
|
-
scaffolds.
|
|
21
|
-
|
|
22
|
-
Restart Cursor. The Ablo Sync tools appear under the MCP icon in the agent
|
|
23
|
-
panel.
|
|
24
|
-
|
|
25
|
-
## Verify
|
|
26
|
-
|
|
27
|
-
In Cursor's agent panel, open the MCP tools list. You should see the
|
|
28
|
-
Ablo Sync integration tools and their JSON schemas: `search_ablo_docs`,
|
|
29
|
-
`get_recipe`, `get_api_surface`, `validate_schema`, `scaffold_app`.
|
|
30
|
-
|
|
31
|
-
## More
|
|
32
|
-
|
|
33
|
-
- [MCP overview](/docs/mcp) — what the server exposes and how the transport works.
|
|
34
|
-
- [Claude Code setup](/docs/mcp/claude-code) — CLI install.
|
|
35
|
-
- [Windsurf setup](/docs/mcp/windsurf) — same JSON shape.
|
package/docs/mcp/windsurf.md
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# Windsurf
|
|
2
|
-
|
|
3
|
-
## Install
|
|
4
|
-
|
|
5
|
-
Add the Ablo Sync MCP server to Windsurf's MCP config:
|
|
6
|
-
|
|
7
|
-
```json
|
|
8
|
-
{
|
|
9
|
-
"mcpServers": {
|
|
10
|
-
"ablo": {
|
|
11
|
-
"transport": "http",
|
|
12
|
-
"url": "https://<your-app>/api/mcp"
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
The config path differs by platform — Windsurf surfaces it in Settings →
|
|
19
|
-
Cascade → MCP. Restart Windsurf after saving. No auth header is needed —
|
|
20
|
-
the endpoint is public and serves only docs, schema lint, and scaffolds.
|
|
21
|
-
|
|
22
|
-
## Verify
|
|
23
|
-
|
|
24
|
-
Cascade's MCP panel lists every configured server with its tools. You
|
|
25
|
-
should see `ablo` with the integration tools enumerated:
|
|
26
|
-
`search_ablo_docs`, `get_recipe`, `get_api_surface`, `validate_schema`,
|
|
27
|
-
`scaffold_app`.
|
|
28
|
-
|
|
29
|
-
## More
|
|
30
|
-
|
|
31
|
-
- [MCP overview](/docs/mcp) — what the server exposes and how the transport works.
|
|
32
|
-
- [Claude Code setup](/docs/mcp/claude-code) — CLI install.
|
|
33
|
-
- [Cursor setup](/docs/mcp/cursor) — same JSON shape.
|