@formo/cli 1.2.1 → 1.3.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/README.md +24 -20
- package/dist/commands/alerts.d.ts +0 -7
- package/dist/commands/alerts.js +0 -53
- package/dist/commands/analytics.js +23 -12
- package/dist/commands/charts.d.ts +1 -1
- package/dist/commands/charts.js +19 -6
- package/dist/commands/contracts.d.ts +0 -5
- package/dist/commands/contracts.js +9 -60
- package/dist/commands/import.js +4 -0
- package/dist/commands/profiles.d.ts +4 -1
- package/dist/commands/profiles.js +70 -67
- package/dist/lib/client.d.ts +1 -1
- package/dist/lib/client.js +8 -2
- package/dist/lib/filters.d.ts +7 -0
- package/dist/lib/filters.js +70 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -91,10 +91,12 @@ Fetch a single wallet profile by address or ENS name.
|
|
|
91
91
|
| Option | Description |
|
|
92
92
|
|---|---|
|
|
93
93
|
| `--expand` | Comma-separated fields: `apps`, `chains`, `tokens`, `labels` |
|
|
94
|
+
| `--timestamp` | ISO-8601 timestamp; return the closest stored wallet-enrichment snapshot |
|
|
94
95
|
|
|
95
96
|
```bash
|
|
96
97
|
formo profiles get 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
|
|
97
98
|
formo profiles get vitalik.eth --expand labels,chains
|
|
99
|
+
formo profiles get vitalik.eth --timestamp 2025-06-21T10:03:00Z
|
|
98
100
|
```
|
|
99
101
|
|
|
100
102
|
### `profiles search`
|
|
@@ -105,6 +107,7 @@ Search wallet profiles with filters, sorting, and pagination. Returns a `Paginat
|
|
|
105
107
|
|---|---|
|
|
106
108
|
| `--address` | Filter by wallet address |
|
|
107
109
|
| `--search` | Free-text search across address and identity fields |
|
|
110
|
+
| `--timestamp` | ISO-8601 timestamp; requires `--address` and returns the closest stored wallet-enrichment snapshot |
|
|
108
111
|
| `--page` | Page number (1-indexed, default `1`) |
|
|
109
112
|
| `--size` | Page size (default `100`, max `1000`) |
|
|
110
113
|
| `--order-by` | `last_onchain`, `first_onchain`, `net_worth_usd`, `updated_at`, `tx_count`, `first_seen`, `last_seen`, `num_sessions`, `revenue`, `volume`, `points` |
|
|
@@ -115,6 +118,7 @@ Search wallet profiles with filters, sorting, and pagination. Returns a `Paginat
|
|
|
115
118
|
|
|
116
119
|
```bash
|
|
117
120
|
formo profiles search --size 10
|
|
121
|
+
formo profiles search --address 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 --timestamp 2025-06-21T10:03:00Z
|
|
118
122
|
formo profiles search --order-by net_worth_usd --order-dir desc --size 5
|
|
119
123
|
formo profiles search --page 2 --size 20
|
|
120
124
|
formo profiles search --filters '[{"field":"users.net_worth_usd","op":"gt","value":10000}]' --size 20
|
|
@@ -122,6 +126,8 @@ formo profiles search --filters '[{"field":"users.net_worth_usd","op":"gt","valu
|
|
|
122
126
|
formo profiles search --filters '[{"field":"chains.balance","op":"gt","value":1000,"chain_id":"1"}]' --size 20
|
|
123
127
|
```
|
|
124
128
|
|
|
129
|
+
With `--timestamp`, wallet-enrichment fields come from the stored snapshot closest to that instant. Exact ties select the later snapshot. Expanded chains, apps, and tokens come from the selected profiling batch, while project engagement fields, identity overrides, and labels remain current. `profiles search --timestamp` requires `--address`.
|
|
130
|
+
|
|
125
131
|
### Lifecycle tuning (advanced)
|
|
126
132
|
|
|
127
133
|
Both `profiles get` and `profiles search` accept optional flags to override the lifecycle stage thresholds used when computing `lifecycle`:
|
|
@@ -142,7 +148,8 @@ Merge-update identity properties on a wallet profile.
|
|
|
142
148
|
|
|
143
149
|
| Option | Description |
|
|
144
150
|
|---|---|
|
|
145
|
-
| `--properties` | JSON object of properties to merge |
|
|
151
|
+
| `--properties` | JSON object of properties to merge; use `null` to unset a property |
|
|
152
|
+
| `--unset` | Comma-separated property keys to unset (`user_id` cannot be unset) |
|
|
146
153
|
|
|
147
154
|
**Allowed property keys:** `user_id`, `display_name`, `email`, `farcaster`, `discord`, `twitter`, `telegram`, `instagram`, `website`, `github`, `linkedin`, `facebook`, `tiktok`, `youtube`, `reddit`, `avatar`, `description`, `location`, `ens`, `lens`, `basenames`, `linea`. Unknown keys are rejected server-side.
|
|
148
155
|
|
|
@@ -151,6 +158,8 @@ formo profiles update 0xd8dA... --properties '{"display_name":"Vitalik","twitter
|
|
|
151
158
|
formo profiles update vitalik.eth --properties '{"email":"alice@example.com"}'
|
|
152
159
|
```
|
|
153
160
|
|
|
161
|
+
`--unset` takes precedence over matching keys in `--properties`. Deletions also mask enriched fallback values and historical snapshot reads until a new value is set.
|
|
162
|
+
|
|
154
163
|
> Requires `profiles:write` scope.
|
|
155
164
|
|
|
156
165
|
### `profiles properties batch`
|
|
@@ -241,13 +250,6 @@ Toggle an alert between `active` and `inactive`.
|
|
|
241
250
|
formo alerts toggle alert_abc123 --status inactive
|
|
242
251
|
```
|
|
243
252
|
|
|
244
|
-
### `alerts test <alertId>`
|
|
245
|
-
Send a test alert delivery with optional sample payloads.
|
|
246
|
-
|
|
247
|
-
```bash
|
|
248
|
-
formo alerts test alert_abc123 --sample-event '{"event":"transaction","revenue":250}'
|
|
249
|
-
```
|
|
250
|
-
|
|
251
253
|
---
|
|
252
254
|
|
|
253
255
|
## `formo boards`
|
|
@@ -266,7 +268,7 @@ Get a single board by ID.
|
|
|
266
268
|
|---|---|
|
|
267
269
|
| `--title` | Board title |
|
|
268
270
|
| `--description` | Optional board description |
|
|
269
|
-
| `--is-public` | Make the board publicly viewable |
|
|
271
|
+
| `--is-public` | Make the board publicly viewable. Omit to keep it private (the default). |
|
|
270
272
|
|
|
271
273
|
```bash
|
|
272
274
|
formo boards create --title "Revenue Metrics" --description "Weekly revenue tracking"
|
|
@@ -278,7 +280,7 @@ formo boards create --title "Revenue Metrics" --description "Weekly revenue trac
|
|
|
278
280
|
|---|---|
|
|
279
281
|
| `--title` | New board title |
|
|
280
282
|
| `--description` | New board description |
|
|
281
|
-
| `--is-public` |
|
|
283
|
+
| `--is-public` | Make the board publicly viewable. Omit to keep the stored setting; pass `--is-public=false` to make it private. |
|
|
282
284
|
|
|
283
285
|
### `boards delete <boardId>`
|
|
284
286
|
Delete a board.
|
|
@@ -290,7 +292,7 @@ Delete a board.
|
|
|
290
292
|
Chart commands. Charts live inside a board. Requires `boards:read` / `boards:write`.
|
|
291
293
|
|
|
292
294
|
### `charts list --board-id <boardId>`
|
|
293
|
-
List
|
|
295
|
+
List charts in a board. Returns lightweight summaries by default; pass `--results` to execute each chart’s query and include full results.
|
|
294
296
|
|
|
295
297
|
### `charts get <chartId> --board-id <boardId>`
|
|
296
298
|
Get a single chart by ID.
|
|
@@ -342,8 +344,6 @@ List all tracked contracts. Returns `{ data: Contract[], deploy: { last_deployed
|
|
|
342
344
|
### `contracts get <chain> <address>`
|
|
343
345
|
Get a single tracked contract.
|
|
344
346
|
|
|
345
|
-
### `contracts recommendations`
|
|
346
|
-
List contracts the project already interacts with but has not added yet.
|
|
347
347
|
|
|
348
348
|
### `contracts create`
|
|
349
349
|
|
|
@@ -354,8 +354,8 @@ List contracts the project already interacts with but has not added yet.
|
|
|
354
354
|
| `--name` | Human-readable contract name |
|
|
355
355
|
| `--abi` | Contract ABI as a JSON string; sent stringified to the API |
|
|
356
356
|
| `--events` | JSON array of ABI event objects to monitor |
|
|
357
|
-
| `--start-block` |
|
|
358
|
-
| `--include-in-pipeline` |
|
|
357
|
+
| `--start-block` | Non-negative safe integer recorded on the contract; does not backfill historical events |
|
|
358
|
+
| `--include-in-pipeline` | Deploy this contract to the events pipeline. Omit for decode-only (the default). |
|
|
359
359
|
|
|
360
360
|
```bash
|
|
361
361
|
formo contracts create --address 0x1f9840a85d5af5bf1d1762f925bdaddc4201f984 --chain 1 \
|
|
@@ -370,11 +370,9 @@ formo contracts create --address 0x1f9840a85d5af5bf1d1762f925bdaddc4201f984 --ch
|
|
|
370
370
|
| `--name` | Updated contract name |
|
|
371
371
|
| `--abi` | Updated ABI |
|
|
372
372
|
| `--events` | Updated JSON array of ABI event objects |
|
|
373
|
-
| `--start-block` |
|
|
374
|
-
| `--include-in-pipeline` |
|
|
373
|
+
| `--start-block` | Non-negative safe integer recorded on the contract; does not backfill historical events |
|
|
374
|
+
| `--include-in-pipeline` | Deploy this contract to the events pipeline. Omit to keep the stored setting; pass `--include-in-pipeline=false` to exclude it. |
|
|
375
375
|
|
|
376
|
-
### `contracts pipeline <chain> <address> --include-in-pipeline <true|false>`
|
|
377
|
-
Toggle pipeline inclusion without re-sending the full ABI/events payload.
|
|
378
376
|
|
|
379
377
|
### `contracts delete <chain> <address>`
|
|
380
378
|
Remove a tracked contract.
|
|
@@ -440,6 +438,12 @@ formo analytics retention --filters '[{"field":"location","op":"eq","value":"US"
|
|
|
440
438
|
|
|
441
439
|
On `kpis`, `top_*`, `revenue_*` and `volume_by_metric`, `--params '{"page_scope":"session"}'` widens a `page` filter from page-scoped metrics (the default) to the legacy session scope.
|
|
442
440
|
|
|
441
|
+
On user-aggregate pipes such as `lifecycle` and `frequency`, either-touch attribution filters can use a `fields` pair in place of `field`, e.g. `{"fields":["first_utm_source","last_utm_source"],"op":"eq","value":"twitter"}`.
|
|
442
|
+
|
|
443
|
+
Overview Data source filters use `field: "channel"` with `web`, `mobile`, `api`, `import`, `server`, or `onchain`; acquisition channel uses `channel_type`. User/lifecycle source filters use `source_filter` through `--params` with `field: "source"`.
|
|
444
|
+
|
|
445
|
+
Retention defaults to rolling (active in week N or later); use `--params '{"retention_type":"recurring"}'` for activity in exactly week N. Funnel steps accept `events` OR alternatives with member-level `filters`; the primary `type`/`event` is always included and step-level `filters` apply to the whole group.
|
|
446
|
+
|
|
443
447
|
All user-attribute, profile, social, lifecycle and resource predicates go in the single `--filters` array, using the canonical envelope with named qualifiers (`chain_id`, `app_id`, `token_address`, `scope`, `tag_id`). The retired per-family params — `socials`, `chain_filters`, `app_filters`, `token_filters`, `label_filters`, `profile_filters`, `lifecycle_filter` — are rejected with a `400` if passed through `--params`.
|
|
444
448
|
|
|
445
449
|
---
|
|
@@ -554,7 +558,7 @@ Every command supports the standard incur output flags:
|
|
|
554
558
|
| `--verbose` | Include the full envelope (`ok`, `data`, `meta`) |
|
|
555
559
|
| `--filter-output <keys>` | Filter output by key paths (e.g. `data,meta.duration`) |
|
|
556
560
|
|
|
557
|
-
Every list endpoint returns a `PaginatedResponse<T>` envelope: `{ data: [...], total, page, size, has_more }`.
|
|
561
|
+
Every list endpoint returns a `PaginatedResponse<T>` envelope: `{ data: [...], total, page, size, has_more }`. Most API errors follow: `{ error: { code, message, doc_url, param?, details? } }` — branch on `error.code` when available. Event-ingestion errors can be `{error:"..."}`, and rate-limit responses can be plain text; HTTP status is authoritative.
|
|
558
562
|
|
|
559
563
|
---
|
|
560
564
|
|
|
@@ -19,10 +19,3 @@ export declare function createAlertRun(options: CreateAlertOptions): Promise<unk
|
|
|
19
19
|
export declare function updateAlertRun(alertId: string, options: AlertBodyOptions): Promise<unknown>;
|
|
20
20
|
export declare function deleteAlertRun(alertId: string): Promise<unknown>;
|
|
21
21
|
export declare function toggleAlertRun(alertId: string, status: string): Promise<unknown>;
|
|
22
|
-
export interface TestAlertOptions {
|
|
23
|
-
sampleEvent?: string;
|
|
24
|
-
sampleUser?: string;
|
|
25
|
-
recipientOverrides?: string;
|
|
26
|
-
}
|
|
27
|
-
export declare function buildTestAlertBody(options: TestAlertOptions): Record<string, unknown> | undefined;
|
|
28
|
-
export declare function testAlertRun(alertId: string, options?: TestAlertOptions): Promise<unknown>;
|
package/dist/commands/alerts.js
CHANGED
|
@@ -8,8 +8,6 @@ exports.createAlertRun = createAlertRun;
|
|
|
8
8
|
exports.updateAlertRun = updateAlertRun;
|
|
9
9
|
exports.deleteAlertRun = deleteAlertRun;
|
|
10
10
|
exports.toggleAlertRun = toggleAlertRun;
|
|
11
|
-
exports.buildTestAlertBody = buildTestAlertBody;
|
|
12
|
-
exports.testAlertRun = testAlertRun;
|
|
13
11
|
const incur_1 = require("incur");
|
|
14
12
|
const client_1 = require("../lib/client");
|
|
15
13
|
const filters_1 = require("../lib/filters");
|
|
@@ -201,54 +199,3 @@ exports.alerts.command('toggle', {
|
|
|
201
199
|
return toggleAlertRun(args.alertId, options.status);
|
|
202
200
|
},
|
|
203
201
|
});
|
|
204
|
-
function buildTestAlertBody(options) {
|
|
205
|
-
const body = {};
|
|
206
|
-
if (options.sampleEvent !== undefined) {
|
|
207
|
-
body.sampleEvent = (0, json_1.parseJsonObject)(options.sampleEvent, '--sample-event');
|
|
208
|
-
}
|
|
209
|
-
if (options.sampleUser !== undefined) {
|
|
210
|
-
body.sampleUser = (0, json_1.parseJsonObject)(options.sampleUser, '--sample-user');
|
|
211
|
-
}
|
|
212
|
-
if (options.recipientOverrides !== undefined) {
|
|
213
|
-
body.recipientOverrides = (0, json_1.parseJsonArray)(options.recipientOverrides, '--recipient-overrides');
|
|
214
|
-
}
|
|
215
|
-
return Object.keys(body).length > 0 ? body : undefined;
|
|
216
|
-
}
|
|
217
|
-
function testAlertRun(alertId, options = {}) {
|
|
218
|
-
(0, client_1.requireApiKey)();
|
|
219
|
-
const client = (0, client_1.createClient)();
|
|
220
|
-
return client.post(`/v0/alerts/${encodeURIComponent(alertId)}/test`, buildTestAlertBody(options));
|
|
221
|
-
}
|
|
222
|
-
exports.alerts.command('test', {
|
|
223
|
-
description: 'Send a test delivery for an alert',
|
|
224
|
-
args: incur_1.z.object({
|
|
225
|
-
alertId: incur_1.z.string().describe('Alert ID to test'),
|
|
226
|
-
}),
|
|
227
|
-
options: incur_1.z.object({
|
|
228
|
-
sampleEvent: incur_1.z
|
|
229
|
-
.string()
|
|
230
|
-
.optional()
|
|
231
|
-
.describe('Optional JSON object to use as the sample event'),
|
|
232
|
-
sampleUser: incur_1.z
|
|
233
|
-
.string()
|
|
234
|
-
.optional()
|
|
235
|
-
.describe('Optional JSON object to use as the sample user/profile'),
|
|
236
|
-
recipientOverrides: incur_1.z
|
|
237
|
-
.string()
|
|
238
|
-
.optional()
|
|
239
|
-
.describe('Optional JSON array of recipient objects to test instead of saved recipients'),
|
|
240
|
-
}),
|
|
241
|
-
examples: [
|
|
242
|
-
{
|
|
243
|
-
args: { alertId: 'alert_abc123' },
|
|
244
|
-
options: {
|
|
245
|
-
sampleEvent: '{"event":"transaction","revenue":250}',
|
|
246
|
-
},
|
|
247
|
-
description: 'Send a test alert with a sample event',
|
|
248
|
-
},
|
|
249
|
-
],
|
|
250
|
-
hint: 'Requires alerts:write scope on your API key.',
|
|
251
|
-
run({ args, options }) {
|
|
252
|
-
return testAlertRun(args.alertId, options);
|
|
253
|
-
},
|
|
254
|
-
});
|
|
@@ -17,15 +17,15 @@ exports.analytics = incur_1.Cli.create('analytics', {
|
|
|
17
17
|
const PIPES = [
|
|
18
18
|
{ name: 'kpis', description: 'Traffic KPIs: visitors, pageviews, bounce rate, session duration' },
|
|
19
19
|
{ name: 'event_timeseries', description: 'Event counts over time' },
|
|
20
|
-
{ name: 'funnel', description: 'Conversion funnel across ordered steps. --params: steps (JSON array of {type,event,name,filters?}), window_seconds, funnel_type, group_by, limit, attribution' },
|
|
20
|
+
{ name: 'funnel', description: 'Conversion funnel across ordered steps. --params: steps (JSON array of {type,event,name,filters?,events?}; events adds OR alternatives with member filters), window_seconds, funnel_type, group_by, limit, attribution' },
|
|
21
21
|
{ name: 'flow', description: 'User path/flow analysis. --params: start_step / end_step (JSON {type,event,...}), global_filters, window_seconds, max_steps' },
|
|
22
22
|
{ name: 'frequency', description: 'Engagement frequency distribution' },
|
|
23
|
-
{ name: 'lifecycle', description: 'User lifecycle stages (
|
|
24
|
-
{ name: 'retention', description: 'Retention cohort analysis (params: id_type, event_type, event_name, min_users)' },
|
|
25
|
-
{ name: 'revenue_overview', description: 'Revenue overview with optional breakdown (params: group_by, rank_by)' },
|
|
26
|
-
{ name: 'revenue_by_metric', description: 'Revenue ranked by a metric column (params: metric_column, limit, offset)' },
|
|
23
|
+
{ name: 'lifecycle', description: 'User lifecycle stages (New, Returning, Power user, At Risk, Churned, Resurrected)' },
|
|
24
|
+
{ name: 'retention', description: 'Retention cohort analysis (params: retention_type — rolling by default or recurring, id_type, event_type, event_name, min_users)' },
|
|
25
|
+
{ name: 'revenue_overview', description: 'Revenue overview with optional breakdown (params: group_by — incl. channel_type and paid_source for ad network, rank_by)' },
|
|
26
|
+
{ name: 'revenue_by_metric', description: 'Revenue ranked by a metric column (params: metric_column — incl. channel and paid_source for ad network, limit, offset)' },
|
|
27
27
|
{ name: 'revenue_timeseries', description: 'Revenue over time (params: address)' },
|
|
28
|
-
{ name: 'volume_by_metric', description: 'Trading volume ranked by a metric column (params: metric_column, limit, offset)' },
|
|
28
|
+
{ name: 'volume_by_metric', description: 'Trading volume ranked by a metric column (params: metric_column — incl. channel and paid_source for ad network, limit, offset)' },
|
|
29
29
|
{ name: 'top_chains', description: 'Top chains by activity (params: limit, offset)' },
|
|
30
30
|
{ name: 'top_events', description: 'Top events by count (params: limit, offset, type)' },
|
|
31
31
|
{ name: 'top_locations', description: 'Top locations (params: limit, offset)' },
|
|
@@ -45,8 +45,11 @@ const RESERVED_PARAM_KEYS = new Set([
|
|
|
45
45
|
'dateTo',
|
|
46
46
|
'filters',
|
|
47
47
|
]);
|
|
48
|
-
const ANALYTICS_FILTER_KEYS = new Set([
|
|
49
|
-
|
|
48
|
+
const ANALYTICS_FILTER_KEYS = new Set([
|
|
49
|
+
'field', 'fields', 'op', 'value', 'filters',
|
|
50
|
+
'chain_id', 'app_id', 'token_address', 'scope', 'tag_id',
|
|
51
|
+
]);
|
|
52
|
+
const ANALYTICS_NESTED_FILTER_KEYS = new Set(['field', 'fields', 'op', 'value']);
|
|
50
53
|
function validateAnalyticsFilter(filter, path, allowNested) {
|
|
51
54
|
if (!filter || typeof filter !== 'object' || Array.isArray(filter)) {
|
|
52
55
|
throw new Error(`${path} must be a {field, op, value} object`);
|
|
@@ -59,11 +62,19 @@ function validateAnalyticsFilter(filter, path, allowNested) {
|
|
|
59
62
|
? ANALYTICS_FILTER_KEYS
|
|
60
63
|
: ANALYTICS_NESTED_FILTER_KEYS;
|
|
61
64
|
if (Object.keys(record).some((key) => !allowedKeys.has(key))) {
|
|
62
|
-
throw new Error(`${path} may only contain field, op, value${allowNested ? ', and
|
|
65
|
+
throw new Error(`${path} may only contain field, op, value, fields${allowNested ? ', filters, and resource qualifiers (chain_id, app_id, token_address, scope, tag_id)' : ''}`);
|
|
66
|
+
}
|
|
67
|
+
if (record.field !== undefined && record.fields !== undefined) {
|
|
68
|
+
throw new Error(`${path} must use only one of field or fields`);
|
|
69
|
+
}
|
|
70
|
+
if (record.fields !== undefined && (!Array.isArray(record.fields) || record.fields.length !== 2 ||
|
|
71
|
+
!record.fields.every((field) => typeof field === 'string' && field.length > 0))) {
|
|
72
|
+
throw new Error(`${path}.fields must name exactly two non-empty columns`);
|
|
63
73
|
}
|
|
64
|
-
if (typeof record.field !== 'string' || record.field.length === 0) {
|
|
65
|
-
throw new Error(`${path} requires a non-empty string "field"`);
|
|
74
|
+
if (record.fields === undefined && (typeof record.field !== 'string' || record.field.length === 0)) {
|
|
75
|
+
throw new Error(`${path} requires a non-empty string "field" or a "fields" pair`);
|
|
66
76
|
}
|
|
77
|
+
(0, filters_1.validateQualifiers)(record, typeof record.field === 'string' ? record.field : '', path);
|
|
67
78
|
if (!(0, filters_1.isCanonicalFilterOperator)(record.op)) {
|
|
68
79
|
throw new Error(`${path} requires a canonical "op"`);
|
|
69
80
|
}
|
|
@@ -163,7 +174,7 @@ const sharedOptions = incur_1.z.object({
|
|
|
163
174
|
.string()
|
|
164
175
|
.optional()
|
|
165
176
|
.describe('JSON array of filter conditions: [{"field","op","value"}]. ' +
|
|
166
|
-
'Use op "in"/"nin" with an array value (e.g. ["chrome","firefox"]); pipe-delimited strings are also accepted. Array string members cannot contain "|".'),
|
|
177
|
+
'Use op "in"/"nin" with an array value (e.g. ["chrome","firefox"]); pipe-delimited strings are also accepted. Array string members cannot contain "|". Resource filters accept chain_id, app_id, token_address, scope, and tag_id qualifiers.'),
|
|
167
178
|
params: incur_1.z
|
|
168
179
|
.string()
|
|
169
180
|
.optional()
|
|
@@ -15,7 +15,7 @@ export interface ChartBodyOptions {
|
|
|
15
15
|
settings?: string;
|
|
16
16
|
}
|
|
17
17
|
export declare function buildChartBody(options: ChartBodyOptions): Record<string, unknown>;
|
|
18
|
-
export declare function listChartsRun(boardId: string, options?: PaginationOptions): Promise<unknown>;
|
|
18
|
+
export declare function listChartsRun(boardId: string, options?: PaginationOptions, results?: boolean): Promise<unknown>;
|
|
19
19
|
export declare function listChartSummariesRun(boardId: string, options?: PaginationOptions): Promise<unknown>;
|
|
20
20
|
export declare function getChartRun(boardId: string, chartId: string): Promise<unknown>;
|
|
21
21
|
export interface QueryChartOptions {
|
package/dist/commands/charts.js
CHANGED
|
@@ -109,35 +109,48 @@ const chartBodyOptions = incur_1.z.object({
|
|
|
109
109
|
.describe('JSON object of type-specific chart settings (for example, user_paths anchors/maxSteps/nodesPerStep or retention entryFilter/retentionFilter)'),
|
|
110
110
|
});
|
|
111
111
|
// ── List charts for a board ──
|
|
112
|
-
function listChartsRun(boardId, options = {}) {
|
|
112
|
+
function listChartsRun(boardId, options = {}, results = false) {
|
|
113
113
|
(0, client_1.requireApiKey)();
|
|
114
114
|
const client = (0, client_1.createClient)();
|
|
115
115
|
return client.get(`/v0/boards/${encodeURIComponent(boardId)}/charts/`, {
|
|
116
|
-
params:
|
|
116
|
+
params: {
|
|
117
|
+
...(0, pagination_1.buildPaginationParams)(options),
|
|
118
|
+
// The public API returns lightweight summaries unless asked to execute.
|
|
119
|
+
...(results ? { include: 'results' } : {}),
|
|
120
|
+
},
|
|
117
121
|
});
|
|
118
122
|
}
|
|
119
123
|
exports.charts.command('list', {
|
|
120
|
-
description: 'List
|
|
124
|
+
description: 'List charts for a board (summaries by default; --results executes each chart query)',
|
|
121
125
|
options: incur_1.z.object({
|
|
122
126
|
boardId: incur_1.z.string().describe('Board ID to list charts from'),
|
|
127
|
+
results: incur_1.z
|
|
128
|
+
.boolean()
|
|
129
|
+
.optional()
|
|
130
|
+
.describe('Execute each chart query and include results (slower)'),
|
|
123
131
|
...pagination_1.paginationOptionsSchema,
|
|
124
132
|
}),
|
|
125
133
|
examples: [
|
|
126
134
|
{
|
|
127
135
|
options: { boardId: 'board_abc123' },
|
|
128
|
-
description: 'List
|
|
136
|
+
description: 'List chart summaries for a board',
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
options: { boardId: 'board_abc123', results: true },
|
|
140
|
+
description: 'List charts with executed query results',
|
|
129
141
|
},
|
|
130
142
|
],
|
|
131
143
|
hint: 'Requires boards:read scope on your API key.',
|
|
132
144
|
run({ options }) {
|
|
133
|
-
return listChartsRun(options.boardId, options);
|
|
145
|
+
return listChartsRun(options.boardId, options, options.results ?? false);
|
|
134
146
|
},
|
|
135
147
|
});
|
|
136
148
|
// ── List chart metadata for a board ──
|
|
137
149
|
function listChartSummariesRun(boardId, options = {}) {
|
|
138
150
|
(0, client_1.requireApiKey)();
|
|
139
151
|
const client = (0, client_1.createClient)();
|
|
140
|
-
|
|
152
|
+
// Summaries are the list endpoint's default; /charts/meta is internal-only.
|
|
153
|
+
return client.get(`/v0/boards/${encodeURIComponent(boardId)}/charts/`, {
|
|
141
154
|
params: (0, pagination_1.buildPaginationParams)(options),
|
|
142
155
|
});
|
|
143
156
|
}
|
|
@@ -4,7 +4,6 @@ export type { PaginationOptions };
|
|
|
4
4
|
export declare const contracts: Cli.Cli<{}, undefined, undefined, undefined>;
|
|
5
5
|
export declare function listContractsRun(options?: PaginationOptions): Promise<unknown>;
|
|
6
6
|
export declare function getContractRun(chain: string, address: string): Promise<unknown>;
|
|
7
|
-
export declare function getContractRecommendationsRun(): Promise<unknown>;
|
|
8
7
|
export interface CreateContractOptions {
|
|
9
8
|
address: string;
|
|
10
9
|
chain: number;
|
|
@@ -25,8 +24,4 @@ export interface UpdateContractOptions {
|
|
|
25
24
|
}
|
|
26
25
|
export declare function buildUpdateContractBody(chain: string | number, address: string, options: UpdateContractOptions): Record<string, unknown>;
|
|
27
26
|
export declare function updateContractRun(chain: string, address: string, options: UpdateContractOptions): Promise<unknown>;
|
|
28
|
-
export declare function updateContractPipelineRun(chain: string, address: string, includeInPipeline: boolean): Promise<unknown>;
|
|
29
|
-
export declare function buildUpdateContractPipelineBody(includeInPipeline: boolean): {
|
|
30
|
-
include_in_pipeline: boolean;
|
|
31
|
-
};
|
|
32
27
|
export declare function deleteContractRun(chain: string, address: string): Promise<unknown>;
|
|
@@ -3,20 +3,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.contracts = void 0;
|
|
4
4
|
exports.listContractsRun = listContractsRun;
|
|
5
5
|
exports.getContractRun = getContractRun;
|
|
6
|
-
exports.getContractRecommendationsRun = getContractRecommendationsRun;
|
|
7
6
|
exports.buildCreateContractBody = buildCreateContractBody;
|
|
8
7
|
exports.createContractRun = createContractRun;
|
|
9
8
|
exports.buildUpdateContractBody = buildUpdateContractBody;
|
|
10
9
|
exports.updateContractRun = updateContractRun;
|
|
11
|
-
exports.updateContractPipelineRun = updateContractPipelineRun;
|
|
12
|
-
exports.buildUpdateContractPipelineBody = buildUpdateContractPipelineBody;
|
|
13
10
|
exports.deleteContractRun = deleteContractRun;
|
|
14
11
|
const incur_1 = require("incur");
|
|
15
12
|
const client_1 = require("../lib/client");
|
|
16
13
|
const json_1 = require("../lib/json");
|
|
17
14
|
const pagination_1 = require("../lib/pagination");
|
|
18
15
|
exports.contracts = incur_1.Cli.create('contracts', {
|
|
19
|
-
description: 'Smart contract commands — register, list,
|
|
16
|
+
description: 'Smart contract commands — register, list, update, and remove tracked contracts',
|
|
20
17
|
});
|
|
21
18
|
function parseChain(chain) {
|
|
22
19
|
const value = typeof chain === 'number' ? chain : Number(chain);
|
|
@@ -25,6 +22,12 @@ function parseChain(chain) {
|
|
|
25
22
|
}
|
|
26
23
|
return value;
|
|
27
24
|
}
|
|
25
|
+
function parseStartBlock(value) {
|
|
26
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
27
|
+
throw new Error('--start-block must be a non-negative safe integer');
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
28
31
|
// ── List contracts ──
|
|
29
32
|
function listContractsRun(options = {}) {
|
|
30
33
|
(0, client_1.requireApiKey)();
|
|
@@ -48,25 +51,6 @@ function getContractRun(chain, address) {
|
|
|
48
51
|
const client = (0, client_1.createClient)();
|
|
49
52
|
return client.get(`/v0/contracts/${parseChain(chain)}/${encodeURIComponent(address)}`);
|
|
50
53
|
}
|
|
51
|
-
// ── Recommended contracts ──
|
|
52
|
-
function getContractRecommendationsRun() {
|
|
53
|
-
(0, client_1.requireApiKey)();
|
|
54
|
-
const client = (0, client_1.createClient)();
|
|
55
|
-
return client.get('/v0/contracts/recommendations');
|
|
56
|
-
}
|
|
57
|
-
exports.contracts.command('recommendations', {
|
|
58
|
-
description: 'List contracts the project already interacts with but has not added yet',
|
|
59
|
-
options: incur_1.z.object({}),
|
|
60
|
-
examples: [
|
|
61
|
-
{
|
|
62
|
-
description: 'Show recommended contracts to add for decoding/monitoring',
|
|
63
|
-
},
|
|
64
|
-
],
|
|
65
|
-
hint: 'Requires contracts:read scope on your API key.',
|
|
66
|
-
run() {
|
|
67
|
-
return getContractRecommendationsRun();
|
|
68
|
-
},
|
|
69
|
-
});
|
|
70
54
|
exports.contracts.command('get', {
|
|
71
55
|
description: 'Get a tracked contract by chain and address',
|
|
72
56
|
args: incur_1.z.object({
|
|
@@ -98,7 +82,7 @@ function buildCreateContractBody(options) {
|
|
|
98
82
|
events: parsedEvents,
|
|
99
83
|
};
|
|
100
84
|
if (options.startBlock !== undefined)
|
|
101
|
-
body.start_block = options.startBlock;
|
|
85
|
+
body.start_block = parseStartBlock(options.startBlock);
|
|
102
86
|
if (options.includeInPipeline !== undefined) {
|
|
103
87
|
body.include_in_pipeline = options.includeInPipeline;
|
|
104
88
|
}
|
|
@@ -151,7 +135,7 @@ function buildUpdateContractBody(chain, address, options) {
|
|
|
151
135
|
events: parsedEvents,
|
|
152
136
|
};
|
|
153
137
|
if (options.startBlock !== undefined)
|
|
154
|
-
body.start_block = options.startBlock;
|
|
138
|
+
body.start_block = parseStartBlock(options.startBlock);
|
|
155
139
|
if (options.includeInPipeline !== undefined) {
|
|
156
140
|
body.include_in_pipeline = options.includeInPipeline;
|
|
157
141
|
}
|
|
@@ -197,41 +181,6 @@ exports.contracts.command('update', {
|
|
|
197
181
|
return updateContractRun(args.chain, args.address, options);
|
|
198
182
|
},
|
|
199
183
|
});
|
|
200
|
-
// ── Toggle contract pipeline inclusion ──
|
|
201
|
-
function updateContractPipelineRun(chain, address, includeInPipeline) {
|
|
202
|
-
(0, client_1.requireApiKey)();
|
|
203
|
-
const client = (0, client_1.createClient)();
|
|
204
|
-
return client.patch(`/v0/contracts/${parseChain(chain)}/${encodeURIComponent(address)}/pipeline`, buildUpdateContractPipelineBody(includeInPipeline));
|
|
205
|
-
}
|
|
206
|
-
function buildUpdateContractPipelineBody(includeInPipeline) {
|
|
207
|
-
return { include_in_pipeline: includeInPipeline };
|
|
208
|
-
}
|
|
209
|
-
exports.contracts.command('pipeline', {
|
|
210
|
-
description: 'Toggle whether a tracked contract is included in the project events pipeline',
|
|
211
|
-
args: incur_1.z.object({
|
|
212
|
-
chain: incur_1.z.string().describe('Chain ID'),
|
|
213
|
-
address: incur_1.z.string().describe('Contract address (0x...)'),
|
|
214
|
-
}),
|
|
215
|
-
options: incur_1.z.object({
|
|
216
|
-
includeInPipeline: incur_1.z
|
|
217
|
-
.boolean()
|
|
218
|
-
.describe('true to include the contract in the pipeline, false to exclude it'),
|
|
219
|
-
}),
|
|
220
|
-
examples: [
|
|
221
|
-
{
|
|
222
|
-
args: {
|
|
223
|
-
chain: '1',
|
|
224
|
-
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
|
|
225
|
-
},
|
|
226
|
-
options: { includeInPipeline: false },
|
|
227
|
-
description: 'Keep ABI decoding but exclude this contract from pipeline deploys',
|
|
228
|
-
},
|
|
229
|
-
],
|
|
230
|
-
hint: 'Requires contracts:write scope on your API key.',
|
|
231
|
-
run({ args, options }) {
|
|
232
|
-
return updateContractPipelineRun(args.chain, args.address, options.includeInPipeline);
|
|
233
|
-
},
|
|
234
|
-
});
|
|
235
184
|
// ── Delete a contract ──
|
|
236
185
|
function deleteContractRun(chain, address) {
|
|
237
186
|
(0, client_1.requireApiKey)();
|
package/dist/commands/import.js
CHANGED
|
@@ -15,6 +15,8 @@ function buildImportBody(options) {
|
|
|
15
15
|
}
|
|
16
16
|
if (options.rows) {
|
|
17
17
|
const rows = (0, json_1.parseJsonArrayOfObjects)(options.rows, '--rows');
|
|
18
|
+
if (rows.length === 0)
|
|
19
|
+
throw new Error('--rows must contain at least one wallet');
|
|
18
20
|
const addresses = rows.map((row) => row.address);
|
|
19
21
|
if (addresses.some((address) => typeof address !== 'string' || !address)) {
|
|
20
22
|
throw new Error('--rows entries must each include a non-empty string address');
|
|
@@ -28,6 +30,8 @@ function buildImportBody(options) {
|
|
|
28
30
|
throw new Error('Provide --addresses or --rows');
|
|
29
31
|
}
|
|
30
32
|
const addresses = (0, json_1.parseJsonArray)(options.addresses, '--addresses');
|
|
33
|
+
if (addresses.length === 0)
|
|
34
|
+
throw new Error('--addresses must contain at least one wallet');
|
|
31
35
|
if (addresses.some((address) => typeof address !== 'string' || !address)) {
|
|
32
36
|
throw new Error('--addresses must be a JSON array of wallet address strings');
|
|
33
37
|
}
|
|
@@ -11,11 +11,13 @@ export interface LifecycleThresholdOptions {
|
|
|
11
11
|
}
|
|
12
12
|
export interface GetProfileOptions extends LifecycleThresholdOptions {
|
|
13
13
|
expand?: string;
|
|
14
|
+
timestamp?: string;
|
|
14
15
|
}
|
|
15
16
|
export declare function getProfileRun(address: string, optionsOrExpand?: GetProfileOptions | string): Promise<unknown>;
|
|
16
17
|
export interface SearchProfilesOptions extends LifecycleThresholdOptions {
|
|
17
18
|
address?: string;
|
|
18
19
|
search?: string;
|
|
20
|
+
timestamp?: string;
|
|
19
21
|
page?: number;
|
|
20
22
|
size?: number;
|
|
21
23
|
orderBy?: string;
|
|
@@ -33,7 +35,8 @@ export interface SearchProfilesOptions extends LifecycleThresholdOptions {
|
|
|
33
35
|
export declare function parseSearchFilters(raw: string): unknown[];
|
|
34
36
|
export declare function searchProfilesRun(options: SearchProfilesOptions): Promise<unknown>;
|
|
35
37
|
export interface UpdateProfileOptions {
|
|
36
|
-
properties
|
|
38
|
+
properties?: string;
|
|
39
|
+
unset?: string;
|
|
37
40
|
}
|
|
38
41
|
export declare function buildUpdateProfileBody(options: UpdateProfileOptions): Record<string, unknown>;
|
|
39
42
|
export declare function updateProfileRun(address: string, options: UpdateProfileOptions): Promise<unknown>;
|
|
@@ -75,6 +75,10 @@ const lifecycleThresholdOptions = {
|
|
|
75
75
|
.optional()
|
|
76
76
|
.describe('Override lifecycle at-risk prior active days threshold'),
|
|
77
77
|
};
|
|
78
|
+
const profileTimestampOption = incur_1.z
|
|
79
|
+
.string()
|
|
80
|
+
.datetime({ offset: true })
|
|
81
|
+
.optional();
|
|
78
82
|
function getProfileRun(address, optionsOrExpand = {}) {
|
|
79
83
|
(0, client_1.requireApiKey)();
|
|
80
84
|
const client = (0, client_1.createClient)();
|
|
@@ -84,6 +88,8 @@ function getProfileRun(address, optionsOrExpand = {}) {
|
|
|
84
88
|
const params = {};
|
|
85
89
|
if (options.expand)
|
|
86
90
|
params.expand = options.expand;
|
|
91
|
+
if (options.timestamp)
|
|
92
|
+
params.timestamp = options.timestamp;
|
|
87
93
|
addLifecycleThresholdParams(params, options);
|
|
88
94
|
return client.get(`/v0/profiles/${encodeURIComponent(address)}`, { params });
|
|
89
95
|
}
|
|
@@ -97,6 +103,7 @@ exports.profiles.command('get', {
|
|
|
97
103
|
.string()
|
|
98
104
|
.optional()
|
|
99
105
|
.describe('Comma-separated list of fields to expand: apps,chains,tokens,labels'),
|
|
106
|
+
timestamp: profileTimestampOption.describe('Return the wallet-enrichment snapshot closest to this ISO-8601 timestamp'),
|
|
100
107
|
...lifecycleThresholdOptions,
|
|
101
108
|
}),
|
|
102
109
|
examples: [
|
|
@@ -106,6 +113,11 @@ exports.profiles.command('get', {
|
|
|
106
113
|
options: { expand: 'labels,chains' },
|
|
107
114
|
description: 'Get profile with expanded labels and chains',
|
|
108
115
|
},
|
|
116
|
+
{
|
|
117
|
+
args: { address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' },
|
|
118
|
+
options: { timestamp: '2025-06-21T10:03:00Z' },
|
|
119
|
+
description: 'Get the closest stored wallet-enrichment snapshot',
|
|
120
|
+
},
|
|
109
121
|
],
|
|
110
122
|
hint: 'Requires profiles:read scope on your API key.',
|
|
111
123
|
run({ args, options }) {
|
|
@@ -138,72 +150,12 @@ const RESOURCE_FIELD_PREFIXES = new Set([
|
|
|
138
150
|
'label',
|
|
139
151
|
'labels',
|
|
140
152
|
]);
|
|
141
|
-
const QUALIFIER_KEYS = [
|
|
142
|
-
'chain_id',
|
|
143
|
-
'app_id',
|
|
144
|
-
'token_address',
|
|
145
|
-
'tag_id',
|
|
146
|
-
'scope',
|
|
147
|
-
];
|
|
148
153
|
const FILTER_ENTRY_KEYS = new Set([
|
|
149
154
|
'field',
|
|
150
155
|
'op',
|
|
151
156
|
'value',
|
|
152
|
-
...QUALIFIER_KEYS,
|
|
157
|
+
...filters_1.QUALIFIER_KEYS,
|
|
153
158
|
]);
|
|
154
|
-
/**
|
|
155
|
-
* Enforce the per-field qualifier rules, mirroring the API's schema. Sending a
|
|
156
|
-
* qualifier the field does not accept — or omitting a required one — is a 400,
|
|
157
|
-
* so we fail here with a message that names the offending key.
|
|
158
|
-
*/
|
|
159
|
-
function validateQualifiers(record, field) {
|
|
160
|
-
const present = (key) => record[key] !== undefined;
|
|
161
|
-
const required = (key) => {
|
|
162
|
-
if (!present(key)) {
|
|
163
|
-
throw new Error(`--filters: "${key}" is required for "${field}"`);
|
|
164
|
-
}
|
|
165
|
-
};
|
|
166
|
-
const forbidden = (keys) => {
|
|
167
|
-
for (const key of keys) {
|
|
168
|
-
if (present(key)) {
|
|
169
|
-
throw new Error(`--filters: "${key}" is not valid for "${field}"`);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
};
|
|
173
|
-
switch (field) {
|
|
174
|
-
case 'chains.balance':
|
|
175
|
-
// chain_id optional — omit it to match any chain.
|
|
176
|
-
forbidden(['app_id', 'token_address', 'tag_id', 'scope']);
|
|
177
|
-
break;
|
|
178
|
-
case 'apps.balance':
|
|
179
|
-
required('app_id');
|
|
180
|
-
forbidden(['token_address', 'tag_id', 'scope']);
|
|
181
|
-
break;
|
|
182
|
-
case 'tokens.balance':
|
|
183
|
-
required('token_address');
|
|
184
|
-
required('scope');
|
|
185
|
-
if (record.scope !== 'any' && record.scope !== 'protocol') {
|
|
186
|
-
throw new Error(`--filters: "scope" must be "any" or "protocol"`);
|
|
187
|
-
}
|
|
188
|
-
// app_id identifies the protocol, so it is required by (and only by)
|
|
189
|
-
// scope: "protocol".
|
|
190
|
-
if (record.scope === 'protocol') {
|
|
191
|
-
required('app_id');
|
|
192
|
-
}
|
|
193
|
-
else {
|
|
194
|
-
forbidden(['app_id']);
|
|
195
|
-
}
|
|
196
|
-
forbidden(['tag_id']);
|
|
197
|
-
break;
|
|
198
|
-
case 'labels.value':
|
|
199
|
-
required('tag_id');
|
|
200
|
-
forbidden(['app_id', 'token_address', 'scope']);
|
|
201
|
-
break;
|
|
202
|
-
default:
|
|
203
|
-
// users.* — a user attribute carries no resource identity.
|
|
204
|
-
forbidden(QUALIFIER_KEYS);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
159
|
/**
|
|
208
160
|
* Parse and validate the --filters JSON. Ensures it is an array of
|
|
209
161
|
* `{ field, op, value }` objects carrying a canonical `field` — either
|
|
@@ -251,7 +203,7 @@ function parseSearchFilters(raw) {
|
|
|
251
203
|
'(a bare name is silently ignored by the API and returns the entire unfiltered dataset)');
|
|
252
204
|
}
|
|
253
205
|
}
|
|
254
|
-
validateQualifiers(record, field);
|
|
206
|
+
(0, filters_1.validateQualifiers)(record, field);
|
|
255
207
|
// The balance fields compare numerically; a stringified number is a 400.
|
|
256
208
|
if (field !== 'labels.value' &&
|
|
257
209
|
RESOURCE_FILTER_FIELDS.has(field) &&
|
|
@@ -275,6 +227,9 @@ function parseSearchFilters(raw) {
|
|
|
275
227
|
return parsed;
|
|
276
228
|
}
|
|
277
229
|
function searchProfilesRun(options) {
|
|
230
|
+
if (options.timestamp && !options.address) {
|
|
231
|
+
throw new Error('--timestamp requires --address');
|
|
232
|
+
}
|
|
278
233
|
(0, client_1.requireApiKey)();
|
|
279
234
|
const client = (0, client_1.createClient)();
|
|
280
235
|
const params = {};
|
|
@@ -282,6 +237,8 @@ function searchProfilesRun(options) {
|
|
|
282
237
|
params.address = options.address;
|
|
283
238
|
if (options.search)
|
|
284
239
|
params.search = options.search;
|
|
240
|
+
if (options.timestamp)
|
|
241
|
+
params.timestamp = options.timestamp;
|
|
285
242
|
if (options.page !== undefined)
|
|
286
243
|
params.page = options.page;
|
|
287
244
|
if (options.size !== undefined)
|
|
@@ -313,6 +270,7 @@ exports.profiles.command('search', {
|
|
|
313
270
|
options: incur_1.z.object({
|
|
314
271
|
address: incur_1.z.string().optional().describe('Filter by wallet address'),
|
|
315
272
|
search: incur_1.z.string().optional().describe('Free-text search across address and identity fields'),
|
|
273
|
+
timestamp: profileTimestampOption.describe('Return the closest wallet-enrichment snapshot; requires --address'),
|
|
316
274
|
page: incur_1.z.coerce
|
|
317
275
|
.number()
|
|
318
276
|
.int()
|
|
@@ -373,6 +331,13 @@ exports.profiles.command('search', {
|
|
|
373
331
|
}),
|
|
374
332
|
examples: [
|
|
375
333
|
{ options: { size: 10 }, description: 'List first 10 profiles' },
|
|
334
|
+
{
|
|
335
|
+
options: {
|
|
336
|
+
address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
|
|
337
|
+
timestamp: '2025-06-21T10:03:00Z',
|
|
338
|
+
},
|
|
339
|
+
description: 'Search the closest stored wallet-enrichment snapshot',
|
|
340
|
+
},
|
|
376
341
|
{
|
|
377
342
|
options: { orderBy: 'net_worth_usd', orderDir: 'desc', size: 5 },
|
|
378
343
|
description: 'Top 5 profiles by net worth',
|
|
@@ -417,9 +382,26 @@ exports.profiles.command('search', {
|
|
|
417
382
|
},
|
|
418
383
|
});
|
|
419
384
|
function buildUpdateProfileBody(options) {
|
|
420
|
-
const body =
|
|
385
|
+
const body = options.properties
|
|
386
|
+
? (0, json_1.parseJsonObject)(options.properties, '--properties')
|
|
387
|
+
: {};
|
|
388
|
+
if (options.unset) {
|
|
389
|
+
const keys = options.unset
|
|
390
|
+
.split(',')
|
|
391
|
+
.map((key) => key.trim())
|
|
392
|
+
.filter((key) => key.length > 0);
|
|
393
|
+
if (keys.length === 0) {
|
|
394
|
+
throw new Error('--unset must list at least one property key');
|
|
395
|
+
}
|
|
396
|
+
for (const key of keys) {
|
|
397
|
+
if (key === 'user_id') {
|
|
398
|
+
throw new Error('user_id cannot be unset (it participates in identity stitching)');
|
|
399
|
+
}
|
|
400
|
+
body[key] = null;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
421
403
|
if (Object.keys(body).length === 0) {
|
|
422
|
-
throw new Error('--properties
|
|
404
|
+
throw new Error('provide --properties and/or --unset with at least one key');
|
|
423
405
|
}
|
|
424
406
|
return body;
|
|
425
407
|
}
|
|
@@ -436,7 +418,12 @@ exports.profiles.command('update', {
|
|
|
436
418
|
options: incur_1.z.object({
|
|
437
419
|
properties: incur_1.z
|
|
438
420
|
.string()
|
|
439
|
-
.
|
|
421
|
+
.optional()
|
|
422
|
+
.describe('JSON object of properties to merge; a null value unsets (deletes) that property. Allowed keys: user_id, display_name, email, farcaster, discord, twitter, telegram, instagram, website, github, linkedin, facebook, tiktok, youtube, reddit, avatar, description, location, ens, lens, basenames, linea'),
|
|
423
|
+
unset: incur_1.z
|
|
424
|
+
.string()
|
|
425
|
+
.optional()
|
|
426
|
+
.describe('Comma-separated property keys to unset (delete), e.g. "email,twitter". Shorthand for null values in --properties. user_id cannot be unset.'),
|
|
440
427
|
}),
|
|
441
428
|
examples: [
|
|
442
429
|
{
|
|
@@ -451,8 +438,18 @@ exports.profiles.command('update', {
|
|
|
451
438
|
options: { properties: '{"email":"alice@example.com"}' },
|
|
452
439
|
description: 'Set just the email',
|
|
453
440
|
},
|
|
441
|
+
{
|
|
442
|
+
args: { address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' },
|
|
443
|
+
options: { unset: 'email,twitter' },
|
|
444
|
+
description: 'Delete the email and Twitter properties',
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
args: { address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' },
|
|
448
|
+
options: { properties: '{"display_name":"alice.eth"}', unset: 'email' },
|
|
449
|
+
description: 'Set a new display name and delete the email in one call',
|
|
450
|
+
},
|
|
454
451
|
],
|
|
455
|
-
hint: 'Requires profiles:write scope on your API key. Only the listed keys are accepted; unknown keys are rejected.',
|
|
452
|
+
hint: 'Requires profiles:write scope on your API key. Only the listed keys are accepted; unknown keys are rejected. Deleting a property hides any globally-enriched fallback value too; user_id cannot be unset.',
|
|
456
453
|
run({ args, options }) {
|
|
457
454
|
return updateProfileRun(args.address, options);
|
|
458
455
|
},
|
|
@@ -482,7 +479,7 @@ exports.profilesProperties.command('batch', {
|
|
|
482
479
|
options: incur_1.z.object({
|
|
483
480
|
rows: incur_1.z
|
|
484
481
|
.string()
|
|
485
|
-
.describe('JSON array of flat {address,...properties} objects. ENS names are not resolved in batch requests.'),
|
|
482
|
+
.describe('JSON array of flat {address,...properties} objects; a null value unsets (deletes) that property (user_id cannot be unset). ENS names are not resolved in batch requests.'),
|
|
486
483
|
}),
|
|
487
484
|
examples: [
|
|
488
485
|
{
|
|
@@ -491,6 +488,12 @@ exports.profilesProperties.command('batch', {
|
|
|
491
488
|
},
|
|
492
489
|
description: 'Batch set display names and emails',
|
|
493
490
|
},
|
|
491
|
+
{
|
|
492
|
+
options: {
|
|
493
|
+
rows: '[{"address":"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045","email":null}]',
|
|
494
|
+
},
|
|
495
|
+
description: 'Batch delete emails (null unsets a property)',
|
|
496
|
+
},
|
|
494
497
|
],
|
|
495
498
|
hint: 'Requires profiles:write scope on your API key. Unknown keys are ignored by the API; invalid rows are quarantined.',
|
|
496
499
|
run({ options }) {
|
package/dist/lib/client.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export declare const DEFAULT_EVENTS_BASE_URL = "https://events.formo.so";
|
|
|
4
4
|
export declare function getApiBaseUrl(): string;
|
|
5
5
|
export declare function getEventsBaseUrl(): string;
|
|
6
6
|
export interface ApiErrorBody {
|
|
7
|
-
error?: {
|
|
7
|
+
error?: string | {
|
|
8
8
|
code?: string;
|
|
9
9
|
message?: string;
|
|
10
10
|
doc_url?: string;
|
package/dist/lib/client.js
CHANGED
|
@@ -30,8 +30,14 @@ function getEventsBaseUrl() {
|
|
|
30
30
|
function parseApiError(error) {
|
|
31
31
|
const status = error.response?.status;
|
|
32
32
|
const body = error.response?.data;
|
|
33
|
-
const
|
|
34
|
-
const
|
|
33
|
+
const rawError = body?.error;
|
|
34
|
+
const apiError = rawError && typeof rawError === 'object' ? rawError : undefined;
|
|
35
|
+
const plainMessage = typeof rawError === 'string'
|
|
36
|
+
? rawError
|
|
37
|
+
: typeof error.response?.data === 'string'
|
|
38
|
+
? error.response.data
|
|
39
|
+
: undefined;
|
|
40
|
+
const baseMessage = apiError?.message ?? plainMessage ?? error.message;
|
|
35
41
|
const parts = [];
|
|
36
42
|
parts.push(apiError?.code ? `[${apiError.code}] ${baseMessage}` : baseMessage);
|
|
37
43
|
if (apiError?.param)
|
package/dist/lib/filters.d.ts
CHANGED
|
@@ -4,3 +4,10 @@ export declare function isValuelessFilterOperator(op: unknown): boolean;
|
|
|
4
4
|
export declare function isCanonicalFilterValue(value: unknown): boolean;
|
|
5
5
|
export declare function isEmptyMembershipArray(value: unknown): boolean;
|
|
6
6
|
export declare function hasTinybirdMembershipDelimiter(value: unknown): boolean;
|
|
7
|
+
export declare const QUALIFIER_KEYS: readonly ["chain_id", "app_id", "token_address", "tag_id", "scope"];
|
|
8
|
+
/**
|
|
9
|
+
* Enforce the per-field qualifier rules, mirroring the API's schema. Sending a
|
|
10
|
+
* qualifier the field does not accept — or omitting a required one — is a 400,
|
|
11
|
+
* so we fail here with a message that names the offending key.
|
|
12
|
+
*/
|
|
13
|
+
export declare function validateQualifiers(record: Record<string, unknown>, field: string, path?: string): void;
|
package/dist/lib/filters.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CANONICAL_FILTER_OPERATORS = void 0;
|
|
3
|
+
exports.QUALIFIER_KEYS = exports.CANONICAL_FILTER_OPERATORS = void 0;
|
|
4
4
|
exports.isCanonicalFilterOperator = isCanonicalFilterOperator;
|
|
5
5
|
exports.isValuelessFilterOperator = isValuelessFilterOperator;
|
|
6
6
|
exports.isCanonicalFilterValue = isCanonicalFilterValue;
|
|
7
7
|
exports.isEmptyMembershipArray = isEmptyMembershipArray;
|
|
8
8
|
exports.hasTinybirdMembershipDelimiter = hasTinybirdMembershipDelimiter;
|
|
9
|
+
exports.validateQualifiers = validateQualifiers;
|
|
9
10
|
exports.CANONICAL_FILTER_OPERATORS = [
|
|
10
11
|
'eq',
|
|
11
12
|
'neq',
|
|
@@ -43,3 +44,71 @@ function hasTinybirdMembershipDelimiter(value) {
|
|
|
43
44
|
return (Array.isArray(value) &&
|
|
44
45
|
value.some((item) => typeof item === 'string' && item.includes('|')));
|
|
45
46
|
}
|
|
47
|
+
exports.QUALIFIER_KEYS = [
|
|
48
|
+
'chain_id',
|
|
49
|
+
'app_id',
|
|
50
|
+
'token_address',
|
|
51
|
+
'tag_id',
|
|
52
|
+
'scope',
|
|
53
|
+
];
|
|
54
|
+
/**
|
|
55
|
+
* Enforce the per-field qualifier rules, mirroring the API's schema. Sending a
|
|
56
|
+
* qualifier the field does not accept — or omitting a required one — is a 400,
|
|
57
|
+
* so we fail here with a message that names the offending key.
|
|
58
|
+
*/
|
|
59
|
+
function validateQualifiers(record, field, path = '--filters') {
|
|
60
|
+
for (const key of exports.QUALIFIER_KEYS) {
|
|
61
|
+
if (record[key] !== undefined && (typeof record[key] !== 'string' || record[key] === '')) {
|
|
62
|
+
throw new Error(`${path}.${key} must be a non-empty string`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (record.scope !== undefined && record.scope !== 'any' && record.scope !== 'protocol') {
|
|
66
|
+
throw new Error(`${path}.scope must be any or protocol`);
|
|
67
|
+
}
|
|
68
|
+
const present = (key) => record[key] !== undefined;
|
|
69
|
+
const required = (key) => {
|
|
70
|
+
if (!present(key)) {
|
|
71
|
+
throw new Error(`${path}: "${key}" is required for "${field}"`);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const forbidden = (keys) => {
|
|
75
|
+
for (const key of keys) {
|
|
76
|
+
if (present(key)) {
|
|
77
|
+
throw new Error(`${path}: "${key}" is not valid for "${field}"`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
switch (field) {
|
|
82
|
+
case 'chains.balance':
|
|
83
|
+
// chain_id optional — omit it to match any chain.
|
|
84
|
+
forbidden(['app_id', 'token_address', 'tag_id', 'scope']);
|
|
85
|
+
break;
|
|
86
|
+
case 'apps.balance':
|
|
87
|
+
required('app_id');
|
|
88
|
+
forbidden(['token_address', 'tag_id', 'scope']);
|
|
89
|
+
break;
|
|
90
|
+
case 'tokens.balance':
|
|
91
|
+
required('token_address');
|
|
92
|
+
required('scope');
|
|
93
|
+
if (record.scope !== 'any' && record.scope !== 'protocol') {
|
|
94
|
+
throw new Error(`${path}: "scope" must be "any" or "protocol"`);
|
|
95
|
+
}
|
|
96
|
+
// app_id identifies the protocol, so it is required by (and only by)
|
|
97
|
+
// scope: "protocol".
|
|
98
|
+
if (record.scope === 'protocol') {
|
|
99
|
+
required('app_id');
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
forbidden(['app_id']);
|
|
103
|
+
}
|
|
104
|
+
forbidden(['tag_id']);
|
|
105
|
+
break;
|
|
106
|
+
case 'labels.value':
|
|
107
|
+
required('tag_id');
|
|
108
|
+
forbidden(['app_id', 'token_address', 'scope']);
|
|
109
|
+
break;
|
|
110
|
+
default:
|
|
111
|
+
// users.* — a user attribute carries no resource identity.
|
|
112
|
+
forbidden(exports.QUALIFIER_KEYS);
|
|
113
|
+
}
|
|
114
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@formo/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"packageManager": "pnpm@11.1.2",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.12"
|
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
"dist",
|
|
24
24
|
"README.md"
|
|
25
25
|
],
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public",
|
|
28
|
+
"provenance": true
|
|
29
|
+
},
|
|
26
30
|
"scripts": {
|
|
27
31
|
"build": "tsc",
|
|
28
32
|
"prepublishOnly": "pnpm build",
|