fullstackgtm 0.55.1 → 0.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/CONTRIBUTING.md +6 -0
- package/dist/cli/enrich.js +22 -4
- package/dist/cli/help.js +2 -2
- package/dist/cli/icp.js +9 -0
- package/dist/cli/signals.js +13 -0
- package/dist/connectors/clay.d.ts +2 -12
- package/dist/connectors/clay.js +2 -46
- package/dist/connectors/prospectSources.d.ts +8 -0
- package/dist/connectors/prospectSources.js +32 -0
- package/dist/contactProviders.js +1 -1
- package/dist/hostedArtifacts.d.ts +22 -0
- package/dist/hostedArtifacts.js +45 -0
- package/dist/icpDerive.d.ts +3 -20
- package/dist/icpDerive.js +8 -100
- package/dist/portable/clay.d.ts +13 -0
- package/dist/portable/clay.js +54 -0
- package/dist/portable/icpDeriveContract.d.ts +202 -0
- package/dist/portable/icpDeriveContract.js +105 -0
- package/package.json +17 -2
- package/skills/fullstackgtm/SKILL.md +6 -5
- package/src/cli/enrich.ts +21 -4
- package/src/cli/help.ts +2 -2
- package/src/cli/icp.ts +7 -0
- package/src/cli/signals.ts +18 -0
- package/src/connectors/clay.ts +7 -61
- package/src/connectors/prospectSources.ts +37 -0
- package/src/contactProviders.ts +1 -1
- package/src/hostedArtifacts.ts +57 -0
- package/src/icpDerive.ts +17 -104
- package/src/portable/clay.ts +71 -0
- package/src/portable/icpDeriveContract.ts +123 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,36 @@ The path to 1.0 is planned in [docs/roadmap-to-1.0.md](https://github.com/fullst
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.56.0] — 2026-07-13
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Portable ICP derivation, Clay normalization, filter-routing, and fit-scoring
|
|
15
|
+
package exports let browser and Worker surfaces execute the same contracts as
|
|
16
|
+
the CLI instead of maintaining behavioral copies.
|
|
17
|
+
- Human-paired CLIs now mirror reviewed ICP derivations and explicitly saved
|
|
18
|
+
signal runs into tenant-scoped hosted artifact records for later analysis;
|
|
19
|
+
local files remain authoritative and the mirror is not used as a cache.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- Published agent-skill guidance now documents Clay acquisition, targeted
|
|
24
|
+
company discovery, Exa-backed behavioral evidence, and hosted artifact
|
|
25
|
+
mirroring.
|
|
26
|
+
|
|
27
|
+
## [0.55.2] — 2026-07-13
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- The OSS repository now publishes a representative contributor test suite
|
|
32
|
+
covering CLI contracts, planning, configuration, HTTP safety, and security,
|
|
33
|
+
while hosted, live-integration, and adversarial release tests remain private.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- OSS mirror generation and `npm test` now fail when zero public tests are
|
|
38
|
+
discovered, preventing false-green contributor and release checks.
|
|
39
|
+
|
|
10
40
|
## [0.55.1] — 2026-07-13
|
|
11
41
|
|
|
12
42
|
### Fixed
|
package/CONTRIBUTING.md
CHANGED
|
@@ -64,6 +64,12 @@ Linux, macOS, and Windows; source-level tests remain on Node 22.6+.
|
|
|
64
64
|
> (a `pretest` guard) rather than silently passing with zero tests. On the
|
|
65
65
|
> published mirror, `tests/` is present and `npm test` works there.
|
|
66
66
|
|
|
67
|
+
The public mirror includes a representative suite covering supported CLI,
|
|
68
|
+
planning, configuration, HTTP, and security behavior. Additional hosted-product,
|
|
69
|
+
live-integration, and adversarial release tests remain in the private monorepo;
|
|
70
|
+
both suites gate releases. The public test selection is reviewed explicitly in
|
|
71
|
+
`oss/public-tests.txt`, and `npm test` fails if the mirror contains zero tests.
|
|
72
|
+
|
|
67
73
|
**Node:** the published runtime supports Node ≥ 20 (`engines`), but the test
|
|
68
74
|
runner needs Node **≥ 22.6** for `--experimental-strip-types`. Develop on 22.6+.
|
|
69
75
|
|
package/dist/cli/enrich.js
CHANGED
|
@@ -7,7 +7,7 @@ import { patchPlanToMarkdown } from "../format.js";
|
|
|
7
7
|
import { createFilePlanStore } from "../planStore.js";
|
|
8
8
|
import { buildAcquirePlan, buildEnrichPlan, createFileEnrichRunStore, DEFAULT_STALE_DAYS, ENRICH_CONFIG_FILE_NAME, builtinAcquirePreset, builtinEnrichPreset, enrichRunId, inferIngestObjectType, latestStamps, loadEnrichConfig, parseCsv, resolveCrmField, selectStaleWork, stagedSourceRecords, staleDaysFor } from "../enrich.js";
|
|
9
9
|
import { loadMeter, remaining } from "../acquireMeter.js";
|
|
10
|
-
import { crmContactKeys, fetchExploriumProspects, fetchPipe0CrustdataProspectPage, partitionFreshProspects, pipe0ResolveCompanyDomains, pipe0ResolveWorkEmails, prospectIdentityKeys } from "../connectors/prospectSources.js";
|
|
10
|
+
import { crmContactKeys, fetchExploriumProspects, fetchPipe0CrustdataProspectPage, partitionFreshProspects, pipe0ResolveCompanyDomains, pipe0ResolveProfileContacts, pipe0ResolveWorkEmails, prospectIdentityKeys } from "../connectors/prospectSources.js";
|
|
11
11
|
import { createClaySearch, discoverClayInvestmentProspects, runClayPeopleSearchPage } from "../connectors/clay.js";
|
|
12
12
|
import { runContactWaterfall } from "../contactProviders.js";
|
|
13
13
|
import { loadSeen, recordSeen } from "../acquireSeen.js";
|
|
@@ -44,7 +44,7 @@ enrich append [--source apollo] [--objects companies,contacts] [--save] [--conf
|
|
|
44
44
|
enrich refresh [--source apollo] [--stale-days <n>] [--save] [--config <path>]
|
|
45
45
|
[source options] [--run-label <label>] [--json]
|
|
46
46
|
enrich ingest <file.csv|payload.json|spool.jsonl|spool-dir> --source clay [--run-label <label>] [--objects companies|contacts] [--config <path>]
|
|
47
|
-
enrich acquire [--source <id>] [--max <new-leads>] [--scan-limit <candidates>] [--list <id>] [--assign-owner <id>] [--save] [--config <path>] [--json]
|
|
47
|
+
enrich acquire [--source <id>] [--max <new-leads>] [--scan-limit <candidates>] [--company-domain <domain>] [--list <id>] [--assign-owner <id>] [--save] [--config <path>] [--json]
|
|
48
48
|
enrich status [--runs] [--source <id>] [--config <path>] [--json]
|
|
49
49
|
|
|
50
50
|
acquire creates NET-NEW leads from a staged prospect list (ingest first):
|
|
@@ -515,8 +515,14 @@ async function acquireFromApi(config, source, rest, icp, snapshot, seen, priorRu
|
|
|
515
515
|
: disc.provider === "clay"
|
|
516
516
|
? (icp ? icpToClayPeopleFilters(icp) : (disc.filters ?? {}))
|
|
517
517
|
: {};
|
|
518
|
+
const targetCompanyDomain = option(rest, "--company-domain")?.trim().toLowerCase().replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/.*$/, "");
|
|
519
|
+
if (targetCompanyDomain) {
|
|
520
|
+
if (disc.provider !== "clay")
|
|
521
|
+
throw new Error("enrich acquire: --company-domain currently requires --source clay");
|
|
522
|
+
filters = { ...filters, company_identifier: [targetCompanyDomain] };
|
|
523
|
+
}
|
|
518
524
|
const queryFingerprint = createHash("sha256")
|
|
519
|
-
.update(JSON.stringify({ source, provider: disc.provider, listId, filters, icp: icp ?? null }))
|
|
525
|
+
.update(JSON.stringify({ source, provider: disc.provider, listId, filters, icp: icp ?? null, targetCompanyDomain: targetCompanyDomain ?? null }))
|
|
520
526
|
.digest("hex");
|
|
521
527
|
const checkpointKey = {
|
|
522
528
|
provider: disc.provider,
|
|
@@ -599,7 +605,7 @@ async function acquireFromApi(config, source, rest, icp, snapshot, seen, priorRu
|
|
|
599
605
|
if (!cursor) {
|
|
600
606
|
const route = clayRoutes[clayRouteIndex];
|
|
601
607
|
if (route)
|
|
602
|
-
filters = route.filters;
|
|
608
|
+
filters = { ...route.filters, ...(targetCompanyDomain ? { company_identifier: [targetCompanyDomain] } : {}) };
|
|
603
609
|
progress.note(`creating Clay people-search iterator · ${route?.id ?? "configured"}`);
|
|
604
610
|
cursor = await createClaySearch({ apiKey: providerKey("clay"), sourceType: "people", filters });
|
|
605
611
|
}
|
|
@@ -643,6 +649,8 @@ async function acquireFromApi(config, source, rest, icp, snapshot, seen, priorRu
|
|
|
643
649
|
}
|
|
644
650
|
discovered += page.length;
|
|
645
651
|
progress.items(discovered, scanLimit);
|
|
652
|
+
if (targetCompanyDomain)
|
|
653
|
+
page = page.filter((prospect) => normalizedCompanyDomain(prospect.companyDomain) === targetCompanyDomain);
|
|
646
654
|
if (page.length === 0) {
|
|
647
655
|
exhausted = true;
|
|
648
656
|
break;
|
|
@@ -716,6 +724,13 @@ async function acquireFromApi(config, source, rest, icp, snapshot, seen, priorRu
|
|
|
716
724
|
progress.note(`resolving work emails with pipe0 for ${resolved.length} candidate(s)`);
|
|
717
725
|
resolved = await pipe0ResolveWorkEmails({ apiKey: providerKey("pipe0"), prospects: resolved });
|
|
718
726
|
}
|
|
727
|
+
const profileFields = fields
|
|
728
|
+
.filter((field) => field === "work_email" || field === "mobile")
|
|
729
|
+
.filter((field) => resolved.some((prospect) => prospect.linkedin && (field === "mobile" ? !prospect.mobile : !prospect.email)));
|
|
730
|
+
if (profileFields.length) {
|
|
731
|
+
progress.note(`resolving LinkedIn contact details with pipe0 for ${resolved.length} candidate(s)`);
|
|
732
|
+
resolved = await pipe0ResolveProfileContacts({ apiKey: providerKey("pipe0"), prospects: resolved, fields: profileFields });
|
|
733
|
+
}
|
|
719
734
|
return resolved;
|
|
720
735
|
},
|
|
721
736
|
},
|
|
@@ -885,6 +900,9 @@ function printAcquireOutput(options) {
|
|
|
885
900
|
console.log("The meter is charged only when a create lands at apply.");
|
|
886
901
|
}
|
|
887
902
|
}
|
|
903
|
+
function normalizedCompanyDomain(value) {
|
|
904
|
+
return (value ?? "").trim().toLowerCase().replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/.*$/, "");
|
|
905
|
+
}
|
|
888
906
|
function hostedRunsUrl() {
|
|
889
907
|
const baseUrl = getCredential("broker")?.baseUrl?.replace(/\/+$/, "");
|
|
890
908
|
return baseUrl ? `${baseUrl}/dashboard/runs` : null;
|
package/dist/cli/help.js
CHANGED
|
@@ -630,7 +630,7 @@ export const COMMAND_FLAGS = {
|
|
|
630
630
|
dedupe: [...SOURCE_FLAGS, "--key", "--keep", "--reason", "--max-operations", "--save", "--dry-run", "--verbose", "--json", "--out"],
|
|
631
631
|
reassign: [...SOURCE_FLAGS, "--from", "--assign-unowned", "--to", "--objects", "--where", "--except-deal-stage", "--include-closed-deals", "--reason", "--max-operations", "--save", "--dry-run", "--verbose", "--json", "--out"],
|
|
632
632
|
backfill: [...SOURCE_FLAGS, "--since", "--pipeline", "--match-property", "--skip-unmatched", "--save", "--dry-run", "--verbose", "--json"],
|
|
633
|
-
enrich: [...SOURCE_FLAGS, "--config", "--source", "--icp", "--input", "--provider", "--list", "--stale-days", "--assign-owner", "--objects", "--max", "--scan-limit", "--staged-run", "--run-label", "--label", "--runs", "--save", "--dry-run", "--verbose", "--json", "--out"],
|
|
633
|
+
enrich: [...SOURCE_FLAGS, "--config", "--source", "--icp", "--input", "--provider", "--list", "--stale-days", "--assign-owner", "--objects", "--max", "--scan-limit", "--company-domain", "--staged-run", "--run-label", "--label", "--runs", "--save", "--dry-run", "--verbose", "--json", "--out"],
|
|
634
634
|
call: [...SOURCE_FLAGS, "--transcript", "--title", "--source", "--model", "--deterministic", "--heuristics", "--llm", "--verbose", "--json", "--ndjson", "--out", "--call", "--call-type", "--rubric", "--list", "--list-rubrics", "--attendees", "--domain", "--deal", "--save", "--dry-run"],
|
|
635
635
|
suggest: [...SOURCE_FLAGS, "--plan-id", "--plan", "--json", "--out"],
|
|
636
636
|
plans: ["--status", "--operations", "--value", "--values-from", "--min-confidence", "--include-creates", "--acknowledge-uncertain-writes", "--verbose", "--json"],
|
|
@@ -645,7 +645,7 @@ export const COMMAND_FLAGS = {
|
|
|
645
645
|
schedule: ["--cron", "--label", "--provider", "--trigger", "--timer", "--runs", "--verbose", "--json"],
|
|
646
646
|
};
|
|
647
647
|
export const FLAGS_WITH_VALUES = new Set([
|
|
648
|
-
"--account", "--account-id", "--accounts", "--acv", "--acv-basis", "--after", "--anchor", "--api-key", "--approve", "--archive", "--assign-owner", "--attendees", "--before", "--bucket", "--buyers-per-account", "--call", "--call-type", "--calls", "--capture-run", "--category", "--channel", "--client", "--client-id", "--client-secret", "--config", "--connector", "--connector-opt", "--contact", "--cron", "--cross-checks", "--deal", "--deal-period", "--diff", "--domain", "--email", "--except-deal-stage", "--format", "--from", "--from-judge", "--golden", "--guard", "--icp", "--in", "--input", "--instance-url", "--keep", "--key", "--keywords", "--label", "--list", "--login-url", "--match", "--match-property", "--max", "--max-accounts", "--max-claims", "--max-credits", "--max-examples", "--max-operations", "--max-results", "--max-searches", "--max-usd", "--min-accuracy", "--min-confidence", "--min-mentions", "--min-score", "--model", "--name", "--objects", "--operations", "--out", "--pipeline", "--plan", "--plan-id", "--policy", "--port", "--prepared-by", "--prior-run", "--profile", "--promote-lift", "--prompt", "--provider", "--reason", "--require", "--result", "--rubric", "--rule", "--rules", "--run", "--run-label", "--runs", "--scan-limit", "--scopes", "--seed", "--set", "--signals-from", "--since", "--snapshot", "--source", "--staged-run", "--stale-days", "--status", "--task-account", "--task-deal", "--timer", "--title", "--to", "--today", "--token", "--token-env", "--touch", "--transcript", "--trigger", "--usd-per-credit", "--value", "--values-from", "--vendor", "--via", "--watchlist", "--where",
|
|
648
|
+
"--account", "--account-id", "--accounts", "--acv", "--acv-basis", "--after", "--anchor", "--api-key", "--approve", "--archive", "--assign-owner", "--attendees", "--before", "--bucket", "--buyers-per-account", "--call", "--call-type", "--calls", "--capture-run", "--category", "--channel", "--client", "--client-id", "--client-secret", "--company-domain", "--config", "--connector", "--connector-opt", "--contact", "--cron", "--cross-checks", "--deal", "--deal-period", "--diff", "--domain", "--email", "--except-deal-stage", "--format", "--from", "--from-judge", "--golden", "--guard", "--icp", "--in", "--input", "--instance-url", "--keep", "--key", "--keywords", "--label", "--list", "--login-url", "--match", "--match-property", "--max", "--max-accounts", "--max-claims", "--max-credits", "--max-examples", "--max-operations", "--max-results", "--max-searches", "--max-usd", "--min-accuracy", "--min-confidence", "--min-mentions", "--min-score", "--model", "--name", "--objects", "--operations", "--out", "--pipeline", "--plan", "--plan-id", "--policy", "--port", "--prepared-by", "--prior-run", "--profile", "--promote-lift", "--prompt", "--provider", "--reason", "--require", "--result", "--rubric", "--rule", "--rules", "--run", "--run-label", "--runs", "--scan-limit", "--scopes", "--seed", "--set", "--signals-from", "--since", "--snapshot", "--source", "--staged-run", "--stale-days", "--status", "--task-account", "--task-deal", "--timer", "--title", "--to", "--today", "--token", "--token-env", "--touch", "--transcript", "--trigger", "--usd-per-credit", "--value", "--values-from", "--vendor", "--via", "--watchlist", "--where",
|
|
649
649
|
]);
|
|
650
650
|
// Lifecycle-grouped front door. One line per verb, organized by the
|
|
651
651
|
// Prevent→Detect→Remediate→Verify loop so a new user sees ~6 jobs, not 22 verbs.
|
package/dist/cli/icp.js
CHANGED
|
@@ -14,6 +14,7 @@ import { box, colorEnabled, paint, truncateToWidth } from "./ui.js";
|
|
|
14
14
|
import { getCredential, storeCredential } from "../credentials.js";
|
|
15
15
|
import { deriveWebsiteIcp, icpReviewSegments, OPENROUTER_API_BASE } from "../icpDerive.js";
|
|
16
16
|
import { unknownSubcommandError } from "./suggest.js";
|
|
17
|
+
import { writeHostedArtifact } from "../hostedArtifacts.js";
|
|
17
18
|
function renderJudgeDecisions(decisions) {
|
|
18
19
|
if (decisions.length === 0)
|
|
19
20
|
return "No accounts cleared the score threshold.";
|
|
@@ -242,6 +243,14 @@ ops. Develop one by interview, then \`enrich acquire\` picks up ./icp.json.
|
|
|
242
243
|
writeFileSync(path, `${JSON.stringify(derived.icp, null, 2)}\n`);
|
|
243
244
|
console.error(`Wrote reviewed ICP to ${path}. Next: fullstackgtm enrich acquire --source clay --icp ${path}`);
|
|
244
245
|
}
|
|
246
|
+
const mirrored = await writeHostedArtifact({
|
|
247
|
+
kind: "icp", key: `icp:${derived.company.domain}`, label: derived.icp.name,
|
|
248
|
+
domain: derived.company.domain, document: derived,
|
|
249
|
+
});
|
|
250
|
+
if (mirrored.status === "saved")
|
|
251
|
+
console.error("Mirrored the reviewed ICP to the paired hosted workspace.");
|
|
252
|
+
else if (mirrored.status === "unavailable")
|
|
253
|
+
console.error(`Warning: ${mirrored.reason}. The local ICP is still authoritative.`);
|
|
245
254
|
if (rest.includes("--json"))
|
|
246
255
|
console.log(JSON.stringify(derived, null, 2));
|
|
247
256
|
else if (!reviewedInteractively)
|
package/dist/cli/signals.js
CHANGED
|
@@ -10,6 +10,7 @@ import { isSpoolPath, readSpoolPath } from "../spoolFiles.js";
|
|
|
10
10
|
import { loadIcp, option, readSnapshot, repeatedOption, saveRequested } from "./shared.js";
|
|
11
11
|
import { createStatusLine } from "./ui.js";
|
|
12
12
|
import { unknownSubcommandError } from "./suggest.js";
|
|
13
|
+
import { writeHostedArtifact } from "../hostedArtifacts.js";
|
|
13
14
|
/**
|
|
14
15
|
* Resolve a signals config: explicit --config, else signals.config.json in cwd,
|
|
15
16
|
* else the zero-config DEFAULT_SIGNALS_CONFIG (preset-first, like enrich).
|
|
@@ -169,6 +170,7 @@ from the credential ladder, never argv; --connector-opt carries non-secret knobs
|
|
|
169
170
|
await store.appendRun({ id: signalRunId(runLabel), runLabel, startedAt: now.toISOString(), completedAt: new Date().toISOString(),
|
|
170
171
|
buckets: ["job"], counts: { fetched: discovered.summary.rawResults, new: ranked.length, deduped: deduped.length }, signals: ranked });
|
|
171
172
|
console.error(`Saved signal run "${runLabel}". Next: \`fullstackgtm icp judge --signals-from ${runLabel} --save\`.`);
|
|
173
|
+
await mirrorSignalRun(runLabel, now, ["job"], { fetched: discovered.summary.rawResults, new: ranked.length, deduped: deduped.length }, ranked);
|
|
172
174
|
}
|
|
173
175
|
else {
|
|
174
176
|
console.error("(not saved — re-run with --save to persist this evidence to the signal ledger)");
|
|
@@ -297,6 +299,7 @@ from the credential ladder, never argv; --connector-opt carries non-secret knobs
|
|
|
297
299
|
signals: ranked,
|
|
298
300
|
});
|
|
299
301
|
console.error(`Saved signal run "${runLabel}" (${fresh.length} fresh). Next: \`fullstackgtm icp judge --save\`.`);
|
|
302
|
+
await mirrorSignalRun(runLabel, now, buckets, { fetched, new: fresh.length, deduped: deduped.length }, ranked);
|
|
300
303
|
}
|
|
301
304
|
else {
|
|
302
305
|
console.error("(not saved — re-run with --save to persist this run to the signal ledger)");
|
|
@@ -385,6 +388,16 @@ from the credential ladder, never argv; --connector-opt carries non-secret knobs
|
|
|
385
388
|
}
|
|
386
389
|
throw unknownSubcommandError("signals", sub, ["fetch", "discover", "list", "outcome", "weights"]);
|
|
387
390
|
}
|
|
391
|
+
async function mirrorSignalRun(runLabel, startedAt, buckets, counts, signals) {
|
|
392
|
+
const mirrored = await writeHostedArtifact({
|
|
393
|
+
kind: "signal_run", key: `signals:${signalRunId(runLabel)}`, label: runLabel,
|
|
394
|
+
document: { runLabel, startedAt: startedAt.toISOString(), completedAt: new Date().toISOString(), buckets, counts, signals },
|
|
395
|
+
});
|
|
396
|
+
if (mirrored.status === "saved")
|
|
397
|
+
console.error("Mirrored the signal run to the paired hosted workspace.");
|
|
398
|
+
else if (mirrored.status === "unavailable")
|
|
399
|
+
console.error(`Warning: ${mirrored.reason}. The local signal ledger is still authoritative.`);
|
|
400
|
+
}
|
|
388
401
|
function positiveIntegerOption(args, flag, fallback) {
|
|
389
402
|
const raw = option(args, flag);
|
|
390
403
|
if (raw == null)
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Prospect } from "./prospectSources.ts";
|
|
2
|
+
import { type ClayCompany } from "../portable/clay.ts";
|
|
3
|
+
export { normalizeClayCompany, normalizeClayPerson, type ClayCompany } from "../portable/clay.ts";
|
|
2
4
|
export declare const CLAY_PUBLIC_API_BASE = "https://api.clay.com/public/v0";
|
|
3
5
|
export type ClayKeyValidation = {
|
|
4
6
|
ok: boolean;
|
|
@@ -10,16 +12,6 @@ export type ClayPeopleSearchPage = {
|
|
|
10
12
|
prospects: Prospect[];
|
|
11
13
|
hasMore: boolean;
|
|
12
14
|
};
|
|
13
|
-
export type ClayCompany = {
|
|
14
|
-
name?: string;
|
|
15
|
-
domain?: string;
|
|
16
|
-
linkedin?: string;
|
|
17
|
-
description?: string;
|
|
18
|
-
industry?: string;
|
|
19
|
-
size?: string;
|
|
20
|
-
location?: string;
|
|
21
|
-
fundingAmountRange?: string;
|
|
22
|
-
};
|
|
23
15
|
export type ClayCompanySearchPage = {
|
|
24
16
|
companies: ClayCompany[];
|
|
25
17
|
hasMore: boolean;
|
|
@@ -48,7 +40,6 @@ export declare function runClayCompanySearchPage(opts: {
|
|
|
48
40
|
fetchImpl?: typeof fetch;
|
|
49
41
|
apiBaseUrl?: string;
|
|
50
42
|
}): Promise<ClayCompanySearchPage>;
|
|
51
|
-
export declare function normalizeClayCompany(value: unknown): ClayCompany;
|
|
52
43
|
/** Account-first investment discovery: find thesis-shaped companies, then
|
|
53
44
|
* resolve founders/operators only inside those accounts. */
|
|
54
45
|
export declare function discoverClayInvestmentProspects(opts: {
|
|
@@ -64,7 +55,6 @@ export declare function discoverClayInvestmentProspects(opts: {
|
|
|
64
55
|
companiesScanned: number;
|
|
65
56
|
companiesMatched: ClayCompany[];
|
|
66
57
|
}>;
|
|
67
|
-
export declare function normalizeClayPerson(value: unknown): Prospect;
|
|
68
58
|
/** Validate a Clay Public API key without creating a search or spending enrichment credits. */
|
|
69
59
|
export declare function validateClayApiKey(apiKey: string, fetchImpl?: typeof fetch, apiBaseUrl?: string): Promise<ClayKeyValidation>;
|
|
70
60
|
/** Resolve Clay credentials using the standard env -> profile-scoped store ladder. */
|
package/dist/connectors/clay.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { getCredential } from "../credentials.js";
|
|
2
2
|
import { ProviderHttpError } from "../providerError.js";
|
|
3
|
+
import { normalizeClayCompany, normalizeClayPerson, } from "../portable/clay.js";
|
|
4
|
+
export { normalizeClayCompany, normalizeClayPerson } from "../portable/clay.js";
|
|
3
5
|
export const CLAY_PUBLIC_API_BASE = "https://api.clay.com/public/v0";
|
|
4
6
|
function clayHeaders(apiKey) {
|
|
5
7
|
return { "clay-api-key": apiKey, Accept: "application/json", "Content-Type": "application/json" };
|
|
@@ -59,15 +61,6 @@ export async function runClayCompanySearchPage(opts) {
|
|
|
59
61
|
throw new Error("Clay company search returned no has_more flag.");
|
|
60
62
|
return { companies: body.data.map(normalizeClayCompany), hasMore };
|
|
61
63
|
}
|
|
62
|
-
export function normalizeClayCompany(value) {
|
|
63
|
-
const row = value && typeof value === "object" ? value : {};
|
|
64
|
-
return {
|
|
65
|
-
name: stringValue(row.name), domain: bareDomain(stringValue(row.domain)),
|
|
66
|
-
linkedin: normalizeLinkedin(stringValue(row.linkedin_url)), description: stringValue(row.description),
|
|
67
|
-
industry: stringValue(row.industry), size: stringValue(row.size), location: stringValue(row.location),
|
|
68
|
-
fundingAmountRange: stringValue(row.total_funding_amount_range_usd),
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
64
|
/** Account-first investment discovery: find thesis-shaped companies, then
|
|
72
65
|
* resolve founders/operators only inside those accounts. */
|
|
73
66
|
export async function discoverClayInvestmentProspects(opts) {
|
|
@@ -99,43 +92,6 @@ export async function discoverClayInvestmentProspects(opts) {
|
|
|
99
92
|
}
|
|
100
93
|
return { prospects, companiesScanned: companyPage.companies.length, companiesMatched: companyPage.companies };
|
|
101
94
|
}
|
|
102
|
-
export function normalizeClayPerson(value) {
|
|
103
|
-
const row = value && typeof value === "object" ? value : {};
|
|
104
|
-
const location = row.structured_location && typeof row.structured_location === "object"
|
|
105
|
-
? row.structured_location
|
|
106
|
-
: {};
|
|
107
|
-
const fullName = stringValue(row.name);
|
|
108
|
-
return {
|
|
109
|
-
firstName: stringValue(row.first_name),
|
|
110
|
-
lastName: stringValue(row.last_name),
|
|
111
|
-
fullName,
|
|
112
|
-
jobTitle: stringValue(row.latest_experience_title),
|
|
113
|
-
companyName: stringValue(row.latest_experience_company),
|
|
114
|
-
companyDomain: bareDomain(stringValue(row.domain)),
|
|
115
|
-
linkedin: normalizeLinkedin(stringValue(row.url)),
|
|
116
|
-
headline: undefined,
|
|
117
|
-
sourceId: normalizeLinkedin(stringValue(row.url)) ?? fullName,
|
|
118
|
-
location: {
|
|
119
|
-
city: stringValue(location.city),
|
|
120
|
-
state: stringValue(location.state),
|
|
121
|
-
region: stringValue(location.region),
|
|
122
|
-
country: stringValue(location.country),
|
|
123
|
-
countryCode: stringValue(location.country_iso),
|
|
124
|
-
},
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
function stringValue(value) {
|
|
128
|
-
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
129
|
-
}
|
|
130
|
-
function bareDomain(value) {
|
|
131
|
-
return value?.replace(/^https?:\/\//i, "").replace(/^www\./i, "").replace(/\/.*$/, "").toLowerCase() || undefined;
|
|
132
|
-
}
|
|
133
|
-
function normalizeLinkedin(value) {
|
|
134
|
-
if (!value)
|
|
135
|
-
return undefined;
|
|
136
|
-
const normalized = value.startsWith("http") ? value : `https://${value.replace(/^\/+/, "")}`;
|
|
137
|
-
return normalized.replace(/\/$/, "");
|
|
138
|
-
}
|
|
139
95
|
/** Validate a Clay Public API key without creating a search or spending enrichment credits. */
|
|
140
96
|
export async function validateClayApiKey(apiKey, fetchImpl = fetch, apiBaseUrl = CLAY_PUBLIC_API_BASE) {
|
|
141
97
|
let response;
|
|
@@ -128,6 +128,14 @@ export declare function pipe0ResolveWorkEmails(opts: {
|
|
|
128
128
|
* at scale). */
|
|
129
129
|
concurrency?: number;
|
|
130
130
|
}): Promise<Prospect[]>;
|
|
131
|
+
/** Resolve work email and mobile from a known LinkedIn profile URL. */
|
|
132
|
+
export declare function pipe0ResolveProfileContacts(opts: {
|
|
133
|
+
apiKey: string;
|
|
134
|
+
prospects: Prospect[];
|
|
135
|
+
fields: Array<"work_email" | "mobile">;
|
|
136
|
+
apiBaseUrl?: string;
|
|
137
|
+
fetchImpl?: FetchImpl;
|
|
138
|
+
}): Promise<Prospect[]>;
|
|
131
139
|
/** Bare registrable host from a URL or domain string ("https://www.x.com/a" → "x.com"). */
|
|
132
140
|
export declare function hostFromUrl(url: string | undefined): string | undefined;
|
|
133
141
|
/**
|
|
@@ -245,6 +245,38 @@ export async function pipe0ResolveWorkEmails(opts) {
|
|
|
245
245
|
function personKey(name, company) {
|
|
246
246
|
return `${(name ?? "").trim().toLowerCase()}|${(company ?? "").trim().toLowerCase()}`;
|
|
247
247
|
}
|
|
248
|
+
/** Resolve work email and mobile from a known LinkedIn profile URL. */
|
|
249
|
+
export async function pipe0ResolveProfileContacts(opts) {
|
|
250
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
251
|
+
const base = (opts.apiBaseUrl ?? "https://api.pipe0.com").replace(/\/$/, "");
|
|
252
|
+
const resolvable = opts.prospects.filter((prospect) => prospect.linkedin);
|
|
253
|
+
if (!resolvable.length || !opts.fields.length)
|
|
254
|
+
return opts.prospects;
|
|
255
|
+
const pipes = [
|
|
256
|
+
...(opts.fields.includes("work_email") ? [{ pipe_id: "person:workemail:profileurl:waterfall@1" }] : []),
|
|
257
|
+
...(opts.fields.includes("mobile") ? [{ pipe_id: "person:mobile:profileurl:waterfall@1" }] : []),
|
|
258
|
+
];
|
|
259
|
+
const body = await pipe0Post(fetchImpl, base, opts.apiKey, {
|
|
260
|
+
pipes,
|
|
261
|
+
input: resolvable.map((prospect) => ({ profile_url: prospect.linkedin })),
|
|
262
|
+
});
|
|
263
|
+
const details = new Map();
|
|
264
|
+
for (const recordId of body?.order ?? []) {
|
|
265
|
+
const fields = body?.records?.[recordId]?.fields ?? {};
|
|
266
|
+
const profile = fieldValue(fields.profile_url)?.trim().toLowerCase().replace(/\/+$/, "");
|
|
267
|
+
if (!profile)
|
|
268
|
+
continue;
|
|
269
|
+
details.set(profile, {
|
|
270
|
+
email: fields.work_email?.status === "completed" ? fieldValue(fields.work_email) : undefined,
|
|
271
|
+
mobile: fields.mobile?.status === "completed" ? fieldValue(fields.mobile) : undefined,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return opts.prospects.map((prospect) => {
|
|
275
|
+
const key = prospect.linkedin?.trim().toLowerCase().replace(/\/+$/, "");
|
|
276
|
+
const match = key ? details.get(key) : undefined;
|
|
277
|
+
return match ? { ...prospect, email: prospect.email ?? match.email, mobile: prospect.mobile ?? match.mobile } : prospect;
|
|
278
|
+
});
|
|
279
|
+
}
|
|
248
280
|
/** Bare registrable host from a URL or domain string ("https://www.x.com/a" → "x.com"). */
|
|
249
281
|
export function hostFromUrl(url) {
|
|
250
282
|
if (!url || !url.trim())
|
package/dist/contactProviders.js
CHANGED
|
@@ -4,7 +4,7 @@ export const CONTACT_PROVIDER_CAPABILITIES = {
|
|
|
4
4
|
provider: "pipe0",
|
|
5
5
|
operation: "person:workemail:waterfall@1",
|
|
6
6
|
inputShapes: ["name_domain"],
|
|
7
|
-
outputFields: ["work_email"],
|
|
7
|
+
outputFields: ["work_email", "mobile"],
|
|
8
8
|
execution: "batch",
|
|
9
9
|
billing: "per_attempt",
|
|
10
10
|
chargesOn: ["request"],
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type HostedArtifact = {
|
|
2
|
+
kind: "icp" | "signal_run";
|
|
3
|
+
key: string;
|
|
4
|
+
label: string;
|
|
5
|
+
domain?: string;
|
|
6
|
+
document: unknown;
|
|
7
|
+
sourceVersion?: string;
|
|
8
|
+
};
|
|
9
|
+
export type HostedArtifactResult = {
|
|
10
|
+
status: "unpaired";
|
|
11
|
+
} | {
|
|
12
|
+
status: "saved";
|
|
13
|
+
created: boolean;
|
|
14
|
+
updatedAt: number;
|
|
15
|
+
} | {
|
|
16
|
+
status: "unavailable";
|
|
17
|
+
reason: string;
|
|
18
|
+
};
|
|
19
|
+
export declare function writeHostedArtifact(artifact: HostedArtifact, options?: {
|
|
20
|
+
fetchImpl?: typeof fetch;
|
|
21
|
+
timeoutMs?: number;
|
|
22
|
+
}): Promise<HostedArtifactResult>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Best-effort artifact mirroring for a human-paired CLI profile. */
|
|
2
|
+
import { getCredential } from "./credentials.js";
|
|
3
|
+
const ENDPOINT = "/api/cli/artifact";
|
|
4
|
+
const DEFAULT_TIMEOUT_MS = 4000;
|
|
5
|
+
function broker() {
|
|
6
|
+
const credential = getCredential("broker");
|
|
7
|
+
if (!credential?.baseUrl || !credential.accessToken)
|
|
8
|
+
return null;
|
|
9
|
+
try {
|
|
10
|
+
const url = new URL(credential.baseUrl);
|
|
11
|
+
const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
12
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && local))
|
|
13
|
+
return null;
|
|
14
|
+
return { baseUrl: url.href.replace(/\/+$/, ""), accessToken: credential.accessToken };
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export async function writeHostedArtifact(artifact, options = {}) {
|
|
21
|
+
if (!artifact.key || artifact.key.length > 256 || !artifact.label || artifact.label.length > 200) {
|
|
22
|
+
throw new Error("Hosted artifact key/label is invalid.");
|
|
23
|
+
}
|
|
24
|
+
const paired = broker();
|
|
25
|
+
if (!paired)
|
|
26
|
+
return { status: "unpaired" };
|
|
27
|
+
try {
|
|
28
|
+
const response = await (options.fetchImpl ?? fetch)(`${paired.baseUrl}${ENDPOINT}`, {
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers: { Authorization: `Bearer ${paired.accessToken}`, "Content-Type": "application/json" },
|
|
31
|
+
body: JSON.stringify(artifact),
|
|
32
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS),
|
|
33
|
+
});
|
|
34
|
+
if (!response.ok)
|
|
35
|
+
return { status: "unavailable", reason: `hosted artifact request failed (HTTP ${response.status})` };
|
|
36
|
+
const body = await response.json();
|
|
37
|
+
if (typeof body.created !== "boolean" || typeof body.updatedAt !== "number") {
|
|
38
|
+
return { status: "unavailable", reason: "hosted artifact response was invalid" };
|
|
39
|
+
}
|
|
40
|
+
return { status: "saved", created: body.created, updatedAt: body.updatedAt };
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return { status: "unavailable", reason: "hosted artifact request was unavailable" };
|
|
44
|
+
}
|
|
45
|
+
}
|
package/dist/icpDerive.d.ts
CHANGED
|
@@ -1,26 +1,9 @@
|
|
|
1
1
|
import { type LlmCallOptions } from "./llm.ts";
|
|
2
|
-
import {
|
|
2
|
+
import type { Icp } from "./icp.ts";
|
|
3
|
+
import { type WebsiteIcpDerivation } from "./portable/icpDeriveContract.ts";
|
|
4
|
+
export type { WebsiteIcpDerivation, WebsiteIcpEvidence } from "./portable/icpDeriveContract.ts";
|
|
3
5
|
export declare const DEFAULT_ICP_DERIVATION_MODEL = "z-ai/glm-5.2";
|
|
4
6
|
export declare const OPENROUTER_API_BASE = "https://openrouter.ai/api";
|
|
5
|
-
export type WebsiteIcpEvidence = {
|
|
6
|
-
label: string;
|
|
7
|
-
excerpt: string;
|
|
8
|
-
sourceUrl: string;
|
|
9
|
-
};
|
|
10
|
-
export type WebsiteIcpDerivation = {
|
|
11
|
-
company: {
|
|
12
|
-
name: string;
|
|
13
|
-
domain: string;
|
|
14
|
-
summary: string;
|
|
15
|
-
};
|
|
16
|
-
icp: Icp;
|
|
17
|
-
evidence: WebsiteIcpEvidence[];
|
|
18
|
-
confidence: number;
|
|
19
|
-
derivation: {
|
|
20
|
-
mode: "model";
|
|
21
|
-
model: string;
|
|
22
|
-
};
|
|
23
|
-
};
|
|
24
7
|
export type IcpDerivationProgress = {
|
|
25
8
|
stage: "fetch" | "model" | "verify";
|
|
26
9
|
message: string;
|