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/src/connector.ts
CHANGED
|
@@ -105,6 +105,22 @@ export type ApplyPatchPlanOptions = {
|
|
|
105
105
|
* edited mid-apply is conflicted out instead of overwritten. Default 25.
|
|
106
106
|
*/
|
|
107
107
|
recheckEvery?: number;
|
|
108
|
+
/**
|
|
109
|
+
* Per-operation progress as the run executes (presentation only — a
|
|
110
|
+
* throwing callback never affects the run). `completed` counts every
|
|
111
|
+
* resolved operation including skips and conflicts; `total` is the plan's
|
|
112
|
+
* full operation count.
|
|
113
|
+
*/
|
|
114
|
+
onOperation?: (progress: ApplyProgress) => void;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export type ApplyProgress = {
|
|
118
|
+
completed: number;
|
|
119
|
+
total: number;
|
|
120
|
+
applied: number;
|
|
121
|
+
failed: number;
|
|
122
|
+
conflicts: number;
|
|
123
|
+
skipped: number;
|
|
108
124
|
};
|
|
109
125
|
|
|
110
126
|
const FIELD_WRITE_OPERATIONS = new Set(["set_field", "clear_field", "link_record"]);
|
|
@@ -313,7 +329,36 @@ export async function applyPatchPlan(
|
|
|
313
329
|
}
|
|
314
330
|
}
|
|
315
331
|
|
|
332
|
+
// Progress ticker state: every loop iteration below pushes at most one
|
|
333
|
+
// result; `lastNotified` guards the rare early-throw path so an iteration
|
|
334
|
+
// that pushed nothing reports nothing.
|
|
335
|
+
const progressCounts = { applied: 0, failed: 0, conflicts: 0, skipped: 0 };
|
|
336
|
+
const resultsBefore = results.length;
|
|
337
|
+
let lastNotified = results.length;
|
|
338
|
+
const notifyProgress = () => {
|
|
339
|
+
if (!options.onOperation || results.length === lastNotified) return;
|
|
340
|
+
lastNotified = results.length;
|
|
341
|
+
const last = results[results.length - 1];
|
|
342
|
+
if (last.status === "applied") progressCounts.applied += 1;
|
|
343
|
+
else if (last.status === "failed") progressCounts.failed += 1;
|
|
344
|
+
else if (last.status === "conflict") progressCounts.conflicts += 1;
|
|
345
|
+
else progressCounts.skipped += 1;
|
|
346
|
+
try {
|
|
347
|
+
options.onOperation({
|
|
348
|
+
completed: results.length - resultsBefore,
|
|
349
|
+
total: plan.operations.length,
|
|
350
|
+
...progressCounts,
|
|
351
|
+
});
|
|
352
|
+
} catch {
|
|
353
|
+
// progress is presentation-only
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
|
|
316
357
|
for (const operation of plan.operations) {
|
|
358
|
+
// Report the previous iteration's result (guarded: no-op if it pushed
|
|
359
|
+
// nothing). One-iteration lag keeps this a two-line hook instead of a
|
|
360
|
+
// notify after all ten push sites; the loop-exit call below flushes the last.
|
|
361
|
+
notifyProgress();
|
|
317
362
|
const batchedResult = batched.get(operation.id);
|
|
318
363
|
if (batchedResult) {
|
|
319
364
|
results.push(batchedResult);
|
|
@@ -403,6 +448,7 @@ export async function applyPatchPlan(
|
|
|
403
448
|
});
|
|
404
449
|
}
|
|
405
450
|
}
|
|
451
|
+
notifyProgress();
|
|
406
452
|
|
|
407
453
|
return {
|
|
408
454
|
planId: plan.id,
|
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
GtmObjectType,
|
|
19
19
|
PatchOperation,
|
|
20
20
|
PatchOperationResult,
|
|
21
|
+
SnapshotProgress,
|
|
21
22
|
} from "../types.ts";
|
|
22
23
|
|
|
23
24
|
const DEFAULT_API_BASE_URL = "https://api.hubapi.com";
|
|
@@ -30,6 +31,8 @@ export type HubspotConnectorOptions = {
|
|
|
30
31
|
apiBaseUrl?: string;
|
|
31
32
|
/** Injectable fetch for testing. */
|
|
32
33
|
fetchImpl?: typeof fetch;
|
|
34
|
+
/** Per-page snapshot-pull progress (presentation only — errors are swallowed). */
|
|
35
|
+
onProgress?: (progress: SnapshotProgress) => void;
|
|
33
36
|
};
|
|
34
37
|
|
|
35
38
|
const OBJECT_PATHS: Partial<Record<GtmObjectType, string>> = {
|
|
@@ -123,7 +126,7 @@ export function createHubspotConnector(options: HubspotConnectorOptions): Requir
|
|
|
123
126
|
return map;
|
|
124
127
|
}
|
|
125
128
|
|
|
126
|
-
async function list(path: string): Promise<any[]> {
|
|
129
|
+
async function list(path: string, onPage?: (fetched: number) => void): Promise<any[]> {
|
|
127
130
|
const results: any[] = [];
|
|
128
131
|
let after: string | undefined;
|
|
129
132
|
const seen = new Set<string>();
|
|
@@ -137,6 +140,11 @@ export function createHubspotConnector(options: HubspotConnectorOptions): Requir
|
|
|
137
140
|
const separator = path.includes("?") ? "&" : "?";
|
|
138
141
|
const data = await request(`${path}${after ? `${separator}after=${encodeURIComponent(after)}` : ""}`);
|
|
139
142
|
results.push(...(data.results ?? []));
|
|
143
|
+
try {
|
|
144
|
+
onPage?.(results.length);
|
|
145
|
+
} catch {
|
|
146
|
+
// progress is presentation-only; never let it fail a pull
|
|
147
|
+
}
|
|
140
148
|
after = data.paging?.next?.after;
|
|
141
149
|
} while (after);
|
|
142
150
|
return results;
|
|
@@ -149,7 +157,9 @@ export function createHubspotConnector(options: HubspotConnectorOptions): Requir
|
|
|
149
157
|
withAssociations: boolean,
|
|
150
158
|
) => Promise<any[]>,
|
|
151
159
|
): Promise<CanonicalGtmSnapshot> {
|
|
152
|
-
const owners = await list("/crm/v3/owners?limit=100")
|
|
160
|
+
const owners = await list("/crm/v3/owners?limit=100", (fetched) =>
|
|
161
|
+
options.onProgress?.({ objectType: "user", fetched }),
|
|
162
|
+
);
|
|
153
163
|
const users: CanonicalUser[] = owners
|
|
154
164
|
.filter((owner) => owner.id)
|
|
155
165
|
.map((owner) => ({
|
|
@@ -310,9 +320,11 @@ export function createHubspotConnector(options: HubspotConnectorOptions): Requir
|
|
|
310
320
|
}
|
|
311
321
|
|
|
312
322
|
async function fetchSnapshot(): Promise<CanonicalGtmSnapshot> {
|
|
323
|
+
const canonicalType = { companies: "account", contacts: "contact", deals: "deal" } as const;
|
|
313
324
|
return assembleSnapshot((objectType, properties, withAssociations) =>
|
|
314
325
|
list(
|
|
315
326
|
`/crm/v3/objects/${objectType}?limit=100&properties=${properties}${withAssociations ? "&associations=companies" : ""}`,
|
|
327
|
+
(fetched) => options.onProgress?.({ objectType: canonicalType[objectType], fetched }),
|
|
316
328
|
),
|
|
317
329
|
);
|
|
318
330
|
}
|
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
GtmObjectType,
|
|
18
18
|
PatchOperation,
|
|
19
19
|
PatchOperationResult,
|
|
20
|
+
SnapshotProgress,
|
|
20
21
|
} from "../types.ts";
|
|
21
22
|
|
|
22
23
|
const DEFAULT_API_VERSION = "v59.0";
|
|
@@ -35,6 +36,8 @@ export type SalesforceConnectorOptions = {
|
|
|
35
36
|
apiVersion?: string;
|
|
36
37
|
/** Injectable fetch for testing. */
|
|
37
38
|
fetchImpl?: typeof fetch;
|
|
39
|
+
/** Per-page snapshot-pull progress (presentation only — errors are swallowed). */
|
|
40
|
+
onProgress?: (progress: SnapshotProgress) => void;
|
|
38
41
|
};
|
|
39
42
|
|
|
40
43
|
const SOBJECT_TYPES: Partial<Record<GtmObjectType, string>> = {
|
|
@@ -161,7 +164,7 @@ export function createSalesforceConnector(
|
|
|
161
164
|
return text ? JSON.parse(text) : null;
|
|
162
165
|
}
|
|
163
166
|
|
|
164
|
-
async function query(soql: string): Promise<any[]> {
|
|
167
|
+
async function query(soql: string, onPage?: (fetched: number) => void): Promise<any[]> {
|
|
165
168
|
const records: any[] = [];
|
|
166
169
|
let next: string | undefined = `/services/data/${apiVersion}/query?q=${encodeURIComponent(soql)}`;
|
|
167
170
|
const seen = new Set<string>();
|
|
@@ -171,6 +174,11 @@ export function createSalesforceConnector(
|
|
|
171
174
|
seen.add(next);
|
|
172
175
|
const data = await request(next);
|
|
173
176
|
records.push(...(data?.records ?? []));
|
|
177
|
+
try {
|
|
178
|
+
onPage?.(records.length);
|
|
179
|
+
} catch {
|
|
180
|
+
// progress is presentation-only; never let it fail a pull
|
|
181
|
+
}
|
|
174
182
|
next = data?.nextRecordsUrl ?? undefined;
|
|
175
183
|
}
|
|
176
184
|
return records;
|
|
@@ -194,7 +202,12 @@ export function createSalesforceConnector(
|
|
|
194
202
|
}
|
|
195
203
|
|
|
196
204
|
async function assembleSnapshot(whereClause: string): Promise<CanonicalGtmSnapshot> {
|
|
197
|
-
const
|
|
205
|
+
const progressFor = (objectType: SnapshotProgress["objectType"]) => (fetched: number) =>
|
|
206
|
+
options.onProgress?.({ objectType, fetched });
|
|
207
|
+
const sfUsers = await query(
|
|
208
|
+
`SELECT ${selectFields("owners")} FROM User${whereClause}`,
|
|
209
|
+
progressFor("user"),
|
|
210
|
+
);
|
|
198
211
|
const users: CanonicalUser[] = sfUsers.map((user) => {
|
|
199
212
|
const id = String(readMapped(user, "owners", "id", "Id"));
|
|
200
213
|
const email = stringOrUndefined(readMapped(user, "owners", "email", "Email"));
|
|
@@ -212,6 +225,7 @@ export function createSalesforceConnector(
|
|
|
212
225
|
|
|
213
226
|
const sfAccounts = await query(
|
|
214
227
|
`SELECT ${selectFields("accounts")} FROM Account${whereClause}`,
|
|
228
|
+
progressFor("account"),
|
|
215
229
|
);
|
|
216
230
|
const accounts: CanonicalAccount[] = sfAccounts.map((account) => {
|
|
217
231
|
const id = String(readMapped(account, "accounts", "id", "Id"));
|
|
@@ -236,6 +250,7 @@ export function createSalesforceConnector(
|
|
|
236
250
|
|
|
237
251
|
const sfContacts = await query(
|
|
238
252
|
`SELECT ${selectFields("contacts")} FROM Contact${whereClause}`,
|
|
253
|
+
progressFor("contact"),
|
|
239
254
|
);
|
|
240
255
|
const contacts: CanonicalContact[] = sfContacts.map((contact) => {
|
|
241
256
|
const id = String(readMapped(contact, "contacts", "id", "Id"));
|
|
@@ -257,6 +272,7 @@ export function createSalesforceConnector(
|
|
|
257
272
|
|
|
258
273
|
const sfOpportunities = await query(
|
|
259
274
|
`SELECT ${selectFields("deals")} FROM Opportunity${whereClause}`,
|
|
275
|
+
progressFor("deal"),
|
|
260
276
|
);
|
|
261
277
|
const deals: CanonicalDeal[] = sfOpportunities.map((opportunity) => {
|
|
262
278
|
const id = String(readMapped(opportunity, "deals", "id", "Id"));
|
|
@@ -19,10 +19,11 @@
|
|
|
19
19
|
* try/catch idiom of atsBoards/prospectSources.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import { readFileSync,
|
|
23
|
-
import {
|
|
22
|
+
import { readFileSync, statSync } from "node:fs";
|
|
23
|
+
import { resolve } from "node:path";
|
|
24
24
|
import type { SignalBucket, StagedSignalRow } from "../signals.ts";
|
|
25
25
|
import { SIGNAL_BUCKETS } from "../signals.ts";
|
|
26
|
+
import { parseSpoolText, spoolFilesIn } from "../spoolFiles.ts";
|
|
26
27
|
|
|
27
28
|
export type SignalSourceShape = "pull" | "push";
|
|
28
29
|
export type SignalSourceAuth = "none" | "api_key" | "oauth";
|
|
@@ -99,6 +100,9 @@ export const fileSource: SignalSourceConnector = {
|
|
|
99
100
|
} catch {
|
|
100
101
|
return []; // missing path: an empty source, not a crash
|
|
101
102
|
}
|
|
103
|
+
// Container parsing lives in the shared spool reader (src/spoolFiles.ts) so
|
|
104
|
+
// `signals fetch --from`, `enrich ingest`, and `market observe --from` read
|
|
105
|
+
// the same convention; the "file source" label keeps error text unchanged.
|
|
102
106
|
const files = isDir ? spoolFilesIn(abs) : [abs];
|
|
103
107
|
const rows: Record<string, unknown>[] = [];
|
|
104
108
|
for (const file of files) {
|
|
@@ -108,65 +112,12 @@ export const fileSource: SignalSourceConnector = {
|
|
|
108
112
|
} catch {
|
|
109
113
|
continue; // one unreadable file in a spool dir must not sink the rest
|
|
110
114
|
}
|
|
111
|
-
|
|
112
|
-
if (!trimmed) continue;
|
|
113
|
-
const parsed = trimmed.startsWith("[") ? parseJsonArray(trimmed, file) : parseJsonl(trimmed, file);
|
|
114
|
-
rows.push(...parsed);
|
|
115
|
+
rows.push(...parseSpoolText(raw, file, "file source"));
|
|
115
116
|
}
|
|
116
117
|
return rows.map((row) => coerceRow(row, this.bucket));
|
|
117
118
|
},
|
|
118
119
|
};
|
|
119
120
|
|
|
120
|
-
/** Spool files in a landing-zone directory: `*.jsonl` / `*.json`, name-sorted. */
|
|
121
|
-
function spoolFilesIn(dir: string): string[] {
|
|
122
|
-
let names: string[];
|
|
123
|
-
try {
|
|
124
|
-
names = readdirSync(dir);
|
|
125
|
-
} catch {
|
|
126
|
-
return [];
|
|
127
|
-
}
|
|
128
|
-
return names
|
|
129
|
-
.filter((name) => name.endsWith(".jsonl") || name.endsWith(".json"))
|
|
130
|
-
.sort()
|
|
131
|
-
.map((name) => join(dir, name));
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function parseJsonArray(raw: string, path: string): Record<string, unknown>[] {
|
|
135
|
-
let parsed: unknown;
|
|
136
|
-
try {
|
|
137
|
-
parsed = JSON.parse(raw);
|
|
138
|
-
} catch (error) {
|
|
139
|
-
throw new Error(`file source ${path}: not valid JSON (${error instanceof Error ? error.message : String(error)}).`);
|
|
140
|
-
}
|
|
141
|
-
if (!Array.isArray(parsed)) throw new Error(`file source ${path}: expected a JSON array of staged signal rows.`);
|
|
142
|
-
return parsed.map((entry, index) => {
|
|
143
|
-
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
144
|
-
throw new Error(`file source ${path}: row ${index} is not an object.`);
|
|
145
|
-
}
|
|
146
|
-
return entry as Record<string, unknown>;
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function parseJsonl(raw: string, path: string): Record<string, unknown>[] {
|
|
151
|
-
const out: Record<string, unknown>[] = [];
|
|
152
|
-
const lines = raw.split("\n");
|
|
153
|
-
lines.forEach((line, index) => {
|
|
154
|
-
const t = line.trim();
|
|
155
|
-
if (!t) return;
|
|
156
|
-
let parsed: unknown;
|
|
157
|
-
try {
|
|
158
|
-
parsed = JSON.parse(t);
|
|
159
|
-
} catch (error) {
|
|
160
|
-
throw new Error(`file source ${path}: line ${index} is not valid JSON (${error instanceof Error ? error.message : String(error)}).`);
|
|
161
|
-
}
|
|
162
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
163
|
-
throw new Error(`file source ${path}: line ${index} is not a JSON object.`);
|
|
164
|
-
}
|
|
165
|
-
out.push(parsed as Record<string, unknown>);
|
|
166
|
-
});
|
|
167
|
-
return out;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
121
|
// ---------------------------------------------------------------------------
|
|
171
122
|
// serpapi-news — funding/company signals from a news REST query (API key)
|
|
172
123
|
|
package/src/icp.ts
CHANGED
|
@@ -158,26 +158,10 @@ export function icpToTheirStackFilters(icp: Icp): {
|
|
|
158
158
|
return f;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
*/
|
|
166
|
-
const CRUSTDATA_SENIORITY: Record<string, string> = {
|
|
167
|
-
cxo: "CXO",
|
|
168
|
-
"c-suite": "CXO",
|
|
169
|
-
c_suite: "CXO",
|
|
170
|
-
founder: "Owner",
|
|
171
|
-
owner: "Owner",
|
|
172
|
-
partner: "Partner",
|
|
173
|
-
vp: "Vice President",
|
|
174
|
-
"vice president": "Vice President",
|
|
175
|
-
head: "Director",
|
|
176
|
-
director: "Director",
|
|
177
|
-
manager: "Manager",
|
|
178
|
-
senior: "Senior",
|
|
179
|
-
entry: "Entry",
|
|
180
|
-
};
|
|
161
|
+
// NOTE: a CRUSTDATA_SENIORITY vocab table lived here until 2026-07-02 — the
|
|
162
|
+
// `current_seniority_levels` filter is broken upstream (see the live findings
|
|
163
|
+
// on icpToCrustdataFilters below), so job levels are no longer sent to the
|
|
164
|
+
// provider at all. Resurrect the table from git history if pipe0 fixes it.
|
|
181
165
|
|
|
182
166
|
/**
|
|
183
167
|
* ICP industry keyword → LinkedIn industry names Crustdata filters on.
|
|
@@ -212,18 +196,22 @@ const CRUSTDATA_INDUSTRY: Record<string, string[]> = {
|
|
|
212
196
|
/**
|
|
213
197
|
* pipe0 Crustdata people-search config.filters from the ICP. `current_job_titles`
|
|
214
198
|
* matches real LinkedIn title strings (case-sensitive), so keywords are Title
|
|
215
|
-
* Cased.
|
|
216
|
-
* tables above.
|
|
199
|
+
* Cased. Industry is mapped to Crustdata's controlled vocab via the table above.
|
|
217
200
|
*
|
|
218
|
-
* LIVE FINDINGS (2026-06-26
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
201
|
+
* LIVE FINDINGS (2026-06-26, re-confirmed 2026-07-02 with fresh credits):
|
|
202
|
+
* `current_job_titles` works; `current_title` is rejected (422). The culprit
|
|
203
|
+
* that zeroed the full ICP filter is `current_seniority_levels`: ANY non-empty
|
|
204
|
+
* `include` returns 0 results through pipe0's people:profiles:crustdata@1 —
|
|
205
|
+
* including Crustdata's own documented values ("CXO", "Director",
|
|
206
|
+
* "Vice President"), lowercase, and SNAKE_CASE variants — while the identical
|
|
207
|
+
* search without it returns results. (An array instead of the
|
|
208
|
+
* {include, exclude} object is a 422, so the shape was right; the filter is
|
|
209
|
+
* broken upstream.) So job levels are NOT sent to the provider: persona
|
|
210
|
+
* seniority is enforced by fit scoring (scoreProspectAgainstIcp weights
|
|
211
|
+
* jobLevels), which was always the precision backstop. `locations` and
|
|
212
|
+
* `current_employers_linkedin_industries` (both-taxonomy hedge) are live-
|
|
213
|
+
* verified working in combination with titles. Note fit-scoring backs up
|
|
214
|
+
* PERSONA precision but NOT industry, so the industry filter is load-bearing.
|
|
227
215
|
*/
|
|
228
216
|
export function icpToCrustdataFilters(icp: Icp): Record<string, unknown> {
|
|
229
217
|
const f: Record<string, unknown> = {};
|
|
@@ -231,10 +219,6 @@ export function icpToCrustdataFilters(icp: Icp): Record<string, unknown> {
|
|
|
231
219
|
if (icp.firmographics.geos?.length) {
|
|
232
220
|
f.locations = icp.firmographics.geos.map((g) => COUNTRY_NAMES[g.toLowerCase()] ?? g);
|
|
233
221
|
}
|
|
234
|
-
if (icp.persona.jobLevels?.length) {
|
|
235
|
-
const include = [...new Set(icp.persona.jobLevels.map((l) => CRUSTDATA_SENIORITY[l.toLowerCase()]).filter(Boolean))];
|
|
236
|
-
if (include.length) f.current_seniority_levels = { include, exclude: [] };
|
|
237
|
-
}
|
|
238
222
|
if (icp.firmographics.industries?.length) {
|
|
239
223
|
const inds = [
|
|
240
224
|
...new Set(icp.firmographics.industries.flatMap((i) => CRUSTDATA_INDUSTRY[i.toLowerCase()] ?? [titleCase(i)])),
|
package/src/judge.ts
CHANGED
|
@@ -612,6 +612,8 @@ export async function judgeSignals(opts: {
|
|
|
612
612
|
promptTemplate?: string;
|
|
613
613
|
llm?: LlmCallOptions;
|
|
614
614
|
now?: Date;
|
|
615
|
+
/** Per-account progress (presentation only — a throwing callback never fails the run). */
|
|
616
|
+
onAccount?: (done: number, total: number, domain: string) => void;
|
|
615
617
|
}): Promise<JudgeDecision[]> {
|
|
616
618
|
const now = opts.now ?? new Date();
|
|
617
619
|
const signalsById = new Map(opts.signals.map((s) => [s.id, s]));
|
|
@@ -629,6 +631,11 @@ export async function judgeSignals(opts: {
|
|
|
629
631
|
|
|
630
632
|
const decisions: JudgeDecision[] = [];
|
|
631
633
|
for (const [domain, signals] of byAccount) {
|
|
634
|
+
try {
|
|
635
|
+
opts.onAccount?.(decisions.length, byAccount.size, domain);
|
|
636
|
+
} catch {
|
|
637
|
+
// progress is presentation-only
|
|
638
|
+
}
|
|
632
639
|
// Memory + fit stay gated on --with-history (scoring semantics unchanged).
|
|
633
640
|
const recentlyTouched =
|
|
634
641
|
opts.withHistory && opts.snapshot ? accountRecentlyTouched(domain, opts.snapshot, now) : false;
|
package/src/marketClassify.ts
CHANGED
|
@@ -113,6 +113,8 @@ export type ClassifyMarketOptions = {
|
|
|
113
113
|
/** Captures directory override (tests); defaults to the profile market home. */
|
|
114
114
|
capturesDir?: string;
|
|
115
115
|
now?: () => Date;
|
|
116
|
+
/** Per-vendor progress (presentation only — a throwing callback never fails the run). */
|
|
117
|
+
onVendor?: (done: number, total: number, vendorId: string) => void;
|
|
116
118
|
};
|
|
117
119
|
|
|
118
120
|
export type ClassifyMarketResult = {
|
|
@@ -143,7 +145,12 @@ export async function classifyMarket(
|
|
|
143
145
|
const observations: MarketObservation[] = [];
|
|
144
146
|
const retriedVendorIds: string[] = [];
|
|
145
147
|
|
|
146
|
-
for (const vendorId of vendorIds) {
|
|
148
|
+
for (const [vendorIndex, vendorId] of vendorIds.entries()) {
|
|
149
|
+
try {
|
|
150
|
+
options.onVendor?.(vendorIndex, vendorIds.length, vendorId);
|
|
151
|
+
} catch {
|
|
152
|
+
// progress is presentation-only
|
|
153
|
+
}
|
|
147
154
|
const vendor = config.vendors.find((candidate) => candidate.id === vendorId);
|
|
148
155
|
if (!vendor) throw new Error(`Unknown vendor "${vendorId}"`);
|
|
149
156
|
const vendorEntries = runEntries.filter((entry) => entry.vendorId === vendorId);
|
package/src/mcp-bin.ts
CHANGED
|
@@ -9,8 +9,8 @@ const MISSING_PEER_HELP = `The MCP server needs the optional peer dependencies @
|
|
|
9
9
|
In a project:
|
|
10
10
|
npm install fullstackgtm @modelcontextprotocol/sdk zod
|
|
11
11
|
|
|
12
|
-
Zero-install:
|
|
13
|
-
npx -
|
|
12
|
+
Zero-install (the fullstackgtm-mcp wrapper package bundles the peers):
|
|
13
|
+
npx -y fullstackgtm-mcp`;
|
|
14
14
|
|
|
15
15
|
function isMissingPeerError(error: unknown): boolean {
|
|
16
16
|
if (!(error instanceof Error)) return false;
|