scrapeloop-mcp 0.32.0 → 0.34.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/package.json +2 -2
- package/src/index.js +70 -13
- package/src/mutation-policies.js +4 -0
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scrapeloop-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"description": "Scrapeloop MCP server - set up and run Scrapeloop end-to-end (integrations, scraping, lead database, verification, cleaners, enrichment, strategies, Instantly campaigns) from any MCP client (Claude Desktop, Cursor, ChatGPT, …).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "node --check src/index.js && node test/mcp_table_tools.mjs && node test/mcp_playbook_plans.mjs && node test/mcp_community_intent.mjs && node test/mcp_api_key_preflight.mjs && node test/mutation_retry_contract.mjs && node test/release_readiness.mjs && node test/mcp_server_version.mjs",
|
|
7
|
+
"test": "node --check src/index.js && node test/mcp_table_tools.mjs && node test/mcp_playbook_plans.mjs && node test/mcp_campaign_setup.mjs && node test/mcp_community_intent.mjs && node test/mcp_api_key_preflight.mjs && node test/mutation_retry_contract.mjs && node test/release_readiness.mjs && node test/mcp_server_version.mjs",
|
|
8
8
|
"release:preflight": "node scripts/release-preflight.mjs",
|
|
9
9
|
"release:verify": "node scripts/release-installed-verify.mjs"
|
|
10
10
|
},
|
package/src/index.js
CHANGED
|
@@ -30,6 +30,8 @@ const API_KEY = process.env.SCRAPELOOP_API_KEY;
|
|
|
30
30
|
const BASE = (process.env.SCRAPELOOP_API_URL || 'https://api.scrapeloop.com').replace(/\/$/, '');
|
|
31
31
|
const TIMEOUT_MS = Number(process.env.SCRAPELOOP_TIMEOUT_MS) || 30000;
|
|
32
32
|
const MAX_RETRIES = 3;
|
|
33
|
+
// The API sits behind a proxy that ends a request after 100 seconds.
|
|
34
|
+
const DEEP_READ = { timeoutMs: Math.max(TIMEOUT_MS, 90000), maxRetries: 1 };
|
|
33
35
|
const MAX_TABLE_CELL_WRITES = 500;
|
|
34
36
|
const MAX_TABLE_CELL_REQUEST_BYTES = 256 * 1024;
|
|
35
37
|
// Native Smart Table tool keys, mirroring apps/api/lib/native_tool_columns.json.
|
|
@@ -47,6 +49,8 @@ const NATIVE_TABLE_TOOLS = Object.freeze([
|
|
|
47
49
|
'email_finder',
|
|
48
50
|
'business_email_finder',
|
|
49
51
|
'linkedin_finder',
|
|
52
|
+
'linkedin_activity',
|
|
53
|
+
'google_profile',
|
|
50
54
|
]);
|
|
51
55
|
const TRACE_HEADER = 'X-Scrapeloop-Trace-Id';
|
|
52
56
|
const SAFE_TRACE_ID = /^[A-Za-z0-9_-]{8,80}$/;
|
|
@@ -132,6 +136,9 @@ async function api(method, path, body, options = {}) {
|
|
|
132
136
|
|| policy?.retry === 'idempotent'
|
|
133
137
|
|| policy?.retry === 'idempotency_key';
|
|
134
138
|
const maxRetries = canRetry ? (options.maxRetries ?? MAX_RETRIES) : 0;
|
|
139
|
+
// A few deep reads ask the sender and walk a whole campaign. They may take
|
|
140
|
+
// longer than an ordinary call, and are not worth sending three more times.
|
|
141
|
+
const timeoutMs = options.timeoutMs ?? TIMEOUT_MS;
|
|
135
142
|
const headers = {
|
|
136
143
|
Authorization: `Bearer ${API_KEY}`,
|
|
137
144
|
'Content-Type': 'application/json',
|
|
@@ -148,7 +155,7 @@ async function api(method, path, body, options = {}) {
|
|
|
148
155
|
method: verb,
|
|
149
156
|
headers,
|
|
150
157
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
151
|
-
signal: AbortSignal.timeout(
|
|
158
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
152
159
|
});
|
|
153
160
|
} catch (e) {
|
|
154
161
|
lastErr = e;
|
|
@@ -163,7 +170,7 @@ async function api(method, path, body, options = {}) {
|
|
|
163
170
|
return {
|
|
164
171
|
ok: false,
|
|
165
172
|
status: 0,
|
|
166
|
-
error: { detail: `Could not reach Scrapeloop (${timedOut ? `timeout after ${
|
|
173
|
+
error: { detail: `Could not reach Scrapeloop (${timedOut ? `timeout after ${timeoutMs}ms` : String(e)}).` },
|
|
167
174
|
...(options.captureTrace
|
|
168
175
|
? { trace_id: traceId, network_error: timedOut ? 'timeout' : 'transport' }
|
|
169
176
|
: {}),
|
|
@@ -574,14 +581,15 @@ const TOOLS = {
|
|
|
574
581
|
run: (a) => api('GET', `/plans/${enc(a.plan_id)}/yield`),
|
|
575
582
|
},
|
|
576
583
|
set_scrape_plan_ready_goal: {
|
|
577
|
-
def: { description: 'Save a verified-email goal on a native Maps draft with optional whole-scope repeat searches after the saved delay. Choose the ready Table and its email campaign, monthly goal/minimum, optional hard-maximum mode, unused-stock ceiling and evidence rules. Pass the reviewed expected_updated_at from get_scrape_plan. Counts require current input-bound verification, qualification, campaign fields and sender-history checks. This does not start scraping, verification or campaigns. Complete-search pacing must be available before Start.', inputSchema: obj({
|
|
584
|
+
def: { description: 'Save a verified-email goal on a native Maps draft with optional whole-scope repeat searches after the saved delay. Choose the ready Table and its email campaign, monthly goal/minimum, optional hard-maximum mode, unused-stock ceiling and evidence rules. supply_mode on_demand searches only while the linked campaign has room (plus buffer_days of its daily intake); only then may monthly_goal be null, with limit_mode target and monthly_minimum 0. allow_role counts an exact Role result (info@, office@) as ready and allow_resolved_catchall counts a catch-all that passed the second check: both must match the Table rules and the campaign, which check_campaign_setup verifies. Always send monthly_minimum and timezone: their defaults suit one business only. Pass the reviewed expected_updated_at from get_scrape_plan. Counts require current input-bound verification, qualification, campaign fields and sender-history checks. This does not start scraping, verification or campaigns. Complete-search pacing must be available before Start.', inputSchema: obj({
|
|
578
585
|
plan_id: { ...S, format: 'uuid' }, expected_updated_at: { ...S, format: 'date-time' },
|
|
579
586
|
goal: obj({ kind: { ...S, enum: ['verified_email'] }, ready_list_id: { ...S, format: 'uuid' },
|
|
580
|
-
campaign_id: { ...S, format: 'uuid' }, monthly_goal: { type: 'integer', minimum: 1, maximum: 1000000 },
|
|
587
|
+
campaign_id: { ...S, format: 'uuid' }, monthly_goal: { type: ['integer', 'null'], minimum: 1, maximum: 1000000 },
|
|
581
588
|
limit_mode: { ...S, enum: ['target', 'maximum'] },
|
|
589
|
+
supply_mode: { ...S, enum: ['monthly', 'on_demand'] }, buffer_days: { type: 'integer', minimum: 0, maximum: 30 },
|
|
582
590
|
monthly_minimum: { type: 'integer', minimum: 0, maximum: 1000000 },
|
|
583
591
|
stock_ceiling: { type: 'integer', minimum: 1, maximum: 1000000 }, timezone: S,
|
|
584
|
-
evidence_max_age_days: { type: 'integer', minimum: 1, maximum: 90 }, allow_resolved_catchall: B,
|
|
592
|
+
evidence_max_age_days: { type: 'integer', minimum: 1, maximum: 90 }, allow_resolved_catchall: B, allow_role: B,
|
|
585
593
|
required_fields: ARR({ ...S, enum: ['name', 'organization_name', 'first_name', 'last_name', 'industry', 'city', 'state', 'postal_code', 'domain', 'language'] }),
|
|
586
594
|
languages: ARR({ ...S, pattern: '^[a-z]{2}$' }),
|
|
587
595
|
}, ['ready_list_id', 'campaign_id', 'monthly_goal', 'monthly_minimum', 'stock_ceiling', 'timezone']),
|
|
@@ -593,10 +601,10 @@ const TOOLS = {
|
|
|
593
601
|
run: (a) => api('GET', `/plans/${enc(a.plan_id)}/budget`),
|
|
594
602
|
},
|
|
595
603
|
set_scrape_plan_budget: {
|
|
596
|
-
def: { description: 'Save reviewed credit limits and Table ownership for a paused complete-search plan with a ready-email goal.
|
|
604
|
+
def: { description: 'Save reviewed credit limits and Table ownership for a paused complete-search plan with a ready-email goal. All four credit answers are required. A number is a limit; explicit null means no plan limit for that one (then only the workspace balance stops the plan), and omission is rejected. Zero blocks spending. No-cap checks still require allowed tools, bounded jobs and available workspace credits. Never infer no cap without approval. Include Raw, Email ready and every check Table. One Table belongs to one budget. Preserve expected_revision from the current budget; omit it only for the first save. A lost reply requires readback before retry. This starts no work and does not enable checks. website_language permits the free bounded Website languages preset with a saved attempt. owner_website explicitly permits the free bounded system Owner name finder; it still needs a saved job and attempt. review_history permits only the system Google review stats preset with source explicitly outscraper and a separate review_budget in USD, default zero. Select an existing connection from get_scrape_plan_budget.review_connections. Omitted review_budget preserves saved settings. Each row reserves 0.147 USD; recorded units use the published ceiling, not a confirmed account invoice. Unknown requests stay held and never retry. Google-first, custom recipes and other paid vendors remain blocked. Use review_complete_scrape_plan to check readiness before requesting exact launch approval.', inputSchema: obj({
|
|
597
605
|
plan_id: { ...S, format: 'uuid' }, expected_revision: { ...S, format: 'uuid' },
|
|
598
606
|
limits: obj({
|
|
599
|
-
monthly_lead_credits: { type: 'integer', minimum: 0, maximum: 1000000000 },
|
|
607
|
+
monthly_lead_credits: { type: ['integer', 'null'], minimum: 0, maximum: 1000000000 },
|
|
600
608
|
lifetime_lead_credits: { type: ['integer', 'null'], minimum: 0, maximum: 1000000000 },
|
|
601
609
|
monthly_verify_credits: { type: ['integer', 'null'], minimum: 0, maximum: 1000000000 },
|
|
602
610
|
lifetime_verify_credits: { type: ['integer', 'null'], minimum: 0, maximum: 1000000000 },
|
|
@@ -607,6 +615,40 @@ const TOOLS = {
|
|
|
607
615
|
}, ['plan_id', 'limits', 'list_ids']) },
|
|
608
616
|
run: ({ plan_id, ...body }) => api('PUT', `/plans/${enc(plan_id)}/budget`, body),
|
|
609
617
|
},
|
|
618
|
+
set_scrape_plan_supply: {
|
|
619
|
+
def: { description: 'Change when a paused complete plan searches, and whether it counts role addresses. supply_mode monthly works toward monthly_goal; on_demand searches only while the linked campaign has room plus buffer_days of its daily intake. monthly_goal null (no monthly ready-lead limit) is allowed only with on_demand, limit_mode target and monthly_minimum 0. allow_role true counts an exact Role result (info@, office@) as ready; omit it to keep the saved choice. Always send buffer_days and monthly_minimum: left out, they are reset to 3 and 0. The scope, ready count, campaign and credit limits stay. Pass expected_updated_at from get_scrape_plan read after the pause. The plan then needs review_complete_scrape_plan and launch_complete_scrape_plan again. This starts no search.', inputSchema: obj({
|
|
620
|
+
plan_id: { ...S, format: 'uuid' }, expected_updated_at: { ...S, format: 'date-time' },
|
|
621
|
+
supply_mode: { ...S, enum: ['monthly', 'on_demand'] }, buffer_days: { type: 'integer', minimum: 0, maximum: 30 },
|
|
622
|
+
monthly_goal: { type: ['integer', 'null'], minimum: 1, maximum: 1000000 },
|
|
623
|
+
limit_mode: { ...S, enum: ['target', 'maximum'] },
|
|
624
|
+
monthly_minimum: { type: 'integer', minimum: 0, maximum: 1000000 }, allow_role: B,
|
|
625
|
+
}, ['plan_id', 'expected_updated_at', 'supply_mode', 'buffer_days', 'monthly_goal', 'limit_mode', 'monthly_minimum']) },
|
|
626
|
+
run: ({ plan_id, ...body }) => api('PUT', `/plans/${enc(plan_id)}/ready/supply`, body),
|
|
627
|
+
},
|
|
628
|
+
switch_scrape_plan_campaign: {
|
|
629
|
+
def: { description: 'Point a paused complete plan at another email campaign that reads the same ready Table. The plan must be paused by a person, both campaigns must have filling off, the new campaign must be an open Instantly campaign, and no check or Table move may be running (otherwise 409: wait and try again). The ready count, the searches done and the credit limits stay. Afterwards switch filling on for the new campaign, then review and launch the plan again. This uploads nothing and starts no search.', inputSchema: obj({
|
|
630
|
+
plan_id: { ...S, format: 'uuid' }, expected_updated_at: { ...S, format: 'date-time' }, campaign_id: { ...S, format: 'uuid' },
|
|
631
|
+
}, ['plan_id', 'expected_updated_at', 'campaign_id']) },
|
|
632
|
+
run: ({ plan_id, ...body }) => api('PUT', `/plans/${enc(plan_id)}/ready/campaign`, body),
|
|
633
|
+
},
|
|
634
|
+
update_scrape_plan: {
|
|
635
|
+
def: { description: 'Switch Google review counting on or off for the next searches of a Scrapeloop Maps plan. Needed when a Table rule asks for the review count. The plan must be paused, and a complete plan must have no place half searched. A complete plan then needs review_complete_scrape_plan and launch_complete_scrape_plan again. Limits and search area are not changed by this tool.', inputSchema: obj({
|
|
636
|
+
plan_id: { ...S, format: 'uuid' }, count_reviews: B,
|
|
637
|
+
}, ['plan_id', 'count_reviews']) },
|
|
638
|
+
run: ({ plan_id, ...body }) => api('PATCH', `/plans/${enc(plan_id)}`, body),
|
|
639
|
+
},
|
|
640
|
+
check_campaign_setup: {
|
|
641
|
+
def: { description: 'One free read that walks a whole self-filling campaign and says whether its parts fit each other: the search plan, the Tables and rules on the way, the checks, the email answer (Table rule, plan goal and campaign must agree), the campaign, the credit limits and the approval. Pass plan_id for a search plan, or campaign_id to start from a campaign (works with no plan too). Returns fits (true only when nothing is fail and nothing is unknown), counts, and checks in the order a lead travels; each has step, key, status (ok, warn, fail or unknown), a plain title and detail, and often fixes: the exact tool and args that end it. Run it after every build step and before asking for a start. Apply a fix only as its note says, ask the person before any fix that changes who gets an email or what may be spent, and run this again afterwards. It changes nothing, starts nothing and approves nothing. deep=false skips the sender read.', inputSchema: obj({
|
|
642
|
+
plan_id: { ...S, format: 'uuid' }, campaign_id: { ...S, format: 'uuid' }, deep: B,
|
|
643
|
+
}) },
|
|
644
|
+
run: (a) => {
|
|
645
|
+
if (!!a.plan_id === !!a.campaign_id) {
|
|
646
|
+
return { ok: false, status: 400, error: { detail: 'Pass plan_id or campaign_id, one of the two.' } };
|
|
647
|
+
}
|
|
648
|
+
const path = a.plan_id ? `/plans/${enc(a.plan_id)}/setup-check` : `/campaigns/${enc(a.campaign_id)}/setup-check`;
|
|
649
|
+
return api('GET', a.deep === false ? `${path}?deep=false` : path, undefined, a.deep === false ? {} : DEEP_READ);
|
|
650
|
+
},
|
|
651
|
+
},
|
|
610
652
|
review_complete_scrape_plan: {
|
|
611
653
|
def: { description: 'Read the exact complete-search scope, ready-output goal, Table rules, shared limits, repeat delay and current launch blockers. Free read. This does not start source work, checks or campaigns.', inputSchema: obj({ plan_id: { ...S, format: 'uuid' } }, ['plan_id']) },
|
|
612
654
|
run: (a) => api('GET', `/plans/${enc(a.plan_id)}/launch-review`),
|
|
@@ -1069,7 +1111,7 @@ const TOOLS = {
|
|
|
1069
1111
|
},
|
|
1070
1112
|
get_campaign_handoff_status: {
|
|
1071
1113
|
def: {
|
|
1072
|
-
description: 'Read waiting, completed and held review Table handoffs for a campaign. No provider call, retry or write. Held copies may already have happened; inspect before moving again.',
|
|
1114
|
+
description: 'Read waiting, completed and held review Table handoffs for a campaign. No provider call, retry or write. Held copies may already have happened; inspect before moving again. business_replied counts leads that were not moved because another address of the same business already replied or asked to stop; those need no review.',
|
|
1073
1115
|
inputSchema: obj({ campaign_id: S }, ['campaign_id']),
|
|
1074
1116
|
},
|
|
1075
1117
|
run: (a) => a.campaign_id ? api('GET', `/campaigns/${enc(a.campaign_id)}/handoffs`)
|
|
@@ -1161,7 +1203,7 @@ const TOOLS = {
|
|
|
1161
1203
|
run: (a) => api('GET', `/campaigns/${enc(a.campaign_id)}/qualification`),
|
|
1162
1204
|
},
|
|
1163
1205
|
update_campaign: {
|
|
1164
|
-
def: { description: 'Patch a campaign (feed_mode, target_active_count, cooldown_days, external_list_id, …). Switching feed_mode on for an already active campaign runs the same check as campaign_readiness_check: any fail answers 422 with the report under detail.readiness.', inputSchema: obj({ campaign_id: S, patch: { ...O, description: "Campaign settings to change. eligibility_rule uses {source_list_id, filter, optional excluded_campaign_ids} for Current qualification, or null to turn it off. The optional campaign IDs hold businesses already in those campaigns by Google place ID. qualification: {} disables the Belgium website language gate; {rule: be_dutch, evidence_column_id: UUID, max_age_days: 90} enables it. Read back after saving. These settings do not start checks or remove sender leads.", properties: { eligibility_rule: { type: ["object", "null"], additionalProperties: false, required: ["source_list_id", "filter"], properties: { source_list_id: S, filter: O, excluded_campaign_ids: { type: 'array', items: S, maxItems: 20 } } }, qualification: { ...O, description: "Belgium website language rule, or {} to disable." }, sender_options: { ...O, description: "GoHighLevel SMS settings. Only supplied keys change." }, icon: { type: ["string", "null"], description: "One or two emoji, or null to clear." } }, additionalProperties: true } }, ['campaign_id', 'patch']) },
|
|
1206
|
+
def: { description: 'Patch a campaign (feed_mode, target_active_count, cooldown_days, external_list_id, …). Switching feed_mode on for an already active campaign runs the same check as campaign_readiness_check: any fail answers 422 with the report under detail.readiness.', inputSchema: obj({ campaign_id: S, patch: { ...O, description: "Campaign settings to change. eligibility_rule uses {source_list_id, filter, optional excluded_campaign_ids} for Current qualification, or null to turn it off. The optional campaign IDs hold businesses already in those campaigns by Google place ID. An edit that leaves excluded_campaign_ids out keeps the saved campaigns; send [] to clear them. qualification: {} disables the Belgium website language gate; {rule: be_dutch, evidence_column_id: UUID, max_age_days: 90} enables it. Read back after saving. These settings do not start checks or remove sender leads.", properties: { eligibility_rule: { type: ["object", "null"], additionalProperties: false, required: ["source_list_id", "filter"], properties: { source_list_id: S, filter: O, excluded_campaign_ids: { type: 'array', items: S, maxItems: 20 } } }, qualification: { ...O, description: "Belgium website language rule, or {} to disable." }, sender_options: { ...O, description: "GoHighLevel SMS settings. Only supplied keys change." }, icon: { type: ["string", "null"], description: "One or two emoji, or null to clear." } }, additionalProperties: true } }, ['campaign_id', 'patch']) },
|
|
1165
1207
|
run: (a) => api('PATCH', `/campaigns/${enc(a.campaign_id)}`, a.patch || {}),
|
|
1166
1208
|
},
|
|
1167
1209
|
duplicate_campaign: {
|
|
@@ -1233,7 +1275,7 @@ const TOOLS = {
|
|
|
1233
1275
|
'Is this campaign ready to start? Run it before activate_campaign. Returns ready, sender_known, sampled and a list of checks, each with a stable key, a level (pass, warn, fail or info), one plain sentence and a fix. Any fail means activate_campaign will refuse with 422 and the same report under detail.readiness. A warn does not block: read it out to the user before they confirm. It judges the leads the feed sends next (not the ones already in the campaign). With feed_mode off nothing is uploaded, so the lead and mailbox checks only warn until the feed is switched on.',
|
|
1234
1276
|
inputSchema: obj({ campaign_id: S }, ['campaign_id']),
|
|
1235
1277
|
},
|
|
1236
|
-
run: (a) => api('GET', `/campaigns/${enc(a.campaign_id)}/readiness
|
|
1278
|
+
run: (a) => api('GET', `/campaigns/${enc(a.campaign_id)}/readiness`, undefined, DEEP_READ),
|
|
1237
1279
|
},
|
|
1238
1280
|
get_sender_config: {
|
|
1239
1281
|
def: { description: "Get an Instantly credential's sender config (daily cap, send accounts, …).", inputSchema: obj({ credential_id: S }, ['credential_id']) },
|
|
@@ -1684,7 +1726,7 @@ const TOOLS = {
|
|
|
1684
1726
|
create_table_native_column: {
|
|
1685
1727
|
def: {
|
|
1686
1728
|
description:
|
|
1687
|
-
`Attach one native Scrapeloop tool as a real Smart Table column: ${NATIVE_TABLE_TOOLS.join(', ')}. These are Scrapeloop's first-party tools, several of which superseded legacy presets, so they are addressed by tool key here rather than by preset slug through create_table_preset_column (which refuses a superseded slug for a new column). Each bills MANAGED VERIFICATION CREDITS per row, never dollars and never the customer's own vendor keys: verify_email reserves 1 credit per selected row, verify_phone reserves 5, each email finder reserves 3, and
|
|
1729
|
+
`Attach one native Scrapeloop tool as a real Smart Table column: ${NATIVE_TABLE_TOOLS.join(', ')}. These are Scrapeloop's first-party tools, several of which superseded legacy presets, so they are addressed by tool key here rather than by preset slug through create_table_preset_column (which refuses a superseded slug for a new column). Each bills MANAGED VERIFICATION CREDITS per row, never dollars and never the customer's own vendor keys: verify_email reserves 1 credit per selected row, verify_phone reserves 5, each email finder reserves 3, linkedin_finder reserves 5, linkedin_activity reserves 3, and google_profile reserves 1. Settlement charges only fresh vendor checks and returns the unused reservation; missing inputs, cached rows, and skipped rows are free. linkedin_finder charges its 5 credits only for a row where it found the owner's LinkedIn profile; a row with no owner found is free. linkedin_activity reads each row's personal LinkedIn profile and newest post and labels it Active, Maybe or Weak (cell text), so LinkedIn invites go to people likely to answer; it charges 3 credits only for a profile it read, and rows with no personal profile, cached rows and profiles that no longer exist are free. google_profile finds a business on Google Maps for a lead that did not come from Google Maps (a Yelp lead, an imported list): it searches the business name in its town, accepts a result only when the phone, the website or the exact name in the same town matches, and saves place_id, gbp_url, rating and reviews_count on the cell (status found, not_found or unclear) and the place ID on a lead that had none; it charges 1 credit only for a profile found, and a business not on Google, more than one matching profile and cached rows are free. Filter on col.<key>.reviews_count to keep small businesses; a business with no reviews yet is saved as 0, and an unknown count has no number and never passes a numeric rule. Creating a column is always free. auto_run defaults false and queues nothing; when auto_run would backfill existing rows the API returns a 409 estimate whose cost.total_credits is the real charge, and you confirm it with confirm_auto_run plus confirmed_credits (CREDITS, not dollars - estimated_cost_usd is our vendor cost and is far smaller than what the workspace is billed). An auto_run that exceeds the workspace verify-credit balance returns 402 with {credit_kind, required, available} before anything is created. The Table master auto-run switch and workspace budget caps stay fail-closed. options are per tool and only the documented names are accepted - call get_table_native_tools for the current list. Owner email finder needs an owner first name plus the company website; point options.owner_name_column at an Owner name finder column to source the name, which auto_run requires. Business email finder is a SEPARATE column - create it with its own call and set options.owner_email_column to run it behind an owner finder. The Catch-all verifier is NOT creatable here: it needs a Verify email column that has already RUN and returned Catch All or Unknown, so use verify_catchall per address, or add the column from the Table's add-column menu.`,
|
|
1688
1730
|
inputSchema: obj(
|
|
1689
1731
|
{
|
|
1690
1732
|
table_id: S,
|
|
@@ -1701,7 +1743,7 @@ const TOOLS = {
|
|
|
1701
1743
|
options: {
|
|
1702
1744
|
...O,
|
|
1703
1745
|
description:
|
|
1704
|
-
'Tool-specific options. verify_phone: phone_col, default_region. email_finder: owner_name_column. business_email_finder: owner_email_column. linkedin_finder: business_col. An undocumented name is rejected, never ignored.',
|
|
1746
|
+
'Tool-specific options. verify_phone: phone_col, default_region. email_finder: owner_name_column. business_email_finder: owner_email_column. linkedin_finder: business_col. linkedin_activity: linkedin_col, business_col. google_profile: business_col. An undocumented name is rejected, never ignored.',
|
|
1705
1747
|
},
|
|
1706
1748
|
operation_id: {
|
|
1707
1749
|
...S,
|
|
@@ -2230,6 +2272,21 @@ const TOOLS = {
|
|
|
2230
2272
|
})
|
|
2231
2273
|
: { ok: false, status: 400, error: { detail: 'list_id and rule_id are required.' } },
|
|
2232
2274
|
},
|
|
2275
|
+
update_table_flow_rule: {
|
|
2276
|
+
def: {
|
|
2277
|
+
description:
|
|
2278
|
+
'Change one saved Table rule in place: its condition, name, action settings, daily limit or delay. Send only what changes. A condition replaces the whole saved condition, so start from the one list_table_flow_rules returns (or from a fix that check_campaign_setup names) and keep every "not in that Table" guard. The rule keeps its id, history and counters. On a Table that belongs to a running complete plan, pause the plan first: a changed rule needs a fresh review and launch. Use toggle_table_flow_rule to switch a rule on or off.',
|
|
2279
|
+
inputSchema: obj({
|
|
2280
|
+
list_id: S, rule_id: S, name: S, condition: { type: 'object' }, action_config: { type: 'object' },
|
|
2281
|
+
daily_fire_cap: { type: 'integer', minimum: 1, maximum: 100000 },
|
|
2282
|
+
delay_seconds: { type: 'integer', minimum: 0, maximum: 2592000 },
|
|
2283
|
+
}, ['list_id', 'rule_id']),
|
|
2284
|
+
},
|
|
2285
|
+
run: ({ list_id, rule_id, ...body }) =>
|
|
2286
|
+
Object.keys(body).length
|
|
2287
|
+
? api('PATCH', `/lists/${enc(list_id)}/flow-rules/${enc(rule_id)}`, body)
|
|
2288
|
+
: { ok: false, status: 400, error: { detail: 'Send at least one thing to change.' } },
|
|
2289
|
+
},
|
|
2233
2290
|
list_campaign_flow_rules: {
|
|
2234
2291
|
def: {
|
|
2235
2292
|
description:
|
|
@@ -2492,7 +2549,7 @@ const TOOLS = {
|
|
|
2492
2549
|
limit: N,
|
|
2493
2550
|
sort: S,
|
|
2494
2551
|
q: S,
|
|
2495
|
-
filter: { ...O, description: 'FilterExpr JSON (object): server-side row filter; adds total_filtered. lead.email_domain derives the exact lowercase domain from the current email, separate from website lead.domain. lead.gcid reads an exact Google category ID from current source data; it does not prove business fit. Use eq/any_of/none_of for exact lists; add not_empty to hold missing or malformed values. No DNS or provider lookup.' },
|
|
2552
|
+
filter: { ...O, description: 'FilterExpr JSON (object): server-side row filter; adds total_filtered. lead.email_domain derives the exact lowercase domain from the current email, separate from website lead.domain. lead.gcid reads an exact Google category ID from current source data; it does not prove business fit. Use eq/any_of/none_of for exact lists; add not_empty to hold missing or malformed values. No DNS or provider lookup. lead.email_check is the exact saved email check result for the current address (safe, role, catch_all, disposable, inbox_full, disabled, invalid, unknown), empty when never checked; lead.email_status risky holds both role and inbox_full, so use lead.email_check any_of ["safe","role"] for an exact rule.' },
|
|
2496
2553
|
view_id: { ...S, description: 'A saved custom view uuid or a system key (errored_rows/fully_enriched/data_only) from list_table_views. An ad-hoc filter/sort replaces the view\'s.' },
|
|
2497
2554
|
},
|
|
2498
2555
|
['list_id'],
|
package/src/mutation-policies.js
CHANGED
|
@@ -53,6 +53,9 @@ export const MUTATION_POLICIES = Object.freeze({
|
|
|
53
53
|
start_scrape_plan: never('POST', ['get_scrape_plan'], ['plan_id']),
|
|
54
54
|
set_scrape_plan_ready_goal: reconcile('PUT', ['get_scrape_plan_ready_goal'], ['plan_id', 'goal']),
|
|
55
55
|
set_scrape_plan_budget: reconcile('PUT', ['get_scrape_plan_budget'], ['plan_id', 'limits', 'list_ids']),
|
|
56
|
+
set_scrape_plan_supply: reconcile('PUT', ['get_scrape_plan_ready_goal'], ['plan_id', 'supply_mode', 'monthly_goal', 'limit_mode']),
|
|
57
|
+
switch_scrape_plan_campaign: reconcile('PUT', ['get_scrape_plan_ready_goal'], ['plan_id', 'campaign_id']),
|
|
58
|
+
update_scrape_plan: idempotent('PATCH', ['get_scrape_plan'], ['plan_id', 'count_reviews']),
|
|
56
59
|
estimate_scrape: read(),
|
|
57
60
|
submit_scrape: keyed('POST', ['list_jobs'], ['kind', 'integration_id', 'credential_id', 'table_id']),
|
|
58
61
|
cancel_job: never('POST', ['get_job'], ['job_id', 'status']),
|
|
@@ -184,6 +187,7 @@ export const MUTATION_POLICIES = Object.freeze({
|
|
|
184
187
|
save_flow_layout: idempotent('PUT', ['get_flow_graph'], ['scope_key']),
|
|
185
188
|
create_table_flow_rule: reconcile('POST', ['list_table_flow_rules'], ['list_id', 'name']),
|
|
186
189
|
toggle_table_flow_rule: idempotent('PATCH', ['list_table_flow_rules'], ['list_id', 'rule_id', 'enabled']),
|
|
190
|
+
update_table_flow_rule: idempotent('PATCH', ['list_table_flow_rules'], ['list_id', 'rule_id']),
|
|
187
191
|
create_campaign_flow_rule: reconcile(
|
|
188
192
|
'POST',
|
|
189
193
|
['list_campaign_flow_rules'],
|