fullstackgtm 0.53.1 → 0.55.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 +42 -0
- package/DATA-FLOWS.md +1 -0
- package/README.md +3 -1
- package/dist/cli/auth.js +4 -3
- package/dist/cli/enrich.js +44 -27
- package/dist/cli/help.js +10 -7
- package/dist/cli/icp.js +4 -0
- package/dist/cli/signals.js +77 -4
- package/dist/cli/ui.js +3 -3
- package/dist/connectors/clay.d.ts +38 -0
- package/dist/connectors/clay.js +58 -0
- package/dist/icp.d.ts +33 -0
- package/dist/icp.js +71 -7
- package/dist/icpDerive.js +50 -4
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/schedule.js +3 -3
- package/dist/signalDiscovery.d.ts +52 -0
- package/dist/signalDiscovery.js +235 -0
- package/dist/signals.d.ts +4 -0
- package/docs/api.md +2 -2
- package/llms.txt +5 -2
- package/package.json +1 -1
- package/skills/fullstackgtm/SKILL.md +1 -1
- package/src/cli/auth.ts +4 -3
- package/src/cli/enrich.ts +23 -7
- package/src/cli/help.ts +10 -7
- package/src/cli/icp.ts +4 -0
- package/src/cli/signals.ts +72 -4
- package/src/cli/ui.ts +4 -4
- package/src/connectors/clay.ts +81 -0
- package/src/icp.ts +98 -8
- package/src/icpDerive.ts +48 -4
- package/src/index.ts +16 -0
- package/src/schedule.ts +3 -3
- package/src/signalDiscovery.ts +298 -0
- package/src/signals.ts +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,48 @@ 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.55.0] — 2026-07-13
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `signals discover` turns editable ICP behavioral-trigger hypotheses into
|
|
15
|
+
evidence-gated public ATS searches through Exa, resolves employers to
|
|
16
|
+
official domains, and emits verbatim-quoted signals for `icp judge`.
|
|
17
|
+
- ICP derivation now proposes structured trigger hypotheses with positive
|
|
18
|
+
evidence, active projects, buyer functions, negative evidence, and preferred
|
|
19
|
+
public source classes; existing `intentTopics` ICPs remain compatible.
|
|
20
|
+
- Exa joins the credential ladder through `EXA_API_KEY`,
|
|
21
|
+
`FSGTM_EXA_API_KEY`, or `fullstackgtm login exa`.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Signal discovery exposes explicit account, result, search, and reported-cost
|
|
26
|
+
limits, structured JSON output, and remains read-only unless `--save` is used
|
|
27
|
+
to persist the local signal ledger.
|
|
28
|
+
- `signals discover` is schedulable alongside `signals fetch`; unattended runs
|
|
29
|
+
still cannot emit or apply CRM patch plans.
|
|
30
|
+
|
|
31
|
+
## [0.54.0] — 2026-07-12
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- Website-derived ICPs classify investment firms as an explicit `investment`
|
|
36
|
+
motion with editable target stages, target-company funding bands, thesis
|
|
37
|
+
keywords, and founder personas.
|
|
38
|
+
- Clay investment acquisition searches thesis-shaped companies first, then
|
|
39
|
+
resolves founders across the matched accounts in one batched people search.
|
|
40
|
+
- The public Clay connector exports normalized company-search pages and the
|
|
41
|
+
shared account-first investment discovery primitive.
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- Investment discovery validates that resolved people currently work at the
|
|
46
|
+
thesis-qualified account, preventing past-employer matches from becoming
|
|
47
|
+
proposed leads.
|
|
48
|
+
- ICP derivation distinguishes fund size, AUM, and check size from a target
|
|
49
|
+
company's prior funding, and maps earliest-stage language conservatively.
|
|
50
|
+
- Clay company-size filters use the provider's lower-bound bucket vocabulary.
|
|
51
|
+
|
|
10
52
|
## [0.53.1] — 2026-07-11
|
|
11
53
|
|
|
12
54
|
### Changed
|
package/DATA-FLOWS.md
CHANGED
|
@@ -24,6 +24,7 @@ is no fullstackgtm-operated server in the data path for the open package.**
|
|
|
24
24
|
| `snapshot`, `audit`, `apply`, `resolve`, `bulk-update`, `dedupe`, `reassign`, `fix`, `enrich` (writeback) | **Your CRM** (HubSpot / Salesforce / Stripe API) | Reads: your CRM records. Writes: only approved patch operations. | Your CRM token (env / stored / broker) |
|
|
25
25
|
| `call parse`, `call score`, `market classify`, `market refresh` | **Your LLM provider** (api.anthropic.com or api.openai.com) | The call transcript / captured competitor page text you point at, plus the extraction prompt | Your `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` (BYO) |
|
|
26
26
|
| `enrich append --source apollo`, `enrich refresh` | **Apollo** (api.apollo.io) | The company domain / contact email being enriched | Your `APOLLO_API_KEY` (BYO) |
|
|
27
|
+
| `signals discover --source exa` | **Exa** (api.exa.ai) | Behavioral trigger labels/evidence terms from the selected ICP; employer names from matching public ATS results are sent in follow-up searches to resolve official domains. No CRM snapshot or credentials other than the Exa key. | Your `EXA_API_KEY` / stored `login exa` key (BYO) |
|
|
27
28
|
| `market capture`, `market refresh` | **Public vendor websites** you list in `market.config.json` | A bounded HTTP GET; DNS answers are validated and pinned, mixed/private/reserved answers are refused, and every redirect is revalidated | none |
|
|
28
29
|
| `login --via <url>` (optional) | **Your hosted deployment's broker** | A pairing handshake; the broker mints short-lived CRM tokens | broker pairing token |
|
|
29
30
|
| paired `enrich acquire` checkpoint sync (optional) | **Your hosted deployment** | Opaque query fingerprint plus provider cursor/list offset, exhaustion flag, and CAS revision. No prospect records, emails, names, or provider credentials. Scoped to the broker token's organization. | broker pairing token |
|
package/README.md
CHANGED
|
@@ -275,6 +275,8 @@ Cleaning and filling the CRM tells you *who* to reach; it never tells you *when*
|
|
|
275
275
|
fullstackgtm signals fetch --bucket job --source greenhouse,lever,ashby --keywords "revops,growth" --save
|
|
276
276
|
fullstackgtm signals fetch --connector serpapi-news,hubspot-forms --save # news + first-party form demand
|
|
277
277
|
fullstackgtm signals fetch --connector file --save # webhook landing zone (see docs/signal-spool-format.md)
|
|
278
|
+
echo "$EXA_API_KEY" | fullstackgtm login exa # once; or keep the key in env
|
|
279
|
+
fullstackgtm signals discover --icp ./icp.json --source exa --since 30d --max-searches 12 --max-usd 0.25 --save
|
|
278
280
|
fullstackgtm signals list --since 7d # ranked triggers, each with a verbatim source quote
|
|
279
281
|
|
|
280
282
|
# 2. Decide who's worth a touch — and who isn't. Scores timing × fit × memory into send/nurture/skip.
|
|
@@ -313,7 +315,7 @@ fullstackgtm schedule status --runs 5 # last runs, exit codes, artifa
|
|
|
313
315
|
fullstackgtm schedule uninstall # remove the managed block, touch nothing else
|
|
314
316
|
```
|
|
315
317
|
|
|
316
|
-
**Scheduling never auto-approves.** Schedulable commands are read/plan-side only — `audit`, `snapshot`, `enrich append|refresh`, `enrich acquire --save`, `market capture|refresh`, `signals fetch`, `icp judge`, `icp eval`, `draft`, `suggest`, `report`, `doctor` — so unattended runs accumulate *proposals* (plans in the queue, run records, reports), never CRM writes. `apply` is schedulable only as `apply --plan-id <id>`, and every firing re-checks the plan's status is approved: an unapproved plan records a `plan_not_approved` no-op run instead of executing, and no flag relaxes this. Arbitrary shell is not schedulable — an entry's argv must resolve to a known fullstackgtm command (validated at `add` time and re-checked at run time), and the timer line you audit — crontab entry or LaunchAgent `ProgramArguments` — is always `fullstackgtm schedule run <id>` and nothing else.
|
|
318
|
+
**Scheduling never auto-approves.** Schedulable commands are read/plan-side only — `audit`, `snapshot`, `enrich append|refresh`, `enrich acquire --save`, `market capture|refresh`, `signals fetch|discover`, `icp judge`, `icp eval`, `draft`, `suggest`, `report`, `doctor` — so unattended runs accumulate *proposals* (plans in the queue, run records, reports), never CRM writes. `apply` is schedulable only as `apply --plan-id <id>`, and every firing re-checks the plan's status is approved: an unapproved plan records a `plan_not_approved` no-op run instead of executing, and no flag relaxes this. Arbitrary shell is not schedulable — an entry's argv must resolve to a known fullstackgtm command (validated at `add` time and re-checked at run time), and the timer line you audit — crontab entry or LaunchAgent `ProgramArguments` — is always `fullstackgtm schedule run <id>` and nothing else.
|
|
317
319
|
|
|
318
320
|
`install` materializes enabled entries into the system timer; `--timer` defaults by platform. On macOS it writes one LaunchAgent plist per entry (`com.fullstackgtm.<profile>.<id>` in `~/Library/LaunchAgents`, loaded via `launchctl bootstrap`) — macOS gates `crontab` writes behind Full Disk Access, a permission no program can request, while LaunchAgents need none; launchd also coalesces firings missed during sleep into one run on wake. Re-install replaces the profile's plist fleet wholesale and never touches foreign plists. Elsewhere (or with `--timer crontab`) it renders a sentinel-delimited block (`# >>> fullstackgtm <profile> >>>` … `# <<< fullstackgtm <profile> <<<`) in your user crontab; re-install replaces the block wholesale and never touches lines outside it. Honest limitation: cron has no catch-up — a laptop asleep at firing time means a missed run. `schedule status` surfaces missed firings by comparing expected-vs-actual run history, so the gap is at least visible. Entries are provider-agnostic; cloud providers (Modal, AWS) arrive as scaffold generators that call the same `schedule run <id>` contract, and are refused as "not yet implemented" until then.
|
|
319
321
|
|
package/dist/cli/auth.js
CHANGED
|
@@ -376,7 +376,7 @@ export async function login(args) {
|
|
|
376
376
|
console.log(`Stored Clay Public API key in ${credentialsPath()}. Clay connector commands resolve it automatically.`);
|
|
377
377
|
return;
|
|
378
378
|
}
|
|
379
|
-
if (provider === "pipe0" || provider === "explorium" || provider === "heyreach" || provider === "theirstack") {
|
|
379
|
+
if (provider === "pipe0" || provider === "explorium" || provider === "heyreach" || provider === "theirstack" || provider === "exa") {
|
|
380
380
|
rejectArgvSecret(args, "--token", "--key", "--api-key");
|
|
381
381
|
const key = await readSecret(`${provider} API key`);
|
|
382
382
|
if (!key)
|
|
@@ -385,12 +385,13 @@ export async function login(args) {
|
|
|
385
385
|
// is stored as-is and validated on the first pull.
|
|
386
386
|
const stamp = new Date().toISOString();
|
|
387
387
|
storeCredential(provider, { kind: "api_key", accessToken: key, createdAt: stamp, updatedAt: stamp });
|
|
388
|
-
const usedBy = provider === "theirstack" ? "`fullstackgtm tam estimate --source theirstack`" :
|
|
388
|
+
const usedBy = provider === "theirstack" ? "`fullstackgtm tam estimate --source theirstack`" :
|
|
389
|
+
provider === "exa" ? "`fullstackgtm signals discover`" : "`fullstackgtm enrich acquire`";
|
|
389
390
|
console.log(`Stored ${provider} API key in ${credentialsPath()}. ${usedBy} uses it automatically (validated on first pull).`);
|
|
390
391
|
return;
|
|
391
392
|
}
|
|
392
393
|
if (provider !== "hubspot") {
|
|
393
|
-
throw new Error("login supports: hubspot, salesforce, stripe, anthropic, openai, openrouter, apollo, clay, pipe0, explorium, heyreach, theirstack, or --via <hosted url>. Usage: fullstackgtm login <provider> | fullstackgtm login --via https://gtm.example.com");
|
|
394
|
+
throw new Error("login supports: hubspot, salesforce, stripe, anthropic, openai, openrouter, apollo, clay, pipe0, explorium, heyreach, theirstack, exa, or --via <hosted url>. Usage: fullstackgtm login <provider> | fullstackgtm login --via https://gtm.example.com");
|
|
394
395
|
}
|
|
395
396
|
const now = new Date().toISOString();
|
|
396
397
|
rejectArgvSecret(args, "--token");
|
package/dist/cli/enrich.js
CHANGED
|
@@ -8,7 +8,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
10
|
import { crmContactKeys, fetchExploriumProspects, fetchPipe0CrustdataProspectPage, partitionFreshProspects, pipe0ResolveCompanyDomains, pipe0ResolveWorkEmails, prospectIdentityKeys } from "../connectors/prospectSources.js";
|
|
11
|
-
import { createClaySearch, runClayPeopleSearchPage } from "../connectors/clay.js";
|
|
11
|
+
import { createClaySearch, discoverClayInvestmentProspects, runClayPeopleSearchPage } from "../connectors/clay.js";
|
|
12
12
|
import { runContactWaterfall } from "../contactProviders.js";
|
|
13
13
|
import { loadSeen, recordSeen } from "../acquireSeen.js";
|
|
14
14
|
import { acquireCheckpointId, createFileAcquireCheckpointStore } from "../acquireCheckpoint.js";
|
|
@@ -17,13 +17,13 @@ import { uploadHostedPatchPlan } from "../hostedPatchPlan.js";
|
|
|
17
17
|
import { progressReporter, reportCounts, reportEvent } from "../runReport.js";
|
|
18
18
|
import { ACQUIRE_STAGES, composeListeners, createProgressEmitter } from "../progress.js";
|
|
19
19
|
import { createLinkedInProvider, discoverLinkedInProspects } from "../acquireLinkedIn.js";
|
|
20
|
-
import { clayPeopleFilterRoutes, fitThreshold, icpToClayPeopleFilters, icpToCrustdataFilters, icpToExploriumFilters, scoreProspectAgainstIcp } from "../icp.js";
|
|
20
|
+
import { clayPeopleFilterRoutes, fitThreshold, icpToClayInvestmentCompanyFilters, icpToClayPeopleFilters, icpToCrustdataFilters, icpToExploriumFilters, scoreProspectAgainstIcp } from "../icp.js";
|
|
21
21
|
import { apolloPullKeysForAppend, apolloPullKeysForRefresh, createApolloClient, pullApolloRecords } from "../enrichApollo.js";
|
|
22
22
|
import { isSpoolPath, readSpoolPath } from "../spoolFiles.js";
|
|
23
23
|
import { isOptionValue, loadIcp, numericOption, option, readSnapshot, saveRequested } from "./shared.js";
|
|
24
24
|
import { providerKey } from "./tam.js";
|
|
25
25
|
import { unknownSubcommandError } from "./suggest.js";
|
|
26
|
-
import { box, colorEnabled, createProgressRenderer, createStatusLine, formatBar, formatDuration, paint, truncateToWidth } from "./ui.js";
|
|
26
|
+
import { box, colorEnabled, createProgressRenderer, createStatusLine, formatBar, formatDuration, paint, scoreColor, truncateToWidth } from "./ui.js";
|
|
27
27
|
import { compactPlan, verbosePlanRequested } from "./planOutput.js";
|
|
28
28
|
/**
|
|
29
29
|
* The enrich layer: governed append/refresh of third-party data (Apollo pull,
|
|
@@ -581,27 +581,42 @@ async function acquireFromApi(config, source, rest, icp, snapshot, seen, priorRu
|
|
|
581
581
|
exhausted = result.nextCursor === null;
|
|
582
582
|
}
|
|
583
583
|
else if (disc.provider === "clay") {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
}
|
|
592
|
-
const result = await runClayPeopleSearchPage({ apiKey: providerKey("clay"), searchId: cursor, limit: requestSize });
|
|
584
|
+
if (icp?.motion === "investment") {
|
|
585
|
+
const companyFilters = icpToClayInvestmentCompanyFilters(icp);
|
|
586
|
+
progress.note("searching Clay companies against the investment thesis");
|
|
587
|
+
const result = await discoverClayInvestmentProspects({
|
|
588
|
+
apiKey: providerKey("clay"), companyFilters,
|
|
589
|
+
titleKeywords: icp.persona.titleKeywords ?? ["Founder", "Co-founder", "CEO", "CTO"],
|
|
590
|
+
companyLimit: Math.min(scanLimit - discovered, 25), prospectLimit: requestSize,
|
|
591
|
+
});
|
|
593
592
|
page = result.prospects;
|
|
594
|
-
|
|
595
|
-
exhausted =
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
593
|
+
progress.note(`${result.companiesScanned} target account(s) scanned · resolving founders`);
|
|
594
|
+
exhausted = true;
|
|
595
|
+
cursor = "investment-account-first";
|
|
596
|
+
}
|
|
597
|
+
else {
|
|
598
|
+
while (true) {
|
|
599
|
+
if (!cursor) {
|
|
600
|
+
const route = clayRoutes[clayRouteIndex];
|
|
601
|
+
if (route)
|
|
602
|
+
filters = route.filters;
|
|
603
|
+
progress.note(`creating Clay people-search iterator · ${route?.id ?? "configured"}`);
|
|
604
|
+
cursor = await createClaySearch({ apiKey: providerKey("clay"), sourceType: "people", filters });
|
|
605
|
+
}
|
|
606
|
+
const result = await runClayPeopleSearchPage({ apiKey: providerKey("clay"), searchId: cursor, limit: requestSize });
|
|
607
|
+
page = result.prospects;
|
|
608
|
+
offset += page.length;
|
|
609
|
+
exhausted = !result.hasMore;
|
|
610
|
+
if (page.length > 0 || !allowClayFallback || clayRouteIndex >= clayRoutes.length - 1)
|
|
611
|
+
break;
|
|
612
|
+
const failed = clayRoutes[clayRouteIndex]?.id ?? "configured";
|
|
613
|
+
clayRouteIndex += 1;
|
|
614
|
+
const next = clayRoutes[clayRouteIndex];
|
|
615
|
+
progress.note(`Clay ${failed} route returned 0 · trying ${next.id}`);
|
|
616
|
+
cursor = null;
|
|
617
|
+
offset = 0;
|
|
618
|
+
exhausted = false;
|
|
619
|
+
}
|
|
605
620
|
}
|
|
606
621
|
}
|
|
607
622
|
else if (disc.provider === "explorium") {
|
|
@@ -815,12 +830,14 @@ function renderAcquireLeadCards(result) {
|
|
|
815
830
|
fit = Number(JSON.parse(item?.text ?? "{}").fitScore);
|
|
816
831
|
}
|
|
817
832
|
catch { /* malformed evidence remains display-only */ }
|
|
833
|
+
const fitPercent = Number.isFinite(fit) ? Math.round((fit ?? 0) * 100) : undefined;
|
|
818
834
|
const lines = [
|
|
819
|
-
truncateToWidth(name, 84),
|
|
820
|
-
truncateToWidth(`${title} · ${company}`, 84),
|
|
821
|
-
truncateToWidth([payload.associateCompanyDomain, props.hs_linkedin_url].filter(Boolean).join(" · "), 84),
|
|
835
|
+
p.bold(truncateToWidth(name, 84)),
|
|
836
|
+
p.cyan(truncateToWidth(`${title} · ${company}`, 84)),
|
|
837
|
+
p.blue(truncateToWidth([payload.associateCompanyDomain, props.hs_linkedin_url].filter(Boolean).join(" · "), 84)),
|
|
822
838
|
];
|
|
823
|
-
|
|
839
|
+
const fitLabel = fitPercent === undefined ? "" : ` · ${scoreColor(fitPercent, p)}% fit`;
|
|
840
|
+
return box(lines, p, `Lead ${index + 1}/${result.plan.operations.length}${fitLabel}`).join("\n");
|
|
824
841
|
}).join("\n");
|
|
825
842
|
}
|
|
826
843
|
function printAcquireOutput(options) {
|
package/dist/cli/help.js
CHANGED
|
@@ -17,9 +17,9 @@ Usage:
|
|
|
17
17
|
fullstackgtm login stripe [--no-validate]
|
|
18
18
|
fullstackgtm login anthropic | openai store an LLM API key for call parse/score
|
|
19
19
|
fullstackgtm login apollo | clay store a data-provider Public API key
|
|
20
|
-
fullstackgtm login pipe0 | explorium | theirstack store a discovery-provider key
|
|
20
|
+
fullstackgtm login pipe0 | explorium | theirstack | exa store a discovery-provider key
|
|
21
21
|
fullstackgtm login heyreach store a HeyReach key for enrich acquire --source linkedin
|
|
22
|
-
fullstackgtm logout <hubspot|salesforce|stripe|anthropic|openai|apollo|clay|pipe0|explorium|heyreach|broker>
|
|
22
|
+
fullstackgtm logout <hubspot|salesforce|stripe|anthropic|openai|apollo|clay|pipe0|explorium|theirstack|exa|heyreach|broker>
|
|
23
23
|
|
|
24
24
|
Secrets (tokens, client secrets) are NEVER passed as flags — they leak via
|
|
25
25
|
the process list and shell history. Pipe them on stdin or enter them at the
|
|
@@ -101,10 +101,12 @@ Usage:
|
|
|
101
101
|
in the same plan (freemail domains never used;
|
|
102
102
|
--skip-unmatched = report-only). HubSpot-only for now.
|
|
103
103
|
fullstackgtm signals fetch [--bucket job,…] [--source greenhouse,lever,ashby] [--watchlist <path|crm:seg>] [--keywords …] [--from <file.json|spool.jsonl|spool-dir>] [--save]
|
|
104
|
+
fullstackgtm signals discover [--icp <path>] [--source exa] [--since 30d] [--max-accounts 10] [--max-results 50] [--max-searches 12] [--max-usd 0.25] [--save]
|
|
104
105
|
fullstackgtm signals list [--since 7d] [--bucket b] [--account d] [--unjudged]
|
|
105
106
|
fullstackgtm signals outcome --account <d> [--touch <id>] --result replied|meeting|bounced|no_reply
|
|
106
107
|
fullstackgtm signals weights [--explain]
|
|
107
|
-
detect fresh buying triggers (
|
|
108
|
+
detect fresh buying triggers (ICP-driven evidence discovery,
|
|
109
|
+
ATS hiring scans, and staged
|
|
108
110
|
funding/company/social ingest), rank them, persist a local
|
|
109
111
|
signal ledger. READ-ONLY re: CRM — fetch NEVER emits a plan;
|
|
110
112
|
--save persists only the signal ledger. outcome feeds the
|
|
@@ -300,7 +302,7 @@ export const HELP = {
|
|
|
300
302
|
logout: {
|
|
301
303
|
summary: "remove stored credentials for a provider",
|
|
302
304
|
phase: "Setup",
|
|
303
|
-
synopsis: ["fullstackgtm logout <hubspot|salesforce|stripe|anthropic|openai|apollo|clay|pipe0|explorium|broker>"],
|
|
305
|
+
synopsis: ["fullstackgtm logout <hubspot|salesforce|stripe|anthropic|openai|apollo|clay|pipe0|explorium|theirstack|exa|broker>"],
|
|
304
306
|
seeAlso: ["login", "doctor"],
|
|
305
307
|
},
|
|
306
308
|
doctor: {
|
|
@@ -566,11 +568,12 @@ export const HELP = {
|
|
|
566
568
|
phase: "Detect",
|
|
567
569
|
synopsis: [
|
|
568
570
|
"fullstackgtm signals fetch [--bucket job,…] [--source greenhouse,lever,ashby] [--connector file,serpapi-news,hubspot-forms] [--connector-opt k=v] [--watchlist <path|crm:seg>] [--keywords …] [--from <file.json|spool.jsonl|spool-dir>] [--save]",
|
|
571
|
+
"fullstackgtm signals discover [--icp <path>] [--source exa] [--since 30d] [--max-accounts 10] [--max-results 50] [--max-searches 12] [--max-usd 0.25] [--save]",
|
|
569
572
|
"fullstackgtm signals list [--since 7d] [--bucket b] [--account d] [--unjudged]",
|
|
570
573
|
"fullstackgtm signals outcome --account <d> [--touch <id>] --result replied|meeting|bounced|no_reply",
|
|
571
574
|
"fullstackgtm signals weights [--explain]",
|
|
572
575
|
],
|
|
573
|
-
detail: "Read-only re: CRM — `fetch` NEVER
|
|
576
|
+
detail: "Read-only re: CRM — `fetch` and `discover` NEVER emit a patch plan; `--save` persists only the local signal ledger. `discover` matches ICP behavioral hypotheses against canonical public ATS evidence through Exa, with explicit request and reported-cost budgets. Secrets resolve through env/login, never argv. `outcome` feeds the learned per-bucket `weights`.",
|
|
574
577
|
seeAlso: ["icp", "draft"],
|
|
575
578
|
},
|
|
576
579
|
icp: {
|
|
@@ -637,12 +640,12 @@ export const COMMAND_FLAGS = {
|
|
|
637
640
|
market: ["--category", "--config", "--vendor", "--snapshot", "--task-account", "--task-deal", "--capture-run", "--run", "--prior-run", "--diff", "--from", "--calls", "--anchor", "--min-mentions", "--max-claims", "--promote-lift", "--model", "--format", "--auto", "--unverified", "--save", "--dry-run", "--verbose", "--json", "--out"],
|
|
638
641
|
tam: [...SOURCE_FLAGS, "--source", "--name", "--icp", "--accounts", "--acv", "--acv-basis", "--acv-from-crm", "--deal-period", "--buyers-per-account", "--cross-checks", "--max", "--max-credits", "--usd-per-credit", "--dry-run", "--confirm", "--cron", "--label", "--save", "--verbose", "--json", "--out"],
|
|
639
642
|
icp: [...SOURCE_FLAGS, "--name", "--icp", "--domain", "--no-interactive", "--signals-from", "--with-history", "--prompt", "--min-score", "--from-judge", "--golden", "--against-outcomes", "--min-accuracy", "--model", "--label", "--save", "--verbose", "--json", "--out"],
|
|
640
|
-
signals: ["--bucket", "--source", "--connector", "--connector-opt", "--watchlist", "--keywords", "--from", "--label", "--since", "--account", "--contact", "--unjudged", "--touch", "--result", "--save", "--verbose", "--json", "--explain"],
|
|
643
|
+
signals: ["--bucket", "--source", "--connector", "--connector-opt", "--watchlist", "--keywords", "--from", "--label", "--since", "--icp", "--max-accounts", "--max-results", "--max-searches", "--max-usd", "--account", "--contact", "--unjudged", "--touch", "--result", "--save", "--verbose", "--json", "--explain"],
|
|
641
644
|
draft: ["--from-judge", "--min-score", "--prompt", "--model", "--channel", "--save", "--dry-run", "--verbose", "--json", "--out"],
|
|
642
645
|
schedule: ["--cron", "--label", "--provider", "--trigger", "--timer", "--runs", "--verbose", "--json"],
|
|
643
646
|
};
|
|
644
647
|
export const FLAGS_WITH_VALUES = new Set([
|
|
645
|
-
"--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-claims", "--max-credits", "--max-examples", "--max-operations", "--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", "--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",
|
|
646
649
|
]);
|
|
647
650
|
// Lifecycle-grouped front door. One line per verb, organized by the
|
|
648
651
|
// Prevent→Detect→Remediate→Verify loop so a new user sees ~6 jobs, not 22 verbs.
|
package/dist/cli/icp.js
CHANGED
|
@@ -38,6 +38,10 @@ function updateReviewSegment(icp, id, raw) {
|
|
|
38
38
|
if (["titleKeywords", "jobLevels", "departments"].includes(id)) {
|
|
39
39
|
return { ...icp, persona: { ...icp.persona, [id]: values } };
|
|
40
40
|
}
|
|
41
|
+
if (["investmentStages", "fundingAmounts", "thesisKeywords"].includes(id)) {
|
|
42
|
+
const field = id === "investmentStages" ? "stages" : id;
|
|
43
|
+
return { ...icp, investment: { ...icp.investment, [field]: values } };
|
|
44
|
+
}
|
|
41
45
|
return { ...icp, signals: { ...icp.signals, intentTopics: values } };
|
|
42
46
|
}
|
|
43
47
|
function renderDerivedIcp(result, selected = -1) {
|
package/dist/cli/signals.js
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { getCredential, resolveHubspotConnection } from "../credentials.js";
|
|
5
|
+
import { discoverSignalsWithExa } from "../signalDiscovery.js";
|
|
5
6
|
import { buildSignalsFromAts, computeWeights, createFileSignalStore, DEFAULT_SIGNALS_CONFIG, dedupeSignals, loadSignalsConfig, makeOutcome, normalizeAccountDomain, SIGNAL_BUCKETS, signalRunId, signalsSpoolDir, stagedRowToSignal } from "../signals.js";
|
|
6
7
|
import { fetchAtsJobs } from "../connectors/atsBoards.js";
|
|
7
8
|
import { getSignalSource, listSignalSources } from "../connectors/signalSources.js";
|
|
8
9
|
import { isSpoolPath, readSpoolPath } from "../spoolFiles.js";
|
|
9
|
-
import { option, readSnapshot, repeatedOption, saveRequested } from "./shared.js";
|
|
10
|
+
import { loadIcp, option, readSnapshot, repeatedOption, saveRequested } from "./shared.js";
|
|
10
11
|
import { createStatusLine } from "./ui.js";
|
|
11
12
|
import { unknownSubcommandError } from "./suggest.js";
|
|
12
13
|
/**
|
|
@@ -103,12 +104,15 @@ export async function signalsCommand(args) {
|
|
|
103
104
|
if (!sub || args.includes("--help") || args.includes("-h")) {
|
|
104
105
|
console.log(`Usage:
|
|
105
106
|
fullstackgtm signals fetch [--bucket job,funding,...] [--source greenhouse,lever,ashby] [--connector file,serpapi-news,hubspot-forms] [--connector-opt k=v ...] [--watchlist <path|crm:segment>] [--keywords "growth,revops"] [--from <file.json|spool.jsonl|spool-dir>] [--config <path>] [--save]
|
|
107
|
+
fullstackgtm signals discover [--icp <path>] [--source exa] [--since 30d] [--max-accounts 10] [--max-results 50] [--max-searches 12] [--max-usd 0.25] [--save]
|
|
106
108
|
fullstackgtm signals list [--since 7d] [--bucket b] [--account d] [--unjudged]
|
|
107
109
|
fullstackgtm signals outcome --account <domain> [--touch <id>] --result replied|meeting|bounced|no_reply
|
|
108
110
|
fullstackgtm signals weights [--explain]
|
|
109
111
|
|
|
110
|
-
Detect fresh buying triggers and rank them. \`
|
|
111
|
-
|
|
112
|
+
Detect fresh buying triggers and rank them. \`discover\` searches canonical
|
|
113
|
+
public ATS pages for evidence matching the ICP's behavioral trigger hypotheses,
|
|
114
|
+
then resolves employer domains through Exa. \`fetch\` and \`discover\` are read-only re: CRM — they
|
|
115
|
+
NEVER emit a patch plan; --save persists only the local signal ledger (used by
|
|
112
116
|
\`icp judge\`). ATS adapters are no-auth. Source connectors (--connector) pull
|
|
113
117
|
from connected platforms: file (local JSON/JSONL spool, no auth), serpapi-news
|
|
114
118
|
(API key via env/login), hubspot-forms (reuses the HubSpot login). Secrets come
|
|
@@ -120,6 +124,57 @@ from the credential ladder, never argv; --connector-opt carries non-secret knobs
|
|
|
120
124
|
(one row per event); see docs/signal-spool-format.md.`);
|
|
121
125
|
return;
|
|
122
126
|
}
|
|
127
|
+
if (sub === "discover") {
|
|
128
|
+
const source = option(rest, "--source") ?? "exa";
|
|
129
|
+
if (source !== "exa")
|
|
130
|
+
throw new Error(`signals discover: unsupported --source "${source}" (currently: exa).`);
|
|
131
|
+
const icp = loadIcp(rest);
|
|
132
|
+
if (!icp)
|
|
133
|
+
throw new Error("signals discover: no ICP found. Pass --icp <path> or create ./icp.json.");
|
|
134
|
+
const apiKey = await resolveSignalSourceKey("exa");
|
|
135
|
+
if (!apiKey)
|
|
136
|
+
throw new Error("signals discover: missing Exa API key. Set EXA_API_KEY or run `fullstackgtm login exa`.");
|
|
137
|
+
const now = new Date();
|
|
138
|
+
const sinceArg = option(rest, "--since") ?? "30d";
|
|
139
|
+
const since = new Date(now.getTime() - parseSinceWindowMs(sinceArg));
|
|
140
|
+
const maxAccounts = positiveIntegerOption(rest, "--max-accounts", 10);
|
|
141
|
+
const maxResults = positiveIntegerOption(rest, "--max-results", 50);
|
|
142
|
+
const maxSearches = positiveIntegerOption(rest, "--max-searches", 12);
|
|
143
|
+
const maxUsd = positiveNumberOption(rest, "--max-usd", 0.25);
|
|
144
|
+
const config = resolveSignalsConfig(rest);
|
|
145
|
+
const status = createStatusLine();
|
|
146
|
+
status.set(`Searching public evidence with Exa… up to ${maxSearches} searches / $${maxUsd.toFixed(2)}`);
|
|
147
|
+
let discovered;
|
|
148
|
+
try {
|
|
149
|
+
discovered = await discoverSignalsWithExa({ icp, apiKey, since, maxAccounts, maxResults, maxSearches, maxUsd, now,
|
|
150
|
+
config });
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
status.done();
|
|
154
|
+
}
|
|
155
|
+
const store = createFileSignalStore();
|
|
156
|
+
const priorSignals = await store.allSignals();
|
|
157
|
+
const { fresh, deduped } = dedupeSignals(discovered.signals, priorSignals, config.dedupWindowDays, now);
|
|
158
|
+
const ranked = [...fresh].sort((a, b) => b.weight - a.weight || a.accountDomain.localeCompare(b.accountDomain));
|
|
159
|
+
const output = { ...discovered.summary, freshSignals: ranked.length, dedupedSignals: deduped.length,
|
|
160
|
+
warnings: discovered.summary.warnings, signals: ranked };
|
|
161
|
+
console.log(rest.includes("--json") || rest.includes("--verbose") ? JSON.stringify(output, null, 2) : renderSignals(ranked));
|
|
162
|
+
console.error(`Exa: ${discovered.summary.searchesUsed}/${maxSearches} searches, $${discovered.summary.costUsd.toFixed(4)} reported cost; ` +
|
|
163
|
+
`${discovered.summary.matchedEvidence} evidence match(es), ${discovered.summary.resolvedAccounts} account(s), ` +
|
|
164
|
+
`${ranked.length} fresh. NO plan emitted — discovery is read-only re: CRM.`);
|
|
165
|
+
for (const warning of discovered.summary.warnings)
|
|
166
|
+
console.error(`Warning: ${warning}`);
|
|
167
|
+
if (saveRequested(rest)) {
|
|
168
|
+
const runLabel = option(rest, "--label") ?? `discover-${now.toISOString().slice(0, 10)}`;
|
|
169
|
+
await store.appendRun({ id: signalRunId(runLabel), runLabel, startedAt: now.toISOString(), completedAt: new Date().toISOString(),
|
|
170
|
+
buckets: ["job"], counts: { fetched: discovered.summary.rawResults, new: ranked.length, deduped: deduped.length }, signals: ranked });
|
|
171
|
+
console.error(`Saved signal run "${runLabel}". Next: \`fullstackgtm icp judge --signals-from ${runLabel} --save\`.`);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
console.error("(not saved — re-run with --save to persist this evidence to the signal ledger)");
|
|
175
|
+
}
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
123
178
|
if (sub === "fetch") {
|
|
124
179
|
const config = resolveSignalsConfig(rest);
|
|
125
180
|
// Merge --keywords into the job bucket; filter buckets + job sources.
|
|
@@ -328,7 +383,25 @@ from the credential ladder, never argv; --connector-opt carries non-secret knobs
|
|
|
328
383
|
}
|
|
329
384
|
return;
|
|
330
385
|
}
|
|
331
|
-
throw unknownSubcommandError("signals", sub, ["fetch", "list", "outcome", "weights"]);
|
|
386
|
+
throw unknownSubcommandError("signals", sub, ["fetch", "discover", "list", "outcome", "weights"]);
|
|
387
|
+
}
|
|
388
|
+
function positiveIntegerOption(args, flag, fallback) {
|
|
389
|
+
const raw = option(args, flag);
|
|
390
|
+
if (raw == null)
|
|
391
|
+
return fallback;
|
|
392
|
+
const value = Number(raw);
|
|
393
|
+
if (!Number.isInteger(value) || value < 1)
|
|
394
|
+
throw new Error(`${flag}: expected a positive integer (got "${raw}").`);
|
|
395
|
+
return value;
|
|
396
|
+
}
|
|
397
|
+
function positiveNumberOption(args, flag, fallback) {
|
|
398
|
+
const raw = option(args, flag);
|
|
399
|
+
if (raw == null)
|
|
400
|
+
return fallback;
|
|
401
|
+
const value = Number(raw);
|
|
402
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
403
|
+
throw new Error(`${flag}: expected a number greater than zero (got "${raw}").`);
|
|
404
|
+
return value;
|
|
332
405
|
}
|
|
333
406
|
/** Parse a "7d"/"30d"/"12h" recency window into milliseconds. */
|
|
334
407
|
function parseSinceWindowMs(value) {
|
package/dist/cli/ui.js
CHANGED
|
@@ -135,14 +135,14 @@ export function formatCount(value) {
|
|
|
135
135
|
* ANSI codes inside `lines` would inflate the measured width.
|
|
136
136
|
*/
|
|
137
137
|
export function box(lines, p, title) {
|
|
138
|
-
const inner = Math.max(...lines.map((line) => line.length), title ? title.length + 2 : 0);
|
|
138
|
+
const inner = Math.max(...lines.map((line) => stripAnsi(line).length), title ? stripAnsi(title).length + 2 : 0);
|
|
139
139
|
const top = title
|
|
140
|
-
? `╭─ ${title} ${"─".repeat(Math.max(0, inner - title.length - 1))}╮`
|
|
140
|
+
? `╭─ ${title} ${"─".repeat(Math.max(0, inner - stripAnsi(title).length - 1))}╮`
|
|
141
141
|
: `╭${"─".repeat(inner + 2)}╮`;
|
|
142
142
|
const bottom = `╰${"─".repeat(inner + 2)}╯`;
|
|
143
143
|
return [
|
|
144
144
|
p.dim(top),
|
|
145
|
-
...lines.map((line) => `${p.dim("│")} ${line.
|
|
145
|
+
...lines.map((line) => `${p.dim("│")} ${line}${" ".repeat(Math.max(0, inner - stripAnsi(line).length))} ${p.dim("│")}`),
|
|
146
146
|
p.dim(bottom),
|
|
147
147
|
];
|
|
148
148
|
}
|
|
@@ -10,6 +10,20 @@ export type ClayPeopleSearchPage = {
|
|
|
10
10
|
prospects: Prospect[];
|
|
11
11
|
hasMore: boolean;
|
|
12
12
|
};
|
|
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
|
+
export type ClayCompanySearchPage = {
|
|
24
|
+
companies: ClayCompany[];
|
|
25
|
+
hasMore: boolean;
|
|
26
|
+
};
|
|
13
27
|
/** Create a forward-only Clay search iterator. This call does not fetch a page. */
|
|
14
28
|
export declare function createClaySearch(opts: {
|
|
15
29
|
apiKey: string;
|
|
@@ -26,6 +40,30 @@ export declare function runClayPeopleSearchPage(opts: {
|
|
|
26
40
|
fetchImpl?: typeof fetch;
|
|
27
41
|
apiBaseUrl?: string;
|
|
28
42
|
}): Promise<ClayPeopleSearchPage>;
|
|
43
|
+
/** Consume the next company page from a Clay search iterator. */
|
|
44
|
+
export declare function runClayCompanySearchPage(opts: {
|
|
45
|
+
apiKey: string;
|
|
46
|
+
searchId: string;
|
|
47
|
+
limit?: number;
|
|
48
|
+
fetchImpl?: typeof fetch;
|
|
49
|
+
apiBaseUrl?: string;
|
|
50
|
+
}): Promise<ClayCompanySearchPage>;
|
|
51
|
+
export declare function normalizeClayCompany(value: unknown): ClayCompany;
|
|
52
|
+
/** Account-first investment discovery: find thesis-shaped companies, then
|
|
53
|
+
* resolve founders/operators only inside those accounts. */
|
|
54
|
+
export declare function discoverClayInvestmentProspects(opts: {
|
|
55
|
+
apiKey: string;
|
|
56
|
+
companyFilters: Record<string, unknown>;
|
|
57
|
+
titleKeywords: string[];
|
|
58
|
+
companyLimit: number;
|
|
59
|
+
prospectLimit: number;
|
|
60
|
+
fetchImpl?: typeof fetch;
|
|
61
|
+
apiBaseUrl?: string;
|
|
62
|
+
}): Promise<{
|
|
63
|
+
prospects: Prospect[];
|
|
64
|
+
companiesScanned: number;
|
|
65
|
+
companiesMatched: ClayCompany[];
|
|
66
|
+
}>;
|
|
29
67
|
export declare function normalizeClayPerson(value: unknown): Prospect;
|
|
30
68
|
/** Validate a Clay Public API key without creating a search or spending enrichment credits. */
|
|
31
69
|
export declare function validateClayApiKey(apiKey: string, fetchImpl?: typeof fetch, apiBaseUrl?: string): Promise<ClayKeyValidation>;
|
package/dist/connectors/clay.js
CHANGED
|
@@ -41,6 +41,64 @@ export async function runClayPeopleSearchPage(opts) {
|
|
|
41
41
|
throw new Error("Clay people search returned no has_more flag.");
|
|
42
42
|
return { prospects: body.data.map(normalizeClayPerson), hasMore };
|
|
43
43
|
}
|
|
44
|
+
/** Consume the next company page from a Clay search iterator. */
|
|
45
|
+
export async function runClayCompanySearchPage(opts) {
|
|
46
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
47
|
+
const base = (opts.apiBaseUrl ?? CLAY_PUBLIC_API_BASE).replace(/\/$/, "");
|
|
48
|
+
const limit = Math.max(1, Math.min(opts.limit ?? 25, 100));
|
|
49
|
+
const response = await fetchImpl(`${base}/search/filters-mode/${encodeURIComponent(opts.searchId)}/run`, {
|
|
50
|
+
method: "POST", headers: clayHeaders(opts.apiKey), body: JSON.stringify({ limit }),
|
|
51
|
+
});
|
|
52
|
+
if (!response.ok)
|
|
53
|
+
throw new ProviderHttpError("Clay", "run company search", response.status);
|
|
54
|
+
const body = await response.json();
|
|
55
|
+
if (!Array.isArray(body.data))
|
|
56
|
+
throw new Error("Clay company search returned an invalid data array.");
|
|
57
|
+
const hasMore = body.has_more ?? body.hasMore;
|
|
58
|
+
if (typeof hasMore !== "boolean")
|
|
59
|
+
throw new Error("Clay company search returned no has_more flag.");
|
|
60
|
+
return { companies: body.data.map(normalizeClayCompany), hasMore };
|
|
61
|
+
}
|
|
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
|
+
/** Account-first investment discovery: find thesis-shaped companies, then
|
|
72
|
+
* resolve founders/operators only inside those accounts. */
|
|
73
|
+
export async function discoverClayInvestmentProspects(opts) {
|
|
74
|
+
const companySearchId = await createClaySearch({
|
|
75
|
+
apiKey: opts.apiKey, sourceType: "companies", filters: opts.companyFilters,
|
|
76
|
+
fetchImpl: opts.fetchImpl, apiBaseUrl: opts.apiBaseUrl,
|
|
77
|
+
});
|
|
78
|
+
const companyPage = await runClayCompanySearchPage({
|
|
79
|
+
apiKey: opts.apiKey, searchId: companySearchId, limit: opts.companyLimit,
|
|
80
|
+
fetchImpl: opts.fetchImpl, apiBaseUrl: opts.apiBaseUrl,
|
|
81
|
+
});
|
|
82
|
+
const prospects = [];
|
|
83
|
+
const identifiers = companyPage.companies.map((company) => company.domain ?? company.linkedin).filter((value) => Boolean(value));
|
|
84
|
+
if (identifiers.length > 0) {
|
|
85
|
+
const peopleSearchId = await createClaySearch({
|
|
86
|
+
apiKey: opts.apiKey, sourceType: "people",
|
|
87
|
+
filters: { company_identifier: identifiers, job_title_keywords: opts.titleKeywords },
|
|
88
|
+
fetchImpl: opts.fetchImpl, apiBaseUrl: opts.apiBaseUrl,
|
|
89
|
+
});
|
|
90
|
+
const peoplePage = await runClayPeopleSearchPage({
|
|
91
|
+
apiKey: opts.apiKey, searchId: peopleSearchId,
|
|
92
|
+
limit: opts.prospectLimit,
|
|
93
|
+
fetchImpl: opts.fetchImpl, apiBaseUrl: opts.apiBaseUrl,
|
|
94
|
+
});
|
|
95
|
+
// company_identifier may match a past experience. Investment discovery is
|
|
96
|
+
// explicitly account-first, so retain only people currently at an account.
|
|
97
|
+
const currentDomains = new Set(companyPage.companies.map((company) => company.domain).filter(Boolean));
|
|
98
|
+
prospects.push(...peoplePage.prospects.filter((person) => person.companyDomain && currentDomains.has(person.companyDomain)));
|
|
99
|
+
}
|
|
100
|
+
return { prospects, companiesScanned: companyPage.companies.length, companiesMatched: companyPage.companies };
|
|
101
|
+
}
|
|
44
102
|
export function normalizeClayPerson(value) {
|
|
45
103
|
const row = value && typeof value === "object" ? value : {};
|
|
46
104
|
const location = row.structured_location && typeof row.structured_location === "object"
|
package/dist/icp.d.ts
CHANGED
|
@@ -12,8 +12,35 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Zero runtime deps; pure functions (translation + scoring take plain data).
|
|
14
14
|
*/
|
|
15
|
+
export type TriggerHypothesis = {
|
|
16
|
+
/** Stable, human-editable key used to trace discovered evidence back to the ICP. */
|
|
17
|
+
id: string;
|
|
18
|
+
/** Observable business condition, e.g. "sales compensation process is breaking". */
|
|
19
|
+
label: string;
|
|
20
|
+
/** Phrases or concepts that count as supporting public evidence. */
|
|
21
|
+
positiveEvidence: string[];
|
|
22
|
+
/** Concrete projects/roles that often expose the condition. */
|
|
23
|
+
activeProjects?: string[];
|
|
24
|
+
/** Functions likely to own or feel the problem. */
|
|
25
|
+
buyerFunctions?: string[];
|
|
26
|
+
/** Terms that make a result a likely false positive. */
|
|
27
|
+
negativeEvidence?: string[];
|
|
28
|
+
/** Preferred public source classes. Discovery currently implements `job`. */
|
|
29
|
+
preferredSources?: Array<"job" | "news" | "company" | "social" | "review" | "legal">;
|
|
30
|
+
};
|
|
15
31
|
export type Icp = {
|
|
16
32
|
name: string;
|
|
33
|
+
/** `investment` means the organization is sourcing companies to invest in,
|
|
34
|
+
* not conventional customers. Discovery becomes company-first, then resolves
|
|
35
|
+
* founders at the matched accounts. */
|
|
36
|
+
motion?: "sales" | "investment";
|
|
37
|
+
investment?: {
|
|
38
|
+
stages?: Array<"pre-seed" | "seed" | "series-a" | "series-b" | "growth" | "bootstrapped">;
|
|
39
|
+
/** Clay's native funding bands. */
|
|
40
|
+
fundingAmounts?: Array<"under_1m" | "1m_5m" | "5m_10m" | "10m_25m" | "25m_50m" | "50m_100m" | "100m_250m" | "over_250m" | "unknown">;
|
|
41
|
+
/** Terms expected in an investment target's company description. */
|
|
42
|
+
thesisKeywords?: string[];
|
|
43
|
+
};
|
|
17
44
|
firmographics: {
|
|
18
45
|
/** human industry labels, e.g. ["software","saas"] — used for keyword/industry filters */
|
|
19
46
|
industries?: string[];
|
|
@@ -38,6 +65,8 @@ export type Icp = {
|
|
|
38
65
|
};
|
|
39
66
|
signals?: {
|
|
40
67
|
intentTopics?: string[];
|
|
68
|
+
/** Testable behavioral hypotheses for evidence-first account discovery. */
|
|
69
|
+
triggerHypotheses?: TriggerHypothesis[];
|
|
41
70
|
};
|
|
42
71
|
scoring?: {
|
|
43
72
|
/** minimum fit (0..1) for a prospect to become a create_record op. Default 0.5. */
|
|
@@ -46,6 +75,10 @@ export type Icp = {
|
|
|
46
75
|
};
|
|
47
76
|
export declare const DEFAULT_FIT_THRESHOLD = 0.5;
|
|
48
77
|
export declare function parseIcp(raw: string): Icp;
|
|
78
|
+
/** Native Clay company filters for an investment thesis. */
|
|
79
|
+
export declare function icpToClayInvestmentCompanyFilters(icp: Icp): Record<string, unknown>;
|
|
80
|
+
/** People filters used only after an investment target account has matched. */
|
|
81
|
+
export declare function clayInvestmentPeopleFilters(icp: Icp, companyIdentifier: string): Record<string, unknown>;
|
|
49
82
|
/** Explorium /v1/prospects filters from the ICP. */
|
|
50
83
|
export declare function icpToExploriumFilters(icp: Icp): Record<string, {
|
|
51
84
|
values?: string[];
|