flashrev-ai-enrich 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/LICENSE +21 -0
- package/README.md +334 -0
- package/bin/flashrev-ai-enrich.js +23 -0
- package/examples/company-profile.job.json +14 -0
- package/examples/leads.csv +4 -0
- package/examples/person-email-unlock.job.json +13 -0
- package/examples/sample-prospects-enriched.csv +8 -0
- package/examples/sample-prospects.csv +8 -0
- package/package.json +42 -0
- package/skills/flashrev-ai-enrich/SKILL.md +212 -0
- package/skills/flashrev-ai-enrich/agents/openai.yaml +3 -0
- package/skills/flashrev-ai-enrich/references/api_contract.md +165 -0
- package/src/args.js +118 -0
- package/src/billing.js +54 -0
- package/src/capabilities.js +2473 -0
- package/src/cli.js +435 -0
- package/src/config.js +114 -0
- package/src/csv.js +81 -0
- package/src/customer-api.js +101 -0
- package/src/estimate.js +64 -0
- package/src/flashrev-client.js +338 -0
- package/src/job.js +126 -0
- package/src/prompt-router.js +144 -0
- package/src/runner.js +269 -0
- package/src/table.js +41 -0
- package/src/tabular.js +17 -0
- package/src/utils.js +104 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: flashrev-ai-enrich
|
|
3
|
+
description: Use this skill when an AI agent needs to enrich a CSV lead list through the flashrev-ai-enrich npm CLI (v1.0+). Triggers on requests involving list enrichment, filling missing company/person fields, verifying emails or phones, unlocking contact emails or phone numbers, finding company CEOs / executives / industry / employees / LinkedIn posts, matching companies or people to FlashRev IDs, Google search / news / maps lookups, scraping a single page, or running an LLM over each row. The CLI is a structured tool — agents should call `flashrev-ai-enrich schema` to discover the 34 capabilities, then invoke `run` with `--capability <funcName> --map ...` directly; `--prompt "..."` exists for ad-hoc human users and costs 1 extra token per invocation. All enrichment decisions and token deductions are owned by the FlashRev backend; the CLI never calls external data providers directly except for the special `customer_api` capability. Dry-run estimates and the 10-row sample preview must be completed before live runs unless the user passed `--yes`. Agents should invoke with `FLASHREV_ENRICH_AI_MODE=1` (or `--ai-mode`) so list outputs (`tokens` / `schema` / `token-history`) and error envelopes are JSON-structured.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# FlashRev AI Enrich
|
|
7
|
+
|
|
8
|
+
Use the `flashrev-ai-enrich` CLI to enrich CSV lead lists through FlashRev. The CLI does not send outreach messages. It reads CSV files, maps CSV columns to FlashRev capability inputs, estimates token cost via dry-run, previews enriched sample rows, then writes an enriched CSV.
|
|
9
|
+
|
|
10
|
+
## Commands
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
flashrev-ai-enrich init [--force] Write default config
|
|
14
|
+
flashrev-ai-enrich doctor [--no-api] Self-check Node / config / API
|
|
15
|
+
flashrev-ai-enrich tokens [--json] Show balance / total / used / plan
|
|
16
|
+
flashrev-ai-enrich token-history [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--limit N] [--json]
|
|
17
|
+
Show consumption log (auto-paginates)
|
|
18
|
+
flashrev-ai-enrich schema [--json] List 34 capabilities (synced from backend at runtime)
|
|
19
|
+
flashrev-ai-enrich dry-run --source leads.csv (--capability ID | --prompt "...") [--map ...] [--output ...]
|
|
20
|
+
Estimate without calling backend
|
|
21
|
+
flashrev-ai-enrich run --source leads.csv --out X.csv (--capability ID | --prompt "...") [--yes] [--concurrency N] [--sample-size N]
|
|
22
|
+
Real enrichment with sample preview. --prompt routes to a funcName via run_llm (1 extra token)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Required confirmations before real `run`
|
|
26
|
+
|
|
27
|
+
1. User has a FlashRev account with available tokens (`flashrev-ai-enrich tokens` → `remaining > 0`).
|
|
28
|
+
2. `FLASHREV_API_KEY` env var is set (generated from https://info.flashlabs.ai/settings/privateApps).
|
|
29
|
+
3. Source CSV path and output CSV path are confirmed.
|
|
30
|
+
4. Either `--capability ID` (from `flashrev-ai-enrich schema`) or `--prompt "<intent>"` is confirmed. Agents should prefer `--capability ID` directly; `--prompt` is for ad-hoc human use because it costs 1 extra token to route through `run_llm`.
|
|
31
|
+
5. Input mappings (`--map flashrev_field=csv_column`) cover at least one capability rule. Skipped only when `--prompt` is used and the LLM returns valid mappings (still subject to rule validation afterwards).
|
|
32
|
+
6. Output mappings (`--output csv_col=response_field`) or `--output-fields` are confirmed. Skipped under `--prompt` if the LLM returned mappings, but always required for dynamic-output capabilities (e.g., `run_llm`, `scrape_and_extract`).
|
|
33
|
+
7. `dry-run` first to see estimated token cost and effective concurrency.
|
|
34
|
+
8. Do not proceed past the sample preview (default 10 rows, configurable via `--sample-size N`) unless the user approves or `--yes` is set.
|
|
35
|
+
|
|
36
|
+
## Input modes
|
|
37
|
+
|
|
38
|
+
### A. CSV mode (typical batch)
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
flashrev-ai-enrich run \
|
|
42
|
+
--source leads.csv --out leads.enriched.csv \
|
|
43
|
+
--capability enrich_email \
|
|
44
|
+
--map first_name=first_name --map last_name=last_name --map company_name=company \
|
|
45
|
+
--output verified_email=verified_business_email \
|
|
46
|
+
--yes
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`--map` connects CSV column → capability input field; `--output` connects CSV output column → backend response field.
|
|
50
|
+
|
|
51
|
+
### B. Inline mode (single row test, no CSV)
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
flashrev-ai-enrich run \
|
|
55
|
+
--capability verify_email \
|
|
56
|
+
--input email=ada@example.com \
|
|
57
|
+
--output ok=deliverable_email \
|
|
58
|
+
--out out.csv --yes
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
In inline mode the `--input key=value` pairs are auto-mapped (no need for `--map`).
|
|
62
|
+
|
|
63
|
+
### C. Job file (for repeatable presets)
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
flashrev-ai-enrich run --source leads.csv --out out.csv --job enrich.job.json --yes
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Job file shape:
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"capability": "enrich_email",
|
|
73
|
+
"inputMapping": {
|
|
74
|
+
"first_name": "first_name",
|
|
75
|
+
"last_name": "last_name",
|
|
76
|
+
"company_name": "company"
|
|
77
|
+
},
|
|
78
|
+
"outputs": {
|
|
79
|
+
"verified_business_email": "verified_business_email",
|
|
80
|
+
"all_verified_business_emails": "all_verified_business_emails"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### D. Prompt routing mode (ad-hoc human use; costs 1 extra token)
|
|
86
|
+
|
|
87
|
+
Skip `--capability` and describe the intent in natural language. The CLI sends the prompt + CSV columns + capability registry to `run_llm`, which returns JSON `{ funcName, inputMapping, outputMapping, reasoning }`; the CLI prints a Routing-decision block and then runs the resulting job through the normal dry-run / sample / run pipeline.
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
flashrev-ai-enrich run --source leads.csv --out leads.enriched.csv \
|
|
91
|
+
--prompt "for each row, take the email column and verify it is a deliverable business email" \
|
|
92
|
+
--yes
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Rules of thumb when writing prompts:
|
|
96
|
+
|
|
97
|
+
- Name the CSV column explicitly ("take the **email** column"); vague prompts make the LLM return empty mappings.
|
|
98
|
+
- Describe the business outcome, not the capability name ("find the CEO" beats "use get_company_ceo").
|
|
99
|
+
- One capability per prompt — the LLM picks exactly one funcName.
|
|
100
|
+
- `--map` / `--output` on the command line override the LLM's choices; use them to lock specific columns while letting the LLM pick the capability.
|
|
101
|
+
- `--capability X --prompt "..."` together: `--capability` wins, `--prompt` is ignored with a stderr warning (no routing token charged).
|
|
102
|
+
- Unroutable prompts (e.g., "make me a sandwich") exit non-zero with the LLM's reasoning printed; zero rows run.
|
|
103
|
+
|
|
104
|
+
Agents calling this CLI should usually skip prompt routing entirely — `schema` + explicit `--capability ID` is cheaper, faster, and deterministic. Prompt routing is for humans at a terminal.
|
|
105
|
+
|
|
106
|
+
## Status semantics (output CSV columns)
|
|
107
|
+
|
|
108
|
+
Every output CSV gets `flashrev_enrich_status` and `flashrev_enrich_error` columns:
|
|
109
|
+
|
|
110
|
+
| status | meaning |
|
|
111
|
+
|---|---|
|
|
112
|
+
| `success` | Got business data; charged per capability `unitPriceToken`. |
|
|
113
|
+
| `cached` | Hit `unlock_contact` dedup (same `person_id` already unlocked). 0 tokens. |
|
|
114
|
+
| `no_data` | Backend returned 200 but the requested output fields are empty / null. 0 tokens. |
|
|
115
|
+
| `failed` | HTTP error from backend, retries exhausted. 0 tokens. |
|
|
116
|
+
|
|
117
|
+
`Failed` count > 0 with `Tokens used` > 0 means some rows got SOMETHING from backend (charged) but not the specific output fields the user asked for.
|
|
118
|
+
|
|
119
|
+
## Cost reporting
|
|
120
|
+
|
|
121
|
+
`Summary` line in `run` output prints `(balance before → balance after)` — that delta is the **authoritative** amount charged for the row enrichments. Each row's individual `cost.tokens` reported by backend may be slightly off under high concurrency (known limitation; `token-history` is always exact).
|
|
122
|
+
|
|
123
|
+
When `--prompt` is used, the Routing-decision block prints its own `routing cost: 1 token(s)` line. That 1 token is **not** included in the `Summary` `balance before → after` delta, since routing happens before the balance snapshot. Total user cost per `--prompt` run = 1 routing token + (rows × capability unitPriceToken).
|
|
124
|
+
|
|
125
|
+
## Special capability: `customer_api`
|
|
126
|
+
|
|
127
|
+
`customer_api` does NOT call FlashRev backend — the CLI fetches the user-provided URL locally and parses the response. 0 tokens.
|
|
128
|
+
|
|
129
|
+
Inputs (via `--map <field>=<csv_col>` or `--input <field>=<value>`):
|
|
130
|
+
|
|
131
|
+
| field | required | default | notes |
|
|
132
|
+
|---|---|---|---|
|
|
133
|
+
| `url` | yes | — | target URL (alias: `endpoint`) |
|
|
134
|
+
| `method` | no | `GET` | HTTP method |
|
|
135
|
+
| `headers` | no | `{}` | JSON object of HTTP headers |
|
|
136
|
+
| `body` | no | — | string (sent as-is) or object (JSON-serialized; Content-Type defaults to application/json) |
|
|
137
|
+
| `params` | no | — | object of query-string params; appended to `url` |
|
|
138
|
+
| `timeout` | no | `30000` | milliseconds before AbortError |
|
|
139
|
+
|
|
140
|
+
The response JSON (or `{ text }` wrapper for non-JSON) becomes the row's enrichment data; map output columns via `--output csv_col=response_field` as usual. Useful for mixing 3rd-party APIs into the same enrichment workflow.
|
|
141
|
+
|
|
142
|
+
## Date format
|
|
143
|
+
|
|
144
|
+
`--from` and `--to` accept `YYYY-MM-DD`. They are interpreted in the local timezone. `--to` alone makes the CLI paginate through history until it covers the date range (up to 2000 records).
|
|
145
|
+
|
|
146
|
+
## Safety rules
|
|
147
|
+
|
|
148
|
+
- Never print or store `FLASHREV_API_KEY` in generated artifacts.
|
|
149
|
+
- Prefer the `FLASHREV_API_KEY` env var over `--api-key`.
|
|
150
|
+
- Treat email / phone enrichment (`enrich_email` / `enrich_phone`) as paid unlock operations.
|
|
151
|
+
- If `tokens` returns `remaining: 0`, tell the user to recharge before running.
|
|
152
|
+
- Do not describe or expose FlashRev backend data sources, routing, or internal service names to end users.
|
|
153
|
+
- Never overwrite the source CSV (CLI refuses `--source == --out`).
|
|
154
|
+
- Preserve row-level errors in `flashrev_enrich_status` and `flashrev_enrich_error` columns.
|
|
155
|
+
|
|
156
|
+
## Failure handling
|
|
157
|
+
|
|
158
|
+
- `402 Insufficient tokens` → run terminates; tell user to recharge.
|
|
159
|
+
- `401` / `403` → invalid API key; verify `FLASHREV_API_KEY`.
|
|
160
|
+
- `429 Rate limit` → CLI auto-retries with exponential backoff (500ms / 1s / 2s, up to 3 retries = 4 total attempts).
|
|
161
|
+
- `503` / `504` → backend timeout/unavailable; auto-retried with the same schedule as 429.
|
|
162
|
+
- Any other 4xx/5xx on a row → that single row is marked `failed`, batch continues.
|
|
163
|
+
- `--prompt` routing failure (LLM returns non-JSON, unknown funcName, or `run_llm` itself errors) → CLI exits non-zero **before** enrichment starts, prints the LLM's reasoning. Suggest the user retry with `--capability ID`.
|
|
164
|
+
- `--prompt` routed to a capability but `Input mapping does not satisfy <funcName>` → the LLM returned empty / wrong mapping; rerun with a more explicit prompt (name the CSV column) or use `--map` to override.
|
|
165
|
+
|
|
166
|
+
## Workflow recipe
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
# 1. (first time) write config
|
|
170
|
+
flashrev-ai-enrich init
|
|
171
|
+
export FLASHREV_API_KEY="sk_xxxx" # from info.flashlabs.ai/settings/privateApps
|
|
172
|
+
|
|
173
|
+
# 2. verify
|
|
174
|
+
flashrev-ai-enrich doctor
|
|
175
|
+
|
|
176
|
+
# 3. browse capabilities and pick one
|
|
177
|
+
flashrev-ai-enrich schema | less
|
|
178
|
+
|
|
179
|
+
# 4. (optional) check balance
|
|
180
|
+
flashrev-ai-enrich tokens
|
|
181
|
+
|
|
182
|
+
# 5. estimate cost
|
|
183
|
+
flashrev-ai-enrich dry-run --source leads.csv \
|
|
184
|
+
--capability enrich_email \
|
|
185
|
+
--map first_name=first_name --map last_name=last_name --map company_name=company
|
|
186
|
+
|
|
187
|
+
# 6. real run with sample preview
|
|
188
|
+
flashrev-ai-enrich run --source leads.csv --out out.csv \
|
|
189
|
+
--capability enrich_email \
|
|
190
|
+
--map first_name=first_name --map last_name=last_name --map company_name=company \
|
|
191
|
+
--output verified_email=verified_business_email
|
|
192
|
+
# (preview shown, type 'y' to continue, or pass --yes to auto-confirm)
|
|
193
|
+
|
|
194
|
+
# 7. audit spend
|
|
195
|
+
flashrev-ai-enrich token-history --from 2026-05-01
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Shortcut for ad-hoc human use (prompt routing)
|
|
199
|
+
|
|
200
|
+
When the user does not know the capability name and is willing to spend 1 extra token to let the LLM pick:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
# dry-run only routes (1 token) — no enrichment
|
|
204
|
+
flashrev-ai-enrich dry-run --source leads.csv \
|
|
205
|
+
--prompt "find the CEO of each company"
|
|
206
|
+
|
|
207
|
+
# real run: 1 routing token + N rows
|
|
208
|
+
flashrev-ai-enrich run --source leads.csv --out out.csv \
|
|
209
|
+
--prompt "find the CEO of each company" --yes
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Agents should skip this and pass `--capability` directly.
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# FlashRev AI Enrich API Contract
|
|
2
|
+
|
|
3
|
+
The wire format this CLI relies on. Anything beyond what is documented here is FlashRev internal and may change without notice.
|
|
4
|
+
|
|
5
|
+
## Base URL & auth
|
|
6
|
+
|
|
7
|
+
| Item | Value |
|
|
8
|
+
|---|---|
|
|
9
|
+
| Base URL | `https://open-ai-api.flashlabs.ai` |
|
|
10
|
+
| Auth header | `X-API-Key: <private app key>` |
|
|
11
|
+
| Key issuance | https://info.flashlabs.ai/settings/privateApps |
|
|
12
|
+
| Path prefix | All CLI-facing routes share the `/flashrev/...` prefix |
|
|
13
|
+
|
|
14
|
+
The API key is exchanged for an authenticated session by the FlashRev gateway. The CLI never sees or handles internal tokens.
|
|
15
|
+
|
|
16
|
+
## Endpoints used by CLI
|
|
17
|
+
|
|
18
|
+
### 1. Token balance — `GET /flashrev/api/v2/oauth/me`
|
|
19
|
+
|
|
20
|
+
Response:
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"code": 200,
|
|
24
|
+
"data": {
|
|
25
|
+
"companyId": 1000001,
|
|
26
|
+
"newCreditFlag": "Y",
|
|
27
|
+
"limit": {
|
|
28
|
+
"tokenTotal": 1121000,
|
|
29
|
+
"tokenCost": 917206.5,
|
|
30
|
+
"tokenCategoryRemain": { "SUBSCRIPTION": 0, "GIFT": 0, "ADDON": 289974 }
|
|
31
|
+
},
|
|
32
|
+
"vip": { "packageName": "...", "...": "..." }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
CLI computes `remaining = tokenTotal - tokenCost`.
|
|
38
|
+
|
|
39
|
+
### 2. Token history — `POST /flashrev/api/v2/commodity/token/transaction/list`
|
|
40
|
+
|
|
41
|
+
Request body:
|
|
42
|
+
```json
|
|
43
|
+
{ "page": 1, "pageSize": 100, "transactionType": 2 }
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`transactionType: 2` = consumption (1 = top-up).
|
|
47
|
+
|
|
48
|
+
Response:
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"code": 200,
|
|
52
|
+
"data": {
|
|
53
|
+
"list": [
|
|
54
|
+
{ "createdAt": "2026-05-29 06:59:56", "featId": "unlock_contact",
|
|
55
|
+
"featName": "Verify Email Address", "tokenAmount": 1, "unit": "Run",
|
|
56
|
+
"quantity": 1, "transactionType": 2 }
|
|
57
|
+
],
|
|
58
|
+
"total": 100, "page": 1, "pageSize": 100
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The endpoint does not currently accept date filters; the CLI paginates and filters locally by `createdAt`. CLI cap: 20 pages × 100 = 2000 records.
|
|
64
|
+
|
|
65
|
+
### 3. Capability registry — `GET /flashrev/api/v1/enrich/configs`
|
|
66
|
+
|
|
67
|
+
Returns the active capability list with pricing and shape.
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"code": 200,
|
|
72
|
+
"data": [
|
|
73
|
+
{
|
|
74
|
+
"funcName": "enrich_email",
|
|
75
|
+
"displayName": "Enrich Person -> Get Emails",
|
|
76
|
+
"featId": "unlock_contact",
|
|
77
|
+
"unitPriceToken": 2,
|
|
78
|
+
"concurrency": 10,
|
|
79
|
+
"inputColumn": [
|
|
80
|
+
{ "key": "first_name", "name": "First Name" },
|
|
81
|
+
{ "key": "last_name", "name": "Last Name" }
|
|
82
|
+
],
|
|
83
|
+
"outputColumn": ["verified_business_email", "all_verified_business_emails", "..."],
|
|
84
|
+
"rules": [
|
|
85
|
+
["first_name", "last_name", "company_name"],
|
|
86
|
+
["person_linkedin_url"],
|
|
87
|
+
["email"]
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 4. Enrich — `POST /flashrev/api/v1/enrich/run`
|
|
95
|
+
|
|
96
|
+
Per-row enrichment, synchronous.
|
|
97
|
+
|
|
98
|
+
Request body (snake_case on the wire):
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"func_name": "enrich_email",
|
|
102
|
+
"input": {
|
|
103
|
+
"first_name": "Ada",
|
|
104
|
+
"last_name": "Lovelace",
|
|
105
|
+
"company_name": "Acme"
|
|
106
|
+
},
|
|
107
|
+
"row_id": "row-42"
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`row_id` is optional and used by the CLI for tracing only.
|
|
112
|
+
|
|
113
|
+
Response (success):
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"code": 200,
|
|
117
|
+
"msg": "OK",
|
|
118
|
+
"data": {
|
|
119
|
+
"code": 200,
|
|
120
|
+
"msg": "Successful",
|
|
121
|
+
"data": {
|
|
122
|
+
"verified_business_email": "ada@acme.com",
|
|
123
|
+
"all_verified_business_emails": ["ada@acme.com"],
|
|
124
|
+
"verified_personal_email": ""
|
|
125
|
+
},
|
|
126
|
+
"cost": { "tokens": 2, "cached": false }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The response uses a nested `data` wrapper; the CLI's `normalizeEnrichResponse` handles both flat and nested shapes.
|
|
132
|
+
|
|
133
|
+
`cost.tokens` may be slightly inaccurate under high concurrency; the values returned by `token/transaction/list` are always exact.
|
|
134
|
+
|
|
135
|
+
### 5. Error codes
|
|
136
|
+
|
|
137
|
+
HTTP-level (gateway / transport):
|
|
138
|
+
|
|
139
|
+
| Code | Meaning |
|
|
140
|
+
|---|---|
|
|
141
|
+
| 401 | Invalid or revoked `X-API-Key` |
|
|
142
|
+
| 402 | Insufficient tokens |
|
|
143
|
+
| 429 | Rate limit exceeded (CLI auto-retries with exponential backoff) |
|
|
144
|
+
| 503 | Service temporarily unavailable |
|
|
145
|
+
| 504 | Upstream timeout |
|
|
146
|
+
|
|
147
|
+
Business-level (HTTP 200 but inner `code != 200`):
|
|
148
|
+
|
|
149
|
+
| Inner code | Meaning |
|
|
150
|
+
|---|---|
|
|
151
|
+
| 200 + `data` populated | Real enrichment, charged at `unitPriceToken` |
|
|
152
|
+
| 200 + `data` empty / requested fields blank | No data for this lead; CLI marks `no_data`, not charged |
|
|
153
|
+
| 422 | Input validation failed (e.g., missing required input combo) |
|
|
154
|
+
| 4xx other | Request rejected |
|
|
155
|
+
|
|
156
|
+
## Deduction semantics
|
|
157
|
+
|
|
158
|
+
- **Pre-check**: The balance is checked before any downstream call. Insufficient → `402` immediately, no charge.
|
|
159
|
+
- **Rate limit**: A per-`funcName` quota is enforced server-side. Overflow → `429`; the CLI retries with backoff.
|
|
160
|
+
- **Charge on success only**: A row is billed only when the response carries real business data. Empty / 4xx / 5xx responses are not billed.
|
|
161
|
+
- **Dedup**: Contact-unlock capabilities (`enrich_email`, `enrich_phone`) consult an unlock cache. Repeat unlocks of the same person return cached data at 0 tokens (`cost.cached: true`).
|
|
162
|
+
|
|
163
|
+
## customer_api
|
|
164
|
+
|
|
165
|
+
`customer_api` is a special capability whose backend route is empty. The CLI fetches the user-provided URL locally and parses the response. Token cost is always 0. Use it to mix third-party data sources into the same enrichment workflow.
|
package/src/args.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Flags that NEVER take a value — they are always boolean. Listing them here
|
|
2
|
+
// prevents the greedy `--flag <next-token>` consumption from swallowing what
|
|
3
|
+
// is actually a command name or positional argument
|
|
4
|
+
// (e.g. `--ai-mode tokens` previously parsed as { aiMode: "tokens" }).
|
|
5
|
+
const BOOLEAN_FLAGS = new Set([
|
|
6
|
+
"ai-mode",
|
|
7
|
+
"yes",
|
|
8
|
+
"y",
|
|
9
|
+
"json",
|
|
10
|
+
"dry-run",
|
|
11
|
+
"help",
|
|
12
|
+
"h",
|
|
13
|
+
"version",
|
|
14
|
+
"v",
|
|
15
|
+
"force",
|
|
16
|
+
"skip-balance-check",
|
|
17
|
+
"save-api-key"
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
export function parseArgv(argv) {
|
|
21
|
+
const positionals = [];
|
|
22
|
+
const flags = {};
|
|
23
|
+
|
|
24
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
25
|
+
const token = argv[index];
|
|
26
|
+
|
|
27
|
+
if (token === "--") {
|
|
28
|
+
positionals.push(...argv.slice(index + 1));
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (token.startsWith("--no-")) {
|
|
33
|
+
setFlag(flags, toCamel(token.slice(5)), false);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (token.startsWith("--")) {
|
|
38
|
+
const raw = token.slice(2);
|
|
39
|
+
const equalsIndex = raw.indexOf("=");
|
|
40
|
+
if (equalsIndex >= 0) {
|
|
41
|
+
setFlag(flags, toCamel(raw.slice(0, equalsIndex)), coerceValue(raw.slice(equalsIndex + 1)));
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const key = toCamel(raw);
|
|
46
|
+
if (BOOLEAN_FLAGS.has(raw)) {
|
|
47
|
+
setFlag(flags, key, true);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const next = argv[index + 1];
|
|
51
|
+
if (next !== undefined && !next.startsWith("-")) {
|
|
52
|
+
setFlag(flags, key, coerceValue(next));
|
|
53
|
+
index += 1;
|
|
54
|
+
} else {
|
|
55
|
+
setFlag(flags, key, true);
|
|
56
|
+
}
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (token.startsWith("-") && token.length > 1) {
|
|
61
|
+
for (const shortFlag of token.slice(1)) setFlag(flags, shortFlag, true);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
positionals.push(token);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { positionals, flags };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function readFlag(flags, names, fallback = undefined) {
|
|
72
|
+
const keys = Array.isArray(names) ? names : [names];
|
|
73
|
+
for (const key of keys) {
|
|
74
|
+
if (Object.prototype.hasOwnProperty.call(flags, key)) {
|
|
75
|
+
const value = flags[key];
|
|
76
|
+
return Array.isArray(value) ? value[value.length - 1] : value;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return fallback;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function readFlagList(flags, names) {
|
|
83
|
+
const keys = Array.isArray(names) ? names : [names];
|
|
84
|
+
const values = [];
|
|
85
|
+
for (const key of keys) {
|
|
86
|
+
const value = flags[key];
|
|
87
|
+
if (Array.isArray(value)) values.push(...value);
|
|
88
|
+
else if (value !== undefined && value !== true && value !== "") values.push(value);
|
|
89
|
+
}
|
|
90
|
+
return values;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function requireFlag(flags, names, label) {
|
|
94
|
+
const value = readFlag(flags, names);
|
|
95
|
+
if (value === undefined || value === true || value === "") {
|
|
96
|
+
throw new Error(`Missing required flag: --${label || names}`);
|
|
97
|
+
}
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function setFlag(flags, key, value) {
|
|
102
|
+
if (Object.prototype.hasOwnProperty.call(flags, key)) {
|
|
103
|
+
flags[key] = Array.isArray(flags[key]) ? [...flags[key], value] : [flags[key], value];
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
flags[key] = value;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function toCamel(value) {
|
|
110
|
+
return value.replace(/-([a-z0-9])/g, (_, char) => char.toUpperCase());
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function coerceValue(value) {
|
|
114
|
+
if (value === "true") return true;
|
|
115
|
+
if (value === "false") return false;
|
|
116
|
+
if (/^-?\d+(\.\d+)?$/.test(value)) return Number(value);
|
|
117
|
+
return value;
|
|
118
|
+
}
|
package/src/billing.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit price fallback.
|
|
3
|
+
*
|
|
4
|
+
* Real prices are maintained in the backend admin table (api_product_feature) and
|
|
5
|
+
* delivered via the `unitPriceToken` field returned by /api/v1/enrich/configs.
|
|
6
|
+
* The CLI fetches this once at startup and attaches unitPriceToken to each capability.
|
|
7
|
+
*
|
|
8
|
+
* This local table is only used when /configs fetch fails or when the capability
|
|
9
|
+
* registry does not include unitPriceToken — values are approximate, intended for
|
|
10
|
+
* dry-run estimation only.
|
|
11
|
+
*/
|
|
12
|
+
const FALLBACK_BY_FEAT_ID = {
|
|
13
|
+
enrich_company: 1,
|
|
14
|
+
enrich_company_profile: 1,
|
|
15
|
+
enrich_prospect: 1,
|
|
16
|
+
unlock_contact: 2,
|
|
17
|
+
verify_email_address: 1,
|
|
18
|
+
verify_phone_number: 1,
|
|
19
|
+
match_company_ceo: 2,
|
|
20
|
+
match_company_executives: 2,
|
|
21
|
+
enrich_flashagent: 2,
|
|
22
|
+
run_llm_with_deep_research: 5,
|
|
23
|
+
run_llm_with_deep_research_by_flashrev: 5,
|
|
24
|
+
run_llm_with_deep_research_by_perplexity: 5,
|
|
25
|
+
custom_api_request: 1
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const FALLBACK_DEFAULT = 1;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Estimated unit price (tokens per call) for a capability.
|
|
32
|
+
* Priority: capability.unitPriceToken (synced from remote) > featId fallback > default 1.
|
|
33
|
+
*/
|
|
34
|
+
export function unitPriceFor(capability) {
|
|
35
|
+
if (!capability) return FALLBACK_DEFAULT;
|
|
36
|
+
if (capability.unitPriceToken != null) return Number(capability.unitPriceToken) || FALLBACK_DEFAULT;
|
|
37
|
+
if (capability.featId && FALLBACK_BY_FEAT_ID[capability.featId]) {
|
|
38
|
+
return FALLBACK_BY_FEAT_ID[capability.featId];
|
|
39
|
+
}
|
|
40
|
+
return FALLBACK_DEFAULT;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Billing summary shown by dry-run / estimate.
|
|
45
|
+
*/
|
|
46
|
+
export function billingForCapability(capability) {
|
|
47
|
+
const cost = unitPriceFor(capability);
|
|
48
|
+
return {
|
|
49
|
+
action: capability?.displayName || capability?.name || capability?.id || "Enrich",
|
|
50
|
+
cost,
|
|
51
|
+
unit: "Run",
|
|
52
|
+
source: capability?.unitPriceToken != null ? "remote" : "fallback"
|
|
53
|
+
};
|
|
54
|
+
}
|