@sellable/mcp 0.1.318 → 0.1.319
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/dist/generated/column-schema-manifest.js +1 -1
- package/dist/server.js +1 -0
- package/dist/tools/blueprint-commit.js +1 -1
- package/dist/tools/inbox.d.ts +308 -0
- package/dist/tools/inbox.js +544 -0
- package/dist/tools/linkedin.d.ts +76 -0
- package/dist/tools/linkedin.js +351 -18
- package/dist/tools/readiness.js +44 -13
- package/dist/tools/registry.d.ts +11 -0
- package/package.json +1 -1
- package/skills/building-gtm-tables/SKILL.md +26 -0
- package/skills/building-gtm-tables/references/column-type-catalog.md +15 -1
- package/skills/building-gtm-tables/references/common-blueprints.fixtures.ts +54 -5
- package/skills/building-gtm-tables/references/common-blueprints.md +9 -0
- package/skills/research/config.json +9 -0
package/dist/tools/linkedin.js
CHANGED
|
@@ -47,7 +47,7 @@ export const linkedinToolDefinitions = [
|
|
|
47
47
|
},
|
|
48
48
|
{
|
|
49
49
|
name: "fetch_linkedin_profile",
|
|
50
|
-
description: "Fetch LinkedIn profile details. Defaults to
|
|
50
|
+
description: "Fetch LinkedIn profile details. Defaults to compact context (fetchMinimal=true) that strips images/media/provider metadata and returns identity, headline, about, current role, company, limited experience, education, skills, certifications, languages, and projects. Pass fetchMinimal=false only when you need raw provider fields or media URLs. Pass full=true ONLY when you need the long-tail experience history (positions 6+) or complete skill list (skills 3+) for deep research; full still returns compact output unless fetchMinimal=false.",
|
|
51
51
|
inputSchema: {
|
|
52
52
|
type: "object",
|
|
53
53
|
properties: {
|
|
@@ -60,6 +60,11 @@ export const linkedinToolDefinitions = [
|
|
|
60
60
|
description: "Opt into the full payload (~38% more expensive, slightly slower). Set true only when you specifically need the long-tail experience history or full skill list. Default false (main payload).",
|
|
61
61
|
default: false,
|
|
62
62
|
},
|
|
63
|
+
fetchMinimal: {
|
|
64
|
+
type: "boolean",
|
|
65
|
+
description: "Return compact agent context by stripping images/media/provider metadata and limiting long lists. Default true. Set false only when raw provider fields or media URLs are needed.",
|
|
66
|
+
default: true,
|
|
67
|
+
},
|
|
63
68
|
},
|
|
64
69
|
required: ["linkedinUrl"],
|
|
65
70
|
},
|
|
@@ -187,6 +192,348 @@ function collectImageUrls(value, urls = new Set()) {
|
|
|
187
192
|
function pickString(value) {
|
|
188
193
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
189
194
|
}
|
|
195
|
+
function isRecord(value) {
|
|
196
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
197
|
+
}
|
|
198
|
+
function pickRecord(value, keys) {
|
|
199
|
+
if (!isRecord(value))
|
|
200
|
+
return undefined;
|
|
201
|
+
for (const key of keys) {
|
|
202
|
+
const candidate = value[key];
|
|
203
|
+
if (isRecord(candidate))
|
|
204
|
+
return candidate;
|
|
205
|
+
}
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
function pickStringFrom(value, keys, maxLength = 1200) {
|
|
209
|
+
if (!isRecord(value))
|
|
210
|
+
return undefined;
|
|
211
|
+
for (const key of keys) {
|
|
212
|
+
const candidate = value[key];
|
|
213
|
+
const picked = typeof candidate === "string" || typeof candidate === "number"
|
|
214
|
+
? compactString(candidate, maxLength)
|
|
215
|
+
: undefined;
|
|
216
|
+
if (picked)
|
|
217
|
+
return picked;
|
|
218
|
+
}
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
function pickNumberFrom(value, keys) {
|
|
222
|
+
if (!isRecord(value))
|
|
223
|
+
return undefined;
|
|
224
|
+
for (const key of keys) {
|
|
225
|
+
const candidate = value[key];
|
|
226
|
+
if (typeof candidate === "number" && Number.isFinite(candidate)) {
|
|
227
|
+
return candidate;
|
|
228
|
+
}
|
|
229
|
+
if (typeof candidate === "string" && candidate.trim()) {
|
|
230
|
+
const parsed = Number(candidate.replace(/,/g, ""));
|
|
231
|
+
if (Number.isFinite(parsed))
|
|
232
|
+
return parsed;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
function pickBooleanFrom(value, keys) {
|
|
238
|
+
if (!isRecord(value))
|
|
239
|
+
return undefined;
|
|
240
|
+
for (const key of keys) {
|
|
241
|
+
const candidate = value[key];
|
|
242
|
+
if (typeof candidate === "boolean")
|
|
243
|
+
return candidate;
|
|
244
|
+
}
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
function compactString(value, maxLength = 1200) {
|
|
248
|
+
if (typeof value !== "string" && typeof value !== "number") {
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
const compacted = String(value)
|
|
252
|
+
.replace(/[ \t]+\n/g, "\n")
|
|
253
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
254
|
+
.trim();
|
|
255
|
+
if (!compacted)
|
|
256
|
+
return undefined;
|
|
257
|
+
return compacted.length > maxLength
|
|
258
|
+
? `${compacted.slice(0, maxLength - 3).trim()}...`
|
|
259
|
+
: compacted;
|
|
260
|
+
}
|
|
261
|
+
function stripEmpty(value) {
|
|
262
|
+
if (Array.isArray(value)) {
|
|
263
|
+
return value
|
|
264
|
+
.map((item) => stripEmpty(item))
|
|
265
|
+
.filter((item) => {
|
|
266
|
+
if (item == null)
|
|
267
|
+
return false;
|
|
268
|
+
if (Array.isArray(item))
|
|
269
|
+
return item.length > 0;
|
|
270
|
+
if (isRecord(item))
|
|
271
|
+
return Object.keys(item).length > 0;
|
|
272
|
+
return true;
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
if (!isRecord(value))
|
|
276
|
+
return value;
|
|
277
|
+
const entries = Object.entries(value)
|
|
278
|
+
.map(([key, item]) => [key, stripEmpty(item)])
|
|
279
|
+
.filter(([, item]) => {
|
|
280
|
+
if (item == null || item === "")
|
|
281
|
+
return false;
|
|
282
|
+
if (Array.isArray(item))
|
|
283
|
+
return item.length > 0;
|
|
284
|
+
if (isRecord(item))
|
|
285
|
+
return Object.keys(item).length > 0;
|
|
286
|
+
return true;
|
|
287
|
+
});
|
|
288
|
+
return Object.fromEntries(entries);
|
|
289
|
+
}
|
|
290
|
+
function asArray(value) {
|
|
291
|
+
if (Array.isArray(value))
|
|
292
|
+
return value;
|
|
293
|
+
return value == null ? [] : [value];
|
|
294
|
+
}
|
|
295
|
+
function pickProfileArray(profile, keys) {
|
|
296
|
+
for (const key of keys) {
|
|
297
|
+
const value = profile[key];
|
|
298
|
+
if (Array.isArray(value))
|
|
299
|
+
return value;
|
|
300
|
+
}
|
|
301
|
+
return [];
|
|
302
|
+
}
|
|
303
|
+
function datePartText(value) {
|
|
304
|
+
if (typeof value === "string")
|
|
305
|
+
return compactString(value, 80);
|
|
306
|
+
if (!isRecord(value))
|
|
307
|
+
return undefined;
|
|
308
|
+
const text = pickStringFrom(value, ["text", "date"], 80);
|
|
309
|
+
if (text)
|
|
310
|
+
return text;
|
|
311
|
+
const year = pickNumberFrom(value, ["year"]);
|
|
312
|
+
const month = pickStringFrom(value, ["month"], 20) || pickNumberFrom(value, ["month"]);
|
|
313
|
+
if (!year)
|
|
314
|
+
return undefined;
|
|
315
|
+
return month ? `${month} ${year}` : String(year);
|
|
316
|
+
}
|
|
317
|
+
function dateRangeFromItem(item) {
|
|
318
|
+
const explicit = pickStringFrom(item, ["dateRange", "date_range", "period"], 120);
|
|
319
|
+
if (explicit)
|
|
320
|
+
return explicit;
|
|
321
|
+
const startText = datePartText(item.startDate) ||
|
|
322
|
+
datePartText(stripEmpty({
|
|
323
|
+
month: item.start_month ?? item.startMonth,
|
|
324
|
+
year: item.start_year ?? item.startYear,
|
|
325
|
+
}));
|
|
326
|
+
const endText = datePartText(item.endDate) ||
|
|
327
|
+
datePartText(stripEmpty({
|
|
328
|
+
month: item.end_month ?? item.endMonth,
|
|
329
|
+
year: item.end_year ?? item.endYear,
|
|
330
|
+
}));
|
|
331
|
+
return [startText, endText].filter(Boolean).join(" - ") || undefined;
|
|
332
|
+
}
|
|
333
|
+
function compactExperienceItem(value) {
|
|
334
|
+
if (!isRecord(value))
|
|
335
|
+
return {};
|
|
336
|
+
const companyRecord = pickRecord(value, ["company"]);
|
|
337
|
+
return stripEmpty({
|
|
338
|
+
title: pickStringFrom(value, ["title", "position", "job_title"], 160),
|
|
339
|
+
companyName: pickStringFrom(value, ["companyName", "company_name", "company"], 160) ||
|
|
340
|
+
pickStringFrom(companyRecord, ["name"], 160),
|
|
341
|
+
companyLinkedinUrl: pickStringFrom(value, [
|
|
342
|
+
"companyLinkedinUrl",
|
|
343
|
+
"companyLinkedInUrl",
|
|
344
|
+
"company_linkedin_url",
|
|
345
|
+
"companyLink",
|
|
346
|
+
"company_public_url",
|
|
347
|
+
], 240),
|
|
348
|
+
dateRange: dateRangeFromItem(value),
|
|
349
|
+
duration: pickStringFrom(value, ["duration", "current_job_duration"], 80),
|
|
350
|
+
location: pickStringFrom(value, ["location"], 180),
|
|
351
|
+
description: pickStringFrom(value, ["description", "summary"], 700),
|
|
352
|
+
isCurrent: pickBooleanFrom(value, ["isCurrent", "is_current"]),
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
function compactEducationItem(value) {
|
|
356
|
+
if (!isRecord(value))
|
|
357
|
+
return {};
|
|
358
|
+
return stripEmpty({
|
|
359
|
+
schoolName: pickStringFrom(value, ["schoolName", "school_name", "school"], 180),
|
|
360
|
+
schoolLinkedinUrl: pickStringFrom(value, ["schoolLinkedinUrl", "schoolLinkedInUrl", "school_linkedin_url"], 240),
|
|
361
|
+
degree: pickStringFrom(value, ["degree"], 180),
|
|
362
|
+
fieldOfStudy: pickStringFrom(value, ["fieldOfStudy", "field_of_study"], 180),
|
|
363
|
+
dateRange: dateRangeFromItem(value),
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
function compactStringList(value, limit) {
|
|
367
|
+
return asArray(value)
|
|
368
|
+
.map((item) => {
|
|
369
|
+
if (typeof item === "string" || typeof item === "number") {
|
|
370
|
+
return compactString(item, 120);
|
|
371
|
+
}
|
|
372
|
+
return pickStringFrom(item, ["name", "title", "skill"], 120);
|
|
373
|
+
})
|
|
374
|
+
.filter((item) => Boolean(item))
|
|
375
|
+
.slice(0, limit);
|
|
376
|
+
}
|
|
377
|
+
function compactLanguages(value) {
|
|
378
|
+
return asArray(value)
|
|
379
|
+
.map((item) => {
|
|
380
|
+
if (typeof item === "string")
|
|
381
|
+
return { name: compactString(item, 80) };
|
|
382
|
+
if (!isRecord(item))
|
|
383
|
+
return {};
|
|
384
|
+
return stripEmpty({
|
|
385
|
+
name: pickStringFrom(item, ["name", "language"], 80),
|
|
386
|
+
proficiency: pickStringFrom(item, ["proficiency"], 120),
|
|
387
|
+
});
|
|
388
|
+
})
|
|
389
|
+
.filter((item) => Object.keys(item).length > 0)
|
|
390
|
+
.slice(0, 10);
|
|
391
|
+
}
|
|
392
|
+
function compactNamedItems(value) {
|
|
393
|
+
return asArray(value)
|
|
394
|
+
.map((item) => {
|
|
395
|
+
if (typeof item === "string")
|
|
396
|
+
return { name: compactString(item, 160) };
|
|
397
|
+
if (!isRecord(item))
|
|
398
|
+
return {};
|
|
399
|
+
return stripEmpty({
|
|
400
|
+
name: pickStringFrom(item, ["name", "title"], 180),
|
|
401
|
+
organization: pickStringFrom(item, ["organization", "authority", "issuer", "companyName"], 180),
|
|
402
|
+
dateRange: dateRangeFromItem(item),
|
|
403
|
+
description: pickStringFrom(item, ["description"], 500),
|
|
404
|
+
url: pickStringFrom(item, ["url", "link"], 240),
|
|
405
|
+
});
|
|
406
|
+
})
|
|
407
|
+
.filter((item) => Object.keys(item).length > 0)
|
|
408
|
+
.slice(0, 5);
|
|
409
|
+
}
|
|
410
|
+
function firstCurrentRole(profile) {
|
|
411
|
+
const current = pickProfileArray(profile, [
|
|
412
|
+
"currentPosition",
|
|
413
|
+
"current_position",
|
|
414
|
+
])
|
|
415
|
+
.map(compactExperienceItem)
|
|
416
|
+
.find((item) => Object.keys(item).length > 0);
|
|
417
|
+
if (current)
|
|
418
|
+
return current;
|
|
419
|
+
const firstExperience = pickProfileArray(profile, [
|
|
420
|
+
"experience",
|
|
421
|
+
"experiences",
|
|
422
|
+
])
|
|
423
|
+
.map(compactExperienceItem)
|
|
424
|
+
.find((item) => Object.keys(item).length > 0);
|
|
425
|
+
if (firstExperience)
|
|
426
|
+
return firstExperience;
|
|
427
|
+
return stripEmpty({
|
|
428
|
+
title: pickStringFrom(profile, ["job_title", "jobTitle", "title"], 160),
|
|
429
|
+
companyName: pickStringFrom(profile, ["company", "companyName"], 160),
|
|
430
|
+
dateRange: dateRangeFromItem(stripEmpty({
|
|
431
|
+
start_month: profile.current_company_join_month ?? profile.currentCompanyJoinMonth,
|
|
432
|
+
start_year: profile.current_company_join_year ?? profile.currentCompanyJoinYear,
|
|
433
|
+
})),
|
|
434
|
+
duration: pickStringFrom(profile, ["current_job_duration", "currentJobDuration"], 80),
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
function compactCompanyFromProfile(profile, currentRole) {
|
|
438
|
+
return stripEmpty({
|
|
439
|
+
name: pickStringFrom(profile, ["company", "companyName"], 180) ||
|
|
440
|
+
currentRole.companyName,
|
|
441
|
+
linkedinUrl: pickStringFrom(profile, [
|
|
442
|
+
"company_linkedin_url",
|
|
443
|
+
"companyLinkedinUrl",
|
|
444
|
+
"companyLinkedInUrl",
|
|
445
|
+
"currentCompanyLinkedinUrl",
|
|
446
|
+
], 240) || currentRole.companyLinkedinUrl,
|
|
447
|
+
website: pickStringFrom(profile, ["company_website", "companyWebsite", "website"], 240),
|
|
448
|
+
domain: pickStringFrom(profile, ["company_domain", "companyDomain", "currentCompanyDomain"], 120),
|
|
449
|
+
industry: pickStringFrom(profile, ["company_industry", "industry"], 160),
|
|
450
|
+
employeeCount: pickNumberFrom(profile, [
|
|
451
|
+
"company_employee_count",
|
|
452
|
+
"companyEmployeeCount",
|
|
453
|
+
"employeeCount",
|
|
454
|
+
]),
|
|
455
|
+
employeeRange: pickStringFrom(profile, ["company_employee_range", "companyEmployeeRange", "employeeCountRange"], 80),
|
|
456
|
+
description: pickStringFrom(profile, ["company_description", "companyDescription"], 700),
|
|
457
|
+
founded: pickStringFrom(profile, ["company_year_founded", "founded"], 80) ||
|
|
458
|
+
pickNumberFrom(profile, ["company_year_founded", "founded"]),
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
export function compactLinkedInProfile(profile) {
|
|
462
|
+
if (!isRecord(profile))
|
|
463
|
+
return {};
|
|
464
|
+
const currentRole = firstCurrentRole(profile);
|
|
465
|
+
const experience = pickProfileArray(profile, ["experience", "experiences"])
|
|
466
|
+
.map(compactExperienceItem)
|
|
467
|
+
.filter((item) => Object.keys(item).length > 0)
|
|
468
|
+
.slice(0, 5);
|
|
469
|
+
const education = pickProfileArray(profile, ["education", "educations"])
|
|
470
|
+
.map(compactEducationItem)
|
|
471
|
+
.filter((item) => Object.keys(item).length > 0)
|
|
472
|
+
.slice(0, 3);
|
|
473
|
+
const skills = compactStringList(profile.topSkills ?? profile.skills, 12);
|
|
474
|
+
return stripEmpty({
|
|
475
|
+
id: pickStringFrom(profile, ["id", "profile_id"], 180),
|
|
476
|
+
publicIdentifier: pickStringFrom(profile, ["publicIdentifier", "public_id", "publicIdentifier"], 120),
|
|
477
|
+
linkedinUrl: pickStringFrom(profile, ["linkedinUrl", "linkedin_url", "profile_url"], 240),
|
|
478
|
+
firstName: pickStringFrom(profile, ["firstName", "first_name"], 120),
|
|
479
|
+
lastName: pickStringFrom(profile, ["lastName", "last_name"], 120),
|
|
480
|
+
fullName: pickStringFrom(profile, ["fullName", "full_name", "name"], 180) ||
|
|
481
|
+
[
|
|
482
|
+
pickStringFrom(profile, ["firstName", "first_name"], 120),
|
|
483
|
+
pickStringFrom(profile, ["lastName", "last_name"], 120),
|
|
484
|
+
]
|
|
485
|
+
.filter(Boolean)
|
|
486
|
+
.join(" "),
|
|
487
|
+
headline: pickStringFrom(profile, ["headline"], 320),
|
|
488
|
+
about: pickStringFrom(profile, ["about", "summary"], 1600),
|
|
489
|
+
location: pickStringFrom(profile, ["location"], 180),
|
|
490
|
+
followerCount: pickNumberFrom(profile, ["followerCount", "follower_count"]),
|
|
491
|
+
connectionCount: pickNumberFrom(profile, [
|
|
492
|
+
"connectionsCount",
|
|
493
|
+
"connection_count",
|
|
494
|
+
"connectionCount",
|
|
495
|
+
]),
|
|
496
|
+
isPremium: pickBooleanFrom(profile, ["premium", "is_premium", "isPremium"]),
|
|
497
|
+
isCreator: pickBooleanFrom(profile, ["is_creator", "isCreator"]),
|
|
498
|
+
isVerified: pickBooleanFrom(profile, [
|
|
499
|
+
"verified",
|
|
500
|
+
"is_verified",
|
|
501
|
+
"isVerified",
|
|
502
|
+
]),
|
|
503
|
+
currentRole,
|
|
504
|
+
company: compactCompanyFromProfile(profile, currentRole),
|
|
505
|
+
experience,
|
|
506
|
+
education,
|
|
507
|
+
skills,
|
|
508
|
+
languages: compactLanguages(profile.languages),
|
|
509
|
+
certifications: compactNamedItems(profile.certifications),
|
|
510
|
+
projects: compactNamedItems(profile.projects),
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
function canonicalizeLinkedInProfileResponse(raw) {
|
|
514
|
+
if (raw &&
|
|
515
|
+
typeof raw === "object" &&
|
|
516
|
+
raw.status === "success" &&
|
|
517
|
+
(raw.data || raw.person || raw.profile?.person)) {
|
|
518
|
+
const canonical = raw.data ?? raw.person ?? raw.profile?.person;
|
|
519
|
+
const { status, data, person, profile, ...rest } = raw;
|
|
520
|
+
return { status, profile: canonical, ...rest };
|
|
521
|
+
}
|
|
522
|
+
return raw;
|
|
523
|
+
}
|
|
524
|
+
function compactLinkedInProfileResponse(raw) {
|
|
525
|
+
const normalized = canonicalizeLinkedInProfileResponse(raw);
|
|
526
|
+
if (normalized &&
|
|
527
|
+
typeof normalized === "object" &&
|
|
528
|
+
normalized.status === "success" &&
|
|
529
|
+
normalized.profile) {
|
|
530
|
+
return {
|
|
531
|
+
status: normalized.status,
|
|
532
|
+
profile: compactLinkedInProfile(normalized.profile),
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
return normalized;
|
|
536
|
+
}
|
|
190
537
|
function serializePostMedia(post) {
|
|
191
538
|
const media = post.media && typeof post.media === "object"
|
|
192
539
|
? post.media
|
|
@@ -295,23 +642,9 @@ export async function fetchLinkedInProfile(linkedinUrl, options = {}) {
|
|
|
295
642
|
params.set("full", "true");
|
|
296
643
|
}
|
|
297
644
|
const raw = await api.get(`/api/v1/scrape/linkedin/profile?${params.toString()}`);
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
// For the MCP layer, that ~3xs the JSON size and routinely breaches
|
|
302
|
-
// Claude Code's max-token cap on tool results — which triggers the
|
|
303
|
-
// harness's "save to file + read in chunks via python3" overflow
|
|
304
|
-
// fallback. Strip the duplicates here so the agent gets one clean
|
|
305
|
-
// copy of the data under a single `profile` key.
|
|
306
|
-
if (raw &&
|
|
307
|
-
typeof raw === "object" &&
|
|
308
|
-
raw.status === "success" &&
|
|
309
|
-
(raw.data || raw.person || raw.profile?.person)) {
|
|
310
|
-
const canonical = raw.data ?? raw.person ?? raw.profile?.person;
|
|
311
|
-
const { status, data, person, profile, ...rest } = raw;
|
|
312
|
-
return { status, profile: canonical, ...rest };
|
|
313
|
-
}
|
|
314
|
-
return raw;
|
|
645
|
+
return options.fetchMinimal === false
|
|
646
|
+
? canonicalizeLinkedInProfileResponse(raw)
|
|
647
|
+
: compactLinkedInProfileResponse(raw);
|
|
315
648
|
}
|
|
316
649
|
export async function fetchCompany(companyUrl) {
|
|
317
650
|
const api = getApi();
|
package/dist/tools/readiness.js
CHANGED
|
@@ -20,6 +20,23 @@ function normalizeLeadSourceProvider(provider) {
|
|
|
20
20
|
return "csv-linkedin";
|
|
21
21
|
return undefined;
|
|
22
22
|
}
|
|
23
|
+
function isPositiveNumber(value) {
|
|
24
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
25
|
+
}
|
|
26
|
+
function hasSignalDiscoveryProgressEvidence(rowCount, importProgress) {
|
|
27
|
+
const phase = importProgress?.phase ?? null;
|
|
28
|
+
return (rowCount > 0 ||
|
|
29
|
+
isPositiveNumber(importProgress?.processed) ||
|
|
30
|
+
isPositiveNumber(importProgress?.leadsImported) ||
|
|
31
|
+
isPositiveNumber(importProgress?.scrapedEngagers) ||
|
|
32
|
+
isPositiveNumber(importProgress?.evaluatedEngagers) ||
|
|
33
|
+
isPositiveNumber(importProgress?.evaluationTotal) ||
|
|
34
|
+
isPositiveNumber(importProgress?.activeEvaluationBatches) ||
|
|
35
|
+
isPositiveNumber(importProgress?.queuedForEvaluation) ||
|
|
36
|
+
isPositiveNumber(importProgress?.postsScraped) ||
|
|
37
|
+
phase === "scraping" ||
|
|
38
|
+
phase === "processing");
|
|
39
|
+
}
|
|
23
40
|
function sleep(ms) {
|
|
24
41
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
25
42
|
}
|
|
@@ -233,6 +250,8 @@ export async function waitForLeadListReady(input) {
|
|
|
233
250
|
if (typeof configTarget === "number" && !targetLeadCount) {
|
|
234
251
|
targetLeadCount = configTarget;
|
|
235
252
|
}
|
|
253
|
+
const signalDiscoveryHasProgressEvidence = provider === "signal-discovery" &&
|
|
254
|
+
hasSignalDiscoveryProgressEvidence(rowCount, importProgress);
|
|
236
255
|
let jobId = input.jobId;
|
|
237
256
|
if (!jobId && typeof config?.importJobId === "string") {
|
|
238
257
|
jobId = config.importJobId;
|
|
@@ -253,7 +272,9 @@ export async function waitForLeadListReady(input) {
|
|
|
253
272
|
targetLeadCount,
|
|
254
273
|
};
|
|
255
274
|
}
|
|
256
|
-
if (configError &&
|
|
275
|
+
if (configError &&
|
|
276
|
+
configStatus !== "complete" &&
|
|
277
|
+
!signalDiscoveryHasProgressEvidence) {
|
|
257
278
|
return {
|
|
258
279
|
ready: false,
|
|
259
280
|
reason: "import_failed",
|
|
@@ -267,21 +288,31 @@ export async function waitForLeadListReady(input) {
|
|
|
267
288
|
error: configError,
|
|
268
289
|
};
|
|
269
290
|
}
|
|
291
|
+
if (configError &&
|
|
292
|
+
configStatus !== "complete" &&
|
|
293
|
+
signalDiscoveryHasProgressEvidence) {
|
|
294
|
+
lastError = configError;
|
|
295
|
+
}
|
|
270
296
|
if (configStatus) {
|
|
271
297
|
lastStatus = configStatus;
|
|
272
298
|
if (configStatus === "error") {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
299
|
+
if (signalDiscoveryHasProgressEvidence) {
|
|
300
|
+
lastError = configError ?? lastError;
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
return {
|
|
304
|
+
ready: false,
|
|
305
|
+
reason: "import_failed",
|
|
306
|
+
leadListId,
|
|
307
|
+
provider: provider ?? null,
|
|
308
|
+
attempts,
|
|
309
|
+
elapsedMs: Date.now() - start,
|
|
310
|
+
rowCount,
|
|
311
|
+
status: configStatus,
|
|
312
|
+
targetLeadCount,
|
|
313
|
+
error: configError,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
285
316
|
}
|
|
286
317
|
if (configStatus === "complete") {
|
|
287
318
|
importComplete = true;
|
package/dist/tools/registry.d.ts
CHANGED
|
@@ -6313,6 +6313,7 @@ export declare const allTools: ({
|
|
|
6313
6313
|
postUrl?: undefined;
|
|
6314
6314
|
sources?: undefined;
|
|
6315
6315
|
full?: undefined;
|
|
6316
|
+
fetchMinimal?: undefined;
|
|
6316
6317
|
companyUrl?: undefined;
|
|
6317
6318
|
sortBy?: undefined;
|
|
6318
6319
|
linkedin_url?: undefined;
|
|
@@ -6343,6 +6344,7 @@ export declare const allTools: ({
|
|
|
6343
6344
|
};
|
|
6344
6345
|
linkedinUrl?: undefined;
|
|
6345
6346
|
full?: undefined;
|
|
6347
|
+
fetchMinimal?: undefined;
|
|
6346
6348
|
companyUrl?: undefined;
|
|
6347
6349
|
sortBy?: undefined;
|
|
6348
6350
|
linkedin_url?: undefined;
|
|
@@ -6365,6 +6367,11 @@ export declare const allTools: ({
|
|
|
6365
6367
|
description: string;
|
|
6366
6368
|
default: boolean;
|
|
6367
6369
|
};
|
|
6370
|
+
fetchMinimal: {
|
|
6371
|
+
type: string;
|
|
6372
|
+
description: string;
|
|
6373
|
+
default: boolean;
|
|
6374
|
+
};
|
|
6368
6375
|
limit?: undefined;
|
|
6369
6376
|
postUrl?: undefined;
|
|
6370
6377
|
sources?: undefined;
|
|
@@ -6390,6 +6397,7 @@ export declare const allTools: ({
|
|
|
6390
6397
|
postUrl?: undefined;
|
|
6391
6398
|
sources?: undefined;
|
|
6392
6399
|
full?: undefined;
|
|
6400
|
+
fetchMinimal?: undefined;
|
|
6393
6401
|
sortBy?: undefined;
|
|
6394
6402
|
linkedin_url?: undefined;
|
|
6395
6403
|
max_posts?: undefined;
|
|
@@ -6421,6 +6429,7 @@ export declare const allTools: ({
|
|
|
6421
6429
|
postUrl?: undefined;
|
|
6422
6430
|
sources?: undefined;
|
|
6423
6431
|
full?: undefined;
|
|
6432
|
+
fetchMinimal?: undefined;
|
|
6424
6433
|
linkedin_url?: undefined;
|
|
6425
6434
|
max_posts?: undefined;
|
|
6426
6435
|
};
|
|
@@ -6446,6 +6455,7 @@ export declare const allTools: ({
|
|
|
6446
6455
|
postUrl?: undefined;
|
|
6447
6456
|
sources?: undefined;
|
|
6448
6457
|
full?: undefined;
|
|
6458
|
+
fetchMinimal?: undefined;
|
|
6449
6459
|
companyUrl?: undefined;
|
|
6450
6460
|
sortBy?: undefined;
|
|
6451
6461
|
};
|
|
@@ -6466,6 +6476,7 @@ export declare const allTools: ({
|
|
|
6466
6476
|
postUrl?: undefined;
|
|
6467
6477
|
sources?: undefined;
|
|
6468
6478
|
full?: undefined;
|
|
6479
|
+
fetchMinimal?: undefined;
|
|
6469
6480
|
companyUrl?: undefined;
|
|
6470
6481
|
sortBy?: undefined;
|
|
6471
6482
|
max_posts?: undefined;
|
package/package.json
CHANGED
|
@@ -111,6 +111,10 @@ Blueprint rules:
|
|
|
111
111
|
- Use stable, readable column ids such as `linkedin_url`, `enrich_prospect`, and
|
|
112
112
|
`score_icp_mcp`.
|
|
113
113
|
- Use registry type keys exactly, not display names.
|
|
114
|
+
- Treat the API column registry as the source of truth for type validation.
|
|
115
|
+
MCP code must not maintain its own hardcoded column-type allowlist; if the API
|
|
116
|
+
returns `phantom_type`, `deprecated_type`, or `not_createable_type`, fix the
|
|
117
|
+
blueprint to match the current registry response.
|
|
114
118
|
- Treat "Enrich Prospect" as a `http_request` preset, not its own type.
|
|
115
119
|
- Use `score_icp_mcp` for new AI ICP scoring. Do not use legacy
|
|
116
120
|
`score_icp` or the old `score_icp_rubric` alias; they are reserved for older
|
|
@@ -145,6 +149,28 @@ Blueprint rules:
|
|
|
145
149
|
synced sender connections, a positive `check_connection`, or accepted invite
|
|
146
150
|
proof from the current flow.
|
|
147
151
|
|
|
152
|
+
Config rules:
|
|
153
|
+
|
|
154
|
+
- Put native column settings in `config`. The commit path preserves full config
|
|
155
|
+
objects and validates them server-side before mutation.
|
|
156
|
+
- Use blueprint-level `inputMapping` for references to existing producer
|
|
157
|
+
columns. The commit path materializes those references into production
|
|
158
|
+
template mappings such as `{{<actual column id>}}` or
|
|
159
|
+
`{{<actual Enrich Prospect column id>.id}}`.
|
|
160
|
+
- Use `runCondition` for branch gates that should exist in production. Template
|
|
161
|
+
tokens in `config`, AI prompts, formula expressions, and `runCondition` are
|
|
162
|
+
dependency-bearing and must point to real producer columns.
|
|
163
|
+
- For outbound HTTP throttling, set `http_request.config.rateLimit` as either
|
|
164
|
+
`{ "mode": "window", "maxRequests": N, "windowSeconds": S }` or
|
|
165
|
+
`{ "mode": "concurrency", "maxConcurrent": N }`.
|
|
166
|
+
- For public webhook intake throttling, set
|
|
167
|
+
`inbound_webhook.config.rateLimit` as
|
|
168
|
+
`{ "maxRequests": N, "windowSeconds": S }`. Omit it to use the server default.
|
|
169
|
+
- Do not invent per-column `rateLimit` for LinkedIn action columns. Those are
|
|
170
|
+
governed by sender/account limits and scheduling windows; configure action
|
|
171
|
+
behavior with `actionConfig`, `waitForEvent`, `runCondition`, and producer
|
|
172
|
+
mappings.
|
|
173
|
+
|
|
148
174
|
## Commit phase
|
|
149
175
|
|
|
150
176
|
Use exactly one tool for the initial commit:
|
|
@@ -55,10 +55,11 @@ read/edit/delete safety, but they are not valid `add_column` or
|
|
|
55
55
|
|
|
56
56
|
- type: `datetime`
|
|
57
57
|
- displayName: `Date/Time`
|
|
58
|
+
- lifecycle: hidden active, load-only; not a valid create target.
|
|
58
59
|
- category: Lead Data
|
|
59
60
|
- inputs: none.
|
|
60
61
|
- outputs: ISO-like date/time value.
|
|
61
|
-
- when to use:
|
|
62
|
+
- when to use: Existing tables may contain system-owned scheduling timestamps.
|
|
62
63
|
- when NOT to use: Do not use to wait; use `wait` for sequence timing.
|
|
63
64
|
|
|
64
65
|
### Next Action
|
|
@@ -117,6 +118,8 @@ read/edit/delete safety, but they are not valid `add_column` or
|
|
|
117
118
|
- displayName: `Inbound Webhook`
|
|
118
119
|
- category: Lead Data
|
|
119
120
|
- inputs: none.
|
|
121
|
+
- config: optional `rateLimit` as `{ "maxRequests": N, "windowSeconds": S }`;
|
|
122
|
+
server defaults to 100 requests per 60 seconds when omitted.
|
|
120
123
|
- outputs: received JSON payload and webhook metadata.
|
|
121
124
|
- when to use: Use when rows are populated by external webhook events.
|
|
122
125
|
- when NOT to use: Do not use for polling APIs; use `http_request`.
|
|
@@ -163,6 +166,8 @@ read/edit/delete safety, but they are not valid `add_column` or
|
|
|
163
166
|
- displayName: `Check Open Profile`
|
|
164
167
|
- category: LinkedIn Actions
|
|
165
168
|
- inputs: required `linkedin_url`.
|
|
169
|
+
- config: may use the same `rateLimit` shape as `http_request` for provider
|
|
170
|
+
lookup throttling.
|
|
166
171
|
- outputs: boolean open-profile decision and lookup metadata.
|
|
167
172
|
- when to use: Use before selecting open-profile InMail behavior.
|
|
168
173
|
- when NOT to use: Do not use to check first-degree connection; use `check_connection`.
|
|
@@ -296,6 +301,11 @@ read/edit/delete safety, but they are not valid `add_column` or
|
|
|
296
301
|
|
|
297
302
|
## Data & Logic
|
|
298
303
|
|
|
304
|
+
LinkedIn action columns do not accept a generic per-column `rateLimit`. They
|
|
305
|
+
use sender/account daily limits, sending hours, and action scheduling outside
|
|
306
|
+
the blueprint column config. Use `actionConfig.waitForEvent`, `runCondition`,
|
|
307
|
+
and explicit producer mappings for action behavior.
|
|
308
|
+
|
|
299
309
|
### AI Column
|
|
300
310
|
|
|
301
311
|
- type: `ai_column`
|
|
@@ -332,6 +342,10 @@ read/edit/delete safety, but they are not valid `add_column` or
|
|
|
332
342
|
- displayName: `HTTP Request`
|
|
333
343
|
- category: Data & Logic
|
|
334
344
|
- inputs: required `method`, required `endpoint`, optional `headers`, optional `body`.
|
|
345
|
+
- config: optional `rateLimit` supports rolling windows
|
|
346
|
+
`{ "mode": "window", "maxRequests": N, "windowSeconds": S }` and concurrency
|
|
347
|
+
lanes `{ "mode": "concurrency", "maxConcurrent": N }`. Omit it to use the
|
|
348
|
+
server's default I/O concurrency lane.
|
|
335
349
|
- outputs: HTTP status, response body, and metadata; Enrich Prospect also returns root `id` as the EnrichedProspect id.
|
|
336
350
|
- when to use: Use for REST API calls, including the canonical "Enrich Prospect" preset at `/api/v4/enrich-prospect`.
|
|
337
351
|
- when NOT to use: Do not use when a first-party typed column already handles the action.
|