@abloatai/ablo 0.11.2 → 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 +34 -0
- package/README.md +3 -3
- package/dist/ai-sdk/claim-broadcast.d.ts +4 -3
- package/dist/ai-sdk/claim-broadcast.js +2 -2
- package/dist/ai-sdk/wrap.d.ts +5 -4
- package/dist/ai-sdk/wrap.js +3 -3
- package/dist/cli.cjs +152 -41
- package/dist/client/Ablo.d.ts +25 -3
- package/dist/client/Ablo.js +5 -5
- package/dist/client/ApiClient.js +26 -11
- package/dist/client/createModelProxy.d.ts +15 -7
- package/dist/client/createModelProxy.js +12 -12
- package/dist/coordination/schema.d.ts +1 -1
- package/dist/coordination/schema.js +3 -1
- package/dist/errors.d.ts +3 -1
- package/dist/errors.js +6 -1
- package/dist/react/AbloProvider.d.ts +11 -7
- package/dist/react/AbloProvider.js +9 -5
- package/dist/react/context.d.ts +9 -14
- package/dist/react/context.js +10 -15
- package/dist/react/index.d.ts +8 -4
- package/dist/react/index.js +8 -4
- package/dist/react/useMutators.js +3 -2
- package/dist/react/useUndoScope.js +3 -2
- package/dist/schema/index.d.ts +2 -2
- package/dist/schema/index.js +2 -2
- package/dist/schema/model.d.ts +38 -77
- 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/dist/sync/createClaimStream.js +5 -4
- package/dist/sync/participants.js +1 -1
- package/dist/types/streams.d.ts +17 -7
- 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/migration.md +2 -1
- 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/AGENTS.md
CHANGED
|
@@ -31,7 +31,7 @@ Every model verb takes ONE options object. The common loop:
|
|
|
31
31
|
|
|
32
32
|
1. **Read** the row — `await ablo.<model>.retrieve({ id })` (async; from the server) or `await ablo.<model>.list({ where })` for many. In React render, read synchronously with `useAblo((a) => a.<model>.get(id))`.
|
|
33
33
|
2. **See who's active** (optional) — `ablo.<model>.claim.state({ id })` (synchronous; never blocks).
|
|
34
|
-
3. **Claim** the row before changing it — `await using claim = await ablo.<model>.claim({ id,
|
|
34
|
+
3. **Claim** the row before changing it — `await using claim = await ablo.<model>.claim({ id, reason?, ttl? })`. If someone else holds it, this waits for them, then gives you the fresh row on `claim.data`. The claim auto-releases when it goes out of scope (`await using`).
|
|
35
35
|
4. **Write** — `await ablo.<model>.update({ id: claim.data.id, data })`. Because you hold the claim, the write is rejected if the row changed underneath you.
|
|
36
36
|
|
|
37
37
|
Keep coding assistants on this schema-backed path.
|
|
@@ -59,7 +59,7 @@ if (!report) throw new Error('Report not found');
|
|
|
59
59
|
// row before resolving. Auto-released at the end of this scope (`await using`).
|
|
60
60
|
await using claim = await ablo.weatherReports.claim({
|
|
61
61
|
id: 'report_stockholm',
|
|
62
|
-
|
|
62
|
+
reason: 'forecasting',
|
|
63
63
|
ttl: '2m',
|
|
64
64
|
});
|
|
65
65
|
const claimed = claim.data;
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.13.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Schema authoring: split model routing into two orthogonal axes — `policy` (row access) and `groups` (sync-group routing).
|
|
8
|
+
|
|
9
|
+
**Breaking (schema authoring).** The flat, collision-prone model options are replaced by two namespaced ones:
|
|
10
|
+
- **`policy`** — row-access / tenant isolation (named after Postgres/Supabase RLS policies: the rule that scopes which rows a tenant may read). A discriminated union on `by` replaces the old `orgScoped` / `scopedVia` / `orgColumn` trio:
|
|
11
|
+
- `{ by: 'column' }` — row-local tenancy column (the default when omitted; column name still overridable).
|
|
12
|
+
- `{ by: 'parent', fk, parent }` — inherit tenancy through a foreign key when the table has no tenancy column of its own (e.g. `slide_layers` → `slides`).
|
|
13
|
+
- Type `TenancyInput` is renamed `PolicyInput`; `policyInputSchema` / `resolvePolicy` are now exported.
|
|
14
|
+
- **`groups: { root, grants, roles }`** — which delta channels a row fans into (orthogonal to `policy`, which governs read access). One namespaced object replaces the old flat `scope` / `grants` / `entityRoles`:
|
|
15
|
+
- `root` (was `scope`) — mark a model a scope root; its records form the group `<kind>:<id>`. Renamed so it no longer collides with the old `scopedVia` tenancy sugar or the inner `grants.scope` relation name.
|
|
16
|
+
- `grants` — a membership edge granting an identity access to a scope root.
|
|
17
|
+
- `roles` (was `entityRoles`) — explicit non-relational record→group roles; accepts one role or an array.
|
|
18
|
+
- `groupsInputSchema` / `GroupsInput` are now exported.
|
|
19
|
+
|
|
20
|
+
**CLI.** `config.json` now stores per-project profile key pairs (`profiles: Record<string, ProfileKeys>`) instead of a single top-level pair; older flat layouts are folded into the active profile automatically on read, so existing logins keep working. `login` / `projects` updated to the profile model.
|
|
21
|
+
|
|
22
|
+
## 0.12.0
|
|
23
|
+
|
|
24
|
+
### Minor Changes
|
|
25
|
+
|
|
26
|
+
- Canonicalize the claim API to one vocabulary, plus DX fixes (breaking).
|
|
27
|
+
- BREAKING: claim phase field `action` → `reason` on every claim surface
|
|
28
|
+
(`Claim`, `ClaimHandle`, `ClaimCreateOptions`, `ModelClaim`, ...). The wire
|
|
29
|
+
is unchanged (still `action`, healed on read) — no server redeploy needed.
|
|
30
|
+
- BREAKING: claim contention flag `wait` → `queue` (one word everywhere).
|
|
31
|
+
- BREAKING: React hook `useParticipant` → `useWatch` (aligns with `ablo.<model>.watch`).
|
|
32
|
+
- `ClaimDeclaration.ttlSeconds` is now `number` (was a `Duration`).
|
|
33
|
+
- Docs: `retrieve` HTTP envelope (`.data`/`.stamp`) called out; `syncGroups`
|
|
34
|
+
reworded (provisional, not deprecated); `orgScoped` cross-tenant security
|
|
35
|
+
warning; React error strings point at `<AbloProvider>`.
|
|
36
|
+
|
|
3
37
|
## 0.11.2
|
|
4
38
|
|
|
5
39
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -274,7 +274,7 @@ ablo.weatherReports.claim.state({ id: 'report_stockholm' });
|
|
|
274
274
|
ablo.weatherReports.claim.queue({ id: 'report_stockholm' });
|
|
275
275
|
|
|
276
276
|
{
|
|
277
|
-
await using claim = await ablo.weatherReports.claim({ id,
|
|
277
|
+
await using claim = await ablo.weatherReports.claim({ id, queue: false });
|
|
278
278
|
/* do the held work */
|
|
279
279
|
}
|
|
280
280
|
|
|
@@ -285,11 +285,11 @@ ablo.weatherReports.claim.queue({ id: 'report_stockholm' });
|
|
|
285
285
|
```
|
|
286
286
|
|
|
287
287
|
`claim.state` returns the holder (or `null`); `claim.queue` returns the line waiting
|
|
288
|
-
behind it. `
|
|
288
|
+
behind it. `queue: false` skips rather than waiting when the row is held;
|
|
289
289
|
`maxQueueDepth: 2` bails when two or more are already ahead.
|
|
290
290
|
|
|
291
291
|
Default reads keep working while a row is claimed. Server reads that need claimed
|
|
292
|
-
semantics can opt in with `ifClaimed: 'return' | '
|
|
292
|
+
semantics can opt in with `ifClaimed: 'return' | 'fail'`.
|
|
293
293
|
|
|
294
294
|
Even an unclaimed write can't land on stale reasoning — the commit is guarded:
|
|
295
295
|
|
|
@@ -58,10 +58,11 @@ export interface ClaimBroadcastMiddlewareOptions<R extends SchemaRecord = Schema
|
|
|
58
58
|
/** Target entity. Null skips the broadcast (purely conversational). */
|
|
59
59
|
readonly target: ClaimTarget | null;
|
|
60
60
|
/**
|
|
61
|
-
*
|
|
62
|
-
* `'edit'`, `'read'`, `'review'`, `'generate'`. Default `'edit'`.
|
|
61
|
+
* Human-readable phase describing what the agent is doing. Convention:
|
|
62
|
+
* `'edit'`, `'read'`, `'review'`, `'generate'`. Default `'edit'`. The same
|
|
63
|
+
* `reason` field used on every claim surface.
|
|
63
64
|
*/
|
|
64
|
-
readonly
|
|
65
|
+
readonly reason?: string;
|
|
65
66
|
/**
|
|
66
67
|
* Peer-visible explanation of the specific work this model call is about to
|
|
67
68
|
* perform. Surfaces to other agents through `ActiveClaim.description`.
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
export function claimBroadcastMiddleware(options) {
|
|
31
31
|
const { agent, target } = options;
|
|
32
|
-
const
|
|
32
|
+
const reason = options.reason ?? 'edit';
|
|
33
33
|
const description = options.description;
|
|
34
34
|
const openClaim = () => {
|
|
35
35
|
if (!agent || !target)
|
|
@@ -42,7 +42,7 @@ export function claimBroadcastMiddleware(options) {
|
|
|
42
42
|
field: target.field,
|
|
43
43
|
meta: target.meta,
|
|
44
44
|
}, {
|
|
45
|
-
reason
|
|
45
|
+
reason,
|
|
46
46
|
description,
|
|
47
47
|
ttl: target.estimatedMs ?? 60_000,
|
|
48
48
|
});
|
package/dist/ai-sdk/wrap.d.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* model: anthropic('claude-opus-4-7'),
|
|
15
15
|
* agent,
|
|
16
16
|
* target: { entityType: 'SlideDeck', entityId: 'deck-abc' },
|
|
17
|
-
*
|
|
17
|
+
* reason: 'renaming',
|
|
18
18
|
* description: 'Renaming the deck title to match the project brief.',
|
|
19
19
|
* });
|
|
20
20
|
*
|
|
@@ -40,10 +40,11 @@ export interface WrapWithMultiplayerOptions {
|
|
|
40
40
|
/** Target entity. Null = pass-through wrap. */
|
|
41
41
|
readonly target: ClaimTarget | null;
|
|
42
42
|
/**
|
|
43
|
-
* Optional
|
|
44
|
-
* Convention: `'edit'`, `'read'`, `'review'`, `'generate'`.
|
|
43
|
+
* Optional human-readable phase for the broadcast. Default `'edit'`.
|
|
44
|
+
* Convention: `'edit'`, `'read'`, `'review'`, `'generate'`. The same
|
|
45
|
+
* `reason` field used on every claim surface.
|
|
45
46
|
*/
|
|
46
|
-
readonly
|
|
47
|
+
readonly reason?: string;
|
|
47
48
|
/**
|
|
48
49
|
* Peer-visible explanation of the specific work this model call is about to
|
|
49
50
|
* perform. Other agents receive it in their coordination context.
|
package/dist/ai-sdk/wrap.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* model: anthropic('claude-opus-4-7'),
|
|
15
15
|
* agent,
|
|
16
16
|
* target: { entityType: 'SlideDeck', entityId: 'deck-abc' },
|
|
17
|
-
*
|
|
17
|
+
* reason: 'renaming',
|
|
18
18
|
* description: 'Renaming the deck title to match the project brief.',
|
|
19
19
|
* });
|
|
20
20
|
*
|
|
@@ -31,12 +31,12 @@ import { wrapLanguageModel } from 'ai';
|
|
|
31
31
|
import { claimBroadcastMiddleware, } from './claim-broadcast.js';
|
|
32
32
|
import { coordinationContextMiddleware } from './coordination-context.js';
|
|
33
33
|
export function wrapWithMultiplayer(options) {
|
|
34
|
-
const { model, agent, target,
|
|
34
|
+
const { model, agent, target, reason, description, excludeClaimIds, extraMiddleware } = options;
|
|
35
35
|
return wrapLanguageModel({
|
|
36
36
|
model,
|
|
37
37
|
middleware: [
|
|
38
38
|
coordinationContextMiddleware({ agent, target, excludeClaimIds }),
|
|
39
|
-
claimBroadcastMiddleware({ agent, target,
|
|
39
|
+
claimBroadcastMiddleware({ agent, target, reason, description }),
|
|
40
40
|
...(extraMiddleware ?? []),
|
|
41
41
|
],
|
|
42
42
|
});
|
package/dist/cli.cjs
CHANGED
|
@@ -277023,6 +277023,7 @@ var roleSchema = import_zod2.z.object({
|
|
|
277023
277023
|
kind: import_zod2.z.string().regex(/^[a-z][a-z0-9_]*$/, 'kind must be a lowercase identifier, e.g. "deck"'),
|
|
277024
277024
|
source: roleSourceSchema
|
|
277025
277025
|
});
|
|
277026
|
+
var entityRoleSchema = roleSchema;
|
|
277026
277027
|
var scopeSchema = import_zod2.z.union([
|
|
277027
277028
|
import_zod2.z.boolean(),
|
|
277028
277029
|
import_zod2.z.string().regex(/^[a-z][a-z0-9_]*$/, 'scope kind must be a lowercase identifier, e.g. "dataroom"')
|
|
@@ -277031,6 +277032,11 @@ var grantsRefSchema = import_zod2.z.object({
|
|
|
277031
277032
|
subject: import_zod2.z.string().regex(/^[a-zA-Z_][a-zA-Z0-9_]*$/, "grants.subject must name a relation"),
|
|
277032
277033
|
scope: import_zod2.z.string().regex(/^[a-zA-Z_][a-zA-Z0-9_]*$/, "grants.scope must name a relation")
|
|
277033
277034
|
});
|
|
277035
|
+
var groupsInputSchema = import_zod2.z.object({
|
|
277036
|
+
root: scopeSchema.optional(),
|
|
277037
|
+
grants: grantsRefSchema.optional(),
|
|
277038
|
+
roles: import_zod2.z.union([entityRoleSchema, import_zod2.z.array(entityRoleSchema)]).optional()
|
|
277039
|
+
});
|
|
277034
277040
|
|
|
277035
277041
|
// src/coordination/schema.ts
|
|
277036
277042
|
var targetRangeSchema = import_zod3.z.object({
|
|
@@ -277121,7 +277127,9 @@ var modelClaimSchema = import_zod3.z.object({
|
|
|
277121
277127
|
id: import_zod3.z.string(),
|
|
277122
277128
|
actor: import_zod3.z.string(),
|
|
277123
277129
|
participantKind: wireParticipantKindSchema,
|
|
277124
|
-
|
|
277130
|
+
/** Human-readable phase (`'editing'`). The public SDK field; the WS/HTTP
|
|
277131
|
+
* wire carries the same value as `action` (healed on read). */
|
|
277132
|
+
reason: import_zod3.z.string(),
|
|
277125
277133
|
description: import_zod3.z.string().optional(),
|
|
277126
277134
|
field: import_zod3.z.string().optional(),
|
|
277127
277135
|
status: import_zod3.z.enum(["active", "queued"]).optional(),
|
|
@@ -279644,6 +279652,7 @@ init_cjs_shims();
|
|
|
279644
279652
|
var import_os2 = require("os");
|
|
279645
279653
|
var import_path2 = require("path");
|
|
279646
279654
|
var import_fs3 = require("fs");
|
|
279655
|
+
var DEFAULT_PROFILE = "default";
|
|
279647
279656
|
function configDir() {
|
|
279648
279657
|
if (process.env.ABLO_CONFIG_DIR) return process.env.ABLO_CONFIG_DIR;
|
|
279649
279658
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
@@ -279655,12 +279664,32 @@ function configPath() {
|
|
|
279655
279664
|
function credentialsPath() {
|
|
279656
279665
|
return (0, import_path2.join)(configDir(), "credentials.json");
|
|
279657
279666
|
}
|
|
279667
|
+
function activeProfileName(cfg) {
|
|
279668
|
+
return cfg.activeProject?.slug ?? DEFAULT_PROFILE;
|
|
279669
|
+
}
|
|
279658
279670
|
function asKeyEntry(value) {
|
|
279659
279671
|
if (value && typeof value === "object" && typeof value.apiKey === "string") {
|
|
279660
279672
|
return value;
|
|
279661
279673
|
}
|
|
279662
279674
|
return void 0;
|
|
279663
279675
|
}
|
|
279676
|
+
function asProfileKeys(value) {
|
|
279677
|
+
if (!value || typeof value !== "object") return void 0;
|
|
279678
|
+
const v = value;
|
|
279679
|
+
const sandbox = asKeyEntry(v.sandbox);
|
|
279680
|
+
const production = asKeyEntry(v.production);
|
|
279681
|
+
if (!sandbox && !production) return void 0;
|
|
279682
|
+
return { ...sandbox ? { sandbox } : {}, ...production ? { production } : {} };
|
|
279683
|
+
}
|
|
279684
|
+
function asProfileMap(value) {
|
|
279685
|
+
if (!value || typeof value !== "object") return {};
|
|
279686
|
+
const out = {};
|
|
279687
|
+
for (const [name, v] of Object.entries(value)) {
|
|
279688
|
+
const keys = asProfileKeys(v);
|
|
279689
|
+
if (keys) out[name] = keys;
|
|
279690
|
+
}
|
|
279691
|
+
return out;
|
|
279692
|
+
}
|
|
279664
279693
|
function readJson(path) {
|
|
279665
279694
|
if (!(0, import_fs3.existsSync)(path)) return null;
|
|
279666
279695
|
try {
|
|
@@ -279681,7 +279710,7 @@ function normalizeStoredMode(value) {
|
|
|
279681
279710
|
if (value === "sandbox" || value === "production") return value;
|
|
279682
279711
|
return void 0;
|
|
279683
279712
|
}
|
|
279684
|
-
function
|
|
279713
|
+
function extractLegacyEntries(obj) {
|
|
279685
279714
|
const sandbox = asKeyEntry(obj.sandbox);
|
|
279686
279715
|
const production = asKeyEntry(obj.production);
|
|
279687
279716
|
if (sandbox || production) {
|
|
@@ -279690,24 +279719,33 @@ function extractEntries(obj) {
|
|
|
279690
279719
|
const flat = asKeyEntry(obj);
|
|
279691
279720
|
return flat ? { sandbox: flat } : {};
|
|
279692
279721
|
}
|
|
279722
|
+
function hasKey(keys) {
|
|
279723
|
+
return !!(keys?.sandbox || keys?.production);
|
|
279724
|
+
}
|
|
279693
279725
|
function readConfig() {
|
|
279694
279726
|
const cfgObj = readJson(configPath());
|
|
279695
279727
|
const credObj = readJson(credentialsPath());
|
|
279696
279728
|
const mode2 = normalizeStoredMode(cfgObj?.mode) ?? normalizeStoredMode(credObj?.mode);
|
|
279697
279729
|
const activeProject = asActiveProject(cfgObj?.activeProject);
|
|
279698
|
-
const
|
|
279699
|
-
const
|
|
279700
|
-
...
|
|
279701
|
-
...
|
|
279702
|
-
// credentials file wins
|
|
279730
|
+
const activeName = activeProject?.slug ?? DEFAULT_PROFILE;
|
|
279731
|
+
const profiles = {
|
|
279732
|
+
...asProfileMap(credObj?.profiles),
|
|
279733
|
+
...asProfileMap(cfgObj?.profiles)
|
|
279703
279734
|
};
|
|
279704
|
-
|
|
279735
|
+
const legacyCfg = cfgObj ? extractLegacyEntries(cfgObj) : {};
|
|
279736
|
+
const legacyCred = credObj ? extractLegacyEntries(credObj) : {};
|
|
279737
|
+
const legacy = { ...legacyCfg, ...legacyCred };
|
|
279738
|
+
const migratedLegacy = hasKey(legacy) && !hasKey(profiles[activeName]);
|
|
279739
|
+
if (migratedLegacy) profiles[activeName] = legacy;
|
|
279740
|
+
const anyKey = Object.values(profiles).some(hasKey);
|
|
279741
|
+
if (!mode2 && !anyKey) return null;
|
|
279705
279742
|
const config = {
|
|
279706
279743
|
mode: mode2 ?? "sandbox",
|
|
279707
279744
|
...activeProject ? { activeProject } : {},
|
|
279708
|
-
|
|
279745
|
+
profiles
|
|
279709
279746
|
};
|
|
279710
|
-
|
|
279747
|
+
const secretsInConfig = hasKey(legacyCfg);
|
|
279748
|
+
if (secretsInConfig || migratedLegacy) writeConfig(config);
|
|
279711
279749
|
return config;
|
|
279712
279750
|
}
|
|
279713
279751
|
function writeConfig(cfg) {
|
|
@@ -279723,16 +279761,34 @@ function writeConfig(cfg) {
|
|
|
279723
279761
|
`,
|
|
279724
279762
|
{ mode: 384 }
|
|
279725
279763
|
);
|
|
279726
|
-
const
|
|
279727
|
-
|
|
279728
|
-
|
|
279729
|
-
|
|
279730
|
-
|
|
279764
|
+
const profiles = {};
|
|
279765
|
+
for (const [name, keys] of Object.entries(cfg.profiles)) {
|
|
279766
|
+
if (!hasKey(keys)) continue;
|
|
279767
|
+
profiles[name] = {
|
|
279768
|
+
...keys.sandbox ? { sandbox: keys.sandbox } : {},
|
|
279769
|
+
...keys.production ? { production: keys.production } : {}
|
|
279770
|
+
};
|
|
279771
|
+
}
|
|
279772
|
+
(0, import_fs3.writeFileSync)(credentialsPath(), `${JSON.stringify({ profiles }, null, 2)}
|
|
279731
279773
|
`, { mode: 384 });
|
|
279732
279774
|
return credentialsPath();
|
|
279733
279775
|
}
|
|
279776
|
+
function emptyConfig(mode2 = "sandbox") {
|
|
279777
|
+
return { mode: mode2, profiles: {} };
|
|
279778
|
+
}
|
|
279779
|
+
function setProfileKeys(profileName, keys, opts) {
|
|
279780
|
+
const cfg = readConfig() ?? emptyConfig(opts.mode);
|
|
279781
|
+
cfg.mode = opts.mode;
|
|
279782
|
+
cfg.profiles[profileName] = {
|
|
279783
|
+
...keys.sandbox ? { sandbox: keys.sandbox } : {},
|
|
279784
|
+
...keys.production ? { production: keys.production } : {}
|
|
279785
|
+
};
|
|
279786
|
+
if (opts.activeProject) cfg.activeProject = opts.activeProject;
|
|
279787
|
+
else delete cfg.activeProject;
|
|
279788
|
+
return writeConfig(cfg);
|
|
279789
|
+
}
|
|
279734
279790
|
function setMode(mode2) {
|
|
279735
|
-
const cfg = readConfig() ??
|
|
279791
|
+
const cfg = readConfig() ?? emptyConfig(mode2);
|
|
279736
279792
|
cfg.mode = mode2;
|
|
279737
279793
|
return writeConfig(cfg);
|
|
279738
279794
|
}
|
|
@@ -279743,13 +279799,15 @@ function getActiveProject() {
|
|
|
279743
279799
|
return readConfig()?.activeProject;
|
|
279744
279800
|
}
|
|
279745
279801
|
function setActiveProject(project) {
|
|
279746
|
-
const cfg = readConfig() ??
|
|
279802
|
+
const cfg = readConfig() ?? emptyConfig("sandbox");
|
|
279747
279803
|
if (project) cfg.activeProject = project;
|
|
279748
279804
|
else delete cfg.activeProject;
|
|
279749
279805
|
return writeConfig(cfg);
|
|
279750
279806
|
}
|
|
279751
279807
|
function getKeyEntry(mode2) {
|
|
279752
|
-
|
|
279808
|
+
const cfg = readConfig();
|
|
279809
|
+
if (!cfg) return void 0;
|
|
279810
|
+
return cfg.profiles[activeProfileName(cfg)]?.[mode2];
|
|
279753
279811
|
}
|
|
279754
279812
|
function modeFromKey(key) {
|
|
279755
279813
|
if (/^(sk|rk)_test_/.test(key)) return "sandbox";
|
|
@@ -279773,11 +279831,21 @@ function resolveApiKey(modeOverride) {
|
|
|
279773
279831
|
if (process.env.ABLO_API_KEY) return process.env.ABLO_API_KEY;
|
|
279774
279832
|
const cfg = readConfig();
|
|
279775
279833
|
if (!cfg) return void 0;
|
|
279776
|
-
const entry = cfg[modeOverride ?? cfg.mode];
|
|
279834
|
+
const entry = cfg.profiles[activeProfileName(cfg)]?.[modeOverride ?? cfg.mode];
|
|
279777
279835
|
if (!entry) return void 0;
|
|
279778
279836
|
if (entry.expiresAt && Date.parse(entry.expiresAt) <= Date.now()) return void 0;
|
|
279779
279837
|
return entry.apiKey;
|
|
279780
279838
|
}
|
|
279839
|
+
function guardActiveProjectKey() {
|
|
279840
|
+
if (process.env.ABLO_API_KEY) {
|
|
279841
|
+
return { ok: true, activeProfile: DEFAULT_PROFILE, available: [] };
|
|
279842
|
+
}
|
|
279843
|
+
const cfg = readConfig();
|
|
279844
|
+
const activeProfile = cfg ? activeProfileName(cfg) : DEFAULT_PROFILE;
|
|
279845
|
+
const profiles = cfg?.profiles ?? {};
|
|
279846
|
+
const available = Object.entries(profiles).filter(([, keys]) => hasKey(keys)).map(([name]) => name);
|
|
279847
|
+
return { ok: hasKey(profiles[activeProfile]), activeProfile, available };
|
|
279848
|
+
}
|
|
279781
279849
|
function resolvePushPlan() {
|
|
279782
279850
|
const envKey = process.env.ABLO_API_KEY;
|
|
279783
279851
|
if (envKey) return { flow: modeFromKey(envKey) ?? getMode(), apiKey: envKey, source: "env" };
|
|
@@ -280423,8 +280491,19 @@ function openBrowser(url) {
|
|
|
280423
280491
|
} catch {
|
|
280424
280492
|
}
|
|
280425
280493
|
}
|
|
280426
|
-
|
|
280494
|
+
function parseProjectFlag(argv) {
|
|
280495
|
+
const i = argv.indexOf("--project");
|
|
280496
|
+
if (i >= 0) {
|
|
280497
|
+
const slug = argv[i + 1];
|
|
280498
|
+
if (slug && !slug.startsWith("-")) return slug;
|
|
280499
|
+
}
|
|
280500
|
+
const eq = argv.find((a) => a.startsWith("--project="));
|
|
280501
|
+
return eq ? eq.slice("--project=".length) || void 0 : void 0;
|
|
280502
|
+
}
|
|
280503
|
+
async function deviceLogin(argv) {
|
|
280427
280504
|
Ie(`${brand("ablo")} login`);
|
|
280505
|
+
const requested = parseProjectFlag(argv) ?? getActiveProject()?.slug;
|
|
280506
|
+
const targetProject = requested === DEFAULT_PROFILE ? void 0 : requested;
|
|
280428
280507
|
const interactive = Boolean(process.stdout.isTTY && process.stdin.isTTY);
|
|
280429
280508
|
let account = "login";
|
|
280430
280509
|
if (interactive) {
|
|
@@ -280502,13 +280581,19 @@ ${import_picocolors7.default.dim(url)}`, "Approve in your browser");
|
|
|
280502
280581
|
s.stop("Timed out waiting for approval.");
|
|
280503
280582
|
process.exit(1);
|
|
280504
280583
|
}
|
|
280505
|
-
s.message(
|
|
280584
|
+
s.message(
|
|
280585
|
+
targetProject ? `Provisioning keys for ${targetProject}\u2026` : "Provisioning a sandbox key\u2026"
|
|
280586
|
+
);
|
|
280506
280587
|
const provRes = await fetch(`${AUTH_URL}/api/cli/provision-key`, {
|
|
280507
280588
|
method: "POST",
|
|
280508
280589
|
headers: { authorization: `Bearer ${accessToken}`, "content-type": "application/json" },
|
|
280509
|
-
//
|
|
280510
|
-
//
|
|
280511
|
-
|
|
280590
|
+
// Scope the minted keys to the chosen project (`--project`/active), with
|
|
280591
|
+
// the device_code as a legacy fallback for the /cli picker. Both harmless
|
|
280592
|
+
// if absent → org-default keys.
|
|
280593
|
+
body: JSON.stringify({
|
|
280594
|
+
device_code: code.device_code,
|
|
280595
|
+
...targetProject ? { project_slug: targetProject } : {}
|
|
280596
|
+
})
|
|
280512
280597
|
}).catch(() => null);
|
|
280513
280598
|
if (!provRes || !provRes.ok) {
|
|
280514
280599
|
s.stop("Could not provision a key.");
|
|
@@ -280526,16 +280611,23 @@ ${import_picocolors7.default.dim(url)}`, "Approve in your browser");
|
|
|
280526
280611
|
...prov.organizationId ? { organizationId: prov.organizationId } : {},
|
|
280527
280612
|
...k3.expiresAt ? { expiresAt: k3.expiresAt } : {}
|
|
280528
280613
|
});
|
|
280529
|
-
const
|
|
280530
|
-
|
|
280531
|
-
|
|
280532
|
-
|
|
280533
|
-
|
|
280614
|
+
const profileName = prov.project?.slug ?? DEFAULT_PROFILE;
|
|
280615
|
+
const path = setProfileKeys(
|
|
280616
|
+
profileName,
|
|
280617
|
+
{
|
|
280618
|
+
sandbox: entry(prov.test),
|
|
280619
|
+
...prov.live ? { production: entry(prov.live) } : {}
|
|
280620
|
+
},
|
|
280621
|
+
{ mode: "sandbox", activeProject: prov.project ?? void 0 }
|
|
280622
|
+
);
|
|
280534
280623
|
s.stop(`Saved keys to ${path}`);
|
|
280535
|
-
|
|
280624
|
+
const where = prov.project ? ` ${import_picocolors7.default.dim(`(project ${prov.project.slug})`)}` : "";
|
|
280625
|
+
Se(
|
|
280626
|
+
`${import_picocolors7.default.green("\u2713")} Logged in ${import_picocolors7.default.dim("(sandbox)")}${where}. Run ${import_picocolors7.default.bold("npx ablo push")} to push your schema.`
|
|
280627
|
+
);
|
|
280536
280628
|
}
|
|
280537
|
-
async function login() {
|
|
280538
|
-
await deviceLogin();
|
|
280629
|
+
async function login(argv = []) {
|
|
280630
|
+
await deviceLogin(argv);
|
|
280539
280631
|
}
|
|
280540
280632
|
function logout() {
|
|
280541
280633
|
const removed = clearCredential();
|
|
@@ -280788,11 +280880,13 @@ async function projects(argv) {
|
|
|
280788
280880
|
setActiveProject({ id: target.id, slug: target.slug });
|
|
280789
280881
|
console.log(` ${import_picocolors9.default.green("\u2713")} now targeting project ${import_picocolors9.default.bold(target.slug)} ${import_picocolors9.default.dim(`(${target.id})`)}`);
|
|
280790
280882
|
}
|
|
280791
|
-
|
|
280792
|
-
|
|
280793
|
-
|
|
280794
|
-
|
|
280795
|
-
|
|
280883
|
+
const guard = guardActiveProjectKey();
|
|
280884
|
+
if (!guard.ok) {
|
|
280885
|
+
const loginCmd = guard.activeProfile === DEFAULT_PROFILE ? "ablo login" : `ablo login --project ${guard.activeProfile}`;
|
|
280886
|
+
console.log(
|
|
280887
|
+
import_picocolors9.default.dim(` No key stored for this project yet \u2014 run ${import_picocolors9.default.bold(loginCmd)} to mint one.`)
|
|
280888
|
+
);
|
|
280889
|
+
}
|
|
280796
280890
|
return;
|
|
280797
280891
|
}
|
|
280798
280892
|
console.error(
|
|
@@ -281368,7 +281462,7 @@ function spreadOpts(optsArg) {
|
|
|
281368
281462
|
}
|
|
281369
281463
|
return `, ...${optsArg.getText()}`;
|
|
281370
281464
|
}
|
|
281371
|
-
function
|
|
281465
|
+
function hasKey2(obj, key) {
|
|
281372
281466
|
return obj.getProperties().some(
|
|
281373
281467
|
(p2) => (import_ts_morph.Node.isPropertyAssignment(p2) || import_ts_morph.Node.isShorthandPropertyAssignment(p2)) && p2.getName() === key
|
|
281374
281468
|
);
|
|
@@ -281381,7 +281475,7 @@ function verbRewrite(call, verb) {
|
|
|
281381
281475
|
const first = args[0];
|
|
281382
281476
|
const calleeText = call.getExpression().getText();
|
|
281383
281477
|
if (verb === "create") {
|
|
281384
|
-
if (import_ts_morph.Node.isObjectLiteralExpression(first) &&
|
|
281478
|
+
if (import_ts_morph.Node.isObjectLiteralExpression(first) && hasKey2(first, "data")) return null;
|
|
281385
281479
|
return `${calleeText}({ data: ${first.getText()}${spreadOpts(args[1])} })`;
|
|
281386
281480
|
}
|
|
281387
281481
|
if (import_ts_morph.Node.isObjectLiteralExpression(first)) return null;
|
|
@@ -282210,7 +282304,7 @@ async function main() {
|
|
|
282210
282304
|
if (command === "init") {
|
|
282211
282305
|
await init(process.argv.slice(3));
|
|
282212
282306
|
} else if (command === "login") {
|
|
282213
|
-
await login();
|
|
282307
|
+
await login(process.argv.slice(3));
|
|
282214
282308
|
} else if (command === "logout") {
|
|
282215
282309
|
logout();
|
|
282216
282310
|
} else if (command === "mode") {
|
|
@@ -282249,6 +282343,22 @@ async function main() {
|
|
|
282249
282343
|
const rest = process.argv.slice(3);
|
|
282250
282344
|
const advanced = rest.some((a) => ["--force", "--rename", "--backfill", "--url"].includes(a));
|
|
282251
282345
|
const watching = rest.includes("--watch");
|
|
282346
|
+
const guard = guardActiveProjectKey();
|
|
282347
|
+
if (!guard.ok && guard.available.length > 0 && !rest.includes("--url")) {
|
|
282348
|
+
console.error(
|
|
282349
|
+
` ${import_picocolors18.default.yellow("\u26A0")} active project ${import_picocolors18.default.bold(guard.activeProfile)} has no stored key ${import_picocolors18.default.dim(
|
|
282350
|
+
`(you have keys for: ${guard.available.join(", ")})`
|
|
282351
|
+
)}`
|
|
282352
|
+
);
|
|
282353
|
+
const loginCmd = guard.activeProfile === "default" ? "ablo login" : `ablo login --project ${guard.activeProfile}`;
|
|
282354
|
+
console.error(
|
|
282355
|
+
import_picocolors18.default.dim(
|
|
282356
|
+
` Mint one with ${import_picocolors18.default.bold(loginCmd)}, or switch with ${import_picocolors18.default.bold("ablo projects use <slug>")}.`
|
|
282357
|
+
)
|
|
282358
|
+
);
|
|
282359
|
+
process.exitCode = 1;
|
|
282360
|
+
return;
|
|
282361
|
+
}
|
|
282252
282362
|
const plan = resolvePushPlan();
|
|
282253
282363
|
if (advanced || plan.flow === "production" && !watching) {
|
|
282254
282364
|
await push(rest);
|
|
@@ -282273,11 +282383,12 @@ async function main() {
|
|
|
282273
282383
|
console.log(` [--auth apikey] [--storage direct|endpoint] [--project <slug>] [--no-project]`);
|
|
282274
282384
|
console.log(` [--no-agent] [--no-pull] [--no-install] [--no-login]`);
|
|
282275
282385
|
console.log(` npx ablo login Authorize in your browser (provisions sandbox + production keys)`);
|
|
282386
|
+
console.log(` npx ablo login --project <slug> Same, scoped to a project (mints its keys, makes it active)`);
|
|
282276
282387
|
console.log(` npx ablo logout Remove the stored API key`);
|
|
282277
282388
|
console.log(` npx ablo mode [sandbox|production] Switch active environment, like Stripe`);
|
|
282278
282389
|
console.log(` npx ablo projects list List the org's projects (default + your own)`);
|
|
282279
282390
|
console.log(` npx ablo projects create <slug> Create a project (its keys/schema/data are isolated)`);
|
|
282280
|
-
console.log(` npx ablo projects use <slug|default>
|
|
282391
|
+
console.log(` npx ablo projects use <slug|default> Switch the active project (run login --project to mint its keys)`);
|
|
282281
282392
|
console.log(` npx ablo status Show org, mode, keys, and server health`);
|
|
282282
282393
|
console.log(` npx ablo status --json Same, machine-readable (mode, key prefix, org id, api host)`);
|
|
282283
282394
|
console.log(` npx ablo logs [-n N] [--since 15m] Tail commit activity (follows; --no-follow to exit)`);
|
package/dist/client/Ablo.d.ts
CHANGED
|
@@ -332,8 +332,14 @@ export interface InternalAbloOptions<S extends SchemaRecord = SchemaRecord> {
|
|
|
332
332
|
*/
|
|
333
333
|
configOverrides?: Partial<SyncEngineConfig>;
|
|
334
334
|
/**
|
|
335
|
-
*
|
|
336
|
-
*
|
|
335
|
+
* Sync groups (entity scopes) this client subscribes to. **Provisional, not
|
|
336
|
+
* deprecated** — pick the right lane: normally the server derives these from
|
|
337
|
+
* the apiKey's scope, but passing them is still REQUIRED today in any config
|
|
338
|
+
* where the key doesn't resolve them (omitting yields a `degenerate
|
|
339
|
+
* syncGroups` warning and a zero-fan-out client). Keep passing it explicitly
|
|
340
|
+
* until the server-derived path ships in Phase 3, at which point it becomes a
|
|
341
|
+
* true no-op and is removed. Build values with `syncGroup(kind, id)` from
|
|
342
|
+
* `@abloatai/ablo/schema`.
|
|
337
343
|
*/
|
|
338
344
|
syncGroups?: string[];
|
|
339
345
|
/**
|
|
@@ -388,7 +394,9 @@ export interface ModelReadOptions extends ClaimedOptions {
|
|
|
388
394
|
}
|
|
389
395
|
export interface ClaimCreateOptions {
|
|
390
396
|
readonly target: ModelTarget;
|
|
391
|
-
|
|
397
|
+
/** Human-readable phase shown to peers — `'editing'`, `'writing'`. The same
|
|
398
|
+
* word on every claim surface; serialized on the wire as `action`. */
|
|
399
|
+
readonly reason: string;
|
|
392
400
|
readonly ttl?: Duration;
|
|
393
401
|
/**
|
|
394
402
|
* Join the server's fair FIFO queue when the target is already claimed,
|
|
@@ -490,6 +498,20 @@ export interface HttpClaimApi<T> {
|
|
|
490
498
|
reorder(params: ClaimReorderParams<T>): Promise<void>;
|
|
491
499
|
}
|
|
492
500
|
export interface ModelClient<T = Record<string, unknown>> {
|
|
501
|
+
/**
|
|
502
|
+
* Single-row read over HTTP. **Returns an envelope, not the bare row** — the
|
|
503
|
+
* row is on `.data`, alongside the `.stamp` watermark (for stale-context
|
|
504
|
+
* guards on the following write) and any active `.claims`. A stateless HTTP
|
|
505
|
+
* client can't synthesize the watermark from a local snapshot, so the
|
|
506
|
+
* envelope is load-bearing here (the WebSocket client's `retrieve` returns
|
|
507
|
+
* `T | undefined` because it reads from the hydrated pool).
|
|
508
|
+
*
|
|
509
|
+
* ```ts
|
|
510
|
+
* const deal = await ablo.deals.retrieve({ id });
|
|
511
|
+
* deal.data?.recommendation; // ← the row is on .data
|
|
512
|
+
* deal.stamp; // watermark — pass to the next write's readAt
|
|
513
|
+
* ```
|
|
514
|
+
*/
|
|
493
515
|
retrieve(params: ModelReadOptions & {
|
|
494
516
|
readonly id: string;
|
|
495
517
|
}): Promise<ModelRead<T>>;
|
package/dist/client/Ablo.js
CHANGED
|
@@ -1124,7 +1124,7 @@ export function Ablo(options) {
|
|
|
1124
1124
|
id: claim.id,
|
|
1125
1125
|
actor: claim.heldBy,
|
|
1126
1126
|
participantKind: claim.participantKind,
|
|
1127
|
-
|
|
1127
|
+
reason: claim.reason,
|
|
1128
1128
|
...(description ? { description } : {}),
|
|
1129
1129
|
field: claim.target.field,
|
|
1130
1130
|
status: 'active',
|
|
@@ -1144,7 +1144,7 @@ export function Ablo(options) {
|
|
|
1144
1144
|
id: claim.id,
|
|
1145
1145
|
actor: claim.heldBy,
|
|
1146
1146
|
participantKind: claim.participantKind,
|
|
1147
|
-
|
|
1147
|
+
reason: claim.reason,
|
|
1148
1148
|
...(claim.description ? { description: claim.description } : {}),
|
|
1149
1149
|
field: claim.target.field,
|
|
1150
1150
|
status: 'queued',
|
|
@@ -1246,7 +1246,7 @@ export function Ablo(options) {
|
|
|
1246
1246
|
return {
|
|
1247
1247
|
object: 'claim',
|
|
1248
1248
|
claimId: claim.claimId,
|
|
1249
|
-
|
|
1249
|
+
reason: claim.reason,
|
|
1250
1250
|
target: claim.target,
|
|
1251
1251
|
waited,
|
|
1252
1252
|
release,
|
|
@@ -1265,7 +1265,7 @@ export function Ablo(options) {
|
|
|
1265
1265
|
field: claimOptions.target.field,
|
|
1266
1266
|
meta: claimOptions.target.meta,
|
|
1267
1267
|
}, {
|
|
1268
|
-
reason: claimOptions.
|
|
1268
|
+
reason: claimOptions.reason,
|
|
1269
1269
|
ttl: claimOptions.ttl,
|
|
1270
1270
|
queue: claimOptions.queue,
|
|
1271
1271
|
});
|
|
@@ -1348,7 +1348,7 @@ export function Ablo(options) {
|
|
|
1348
1348
|
...(held.target.field ? { field: held.target.field } : {}),
|
|
1349
1349
|
...(held.target.meta ? { meta: held.target.meta } : {}),
|
|
1350
1350
|
},
|
|
1351
|
-
|
|
1351
|
+
reason: held.reason,
|
|
1352
1352
|
heldBy: held.actor,
|
|
1353
1353
|
participantKind: held.participantKind,
|
|
1354
1354
|
expiresAt: held.expiresAt,
|