fullstackgtm 0.44.0 → 0.45.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 +185 -1
- package/README.md +19 -7
- package/dist/audit.d.ts +3 -1
- package/dist/audit.js +29 -2
- package/dist/cli/audit.d.ts +15 -0
- package/dist/cli/audit.js +392 -0
- package/dist/cli/auth.d.ts +80 -0
- package/dist/cli/auth.js +500 -0
- package/dist/cli/call.d.ts +1 -0
- package/dist/cli/call.js +373 -0
- package/dist/cli/capabilities.d.ts +30 -0
- package/dist/cli/capabilities.js +197 -0
- package/dist/cli/draft.d.ts +7 -0
- package/dist/cli/draft.js +87 -0
- package/dist/cli/enrich.d.ts +8 -0
- package/dist/cli/enrich.js +788 -0
- package/dist/cli/fix.d.ts +33 -0
- package/dist/cli/fix.js +344 -0
- package/dist/cli/help.d.ts +25 -0
- package/dist/cli/help.js +609 -0
- package/dist/cli/icp.d.ts +7 -0
- package/dist/cli/icp.js +229 -0
- package/dist/cli/init.d.ts +7 -0
- package/dist/cli/init.js +58 -0
- package/dist/cli/market.d.ts +1 -0
- package/dist/cli/market.js +391 -0
- package/dist/cli/plans.d.ts +5 -0
- package/dist/cli/plans.js +454 -0
- package/dist/cli/schedule.d.ts +8 -0
- package/dist/cli/schedule.js +479 -0
- package/dist/cli/shared.d.ts +70 -0
- package/dist/cli/shared.js +331 -0
- package/dist/cli/signals.d.ts +7 -0
- package/dist/cli/signals.js +412 -0
- package/dist/cli/suggest.d.ts +49 -0
- package/dist/cli/suggest.js +135 -0
- package/dist/cli/tam.d.ts +9 -0
- package/dist/cli/tam.js +387 -0
- package/dist/cli/ui.d.ts +121 -0
- package/dist/cli/ui.js +375 -0
- package/dist/cli.d.ts +1 -57
- package/dist/cli.js +100 -5130
- package/dist/connector.d.ts +15 -0
- package/dist/connector.js +35 -0
- package/dist/connectors/hubspot.d.ts +3 -1
- package/dist/connectors/hubspot.js +10 -3
- package/dist/connectors/salesforce.d.ts +3 -1
- package/dist/connectors/salesforce.js +12 -5
- package/dist/connectors/signalSources.js +7 -59
- package/dist/icp.d.ts +15 -11
- package/dist/icp.js +19 -36
- package/dist/judge.d.ts +2 -0
- package/dist/judge.js +6 -0
- package/dist/marketClassify.d.ts +2 -0
- package/dist/marketClassify.js +7 -1
- package/dist/mcp-bin.js +2 -2
- package/dist/mcp.js +259 -166
- package/dist/schedule.d.ts +80 -2
- package/dist/schedule.js +254 -1
- package/dist/spoolFiles.d.ts +44 -0
- package/dist/spoolFiles.js +114 -0
- package/dist/types.d.ts +11 -0
- package/docs/api.md +73 -7
- package/docs/signal-spool-format.md +13 -0
- package/llms.txt +15 -6
- package/package.json +1 -1
- package/skills/fullstackgtm/SKILL.md +1 -1
- package/src/audit.ts +27 -1
- package/src/cli/audit.ts +447 -0
- package/src/cli/auth.ts +549 -0
- package/src/cli/call.ts +398 -0
- package/src/cli/capabilities.ts +215 -0
- package/src/cli/draft.ts +101 -0
- package/src/cli/enrich.ts +885 -0
- package/src/cli/fix.ts +372 -0
- package/src/cli/help.ts +664 -0
- package/src/cli/icp.ts +265 -0
- package/src/cli/init.ts +65 -0
- package/src/cli/market.ts +423 -0
- package/src/cli/plans.ts +523 -0
- package/src/cli/schedule.ts +526 -0
- package/src/cli/shared.ts +375 -0
- package/src/cli/signals.ts +434 -0
- package/src/cli/suggest.ts +151 -0
- package/src/cli/tam.ts +435 -0
- package/src/cli/ui.ts +426 -0
- package/src/cli.ts +99 -5829
- package/src/connector.ts +46 -0
- package/src/connectors/hubspot.ts +14 -2
- package/src/connectors/salesforce.ts +18 -2
- package/src/connectors/signalSources.ts +7 -56
- package/src/icp.ts +19 -35
- package/src/judge.ts +7 -0
- package/src/marketClassify.ts +8 -1
- package/src/mcp-bin.ts +2 -2
- package/src/mcp.ts +130 -57
- package/src/schedule.ts +310 -3
- package/src/spoolFiles.ts +116 -0
- package/src/types.ts +12 -0
package/dist/connector.d.ts
CHANGED
|
@@ -24,6 +24,21 @@ export type ApplyPatchPlanOptions = {
|
|
|
24
24
|
* edited mid-apply is conflicted out instead of overwritten. Default 25.
|
|
25
25
|
*/
|
|
26
26
|
recheckEvery?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Per-operation progress as the run executes (presentation only — a
|
|
29
|
+
* throwing callback never affects the run). `completed` counts every
|
|
30
|
+
* resolved operation including skips and conflicts; `total` is the plan's
|
|
31
|
+
* full operation count.
|
|
32
|
+
*/
|
|
33
|
+
onOperation?: (progress: ApplyProgress) => void;
|
|
34
|
+
};
|
|
35
|
+
export type ApplyProgress = {
|
|
36
|
+
completed: number;
|
|
37
|
+
total: number;
|
|
38
|
+
applied: number;
|
|
39
|
+
failed: number;
|
|
40
|
+
conflicts: number;
|
|
41
|
+
skipped: number;
|
|
27
42
|
};
|
|
28
43
|
/**
|
|
29
44
|
* Apply an approved subset of a patch plan through a connector.
|
package/dist/connector.js
CHANGED
|
@@ -253,7 +253,41 @@ export async function applyPatchPlan(connector, plan, options) {
|
|
|
253
253
|
batched.set(result.operationId, result);
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
|
+
// Progress ticker state: every loop iteration below pushes at most one
|
|
257
|
+
// result; `lastNotified` guards the rare early-throw path so an iteration
|
|
258
|
+
// that pushed nothing reports nothing.
|
|
259
|
+
const progressCounts = { applied: 0, failed: 0, conflicts: 0, skipped: 0 };
|
|
260
|
+
const resultsBefore = results.length;
|
|
261
|
+
let lastNotified = results.length;
|
|
262
|
+
const notifyProgress = () => {
|
|
263
|
+
if (!options.onOperation || results.length === lastNotified)
|
|
264
|
+
return;
|
|
265
|
+
lastNotified = results.length;
|
|
266
|
+
const last = results[results.length - 1];
|
|
267
|
+
if (last.status === "applied")
|
|
268
|
+
progressCounts.applied += 1;
|
|
269
|
+
else if (last.status === "failed")
|
|
270
|
+
progressCounts.failed += 1;
|
|
271
|
+
else if (last.status === "conflict")
|
|
272
|
+
progressCounts.conflicts += 1;
|
|
273
|
+
else
|
|
274
|
+
progressCounts.skipped += 1;
|
|
275
|
+
try {
|
|
276
|
+
options.onOperation({
|
|
277
|
+
completed: results.length - resultsBefore,
|
|
278
|
+
total: plan.operations.length,
|
|
279
|
+
...progressCounts,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
// progress is presentation-only
|
|
284
|
+
}
|
|
285
|
+
};
|
|
256
286
|
for (const operation of plan.operations) {
|
|
287
|
+
// Report the previous iteration's result (guarded: no-op if it pushed
|
|
288
|
+
// nothing). One-iteration lag keeps this a two-line hook instead of a
|
|
289
|
+
// notify after all ten push sites; the loop-exit call below flushes the last.
|
|
290
|
+
notifyProgress();
|
|
257
291
|
const batchedResult = batched.get(operation.id);
|
|
258
292
|
if (batchedResult) {
|
|
259
293
|
results.push(batchedResult);
|
|
@@ -342,6 +376,7 @@ export async function applyPatchPlan(connector, plan, options) {
|
|
|
342
376
|
});
|
|
343
377
|
}
|
|
344
378
|
}
|
|
379
|
+
notifyProgress();
|
|
345
380
|
return {
|
|
346
381
|
planId: plan.id,
|
|
347
382
|
provider: connector.provider,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type FieldMappings } from "../mappings.ts";
|
|
2
|
-
import type { GtmConnector } from "../types.ts";
|
|
2
|
+
import type { GtmConnector, SnapshotProgress } from "../types.ts";
|
|
3
3
|
export type HubspotConnectorOptions = {
|
|
4
4
|
/** Returns a HubSpot access token (private app token or OAuth access token). */
|
|
5
5
|
getAccessToken: () => string | Promise<string>;
|
|
@@ -8,6 +8,8 @@ export type HubspotConnectorOptions = {
|
|
|
8
8
|
apiBaseUrl?: string;
|
|
9
9
|
/** Injectable fetch for testing. */
|
|
10
10
|
fetchImpl?: typeof fetch;
|
|
11
|
+
/** Per-page snapshot-pull progress (presentation only — errors are swallowed). */
|
|
12
|
+
onProgress?: (progress: SnapshotProgress) => void;
|
|
11
13
|
};
|
|
12
14
|
/**
|
|
13
15
|
* Reference connector for HubSpot.
|
|
@@ -89,7 +89,7 @@ export function createHubspotConnector(options) {
|
|
|
89
89
|
}
|
|
90
90
|
return map;
|
|
91
91
|
}
|
|
92
|
-
async function list(path) {
|
|
92
|
+
async function list(path, onPage) {
|
|
93
93
|
const results = [];
|
|
94
94
|
let after;
|
|
95
95
|
const seen = new Set();
|
|
@@ -104,12 +104,18 @@ export function createHubspotConnector(options) {
|
|
|
104
104
|
const separator = path.includes("?") ? "&" : "?";
|
|
105
105
|
const data = await request(`${path}${after ? `${separator}after=${encodeURIComponent(after)}` : ""}`);
|
|
106
106
|
results.push(...(data.results ?? []));
|
|
107
|
+
try {
|
|
108
|
+
onPage?.(results.length);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// progress is presentation-only; never let it fail a pull
|
|
112
|
+
}
|
|
107
113
|
after = data.paging?.next?.after;
|
|
108
114
|
} while (after);
|
|
109
115
|
return results;
|
|
110
116
|
}
|
|
111
117
|
async function assembleSnapshot(fetchObjects) {
|
|
112
|
-
const owners = await list("/crm/v3/owners?limit=100");
|
|
118
|
+
const owners = await list("/crm/v3/owners?limit=100", (fetched) => options.onProgress?.({ objectType: "user", fetched }));
|
|
113
119
|
const users = owners
|
|
114
120
|
.filter((owner) => owner.id)
|
|
115
121
|
.map((owner) => ({
|
|
@@ -235,7 +241,8 @@ export function createHubspotConnector(options) {
|
|
|
235
241
|
};
|
|
236
242
|
}
|
|
237
243
|
async function fetchSnapshot() {
|
|
238
|
-
|
|
244
|
+
const canonicalType = { companies: "account", contacts: "contact", deals: "deal" };
|
|
245
|
+
return assembleSnapshot((objectType, properties, withAssociations) => list(`/crm/v3/objects/${objectType}?limit=100&properties=${properties}${withAssociations ? "&associations=companies" : ""}`, (fetched) => options.onProgress?.({ objectType: canonicalType[objectType], fetched })));
|
|
239
246
|
}
|
|
240
247
|
const MODIFIED_DATE_PROPERTIES = {
|
|
241
248
|
companies: "hs_lastmodifieddate",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type FieldMappings } from "../mappings.ts";
|
|
2
|
-
import type { GtmConnector } from "../types.ts";
|
|
2
|
+
import type { GtmConnector, SnapshotProgress } from "../types.ts";
|
|
3
3
|
export type SalesforceConnection = {
|
|
4
4
|
accessToken: string;
|
|
5
5
|
/** e.g. https://yourorg.my.salesforce.com */
|
|
@@ -13,6 +13,8 @@ export type SalesforceConnectorOptions = {
|
|
|
13
13
|
apiVersion?: string;
|
|
14
14
|
/** Injectable fetch for testing. */
|
|
15
15
|
fetchImpl?: typeof fetch;
|
|
16
|
+
/** Per-page snapshot-pull progress (presentation only — errors are swallowed). */
|
|
17
|
+
onProgress?: (progress: SnapshotProgress) => void;
|
|
16
18
|
};
|
|
17
19
|
/**
|
|
18
20
|
* Reference connector for Salesforce.
|
|
@@ -116,7 +116,7 @@ export function createSalesforceConnector(options) {
|
|
|
116
116
|
const text = await response.text();
|
|
117
117
|
return text ? JSON.parse(text) : null;
|
|
118
118
|
}
|
|
119
|
-
async function query(soql) {
|
|
119
|
+
async function query(soql, onPage) {
|
|
120
120
|
const records = [];
|
|
121
121
|
let next = `/services/data/${apiVersion}/query?q=${encodeURIComponent(soql)}`;
|
|
122
122
|
const seen = new Set();
|
|
@@ -127,6 +127,12 @@ export function createSalesforceConnector(options) {
|
|
|
127
127
|
seen.add(next);
|
|
128
128
|
const data = await request(next);
|
|
129
129
|
records.push(...(data?.records ?? []));
|
|
130
|
+
try {
|
|
131
|
+
onPage?.(records.length);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// progress is presentation-only; never let it fail a pull
|
|
135
|
+
}
|
|
130
136
|
next = data?.nextRecordsUrl ?? undefined;
|
|
131
137
|
}
|
|
132
138
|
return records;
|
|
@@ -138,7 +144,8 @@ export function createSalesforceConnector(options) {
|
|
|
138
144
|
return mappedFields(mappings, objectType, SALESFORCE_DEFAULT_FIELD_MAPPINGS[objectType]).join(", ");
|
|
139
145
|
}
|
|
140
146
|
async function assembleSnapshot(whereClause) {
|
|
141
|
-
const
|
|
147
|
+
const progressFor = (objectType) => (fetched) => options.onProgress?.({ objectType, fetched });
|
|
148
|
+
const sfUsers = await query(`SELECT ${selectFields("owners")} FROM User${whereClause}`, progressFor("user"));
|
|
142
149
|
const users = sfUsers.map((user) => {
|
|
143
150
|
const id = String(readMapped(user, "owners", "id", "Id"));
|
|
144
151
|
const email = stringOrUndefined(readMapped(user, "owners", "email", "Email"));
|
|
@@ -153,7 +160,7 @@ export function createSalesforceConnector(options) {
|
|
|
153
160
|
active: Boolean(readMapped(user, "owners", "isActive", "IsActive")),
|
|
154
161
|
};
|
|
155
162
|
});
|
|
156
|
-
const sfAccounts = await query(`SELECT ${selectFields("accounts")} FROM Account${whereClause}
|
|
163
|
+
const sfAccounts = await query(`SELECT ${selectFields("accounts")} FROM Account${whereClause}`, progressFor("account"));
|
|
157
164
|
const accounts = sfAccounts.map((account) => {
|
|
158
165
|
const id = String(readMapped(account, "accounts", "id", "Id"));
|
|
159
166
|
return {
|
|
@@ -170,7 +177,7 @@ export function createSalesforceConnector(options) {
|
|
|
170
177
|
raw: account,
|
|
171
178
|
};
|
|
172
179
|
});
|
|
173
|
-
const sfContacts = await query(`SELECT ${selectFields("contacts")} FROM Contact${whereClause}
|
|
180
|
+
const sfContacts = await query(`SELECT ${selectFields("contacts")} FROM Contact${whereClause}`, progressFor("contact"));
|
|
174
181
|
const contacts = sfContacts.map((contact) => {
|
|
175
182
|
const id = String(readMapped(contact, "contacts", "id", "Id"));
|
|
176
183
|
return {
|
|
@@ -188,7 +195,7 @@ export function createSalesforceConnector(options) {
|
|
|
188
195
|
raw: contact,
|
|
189
196
|
};
|
|
190
197
|
});
|
|
191
|
-
const sfOpportunities = await query(`SELECT ${selectFields("deals")} FROM Opportunity${whereClause}
|
|
198
|
+
const sfOpportunities = await query(`SELECT ${selectFields("deals")} FROM Opportunity${whereClause}`, progressFor("deal"));
|
|
192
199
|
const deals = sfOpportunities.map((opportunity) => {
|
|
193
200
|
const id = String(readMapped(opportunity, "deals", "id", "Id"));
|
|
194
201
|
const probability = numberOrUndefined(readMapped(opportunity, "deals", "probability", "Probability"));
|
|
@@ -18,9 +18,10 @@
|
|
|
18
18
|
* the caller), it must never sink a multi-source run — the per-provider
|
|
19
19
|
* try/catch idiom of atsBoards/prospectSources.
|
|
20
20
|
*/
|
|
21
|
-
import { readFileSync,
|
|
22
|
-
import {
|
|
21
|
+
import { readFileSync, statSync } from "node:fs";
|
|
22
|
+
import { resolve } from "node:path";
|
|
23
23
|
import { SIGNAL_BUCKETS } from "../signals.js";
|
|
24
|
+
import { parseSpoolText, spoolFilesIn } from "../spoolFiles.js";
|
|
24
25
|
// ---------------------------------------------------------------------------
|
|
25
26
|
// file — local JSON / JSONL intake (also the webhook landing-zone reader)
|
|
26
27
|
/**
|
|
@@ -59,6 +60,9 @@ export const fileSource = {
|
|
|
59
60
|
catch {
|
|
60
61
|
return []; // missing path: an empty source, not a crash
|
|
61
62
|
}
|
|
63
|
+
// Container parsing lives in the shared spool reader (src/spoolFiles.ts) so
|
|
64
|
+
// `signals fetch --from`, `enrich ingest`, and `market observe --from` read
|
|
65
|
+
// the same convention; the "file source" label keeps error text unchanged.
|
|
62
66
|
const files = isDir ? spoolFilesIn(abs) : [abs];
|
|
63
67
|
const rows = [];
|
|
64
68
|
for (const file of files) {
|
|
@@ -69,67 +73,11 @@ export const fileSource = {
|
|
|
69
73
|
catch {
|
|
70
74
|
continue; // one unreadable file in a spool dir must not sink the rest
|
|
71
75
|
}
|
|
72
|
-
|
|
73
|
-
if (!trimmed)
|
|
74
|
-
continue;
|
|
75
|
-
const parsed = trimmed.startsWith("[") ? parseJsonArray(trimmed, file) : parseJsonl(trimmed, file);
|
|
76
|
-
rows.push(...parsed);
|
|
76
|
+
rows.push(...parseSpoolText(raw, file, "file source"));
|
|
77
77
|
}
|
|
78
78
|
return rows.map((row) => coerceRow(row, this.bucket));
|
|
79
79
|
},
|
|
80
80
|
};
|
|
81
|
-
/** Spool files in a landing-zone directory: `*.jsonl` / `*.json`, name-sorted. */
|
|
82
|
-
function spoolFilesIn(dir) {
|
|
83
|
-
let names;
|
|
84
|
-
try {
|
|
85
|
-
names = readdirSync(dir);
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
return [];
|
|
89
|
-
}
|
|
90
|
-
return names
|
|
91
|
-
.filter((name) => name.endsWith(".jsonl") || name.endsWith(".json"))
|
|
92
|
-
.sort()
|
|
93
|
-
.map((name) => join(dir, name));
|
|
94
|
-
}
|
|
95
|
-
function parseJsonArray(raw, path) {
|
|
96
|
-
let parsed;
|
|
97
|
-
try {
|
|
98
|
-
parsed = JSON.parse(raw);
|
|
99
|
-
}
|
|
100
|
-
catch (error) {
|
|
101
|
-
throw new Error(`file source ${path}: not valid JSON (${error instanceof Error ? error.message : String(error)}).`);
|
|
102
|
-
}
|
|
103
|
-
if (!Array.isArray(parsed))
|
|
104
|
-
throw new Error(`file source ${path}: expected a JSON array of staged signal rows.`);
|
|
105
|
-
return parsed.map((entry, index) => {
|
|
106
|
-
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
107
|
-
throw new Error(`file source ${path}: row ${index} is not an object.`);
|
|
108
|
-
}
|
|
109
|
-
return entry;
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
function parseJsonl(raw, path) {
|
|
113
|
-
const out = [];
|
|
114
|
-
const lines = raw.split("\n");
|
|
115
|
-
lines.forEach((line, index) => {
|
|
116
|
-
const t = line.trim();
|
|
117
|
-
if (!t)
|
|
118
|
-
return;
|
|
119
|
-
let parsed;
|
|
120
|
-
try {
|
|
121
|
-
parsed = JSON.parse(t);
|
|
122
|
-
}
|
|
123
|
-
catch (error) {
|
|
124
|
-
throw new Error(`file source ${path}: line ${index} is not valid JSON (${error instanceof Error ? error.message : String(error)}).`);
|
|
125
|
-
}
|
|
126
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
127
|
-
throw new Error(`file source ${path}: line ${index} is not a JSON object.`);
|
|
128
|
-
}
|
|
129
|
-
out.push(parsed);
|
|
130
|
-
});
|
|
131
|
-
return out;
|
|
132
|
-
}
|
|
133
81
|
// ---------------------------------------------------------------------------
|
|
134
82
|
// serpapi-news — funding/company signals from a news REST query (API key)
|
|
135
83
|
const SERPAPI_BASE_URL = "https://serpapi.com";
|
package/dist/icp.d.ts
CHANGED
|
@@ -86,18 +86,22 @@ export declare function icpToTheirStackFilters(icp: Icp): {
|
|
|
86
86
|
/**
|
|
87
87
|
* pipe0 Crustdata people-search config.filters from the ICP. `current_job_titles`
|
|
88
88
|
* matches real LinkedIn title strings (case-sensitive), so keywords are Title
|
|
89
|
-
* Cased.
|
|
90
|
-
* tables above.
|
|
89
|
+
* Cased. Industry is mapped to Crustdata's controlled vocab via the table above.
|
|
91
90
|
*
|
|
92
|
-
* LIVE FINDINGS (2026-06-26
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
91
|
+
* LIVE FINDINGS (2026-06-26, re-confirmed 2026-07-02 with fresh credits):
|
|
92
|
+
* `current_job_titles` works; `current_title` is rejected (422). The culprit
|
|
93
|
+
* that zeroed the full ICP filter is `current_seniority_levels`: ANY non-empty
|
|
94
|
+
* `include` returns 0 results through pipe0's people:profiles:crustdata@1 —
|
|
95
|
+
* including Crustdata's own documented values ("CXO", "Director",
|
|
96
|
+
* "Vice President"), lowercase, and SNAKE_CASE variants — while the identical
|
|
97
|
+
* search without it returns results. (An array instead of the
|
|
98
|
+
* {include, exclude} object is a 422, so the shape was right; the filter is
|
|
99
|
+
* broken upstream.) So job levels are NOT sent to the provider: persona
|
|
100
|
+
* seniority is enforced by fit scoring (scoreProspectAgainstIcp weights
|
|
101
|
+
* jobLevels), which was always the precision backstop. `locations` and
|
|
102
|
+
* `current_employers_linkedin_industries` (both-taxonomy hedge) are live-
|
|
103
|
+
* verified working in combination with titles. Note fit-scoring backs up
|
|
104
|
+
* PERSONA precision but NOT industry, so the industry filter is load-bearing.
|
|
101
105
|
*/
|
|
102
106
|
export declare function icpToCrustdataFilters(icp: Icp): Record<string, unknown>;
|
|
103
107
|
export type IcpFit = {
|
package/dist/icp.js
CHANGED
|
@@ -125,26 +125,10 @@ export function icpToTheirStackFilters(icp) {
|
|
|
125
125
|
f.max_employee_count = range.max;
|
|
126
126
|
return f;
|
|
127
127
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
*/
|
|
133
|
-
const CRUSTDATA_SENIORITY = {
|
|
134
|
-
cxo: "CXO",
|
|
135
|
-
"c-suite": "CXO",
|
|
136
|
-
c_suite: "CXO",
|
|
137
|
-
founder: "Owner",
|
|
138
|
-
owner: "Owner",
|
|
139
|
-
partner: "Partner",
|
|
140
|
-
vp: "Vice President",
|
|
141
|
-
"vice president": "Vice President",
|
|
142
|
-
head: "Director",
|
|
143
|
-
director: "Director",
|
|
144
|
-
manager: "Manager",
|
|
145
|
-
senior: "Senior",
|
|
146
|
-
entry: "Entry",
|
|
147
|
-
};
|
|
128
|
+
// NOTE: a CRUSTDATA_SENIORITY vocab table lived here until 2026-07-02 — the
|
|
129
|
+
// `current_seniority_levels` filter is broken upstream (see the live findings
|
|
130
|
+
// on icpToCrustdataFilters below), so job levels are no longer sent to the
|
|
131
|
+
// provider at all. Resurrect the table from git history if pipe0 fixes it.
|
|
148
132
|
/**
|
|
149
133
|
* ICP industry keyword → LinkedIn industry names Crustdata filters on.
|
|
150
134
|
*
|
|
@@ -177,18 +161,22 @@ const CRUSTDATA_INDUSTRY = {
|
|
|
177
161
|
/**
|
|
178
162
|
* pipe0 Crustdata people-search config.filters from the ICP. `current_job_titles`
|
|
179
163
|
* matches real LinkedIn title strings (case-sensitive), so keywords are Title
|
|
180
|
-
* Cased.
|
|
181
|
-
* tables above.
|
|
164
|
+
* Cased. Industry is mapped to Crustdata's controlled vocab via the table above.
|
|
182
165
|
*
|
|
183
|
-
* LIVE FINDINGS (2026-06-26
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
166
|
+
* LIVE FINDINGS (2026-06-26, re-confirmed 2026-07-02 with fresh credits):
|
|
167
|
+
* `current_job_titles` works; `current_title` is rejected (422). The culprit
|
|
168
|
+
* that zeroed the full ICP filter is `current_seniority_levels`: ANY non-empty
|
|
169
|
+
* `include` returns 0 results through pipe0's people:profiles:crustdata@1 —
|
|
170
|
+
* including Crustdata's own documented values ("CXO", "Director",
|
|
171
|
+
* "Vice President"), lowercase, and SNAKE_CASE variants — while the identical
|
|
172
|
+
* search without it returns results. (An array instead of the
|
|
173
|
+
* {include, exclude} object is a 422, so the shape was right; the filter is
|
|
174
|
+
* broken upstream.) So job levels are NOT sent to the provider: persona
|
|
175
|
+
* seniority is enforced by fit scoring (scoreProspectAgainstIcp weights
|
|
176
|
+
* jobLevels), which was always the precision backstop. `locations` and
|
|
177
|
+
* `current_employers_linkedin_industries` (both-taxonomy hedge) are live-
|
|
178
|
+
* verified working in combination with titles. Note fit-scoring backs up
|
|
179
|
+
* PERSONA precision but NOT industry, so the industry filter is load-bearing.
|
|
192
180
|
*/
|
|
193
181
|
export function icpToCrustdataFilters(icp) {
|
|
194
182
|
const f = {};
|
|
@@ -197,11 +185,6 @@ export function icpToCrustdataFilters(icp) {
|
|
|
197
185
|
if (icp.firmographics.geos?.length) {
|
|
198
186
|
f.locations = icp.firmographics.geos.map((g) => COUNTRY_NAMES[g.toLowerCase()] ?? g);
|
|
199
187
|
}
|
|
200
|
-
if (icp.persona.jobLevels?.length) {
|
|
201
|
-
const include = [...new Set(icp.persona.jobLevels.map((l) => CRUSTDATA_SENIORITY[l.toLowerCase()]).filter(Boolean))];
|
|
202
|
-
if (include.length)
|
|
203
|
-
f.current_seniority_levels = { include, exclude: [] };
|
|
204
|
-
}
|
|
205
188
|
if (icp.firmographics.industries?.length) {
|
|
206
189
|
const inds = [
|
|
207
190
|
...new Set(icp.firmographics.industries.flatMap((i) => CRUSTDATA_INDUSTRY[i.toLowerCase()] ?? [titleCase(i)])),
|
package/dist/judge.d.ts
CHANGED
|
@@ -230,6 +230,8 @@ export declare function judgeSignals(opts: {
|
|
|
230
230
|
promptTemplate?: string;
|
|
231
231
|
llm?: LlmCallOptions;
|
|
232
232
|
now?: Date;
|
|
233
|
+
/** Per-account progress (presentation only — a throwing callback never fails the run). */
|
|
234
|
+
onAccount?: (done: number, total: number, domain: string) => void;
|
|
233
235
|
}): Promise<JudgeDecision[]>;
|
|
234
236
|
export declare function judgeDir(baseDir?: string): string;
|
|
235
237
|
export interface JudgeStore {
|
package/dist/judge.js
CHANGED
|
@@ -438,6 +438,12 @@ export async function judgeSignals(opts) {
|
|
|
438
438
|
}
|
|
439
439
|
const decisions = [];
|
|
440
440
|
for (const [domain, signals] of byAccount) {
|
|
441
|
+
try {
|
|
442
|
+
opts.onAccount?.(decisions.length, byAccount.size, domain);
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
// progress is presentation-only
|
|
446
|
+
}
|
|
441
447
|
// Memory + fit stay gated on --with-history (scoring semantics unchanged).
|
|
442
448
|
const recentlyTouched = opts.withHistory && opts.snapshot ? accountRecentlyTouched(domain, opts.snapshot, now) : false;
|
|
443
449
|
const bestContact = opts.withHistory && opts.icp && opts.snapshot ? bestContactForAccount(domain, opts.snapshot) : undefined;
|
package/dist/marketClassify.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export type ClassifyMarketOptions = {
|
|
|
11
11
|
/** Captures directory override (tests); defaults to the profile market home. */
|
|
12
12
|
capturesDir?: string;
|
|
13
13
|
now?: () => Date;
|
|
14
|
+
/** Per-vendor progress (presentation only — a throwing callback never fails the run). */
|
|
15
|
+
onVendor?: (done: number, total: number, vendorId: string) => void;
|
|
14
16
|
};
|
|
15
17
|
export type ClassifyMarketResult = {
|
|
16
18
|
set: ObservationSet;
|
package/dist/marketClassify.js
CHANGED
|
@@ -92,7 +92,13 @@ export async function classifyMarket(config, options) {
|
|
|
92
92
|
const claimIds = config.claims.map((claim) => claim.id);
|
|
93
93
|
const observations = [];
|
|
94
94
|
const retriedVendorIds = [];
|
|
95
|
-
for (const vendorId of vendorIds) {
|
|
95
|
+
for (const [vendorIndex, vendorId] of vendorIds.entries()) {
|
|
96
|
+
try {
|
|
97
|
+
options.onVendor?.(vendorIndex, vendorIds.length, vendorId);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// progress is presentation-only
|
|
101
|
+
}
|
|
96
102
|
const vendor = config.vendors.find((candidate) => candidate.id === vendorId);
|
|
97
103
|
if (!vendor)
|
|
98
104
|
throw new Error(`Unknown vendor "${vendorId}"`);
|
package/dist/mcp-bin.js
CHANGED
|
@@ -7,8 +7,8 @@ const MISSING_PEER_HELP = `The MCP server needs the optional peer dependencies @
|
|
|
7
7
|
In a project:
|
|
8
8
|
npm install fullstackgtm @modelcontextprotocol/sdk zod
|
|
9
9
|
|
|
10
|
-
Zero-install:
|
|
11
|
-
npx -
|
|
10
|
+
Zero-install (the fullstackgtm-mcp wrapper package bundles the peers):
|
|
11
|
+
npx -y fullstackgtm-mcp`;
|
|
12
12
|
function isMissingPeerError(error) {
|
|
13
13
|
if (!(error instanceof Error))
|
|
14
14
|
return false;
|