fullstackgtm 0.50.0 → 0.51.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 +45 -0
- package/README.md +7 -0
- package/dist/cli/audit.js +6 -1
- package/dist/cli/auth.js +49 -4
- package/dist/cli/backfill.js +4 -0
- package/dist/cli/call.js +28 -6
- package/dist/cli/draft.js +11 -1
- package/dist/cli/enrich.js +124 -57
- package/dist/cli/fix.js +6 -3
- package/dist/cli/help.js +31 -28
- package/dist/cli/icp.js +15 -1
- package/dist/cli/init.js +3 -3
- package/dist/cli/market.js +14 -1
- package/dist/cli/planOutput.d.ts +5 -0
- package/dist/cli/planOutput.js +27 -0
- package/dist/cli/plans.js +184 -33
- package/dist/cli/schedule.js +22 -11
- package/dist/cli/signals.js +22 -3
- package/dist/cli/tam.d.ts +1 -1
- package/dist/cli/tam.js +14 -2
- package/dist/cli/ui.js +14 -6
- package/dist/connectors/clay.d.ts +33 -0
- package/dist/connectors/clay.js +123 -0
- package/dist/connectors/hubspot.d.ts +4 -0
- package/dist/connectors/hubspot.js +36 -18
- package/dist/connectors/prospectSources.d.ts +12 -0
- package/dist/connectors/prospectSources.js +1 -1
- package/dist/contactProviders.d.ts +44 -0
- package/dist/contactProviders.js +100 -0
- package/dist/enrich.d.ts +7 -1
- package/dist/enrich.js +46 -1
- package/dist/hostedPatchPlan.js +6 -1
- package/dist/icp.d.ts +2 -0
- package/dist/icp.js +49 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/init.d.ts +1 -1
- package/dist/init.js +1 -1
- package/docs/api.md +18 -1
- package/package.json +1 -1
- package/src/cli/audit.ts +5 -1
- package/src/cli/auth.ts +46 -4
- package/src/cli/backfill.ts +3 -0
- package/src/cli/call.ts +25 -6
- package/src/cli/draft.ts +9 -1
- package/src/cli/enrich.ts +132 -56
- package/src/cli/fix.ts +5 -3
- package/src/cli/help.ts +31 -28
- package/src/cli/icp.ts +13 -1
- package/src/cli/init.ts +3 -3
- package/src/cli/market.ts +12 -1
- package/src/cli/planOutput.ts +30 -0
- package/src/cli/plans.ts +174 -29
- package/src/cli/schedule.ts +21 -15
- package/src/cli/signals.ts +22 -3
- package/src/cli/tam.ts +12 -3
- package/src/cli/ui.ts +15 -6
- package/src/connectors/clay.ts +155 -0
- package/src/connectors/hubspot.ts +41 -17
- package/src/connectors/prospectSources.ts +7 -1
- package/src/contactProviders.ts +141 -0
- package/src/enrich.ts +51 -2
- package/src/hostedPatchPlan.ts +6 -1
- package/src/icp.ts +51 -0
- package/src/index.ts +25 -0
- package/src/init.ts +2 -2
package/dist/enrich.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AcquireBudget } from "./acquireMeter.ts";
|
|
2
2
|
import type { ProgressEmitter } from "./progress.ts";
|
|
3
3
|
import type { AssignmentContext, AssignmentPolicy } from "./assign.ts";
|
|
4
|
+
import { type ContactWaterfallStep } from "./contactProviders.ts";
|
|
4
5
|
import type { CanonicalGtmSnapshot, PatchPlan } from "./types.ts";
|
|
5
6
|
/**
|
|
6
7
|
* The enrich layer: governed append/refresh of third-party data into the CRM.
|
|
@@ -83,11 +84,16 @@ export type AcquireCreateMap = {
|
|
|
83
84
|
* real emails (e.g. explorium discovers, pipe0 resolves the work email).
|
|
84
85
|
*/
|
|
85
86
|
export type AcquireDiscoveryConfig = {
|
|
86
|
-
provider: "explorium" | "pipe0" | "linkedin" | "heyreach";
|
|
87
|
+
provider: "explorium" | "pipe0" | "clay" | "linkedin" | "heyreach";
|
|
88
|
+
/** Clay search collection. Phase 1 supports people; companies follows separately. */
|
|
89
|
+
sourceType?: "people" | "companies";
|
|
87
90
|
filters?: Record<string, unknown>;
|
|
88
91
|
size?: number;
|
|
89
92
|
/** Maximum raw provider candidates scanned per run while seeking fresh leads. */
|
|
90
93
|
scanLimit?: number;
|
|
94
|
+
/** Ordered fill-only contact providers. Later steps receive only unresolved fields. */
|
|
95
|
+
contactWaterfall?: ContactWaterfallStep[];
|
|
96
|
+
/** @deprecated Use contactWaterfall. Retained for existing configurations. */
|
|
91
97
|
resolveEmailsWith?: "pipe0";
|
|
92
98
|
/** LinkedIn sources only: the provider-native list id to read (HeyReach lead-list id). */
|
|
93
99
|
listId?: string;
|
package/dist/enrich.js
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { credentialsDir, ensureSecureHomeDir, writeSecureFile } from "./credentials.js";
|
|
4
4
|
import { HUBSPOT_DEFAULT_FIELD_MAPPINGS } from "./mappings.js";
|
|
5
5
|
import { parseAssignmentPolicy, resolveAssignment } from "./assign.js";
|
|
6
|
+
import { validateContactWaterfall } from "./contactProviders.js";
|
|
6
7
|
export const ENRICH_CONFIG_FILE_NAME = "enrich.config.json";
|
|
7
8
|
export const DEFAULT_STALE_DAYS = 90;
|
|
8
9
|
const OBJECT_TYPES = ["company", "contact"];
|
|
@@ -12,7 +13,7 @@ const MATCH_KEYS = {
|
|
|
12
13
|
contact: ["email", "name", "linkedin"],
|
|
13
14
|
};
|
|
14
15
|
/** API source ids the MVP can pull from. */
|
|
15
|
-
export const SUPPORTED_API_SOURCES = ["apollo", "explorium", "pipe0", "linkedin"];
|
|
16
|
+
export const SUPPORTED_API_SOURCES = ["apollo", "explorium", "pipe0", "clay", "linkedin"];
|
|
16
17
|
/**
|
|
17
18
|
* Canonical fields enrich may target, plus the HubSpot property spellings the
|
|
18
19
|
* config may use for them (so `"crm": "numberofemployees"` and
|
|
@@ -189,6 +190,22 @@ export function parseEnrichConfig(raw) {
|
|
|
189
190
|
fail(error instanceof Error ? error.message : String(error));
|
|
190
191
|
}
|
|
191
192
|
}
|
|
193
|
+
for (const [sourceId, discovery] of Object.entries(config.acquire?.discovery ?? {})) {
|
|
194
|
+
if (discovery.provider === "clay" && (discovery.sourceType ?? "people") !== "people") {
|
|
195
|
+
fail(`acquire.discovery.${sourceId}.sourceType currently supports only "people" for Clay`);
|
|
196
|
+
}
|
|
197
|
+
if (discovery.contactWaterfall !== undefined) {
|
|
198
|
+
try {
|
|
199
|
+
discovery.contactWaterfall = validateContactWaterfall(discovery.contactWaterfall);
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
fail(`acquire.discovery.${sourceId}.${error instanceof Error ? error.message : String(error)}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (discovery.resolveEmailsWith !== undefined && discovery.resolveEmailsWith !== "pipe0") {
|
|
206
|
+
fail(`acquire.discovery.${sourceId}.resolveEmailsWith must be "pipe0"`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
192
209
|
return config;
|
|
193
210
|
}
|
|
194
211
|
/**
|
|
@@ -254,6 +271,34 @@ export function builtinAcquirePreset(source) {
|
|
|
254
271
|
},
|
|
255
272
|
};
|
|
256
273
|
}
|
|
274
|
+
if (provider === "clay") {
|
|
275
|
+
return {
|
|
276
|
+
sources: { clay: { kind: "api" } },
|
|
277
|
+
match: { contact: { keys: ["linkedin", "email"], onAmbiguous: "skip" } },
|
|
278
|
+
fields: {},
|
|
279
|
+
policy: { overwrite: "never" },
|
|
280
|
+
acquire: {
|
|
281
|
+
budget: { records: { perDay: 50, perMonth: 500 } },
|
|
282
|
+
costPerRecord: { clay: 0 },
|
|
283
|
+
discovery: { clay: { provider: "clay", sourceType: "people", size: 25 } },
|
|
284
|
+
create: {
|
|
285
|
+
contact: {
|
|
286
|
+
matchKey: "linkedin",
|
|
287
|
+
properties: {
|
|
288
|
+
hs_linkedin_url: "linkedin",
|
|
289
|
+
firstname: "firstName",
|
|
290
|
+
lastname: "lastName",
|
|
291
|
+
jobtitle: "jobTitle",
|
|
292
|
+
company: "companyName",
|
|
293
|
+
email: "email",
|
|
294
|
+
},
|
|
295
|
+
associateCompanyFrom: "companyName",
|
|
296
|
+
associateCompanyDomainFrom: "companyDomain",
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
}
|
|
257
302
|
if (provider !== "pipe0" && provider !== "explorium")
|
|
258
303
|
return undefined;
|
|
259
304
|
return {
|
package/dist/hostedPatchPlan.js
CHANGED
|
@@ -191,7 +191,12 @@ export async function reportHostedPlanLifecycle(stored, options = {}) {
|
|
|
191
191
|
return { status: "unpaired" };
|
|
192
192
|
const status = stored.status === "applied" ? "applied" : stored.status === "rejected" ? "rejected" : "approved";
|
|
193
193
|
const runRecord = status === "applied" ? stored.runs.at(-1) : undefined;
|
|
194
|
-
|
|
194
|
+
// The hosted terminal transition is authority-bound to the exact approved
|
|
195
|
+
// subset. Local runs also record excluded operations as `skipped` for a
|
|
196
|
+
// complete human audit trail; those are not execution receipts and must not
|
|
197
|
+
// be echoed as though they were authorized provider work.
|
|
198
|
+
const approved = new Set(stored.approvedOperationIds);
|
|
199
|
+
const operationReceipts = runRecord?.results.filter((result) => approved.has(result.operationId)).map((result) => ({
|
|
195
200
|
packageOpId: result.operationId,
|
|
196
201
|
status: result.status,
|
|
197
202
|
...(result.detail ? { error: result.detail.slice(0, 1000) } : {}),
|
package/dist/icp.d.ts
CHANGED
|
@@ -104,6 +104,8 @@ export declare function icpToTheirStackFilters(icp: Icp): {
|
|
|
104
104
|
* PERSONA precision but NOT industry, so the industry filter is load-bearing.
|
|
105
105
|
*/
|
|
106
106
|
export declare function icpToCrustdataFilters(icp: Icp): Record<string, unknown>;
|
|
107
|
+
/** Clay people-search filters using only fields confirmed in the live catalog. */
|
|
108
|
+
export declare function icpToClayPeopleFilters(icp: Icp): Record<string, unknown>;
|
|
107
109
|
export type IcpFit = {
|
|
108
110
|
score: number;
|
|
109
111
|
reasons: string[];
|
package/dist/icp.js
CHANGED
|
@@ -205,6 +205,55 @@ export function icpToCrustdataFilters(icp) {
|
|
|
205
205
|
}
|
|
206
206
|
return f;
|
|
207
207
|
}
|
|
208
|
+
const CLAY_INDUSTRY = {
|
|
209
|
+
software: ["Software Development"],
|
|
210
|
+
saas: ["Software Development"],
|
|
211
|
+
internet: ["Technology, Information and Internet"],
|
|
212
|
+
fintech: ["Financial Services"],
|
|
213
|
+
"financial services": ["Financial Services"],
|
|
214
|
+
"information technology & services": ["IT Services and IT Consulting"],
|
|
215
|
+
"information technology and services": ["IT Services and IT Consulting"],
|
|
216
|
+
};
|
|
217
|
+
const CLAY_EMPLOYEE_BAND = {
|
|
218
|
+
"1-10": "2-10",
|
|
219
|
+
"11-50": "11-50",
|
|
220
|
+
"51-200": "51-200",
|
|
221
|
+
"201-500": "201-500",
|
|
222
|
+
"501-1000": "501-1,000",
|
|
223
|
+
"1001-5000": "1,001-5,000",
|
|
224
|
+
"5001-10000": "5,001-10,000",
|
|
225
|
+
"10001+": "10,001+",
|
|
226
|
+
};
|
|
227
|
+
const CLAY_SENIORITY = {
|
|
228
|
+
cxo: "c-suite",
|
|
229
|
+
"c-suite": "c-suite",
|
|
230
|
+
vp: "vp",
|
|
231
|
+
director: "director",
|
|
232
|
+
head: "head",
|
|
233
|
+
manager: "manager",
|
|
234
|
+
owner: "owner",
|
|
235
|
+
founder: "founder",
|
|
236
|
+
senior: "senior",
|
|
237
|
+
};
|
|
238
|
+
/** Clay people-search filters using only fields confirmed in the live catalog. */
|
|
239
|
+
export function icpToClayPeopleFilters(icp) {
|
|
240
|
+
const filters = {};
|
|
241
|
+
if (icp.persona.titleKeywords?.length)
|
|
242
|
+
filters.job_title_keywords = icp.persona.titleKeywords;
|
|
243
|
+
const seniorities = [...new Set((icp.persona.jobLevels ?? []).map((level) => CLAY_SENIORITY[level.toLowerCase()]).filter(Boolean))];
|
|
244
|
+
if (seniorities.length)
|
|
245
|
+
filters.job_title_seniority_levels_v2 = seniorities;
|
|
246
|
+
const sizes = [...new Set((icp.firmographics.employeeBands ?? []).map((band) => CLAY_EMPLOYEE_BAND[band.replace(/,/g, "")]).filter(Boolean))];
|
|
247
|
+
if (sizes.length)
|
|
248
|
+
filters.company_sizes = sizes;
|
|
249
|
+
if (icp.firmographics.geos?.length) {
|
|
250
|
+
filters.location_countries_include = icp.firmographics.geos.map((geo) => COUNTRY_NAMES[geo.toLowerCase()] ?? geo);
|
|
251
|
+
}
|
|
252
|
+
const industries = [...new Set((icp.firmographics.industries ?? []).flatMap((industry) => CLAY_INDUSTRY[industry.toLowerCase()] ?? [titleCase(industry)]))];
|
|
253
|
+
if (industries.length)
|
|
254
|
+
filters.company_industries_include = industries;
|
|
255
|
+
return filters;
|
|
256
|
+
}
|
|
208
257
|
const ACRONYMS = new Set(["cro", "coo", "ceo", "cfo", "vp", "crm", "gtm", "saas"]);
|
|
209
258
|
function titleCase(value) {
|
|
210
259
|
return value
|
package/dist/index.d.ts
CHANGED
|
@@ -9,8 +9,10 @@ export { accountHierarchyToMarkdown, buildAccountHierarchy, type AccountHierarch
|
|
|
9
9
|
export { buildRelationshipMap, relationshipMapToMarkdown, type RelationshipMap, type StakeholderNode, type StakeholderRole, type StakeholderSentiment, } from "./relationships.ts";
|
|
10
10
|
export { CONFIG_FILE_NAME, loadConfig, mergePolicy, resolveConfiguredRules, rulePackageTrustFromCli, type FullstackgtmConfig, type LoadedConfig, type RulePackageTrust, } from "./config.ts";
|
|
11
11
|
export { applyPatchPlan, type ApplyPatchPlanOptions } from "./connector.ts";
|
|
12
|
+
export { CONTACT_PROVIDER_CAPABILITIES, runContactWaterfall, validateContactWaterfall, type ContactField, type ContactInputShape, type ContactProviderAdapter, type ContactProviderBilling, type ContactProviderCapability, type ContactProviderExecution, type ContactWaterfallAttempt, type ContactWaterfallResult, type ContactWaterfallStep, } from "./contactProviders.ts";
|
|
12
13
|
export { APPLY_STAGES, BACKFILL_STRIPE_STAGES, composeListeners, createProgressEmitter, CRM_SYNC_STAGES, nullProgressEmitter, SNAPSHOT_PULL_STAGES, STRIPE_SNAPSHOT_STAGES, type ProgressEmitter, type ProgressEvent, type ProgressListener, type ProgressSnapshot, } from "./progress.ts";
|
|
13
14
|
export { createHubspotConnector, type HubspotConnectorOptions } from "./connectors/hubspot.ts";
|
|
15
|
+
export { CLAY_PUBLIC_API_BASE, clayApiKey, createClaySearch, normalizeClayPerson, runClayPeopleSearchPage, validateClayApiKey, type ClayPeopleSearchPage, type ClaySearchSourceType, type ClayKeyValidation, } from "./connectors/clay.ts";
|
|
14
16
|
export { DEFAULT_LOOPBACK_PORT, DEFAULT_OAUTH_SCOPES, exchangeHubspotCode, refreshHubspotToken, runHubspotLoopbackLogin, validateHubspotToken, type HubspotTokenSet, type LoopbackLoginOptions, } from "./connectors/hubspotAuth.ts";
|
|
15
17
|
export { createSalesforceConnector, type SalesforceConnection, type SalesforceConnectorOptions, } from "./connectors/salesforce.ts";
|
|
16
18
|
export { pollSalesforceDeviceLogin, refreshSalesforceToken, startSalesforceDeviceLogin, validateSalesforceOrigin, validateSalesforceToken, type SalesforceDeviceAuthorization, type SalesforceTokenSet, } from "./connectors/salesforceAuth.ts";
|
package/dist/index.js
CHANGED
|
@@ -9,8 +9,10 @@ export { accountHierarchyToMarkdown, buildAccountHierarchy, } from "./hierarchy.
|
|
|
9
9
|
export { buildRelationshipMap, relationshipMapToMarkdown, } from "./relationships.js";
|
|
10
10
|
export { CONFIG_FILE_NAME, loadConfig, mergePolicy, resolveConfiguredRules, rulePackageTrustFromCli, } from "./config.js";
|
|
11
11
|
export { applyPatchPlan } from "./connector.js";
|
|
12
|
+
export { CONTACT_PROVIDER_CAPABILITIES, runContactWaterfall, validateContactWaterfall, } from "./contactProviders.js";
|
|
12
13
|
export { APPLY_STAGES, BACKFILL_STRIPE_STAGES, composeListeners, createProgressEmitter, CRM_SYNC_STAGES, nullProgressEmitter, SNAPSHOT_PULL_STAGES, STRIPE_SNAPSHOT_STAGES, } from "./progress.js";
|
|
13
14
|
export { createHubspotConnector } from "./connectors/hubspot.js";
|
|
15
|
+
export { CLAY_PUBLIC_API_BASE, clayApiKey, createClaySearch, normalizeClayPerson, runClayPeopleSearchPage, validateClayApiKey, } from "./connectors/clay.js";
|
|
14
16
|
export { DEFAULT_LOOPBACK_PORT, DEFAULT_OAUTH_SCOPES, exchangeHubspotCode, refreshHubspotToken, runHubspotLoopbackLogin, validateHubspotToken, } from "./connectors/hubspotAuth.js";
|
|
15
17
|
export { createSalesforceConnector, } from "./connectors/salesforce.js";
|
|
16
18
|
export { pollSalesforceDeviceLogin, refreshSalesforceToken, startSalesforceDeviceLogin, validateSalesforceOrigin, validateSalesforceToken, } from "./connectors/salesforceAuth.js";
|
package/dist/init.d.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { type EnrichConfig } from "./enrich.ts";
|
|
18
18
|
import { type Icp } from "./icp.ts";
|
|
19
19
|
export type InitProvider = "hubspot" | "salesforce";
|
|
20
|
-
export type InitSource = "pipe0" | "explorium" | "linkedin";
|
|
20
|
+
export type InitSource = "pipe0" | "explorium" | "clay" | "linkedin";
|
|
21
21
|
export type ScaffoldOptions = {
|
|
22
22
|
/** discovery source the acquire preset + playbook are wired for. Default "pipe0". */
|
|
23
23
|
source?: InitSource;
|
package/dist/init.js
CHANGED
|
@@ -45,7 +45,7 @@ export function starterIcp() {
|
|
|
45
45
|
export function starterEnrichConfig(source) {
|
|
46
46
|
const preset = builtinAcquirePreset(source);
|
|
47
47
|
if (!preset?.acquire) {
|
|
48
|
-
// builtinAcquirePreset covers pipe0/explorium/linkedin, so this is unreachable
|
|
48
|
+
// builtinAcquirePreset covers pipe0/explorium/clay/linkedin, so this is unreachable
|
|
49
49
|
// for the typed InitSource set — guard anyway rather than emit a broken file.
|
|
50
50
|
throw new Error(`init: no acquire preset for source "${source}"`);
|
|
51
51
|
}
|
package/docs/api.md
CHANGED
|
@@ -261,6 +261,22 @@ domain stamped — so the acquired account is immediately signal-watchable
|
|
|
261
261
|
waterfall, chunked, surfaces upstream errors), `fetchPipe0CrustdataProspects`
|
|
262
262
|
(people search). `prospectIdentityKeys` / `crmContactKeys` /
|
|
263
263
|
`partitionFreshProspects` power the pre-email dedup.
|
|
264
|
+
- **Clay Search** (`connectors/clay.ts`): `createClaySearch` creates Clay's
|
|
265
|
+
forward-only server iterator and `runClayPeopleSearchPage` advances it.
|
|
266
|
+
`enrich acquire --source clay` uses the stored `login clay` credential,
|
|
267
|
+
translates the common ICP title/seniority/industry/size/country constraints,
|
|
268
|
+
normalizes people to `Prospect`, checkpoints the search id on saved runs, and
|
|
269
|
+
keys proposed contacts by LinkedIn URL. It performs no paid contact routine
|
|
270
|
+
by default; add an explicit `contactWaterfall` when contact fields are needed.
|
|
271
|
+
- **Contact provider router** (`contactProviders.ts`):
|
|
272
|
+
`CONTACT_PROVIDER_CAPABILITIES`, `validateContactWaterfall`, and
|
|
273
|
+
`runContactWaterfall` provide an ordered, fill-only field router. Configure
|
|
274
|
+
`acquire.discovery.<source>.contactWaterfall` as steps such as
|
|
275
|
+
`[{ "provider": "pipe0", "fields": ["work_email"] }]`. Later steps receive
|
|
276
|
+
only records still missing a requested field and cannot overwrite an earlier
|
|
277
|
+
accepted value. The registry advertises implemented operations only; the
|
|
278
|
+
broader candidate backlog does not become valid configuration until its
|
|
279
|
+
adapter lands.
|
|
264
280
|
- **LinkedIn source** (`connectors/linkedin.ts`, `acquireLinkedIn.ts`): the
|
|
265
281
|
injectable `LinkedInProvider` interface with a default HeyReach adapter
|
|
266
282
|
(`createHeyReachProvider`, `HEYREACH_BASE`, `X-API-KEY`, `normalizeHeyReachLead`)
|
|
@@ -271,7 +287,8 @@ domain stamped — so the acquired account is immediately signal-watchable
|
|
|
271
287
|
apply are reused unchanged. Phase 1 is discovery-only — read-only, never sends.
|
|
272
288
|
`linkedin` is a `SUPPORTED_API_SOURCES` source, so an explicit config can set
|
|
273
289
|
discovery `size` (read a whole list, not the preset's 25). A LinkedIn list
|
|
274
|
-
carries no emails; opt into resolution with
|
|
290
|
+
carries no emails; opt into resolution with `contactWaterfall`, or use the
|
|
291
|
+
backward-compatible
|
|
275
292
|
`acquire.discovery.linkedin.resolveEmailsWith: "pipe0"` — email resolution is
|
|
276
293
|
no longer gated on the dedupe key being `email`, so a profile-URL-keyed source
|
|
277
294
|
still creates outreach-ready, emailed contacts. ICP scoring matches title
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fullstackgtm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.51.0",
|
|
4
4
|
"description": "Open-source agentic GTM ops framework: canonical GTM data model, pluggable deterministic audits, reviewable dry-run patch plans, approval-gated write-back with conflict detection, and cross-system entity resolution. HubSpot, Salesforce, and Stripe connectors included.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Full Stack GTM LLC <ryan@fullstackgtm.com> (https://fullstackgtm.com)",
|
package/src/cli/audit.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { reportCounts, reportCrm, reportFindings } from "../runReport.ts";
|
|
|
13
13
|
import type { AuditFindingSeverity, CanonicalGtmSnapshot, PatchPlan } from "../types.ts";
|
|
14
14
|
import { connectorFor, numericOption, option, readSnapshot, saveRequested, selectedRules } from "./shared.ts";
|
|
15
15
|
import { colorEnabled, createChecklist, formatCount, paint, scoreColor, sparkline, stylizePlanMarkdown, table, type Paint } from "./ui.ts";
|
|
16
|
+
import { compactPlan, verbosePlanRequested } from "./planOutput.ts";
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
const SEVERITY_RANK: Record<AuditFindingSeverity, number> = {
|
|
@@ -258,13 +259,16 @@ export async function audit(args: string[]) {
|
|
|
258
259
|
}
|
|
259
260
|
if (args.includes("--json")) {
|
|
260
261
|
console.log(JSON.stringify(plan, null, 2));
|
|
262
|
+
} else if (!verbosePlanRequested(args)) {
|
|
263
|
+
console.log(compactPlan(plan, { saved: saveRequested(args) }));
|
|
264
|
+
console.error(`\n${auditNextStep(args, plan)}`);
|
|
261
265
|
} else {
|
|
262
266
|
// Default to the summary view (rule table + counts); the full per-operation
|
|
263
267
|
// dump is opt-in via --full or, for a deliverable, `report`. (dx-punch-list #3)
|
|
264
268
|
// Interactive terminals get the styled rendering; piped output is unchanged.
|
|
265
269
|
console.log(
|
|
266
270
|
stylizePlanMarkdown(
|
|
267
|
-
patchPlanToMarkdown(plan, { summary:
|
|
271
|
+
patchPlanToMarkdown(plan, { summary: false }),
|
|
268
272
|
paint(colorEnabled(process.stdout)),
|
|
269
273
|
),
|
|
270
274
|
);
|
package/src/cli/auth.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { activeProfile, credentialsPath, DEFAULT_PROFILE, deleteCredential, getC
|
|
|
8
8
|
import { activeWorkspaceProfile, readHealthTimeline, summarizeHealth } from "../health.ts";
|
|
9
9
|
import { resolveLlmCredential, validateLlmKey } from "../llm.ts";
|
|
10
10
|
import { createFilePlanStore, type StoredPlan } from "../planStore.ts";
|
|
11
|
+
import { validateClayApiKey } from "../connectors/clay.ts";
|
|
11
12
|
import { isOptionValue, numericOption, option, readPackageInfo, readSecret } from "./shared.ts";
|
|
12
13
|
import { box, colorEnabled, paint, scoreColor, sparkline } from "./ui.ts";
|
|
13
14
|
|
|
@@ -378,6 +379,20 @@ export async function login(args: string[]) {
|
|
|
378
379
|
console.log(`Stored Apollo API key in ${credentialsPath()}. \`fullstackgtm enrich append|refresh\` use it automatically.`);
|
|
379
380
|
return;
|
|
380
381
|
}
|
|
382
|
+
if (provider === "clay") {
|
|
383
|
+
rejectArgvSecret(args, "--token", "--key", "--api-key");
|
|
384
|
+
const key = await readSecret("Clay Public API key");
|
|
385
|
+
if (!key) throw new Error("No Clay Public API key provided.");
|
|
386
|
+
if (!args.includes("--no-validate")) {
|
|
387
|
+
const validation = await validateClayApiKey(key);
|
|
388
|
+
if (!validation.ok) throw new Error(validation.detail);
|
|
389
|
+
console.log(validation.detail);
|
|
390
|
+
}
|
|
391
|
+
const stamp = new Date().toISOString();
|
|
392
|
+
storeCredential("clay", { kind: "api_key", accessToken: key, createdAt: stamp, updatedAt: stamp });
|
|
393
|
+
console.log(`Stored Clay Public API key in ${credentialsPath()}. Clay connector commands resolve it automatically.`);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
381
396
|
if (provider === "pipe0" || provider === "explorium" || provider === "heyreach" || provider === "theirstack") {
|
|
382
397
|
rejectArgvSecret(args, "--token", "--key", "--api-key");
|
|
383
398
|
const key = await readSecret(`${provider} API key`);
|
|
@@ -393,7 +408,7 @@ export async function login(args: string[]) {
|
|
|
393
408
|
}
|
|
394
409
|
if (provider !== "hubspot") {
|
|
395
410
|
throw new Error(
|
|
396
|
-
"login supports: hubspot, salesforce, stripe, anthropic, openai, apollo, pipe0, explorium, or --via <hosted url>. Usage: fullstackgtm login <provider> | fullstackgtm login --via https://gtm.example.com",
|
|
411
|
+
"login supports: hubspot, salesforce, stripe, anthropic, openai, apollo, clay, pipe0, explorium, heyreach, theirstack, or --via <hosted url>. Usage: fullstackgtm login <provider> | fullstackgtm login --via https://gtm.example.com",
|
|
397
412
|
);
|
|
398
413
|
}
|
|
399
414
|
const now = new Date().toISOString();
|
|
@@ -491,6 +506,9 @@ export function doctorReport(env: Record<string, string | undefined> = process.e
|
|
|
491
506
|
stripe: env.STRIPE_SECRET_KEY
|
|
492
507
|
? { source: "env", detail: "STRIPE_SECRET_KEY" }
|
|
493
508
|
: providerStatus("stripe", broker),
|
|
509
|
+
clay: env.CLAY_API_KEY
|
|
510
|
+
? { source: "env", detail: "CLAY_API_KEY" }
|
|
511
|
+
: providerStatus("clay", null),
|
|
494
512
|
};
|
|
495
513
|
|
|
496
514
|
const llm = resolveLlmCredential(env);
|
|
@@ -503,14 +521,14 @@ export function doctorReport(env: Record<string, string | undefined> = process.e
|
|
|
503
521
|
}
|
|
504
522
|
});
|
|
505
523
|
|
|
506
|
-
const
|
|
524
|
+
const connectedCrm = ["hubspot", "salesforce"].filter((provider) => providers[provider].source !== "none");
|
|
507
525
|
const nextSteps =
|
|
508
|
-
|
|
526
|
+
connectedCrm.length === 0
|
|
509
527
|
? [
|
|
510
528
|
"fullstackgtm audit --demo # no credentials needed",
|
|
511
529
|
"fullstackgtm login hubspot # hosted browser OAuth (default; or: login salesforce)",
|
|
512
530
|
]
|
|
513
|
-
: [`fullstackgtm audit --provider ${
|
|
531
|
+
: [`fullstackgtm audit --provider ${connectedCrm[0]}`];
|
|
514
532
|
|
|
515
533
|
return {
|
|
516
534
|
package: packageInfo,
|
|
@@ -638,6 +656,30 @@ export async function doctorCommand(args: string[]) {
|
|
|
638
656
|
workspace.auditCount === 0
|
|
639
657
|
? p.dim("no saved audits yet (fullstackgtm audit --provider <name> --save starts the timeline)")
|
|
640
658
|
: `${scoreColor(workspace.healthScore ?? 0, p)}/100${delta} — ${workspace.auditCount} audit(s) saved, last ${workspace.lastAuditAt}${healthSparkline(workspace.profile, p.enabled)}`;
|
|
659
|
+
const connectedProviders = Object.entries(report.providers).filter(([, provider]) => provider.source !== "none");
|
|
660
|
+
const blockers = [
|
|
661
|
+
...(!report.node.ok ? [`Node v${report.node.version} is unsupported; install ${report.node.required}.`] : []),
|
|
662
|
+
...(connectedProviders.length === 0 ? ["No CRM connected."] : []),
|
|
663
|
+
...(workspace.pendingPlans.length > 0 ? [`${workspace.pendingPlans.length} plan${workspace.pendingPlans.length === 1 ? "" : "s"} awaiting approval.`] : []),
|
|
664
|
+
];
|
|
665
|
+
if (!args.includes("--verbose")) {
|
|
666
|
+
const ready = report.node.ok && connectedProviders.length > 0;
|
|
667
|
+
const compact = [
|
|
668
|
+
`${ready ? p.green("Ready") : p.yellow("Setup needed")} · ${report.package.name} ${report.package.version} · profile ${report.profile}`,
|
|
669
|
+
`CRM ${connectedProviders.length > 0 ? connectedProviders.map(([name]) => name).join(", ") : "not connected"}`,
|
|
670
|
+
`Health ${healthLine}`,
|
|
671
|
+
`Plans ${workspace.pendingPlans.length === 0 ? "none awaiting approval" : `${workspace.pendingPlans.length} awaiting approval · ${workspace.pendingPlans[0].id}`}`,
|
|
672
|
+
...(blockers.length > 0 ? ["", "Attention", ...blockers.map((blocker) => ` ${blocker}`)] : []),
|
|
673
|
+
"",
|
|
674
|
+
"Next",
|
|
675
|
+
...nextSteps.map((step) => ` ${step}`),
|
|
676
|
+
"",
|
|
677
|
+
"Run `fullstackgtm doctor --verbose` for credential paths and optional tooling checks.",
|
|
678
|
+
];
|
|
679
|
+
console.log(p.enabled ? box(compact, p, "Workspace readiness").join("\n") : compact.join("\n"));
|
|
680
|
+
if (!report.node.ok) process.exitCode = 1;
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
641
683
|
const lines = [
|
|
642
684
|
`Package: ${p.bold(`${report.package.name} ${report.package.version}`)}`,
|
|
643
685
|
`Node: v${report.node.version} (${report.node.required} required) ${mark(report.node.ok)}`,
|
package/src/cli/backfill.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { progressReporter } from "../runReport.ts";
|
|
|
15
15
|
import { unknownSubcommandError } from "./suggest.ts";
|
|
16
16
|
import { option, readSnapshot, saveRequested } from "./shared.ts";
|
|
17
17
|
import { colorEnabled, createProgressRenderer, paint, stylizePlanMarkdown, table } from "./ui.ts";
|
|
18
|
+
import { compactPlan, verbosePlanRequested } from "./planOutput.ts";
|
|
18
19
|
|
|
19
20
|
/** STRIPE_SECRET_KEY env ∨ stored `login stripe` credential (same ladder as `--provider stripe`). */
|
|
20
21
|
function resolveStripeKey(): string {
|
|
@@ -93,6 +94,8 @@ export async function backfillCommand(args: string[]) {
|
|
|
93
94
|
2,
|
|
94
95
|
),
|
|
95
96
|
);
|
|
97
|
+
} else if (!verbosePlanRequested(rest)) {
|
|
98
|
+
console.log(compactPlan(result.plan, { saved: save && result.plan.operations.length > 0 }));
|
|
96
99
|
} else {
|
|
97
100
|
// TTY-only styling; piped output stays byte-identical plain text.
|
|
98
101
|
console.log(
|
package/src/cli/call.ts
CHANGED
|
@@ -177,11 +177,11 @@ score always needs a key (scoring is LLM work).`);
|
|
|
177
177
|
if (!outPath) console.log(JSON.stringify(parsed, null, 2));
|
|
178
178
|
return;
|
|
179
179
|
}
|
|
180
|
-
console.log(`Call ${parsed.id}${parsed.title ? ` — ${parsed.title}` : ""}
|
|
181
|
-
console.log(`${parsed.segments.length} segments · ${parsed.insights.length} insights
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
180
|
+
console.log(`Call ${parsed.id}${parsed.title ? ` — ${parsed.title}` : ""}`);
|
|
181
|
+
console.log(`${parsed.segments.length} segments · ${parsed.insights.length} insights · ${parsed.summary.highImportance} high priority`);
|
|
182
|
+
const visible = rest.includes("--verbose") ? parsed.insights : parsed.insights.filter((insight) => insight.importance >= 3);
|
|
183
|
+
for (const insight of visible) console.log(`\n${insight.type.replaceAll("_", " ")} · priority ${insight.importance}\n ${insight.text.replace(/\s+/g, " ").trim()}`);
|
|
184
|
+
if (!rest.includes("--verbose") && visible.length < parsed.insights.length) console.log(`\n${parsed.insights.length - visible.length} lower-priority insight(s) hidden; use --verbose to show all.`);
|
|
185
185
|
return;
|
|
186
186
|
}
|
|
187
187
|
|
|
@@ -301,13 +301,32 @@ score always needs a key (scoring is LLM work).`);
|
|
|
301
301
|
console.log(JSON.stringify(scorecard, null, 2));
|
|
302
302
|
return;
|
|
303
303
|
}
|
|
304
|
-
console.log(renderScorecard(scorecard, title));
|
|
304
|
+
console.log(rest.includes("--verbose") ? renderScorecard(scorecard, title) : renderCompactScorecard(scorecard, title));
|
|
305
305
|
return;
|
|
306
306
|
}
|
|
307
307
|
|
|
308
308
|
throw new Error(`call supports: parse, classify, link, plan, score (got ${subcommand ?? "nothing"})`);
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
+
function renderCompactScorecard(scorecard: CallScorecard, title?: string): string {
|
|
312
|
+
const lines = [
|
|
313
|
+
`${title ?? "Call"} · ${scorecard.overallScore}/${scorecard.scale}${scorecard.band ? ` · ${scorecard.band.label}` : ""}`,
|
|
314
|
+
scorecard.rubricName ? `${scorecard.rubricName}${scorecard.callType ? ` (${scorecard.callType})` : ""}` : "",
|
|
315
|
+
"",
|
|
316
|
+
].filter((line, index) => line || index === 2);
|
|
317
|
+
for (const dimension of scorecard.dimensions) {
|
|
318
|
+
lines.push(`${dimension.score}/${dimension.maxScore} ${dimension.name}`);
|
|
319
|
+
lines.push(` ${dimension.coachingNote.replace(/\s+/g, " ").trim()}`);
|
|
320
|
+
}
|
|
321
|
+
if (scorecard.missedItems.length) {
|
|
322
|
+
lines.push("", "Focus next");
|
|
323
|
+
for (const item of scorecard.missedItems) lines.push(` ${item}`);
|
|
324
|
+
}
|
|
325
|
+
if (scorecard.highlights.length) lines.push("", `Strengths: ${scorecard.highlights.join(" · ")}`);
|
|
326
|
+
lines.push("", "Use --verbose for the full coaching scorecard.");
|
|
327
|
+
return lines.join("\n");
|
|
328
|
+
}
|
|
329
|
+
|
|
311
330
|
function renderScorecard(scorecard: CallScorecard, title?: string): string {
|
|
312
331
|
const bandText = scorecard.band ? ` — ${scorecard.band.label}` : "";
|
|
313
332
|
const rubricLine = scorecard.rubricName ? `Rubric: ${scorecard.rubricName}${scorecard.callType ? ` (${scorecard.callType})` : ""}` : "";
|
package/src/cli/draft.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { createFileJudgeStore } from "../judge.ts";
|
|
|
9
9
|
import { authorOpeners, DEFAULT_DRAFT_PROMPT, DRAFT_CHANNELS, draft, type DraftChannel } from "../draft.ts";
|
|
10
10
|
import type { LlmCallOptions } from "../llm.ts";
|
|
11
11
|
import { numericOption, option, resolveLlmBaseUrls, saveRequested } from "./shared.ts";
|
|
12
|
+
import { compactPlan, verbosePlanRequested } from "./planOutput.ts";
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -78,7 +79,14 @@ LLM key it emits a clearly-labeled stub rather than fake authored copy.`);
|
|
|
78
79
|
openers,
|
|
79
80
|
});
|
|
80
81
|
|
|
81
|
-
|
|
82
|
+
if (args.includes("--json")) {
|
|
83
|
+
console.log(JSON.stringify({ drafts, rejected }, null, 2));
|
|
84
|
+
} else if (verbosePlanRequested(args)) {
|
|
85
|
+
console.log(JSON.stringify({ drafts, rejected }, null, 2));
|
|
86
|
+
console.log(compactPlan(plan, { saved: save }));
|
|
87
|
+
} else {
|
|
88
|
+
console.log(compactPlan(plan, { saved: save }));
|
|
89
|
+
}
|
|
82
90
|
const stale = drafts.filter((d) => d.staleTrigger);
|
|
83
91
|
console.error(
|
|
84
92
|
`${drafts.length} opener(s) staged as create_task proposals (channel ${channel}, min score ${minScore})` +
|