scrapeloop-mcp 0.33.0 → 0.35.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 +73 -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.35.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.
|
|
@@ -48,6 +50,7 @@ const NATIVE_TABLE_TOOLS = Object.freeze([
|
|
|
48
50
|
'business_email_finder',
|
|
49
51
|
'linkedin_finder',
|
|
50
52
|
'linkedin_activity',
|
|
53
|
+
'google_profile',
|
|
51
54
|
]);
|
|
52
55
|
const TRACE_HEADER = 'X-Scrapeloop-Trace-Id';
|
|
53
56
|
const SAFE_TRACE_ID = /^[A-Za-z0-9_-]{8,80}$/;
|
|
@@ -133,6 +136,9 @@ async function api(method, path, body, options = {}) {
|
|
|
133
136
|
|| policy?.retry === 'idempotent'
|
|
134
137
|
|| policy?.retry === 'idempotency_key';
|
|
135
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;
|
|
136
142
|
const headers = {
|
|
137
143
|
Authorization: `Bearer ${API_KEY}`,
|
|
138
144
|
'Content-Type': 'application/json',
|
|
@@ -149,7 +155,7 @@ async function api(method, path, body, options = {}) {
|
|
|
149
155
|
method: verb,
|
|
150
156
|
headers,
|
|
151
157
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
152
|
-
signal: AbortSignal.timeout(
|
|
158
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
153
159
|
});
|
|
154
160
|
} catch (e) {
|
|
155
161
|
lastErr = e;
|
|
@@ -164,7 +170,7 @@ async function api(method, path, body, options = {}) {
|
|
|
164
170
|
return {
|
|
165
171
|
ok: false,
|
|
166
172
|
status: 0,
|
|
167
|
-
error: { detail: `Could not reach Scrapeloop (${timedOut ? `timeout after ${
|
|
173
|
+
error: { detail: `Could not reach Scrapeloop (${timedOut ? `timeout after ${timeoutMs}ms` : String(e)}).` },
|
|
168
174
|
...(options.captureTrace
|
|
169
175
|
? { trace_id: traceId, network_error: timedOut ? 'timeout' : 'transport' }
|
|
170
176
|
: {}),
|
|
@@ -575,14 +581,15 @@ const TOOLS = {
|
|
|
575
581
|
run: (a) => api('GET', `/plans/${enc(a.plan_id)}/yield`),
|
|
576
582
|
},
|
|
577
583
|
set_scrape_plan_ready_goal: {
|
|
578
|
-
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({
|
|
579
585
|
plan_id: { ...S, format: 'uuid' }, expected_updated_at: { ...S, format: 'date-time' },
|
|
580
586
|
goal: obj({ kind: { ...S, enum: ['verified_email'] }, ready_list_id: { ...S, format: 'uuid' },
|
|
581
|
-
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 },
|
|
582
588
|
limit_mode: { ...S, enum: ['target', 'maximum'] },
|
|
589
|
+
supply_mode: { ...S, enum: ['monthly', 'on_demand'] }, buffer_days: { type: 'integer', minimum: 0, maximum: 30 },
|
|
583
590
|
monthly_minimum: { type: 'integer', minimum: 0, maximum: 1000000 },
|
|
584
591
|
stock_ceiling: { type: 'integer', minimum: 1, maximum: 1000000 }, timezone: S,
|
|
585
|
-
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,
|
|
586
593
|
required_fields: ARR({ ...S, enum: ['name', 'organization_name', 'first_name', 'last_name', 'industry', 'city', 'state', 'postal_code', 'domain', 'language'] }),
|
|
587
594
|
languages: ARR({ ...S, pattern: '^[a-z]{2}$' }),
|
|
588
595
|
}, ['ready_list_id', 'campaign_id', 'monthly_goal', 'monthly_minimum', 'stock_ceiling', 'timezone']),
|
|
@@ -594,10 +601,10 @@ const TOOLS = {
|
|
|
594
601
|
run: (a) => api('GET', `/plans/${enc(a.plan_id)}/budget`),
|
|
595
602
|
},
|
|
596
603
|
set_scrape_plan_budget: {
|
|
597
|
-
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({
|
|
598
605
|
plan_id: { ...S, format: 'uuid' }, expected_revision: { ...S, format: 'uuid' },
|
|
599
606
|
limits: obj({
|
|
600
|
-
monthly_lead_credits: { type: 'integer', minimum: 0, maximum: 1000000000 },
|
|
607
|
+
monthly_lead_credits: { type: ['integer', 'null'], minimum: 0, maximum: 1000000000 },
|
|
601
608
|
lifetime_lead_credits: { type: ['integer', 'null'], minimum: 0, maximum: 1000000000 },
|
|
602
609
|
monthly_verify_credits: { type: ['integer', 'null'], minimum: 0, maximum: 1000000000 },
|
|
603
610
|
lifetime_verify_credits: { type: ['integer', 'null'], minimum: 0, maximum: 1000000000 },
|
|
@@ -608,6 +615,40 @@ const TOOLS = {
|
|
|
608
615
|
}, ['plan_id', 'limits', 'list_ids']) },
|
|
609
616
|
run: ({ plan_id, ...body }) => api('PUT', `/plans/${enc(plan_id)}/budget`, body),
|
|
610
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
|
+
},
|
|
611
652
|
review_complete_scrape_plan: {
|
|
612
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']) },
|
|
613
654
|
run: (a) => api('GET', `/plans/${enc(a.plan_id)}/launch-review`),
|
|
@@ -1070,7 +1111,7 @@ const TOOLS = {
|
|
|
1070
1111
|
},
|
|
1071
1112
|
get_campaign_handoff_status: {
|
|
1072
1113
|
def: {
|
|
1073
|
-
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.',
|
|
1074
1115
|
inputSchema: obj({ campaign_id: S }, ['campaign_id']),
|
|
1075
1116
|
},
|
|
1076
1117
|
run: (a) => a.campaign_id ? api('GET', `/campaigns/${enc(a.campaign_id)}/handoffs`)
|
|
@@ -1080,6 +1121,10 @@ const TOOLS = {
|
|
|
1080
1121
|
def: { description: 'Read dated verified ready stock, count shared businesses and emails once, and compare planning shares with estimated remaining-month demand. Includes linked goals, coverage and shared credit allowance. This is a read-only planning view, not a reservation or sender approval. Unknown proof remains unknown. The remaining-month estimate accounts for saved follow-ups and deducts the current sender queue before allocating local stock.', inputSchema: obj({}) },
|
|
1081
1122
|
run: () => api('GET', '/campaigns/supply'),
|
|
1082
1123
|
},
|
|
1124
|
+
get_campaign_supply_status: {
|
|
1125
|
+
def: { description: 'Read the one supply status for every campaign with a linked complete plan, or for one campaign_id. Each answer has exactly one state: Running by itself, Waiting, it carries on by itself (with what it waits for), or Stopped, needs you (with the one next action, and anything else that is wrong listed after it). It also gives leads in the campaign now, leads added in the last 24 hours, searches done of the total, lead and verify credits used of their limits, measured credits per lead, and where the limits run out. Read-only: stored values and small counts, no sender call and no credit use. A number that could not be read is null, never zero. Use this first when the user asks if a campaign is running, done or stuck right now, and say the summary to them as written. To see whether the setup itself fits together, use check_campaign_setup.', inputSchema: obj({ campaign_id: S }) },
|
|
1126
|
+
run: (a) => api('GET', a.campaign_id ? `/campaigns/${enc(a.campaign_id)}/supply-status` : '/campaigns/supply-status'),
|
|
1127
|
+
},
|
|
1083
1128
|
get_campaign_setup_options: {
|
|
1084
1129
|
def: { description: 'Read this workspace\'s channel choices and defaults for new campaign drafts. Explicit create choices win. Reading options changes nothing and starts no campaign.', inputSchema: obj({}) },
|
|
1085
1130
|
run: () => api('GET', '/campaigns/options'),
|
|
@@ -1162,7 +1207,7 @@ const TOOLS = {
|
|
|
1162
1207
|
run: (a) => api('GET', `/campaigns/${enc(a.campaign_id)}/qualification`),
|
|
1163
1208
|
},
|
|
1164
1209
|
update_campaign: {
|
|
1165
|
-
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']) },
|
|
1210
|
+
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']) },
|
|
1166
1211
|
run: (a) => api('PATCH', `/campaigns/${enc(a.campaign_id)}`, a.patch || {}),
|
|
1167
1212
|
},
|
|
1168
1213
|
duplicate_campaign: {
|
|
@@ -1234,7 +1279,7 @@ const TOOLS = {
|
|
|
1234
1279
|
'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.',
|
|
1235
1280
|
inputSchema: obj({ campaign_id: S }, ['campaign_id']),
|
|
1236
1281
|
},
|
|
1237
|
-
run: (a) => api('GET', `/campaigns/${enc(a.campaign_id)}/readiness
|
|
1282
|
+
run: (a) => api('GET', `/campaigns/${enc(a.campaign_id)}/readiness`, undefined, DEEP_READ),
|
|
1238
1283
|
},
|
|
1239
1284
|
get_sender_config: {
|
|
1240
1285
|
def: { description: "Get an Instantly credential's sender config (daily cap, send accounts, …).", inputSchema: obj({ credential_id: S }, ['credential_id']) },
|
|
@@ -1685,7 +1730,7 @@ const TOOLS = {
|
|
|
1685
1730
|
create_table_native_column: {
|
|
1686
1731
|
def: {
|
|
1687
1732
|
description:
|
|
1688
|
-
`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, and
|
|
1733
|
+
`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.`,
|
|
1689
1734
|
inputSchema: obj(
|
|
1690
1735
|
{
|
|
1691
1736
|
table_id: S,
|
|
@@ -1702,7 +1747,7 @@ const TOOLS = {
|
|
|
1702
1747
|
options: {
|
|
1703
1748
|
...O,
|
|
1704
1749
|
description:
|
|
1705
|
-
'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. An undocumented name is rejected, never ignored.',
|
|
1750
|
+
'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.',
|
|
1706
1751
|
},
|
|
1707
1752
|
operation_id: {
|
|
1708
1753
|
...S,
|
|
@@ -2231,6 +2276,21 @@ const TOOLS = {
|
|
|
2231
2276
|
})
|
|
2232
2277
|
: { ok: false, status: 400, error: { detail: 'list_id and rule_id are required.' } },
|
|
2233
2278
|
},
|
|
2279
|
+
update_table_flow_rule: {
|
|
2280
|
+
def: {
|
|
2281
|
+
description:
|
|
2282
|
+
'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.',
|
|
2283
|
+
inputSchema: obj({
|
|
2284
|
+
list_id: S, rule_id: S, name: S, condition: { type: 'object' }, action_config: { type: 'object' },
|
|
2285
|
+
daily_fire_cap: { type: 'integer', minimum: 1, maximum: 100000 },
|
|
2286
|
+
delay_seconds: { type: 'integer', minimum: 0, maximum: 2592000 },
|
|
2287
|
+
}, ['list_id', 'rule_id']),
|
|
2288
|
+
},
|
|
2289
|
+
run: ({ list_id, rule_id, ...body }) =>
|
|
2290
|
+
Object.keys(body).length
|
|
2291
|
+
? api('PATCH', `/lists/${enc(list_id)}/flow-rules/${enc(rule_id)}`, body)
|
|
2292
|
+
: { ok: false, status: 400, error: { detail: 'Send at least one thing to change.' } },
|
|
2293
|
+
},
|
|
2234
2294
|
list_campaign_flow_rules: {
|
|
2235
2295
|
def: {
|
|
2236
2296
|
description:
|
|
@@ -2493,7 +2553,7 @@ const TOOLS = {
|
|
|
2493
2553
|
limit: N,
|
|
2494
2554
|
sort: S,
|
|
2495
2555
|
q: S,
|
|
2496
|
-
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.' },
|
|
2556
|
+
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.' },
|
|
2497
2557
|
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.' },
|
|
2498
2558
|
},
|
|
2499
2559
|
['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'],
|