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.
Files changed (99) hide show
  1. package/CHANGELOG.md +185 -1
  2. package/README.md +19 -7
  3. package/dist/audit.d.ts +3 -1
  4. package/dist/audit.js +29 -2
  5. package/dist/cli/audit.d.ts +15 -0
  6. package/dist/cli/audit.js +392 -0
  7. package/dist/cli/auth.d.ts +80 -0
  8. package/dist/cli/auth.js +500 -0
  9. package/dist/cli/call.d.ts +1 -0
  10. package/dist/cli/call.js +373 -0
  11. package/dist/cli/capabilities.d.ts +30 -0
  12. package/dist/cli/capabilities.js +197 -0
  13. package/dist/cli/draft.d.ts +7 -0
  14. package/dist/cli/draft.js +87 -0
  15. package/dist/cli/enrich.d.ts +8 -0
  16. package/dist/cli/enrich.js +788 -0
  17. package/dist/cli/fix.d.ts +33 -0
  18. package/dist/cli/fix.js +344 -0
  19. package/dist/cli/help.d.ts +25 -0
  20. package/dist/cli/help.js +609 -0
  21. package/dist/cli/icp.d.ts +7 -0
  22. package/dist/cli/icp.js +229 -0
  23. package/dist/cli/init.d.ts +7 -0
  24. package/dist/cli/init.js +58 -0
  25. package/dist/cli/market.d.ts +1 -0
  26. package/dist/cli/market.js +391 -0
  27. package/dist/cli/plans.d.ts +5 -0
  28. package/dist/cli/plans.js +454 -0
  29. package/dist/cli/schedule.d.ts +8 -0
  30. package/dist/cli/schedule.js +479 -0
  31. package/dist/cli/shared.d.ts +70 -0
  32. package/dist/cli/shared.js +331 -0
  33. package/dist/cli/signals.d.ts +7 -0
  34. package/dist/cli/signals.js +412 -0
  35. package/dist/cli/suggest.d.ts +49 -0
  36. package/dist/cli/suggest.js +135 -0
  37. package/dist/cli/tam.d.ts +9 -0
  38. package/dist/cli/tam.js +387 -0
  39. package/dist/cli/ui.d.ts +121 -0
  40. package/dist/cli/ui.js +375 -0
  41. package/dist/cli.d.ts +1 -57
  42. package/dist/cli.js +100 -5130
  43. package/dist/connector.d.ts +15 -0
  44. package/dist/connector.js +35 -0
  45. package/dist/connectors/hubspot.d.ts +3 -1
  46. package/dist/connectors/hubspot.js +10 -3
  47. package/dist/connectors/salesforce.d.ts +3 -1
  48. package/dist/connectors/salesforce.js +12 -5
  49. package/dist/connectors/signalSources.js +7 -59
  50. package/dist/icp.d.ts +15 -11
  51. package/dist/icp.js +19 -36
  52. package/dist/judge.d.ts +2 -0
  53. package/dist/judge.js +6 -0
  54. package/dist/marketClassify.d.ts +2 -0
  55. package/dist/marketClassify.js +7 -1
  56. package/dist/mcp-bin.js +2 -2
  57. package/dist/mcp.js +259 -166
  58. package/dist/schedule.d.ts +80 -2
  59. package/dist/schedule.js +254 -1
  60. package/dist/spoolFiles.d.ts +44 -0
  61. package/dist/spoolFiles.js +114 -0
  62. package/dist/types.d.ts +11 -0
  63. package/docs/api.md +73 -7
  64. package/docs/signal-spool-format.md +13 -0
  65. package/llms.txt +15 -6
  66. package/package.json +1 -1
  67. package/skills/fullstackgtm/SKILL.md +1 -1
  68. package/src/audit.ts +27 -1
  69. package/src/cli/audit.ts +447 -0
  70. package/src/cli/auth.ts +549 -0
  71. package/src/cli/call.ts +398 -0
  72. package/src/cli/capabilities.ts +215 -0
  73. package/src/cli/draft.ts +101 -0
  74. package/src/cli/enrich.ts +885 -0
  75. package/src/cli/fix.ts +372 -0
  76. package/src/cli/help.ts +664 -0
  77. package/src/cli/icp.ts +265 -0
  78. package/src/cli/init.ts +65 -0
  79. package/src/cli/market.ts +423 -0
  80. package/src/cli/plans.ts +523 -0
  81. package/src/cli/schedule.ts +526 -0
  82. package/src/cli/shared.ts +375 -0
  83. package/src/cli/signals.ts +434 -0
  84. package/src/cli/suggest.ts +151 -0
  85. package/src/cli/tam.ts +435 -0
  86. package/src/cli/ui.ts +426 -0
  87. package/src/cli.ts +99 -5829
  88. package/src/connector.ts +46 -0
  89. package/src/connectors/hubspot.ts +14 -2
  90. package/src/connectors/salesforce.ts +18 -2
  91. package/src/connectors/signalSources.ts +7 -56
  92. package/src/icp.ts +19 -35
  93. package/src/judge.ts +7 -0
  94. package/src/marketClassify.ts +8 -1
  95. package/src/mcp-bin.ts +2 -2
  96. package/src/mcp.ts +130 -57
  97. package/src/schedule.ts +310 -3
  98. package/src/spoolFiles.ts +116 -0
  99. package/src/types.ts +12 -0
@@ -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
- return assembleSnapshot((objectType, properties, withAssociations) => list(`/crm/v3/objects/${objectType}?limit=100&properties=${properties}${withAssociations ? "&associations=companies" : ""}`));
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 sfUsers = await query(`SELECT ${selectFields("owners")} FROM User${whereClause}`);
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, readdirSync, statSync } from "node:fs";
22
- import { join, resolve } from "node:path";
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
- const trimmed = raw.trim();
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. Seniority + industry are mapped to Crustdata's controlled vocab via the
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): `current_job_titles` is confirmed working (a
93
- * titles-only RevOps search returns results); `current_title` is rejected (422).
94
- * The full ICP filter returned 0 — the prime suspect is the industry vocab
95
- * (LinkedIn v1/v2 taxonomy mismatch), now hedged by sending BOTH generations in
96
- * CRUSTDATA_INDUSTRY above. NOT yet re-confirmed end-to-end (pipe0 credits were
97
- * exhausted mid-investigation) re-run `enrich acquire --source pipe0` once
98
- * credits refill; if it still returns 0, the next suspects are the
99
- * `current_seniority_levels` shape/values and `locations`. Note fit-scoring backs
100
- * up PERSONA precision but NOT industry, so the industry filter is load-bearing.
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
- * Crustdata / LinkedIn seniority vocab. ICP job levels are normalized lowercase;
130
- * Crustdata expects these exact capitalized strings (verified: "CXO",
131
- * "Vice President", "Director" appear in Crustdata's docs/examples).
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. Seniority + industry are mapped to Crustdata's controlled vocab via the
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): `current_job_titles` is confirmed working (a
184
- * titles-only RevOps search returns results); `current_title` is rejected (422).
185
- * The full ICP filter returned 0 — the prime suspect is the industry vocab
186
- * (LinkedIn v1/v2 taxonomy mismatch), now hedged by sending BOTH generations in
187
- * CRUSTDATA_INDUSTRY above. NOT yet re-confirmed end-to-end (pipe0 credits were
188
- * exhausted mid-investigation) re-run `enrich acquire --source pipe0` once
189
- * credits refill; if it still returns 0, the next suspects are the
190
- * `current_seniority_levels` shape/values and `locations`. Note fit-scoring backs
191
- * up PERSONA precision but NOT industry, so the industry filter is load-bearing.
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;
@@ -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;
@@ -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 -p fullstackgtm -p @modelcontextprotocol/sdk -p zod fullstackgtm-mcp`;
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;