fullstackgtm 0.53.0 → 0.53.1
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 +15 -0
- package/dist/cli/enrich.js +32 -11
- package/dist/cli/ui.js +6 -0
- package/package.json +1 -1
- package/src/cli/enrich.ts +30 -12
- package/src/cli/ui.ts +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,21 @@ The path to 1.0 is planned in [docs/roadmap-to-1.0.md](https://github.com/fullst
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.53.1] — 2026-07-11
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Human-readable `enrich acquire` output shows compact lead cards by default
|
|
15
|
+
before the governed plan preview; `--verbose` remains the full forensic dump.
|
|
16
|
+
- Acquisition budget counters are labeled as applied-create policy headroom,
|
|
17
|
+
explicitly distinguished from market size, and no longer duplicated by a
|
|
18
|
+
second gauge in compact output.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Multi-line progress renderers reserve their terminal rows before painting,
|
|
23
|
+
preventing partial spinner frames from scrolling into final output.
|
|
24
|
+
|
|
10
25
|
## [0.53.0] — 2026-07-11
|
|
11
26
|
|
|
12
27
|
### Added
|
package/dist/cli/enrich.js
CHANGED
|
@@ -23,7 +23,7 @@ import { isSpoolPath, readSpoolPath } from "../spoolFiles.js";
|
|
|
23
23
|
import { isOptionValue, loadIcp, numericOption, option, readSnapshot, saveRequested } from "./shared.js";
|
|
24
24
|
import { providerKey } from "./tam.js";
|
|
25
25
|
import { unknownSubcommandError } from "./suggest.js";
|
|
26
|
-
import { colorEnabled, createProgressRenderer, createStatusLine, formatBar, formatDuration, paint } from "./ui.js";
|
|
26
|
+
import { box, colorEnabled, createProgressRenderer, createStatusLine, formatBar, formatDuration, paint, truncateToWidth } from "./ui.js";
|
|
27
27
|
import { compactPlan, verbosePlanRequested } from "./planOutput.js";
|
|
28
28
|
/**
|
|
29
29
|
* The enrich layer: governed append/refresh of third-party data (Apollo pull,
|
|
@@ -785,8 +785,8 @@ function acquireGaugeLine(headroom, budget, p) {
|
|
|
785
785
|
const painter = fraction >= 0.9 ? p.red : fraction >= 0.7 ? p.yellow : p.green;
|
|
786
786
|
parts.push(`${p.dim(label)} ${painter(formatBar(fraction, 10))} ${fmt(used)}/${fmt(cap)}`);
|
|
787
787
|
};
|
|
788
|
-
segment("
|
|
789
|
-
segment("
|
|
788
|
+
segment("applied/day", headroom.records.day, budget.records?.perDay, String);
|
|
789
|
+
segment("applied/mo", headroom.records.month, budget.records?.perMonth, String);
|
|
790
790
|
segment("spend/day", headroom.spendUsd.day, budget.spend?.perDay, (value) => `$${value.toFixed(2)}`);
|
|
791
791
|
segment("spend/mo", headroom.spendUsd.month, budget.spend?.perMonth, (value) => `$${value.toFixed(2)}`);
|
|
792
792
|
return parts.length > 0 ? parts.join(" · ") : null;
|
|
@@ -795,10 +795,33 @@ function formatAcquireMeter(headroom, costPerRecord) {
|
|
|
795
795
|
const n = (v) => (v === null ? "∞" : String(v));
|
|
796
796
|
const money = (v) => (v === null ? "∞" : `$${v.toFixed(2)}`);
|
|
797
797
|
const max = headroom.maxRecords === null ? "unlimited" : `${headroom.maxRecords}`;
|
|
798
|
-
return
|
|
799
|
-
|
|
800
|
-
`
|
|
801
|
-
`(≈$${costPerRecord.toFixed(2)}/lead)
|
|
798
|
+
return `Apply budget (not market size) — up to ${max} more create(s) may be applied now; ` +
|
|
799
|
+
`${n(headroom.records.day)} left today, ${n(headroom.records.month)} this month · ` +
|
|
800
|
+
`spend headroom ${money(headroom.spendUsd.day)}/day, ${money(headroom.spendUsd.month)}/month ` +
|
|
801
|
+
`(≈$${costPerRecord.toFixed(2)}/lead). Dry runs do not consume this budget.`;
|
|
802
|
+
}
|
|
803
|
+
function renderAcquireLeadCards(result) {
|
|
804
|
+
const p = paint(colorEnabled(process.stdout));
|
|
805
|
+
const evidence = new Map((result.plan.evidence ?? []).map((item) => [item.id, item]));
|
|
806
|
+
return result.plan.operations.map((operation, index) => {
|
|
807
|
+
const payload = operation.afterValue;
|
|
808
|
+
const props = payload.properties ?? {};
|
|
809
|
+
const name = [props.firstname, props.lastname].filter(Boolean).join(" ") || payload.matchValue;
|
|
810
|
+
const title = props.jobtitle || "Title unavailable";
|
|
811
|
+
const company = props.company || payload.associateCompanyName || "Company unavailable";
|
|
812
|
+
let fit;
|
|
813
|
+
const item = operation.evidenceIds?.[0] ? evidence.get(operation.evidenceIds[0]) : undefined;
|
|
814
|
+
try {
|
|
815
|
+
fit = Number(JSON.parse(item?.text ?? "{}").fitScore);
|
|
816
|
+
}
|
|
817
|
+
catch { /* malformed evidence remains display-only */ }
|
|
818
|
+
const lines = [
|
|
819
|
+
truncateToWidth(name, 84),
|
|
820
|
+
truncateToWidth(`${title} · ${company}`, 84),
|
|
821
|
+
truncateToWidth([payload.associateCompanyDomain, props.hs_linkedin_url].filter(Boolean).join(" · "), 84),
|
|
822
|
+
];
|
|
823
|
+
return box(lines, p, `Lead ${index + 1}/${result.plan.operations.length}${Number.isFinite(fit) ? ` · ${Math.round((fit ?? 0) * 100)}% fit` : ""}`).join("\n");
|
|
824
|
+
}).join("\n");
|
|
802
825
|
}
|
|
803
826
|
function printAcquireOutput(options) {
|
|
804
827
|
const { args, result, meter, meterLine, gaugeLine, saved, planSaved, hostedPlanUrl } = options;
|
|
@@ -827,15 +850,13 @@ function printAcquireOutput(options) {
|
|
|
827
850
|
console.log(` Plan ${hostedPlanUrl}`);
|
|
828
851
|
}
|
|
829
852
|
else if (planSaved || !saved) {
|
|
853
|
+
if (result.plan.operations.length > 0)
|
|
854
|
+
console.log(renderAcquireLeadCards(result));
|
|
830
855
|
console.log(compactPlan(result.plan, { saved: planSaved }));
|
|
831
856
|
console.log(meterLine);
|
|
832
|
-
if (gaugeLine)
|
|
833
|
-
console.log(gaugeLine);
|
|
834
857
|
}
|
|
835
858
|
else {
|
|
836
859
|
console.log(meterLine);
|
|
837
|
-
if (gaugeLine)
|
|
838
|
-
console.log(gaugeLine);
|
|
839
860
|
console.log("No net-new leads to create (everything sourced already matched, or was withheld by the meter).");
|
|
840
861
|
}
|
|
841
862
|
if (planSaved) {
|
package/dist/cli/ui.js
CHANGED
|
@@ -266,6 +266,12 @@ export function createChecklist(items, stream = process.stderr, env = process.en
|
|
|
266
266
|
// newline on every repaint can scroll the old top row into permanent
|
|
267
267
|
// history when the board sits at the bottom of the terminal, producing
|
|
268
268
|
// apparent duplicate/errored frames in terminal captures.
|
|
269
|
+
// Reserve the board's rows before the first paint. Writing populated rows
|
|
270
|
+
// directly at the terminal bottom can scroll the first rows into history,
|
|
271
|
+
// after which cursor-up repaints target the wrong lines and leave fragments.
|
|
272
|
+
if (painted === 0 && lines.length > 1) {
|
|
273
|
+
stream.write(`${"\n".repeat(lines.length - 1)}\u001b[${lines.length - 1}A`);
|
|
274
|
+
}
|
|
269
275
|
const up = painted > 1 ? `\u001b[${painted - 1}A` : "";
|
|
270
276
|
stream.write(`${up}${lines.map((line) => `\u001b[2K${line}`).join("\n")}`);
|
|
271
277
|
painted = lines.length;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fullstackgtm",
|
|
3
|
-
"version": "0.53.
|
|
3
|
+
"version": "0.53.1",
|
|
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/enrich.ts
CHANGED
|
@@ -20,12 +20,12 @@ import { ACQUIRE_STAGES, composeListeners, createProgressEmitter } from "../prog
|
|
|
20
20
|
import { createLinkedInProvider, discoverLinkedInProspects } from "../acquireLinkedIn.ts";
|
|
21
21
|
import { clayPeopleFilterRoutes, fitThreshold, icpToClayPeopleFilters, icpToCrustdataFilters, icpToExploriumFilters, scoreProspectAgainstIcp, type Icp } from "../icp.ts";
|
|
22
22
|
import { apolloPullKeysForAppend, apolloPullKeysForRefresh, createApolloClient, pullApolloRecords, type ApolloPullKey } from "../enrichApollo.ts";
|
|
23
|
-
import type { CanonicalGtmSnapshot } from "../types.ts";
|
|
23
|
+
import type { CanonicalGtmSnapshot, CreateRecordPayload } from "../types.ts";
|
|
24
24
|
import { isSpoolPath, readSpoolPath } from "../spoolFiles.ts";
|
|
25
25
|
import { isOptionValue, loadIcp, numericOption, option, readSnapshot, saveRequested } from "./shared.ts";
|
|
26
26
|
import { providerKey } from "./tam.ts";
|
|
27
27
|
import { unknownSubcommandError } from "./suggest.ts";
|
|
28
|
-
import { colorEnabled, createProgressRenderer, createStatusLine, formatBar, formatDuration, paint, type Paint } from "./ui.ts";
|
|
28
|
+
import { box, colorEnabled, createProgressRenderer, createStatusLine, formatBar, formatDuration, paint, truncateToWidth, type Paint } from "./ui.ts";
|
|
29
29
|
import { compactPlan, verbosePlanRequested } from "./planOutput.ts";
|
|
30
30
|
import type { AcquireBudget } from "../acquireMeter.ts";
|
|
31
31
|
|
|
@@ -903,8 +903,8 @@ function acquireGaugeLine(headroom: AcquireRemaining, budget: AcquireBudget, p:
|
|
|
903
903
|
const painter = fraction >= 0.9 ? p.red : fraction >= 0.7 ? p.yellow : p.green;
|
|
904
904
|
parts.push(`${p.dim(label)} ${painter(formatBar(fraction, 10))} ${fmt(used)}/${fmt(cap)}`);
|
|
905
905
|
};
|
|
906
|
-
segment("
|
|
907
|
-
segment("
|
|
906
|
+
segment("applied/day", headroom.records.day, budget.records?.perDay, String);
|
|
907
|
+
segment("applied/mo", headroom.records.month, budget.records?.perMonth, String);
|
|
908
908
|
segment("spend/day", headroom.spendUsd.day, budget.spend?.perDay, (value) => `$${value.toFixed(2)}`);
|
|
909
909
|
segment("spend/mo", headroom.spendUsd.month, budget.spend?.perMonth, (value) => `$${value.toFixed(2)}`);
|
|
910
910
|
return parts.length > 0 ? parts.join(" · ") : null;
|
|
@@ -914,12 +914,31 @@ function formatAcquireMeter(headroom: AcquireRemaining, costPerRecord: number):
|
|
|
914
914
|
const n = (v: number | null) => (v === null ? "∞" : String(v));
|
|
915
915
|
const money = (v: number | null) => (v === null ? "∞" : `$${v.toFixed(2)}`);
|
|
916
916
|
const max = headroom.maxRecords === null ? "unlimited" : `${headroom.maxRecords}`;
|
|
917
|
-
return (
|
|
918
|
-
|
|
919
|
-
`
|
|
920
|
-
`
|
|
921
|
-
|
|
922
|
-
|
|
917
|
+
return `Apply budget (not market size) — up to ${max} more create(s) may be applied now; ` +
|
|
918
|
+
`${n(headroom.records.day)} left today, ${n(headroom.records.month)} this month · ` +
|
|
919
|
+
`spend headroom ${money(headroom.spendUsd.day)}/day, ${money(headroom.spendUsd.month)}/month ` +
|
|
920
|
+
`(≈$${costPerRecord.toFixed(2)}/lead). Dry runs do not consume this budget.`;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function renderAcquireLeadCards(result: ReturnType<typeof buildAcquirePlan>): string {
|
|
924
|
+
const p = paint(colorEnabled(process.stdout));
|
|
925
|
+
const evidence = new Map((result.plan.evidence ?? []).map((item) => [item.id, item]));
|
|
926
|
+
return result.plan.operations.map((operation, index) => {
|
|
927
|
+
const payload = operation.afterValue as CreateRecordPayload;
|
|
928
|
+
const props = payload.properties ?? {};
|
|
929
|
+
const name = [props.firstname, props.lastname].filter(Boolean).join(" ") || payload.matchValue;
|
|
930
|
+
const title = props.jobtitle || "Title unavailable";
|
|
931
|
+
const company = props.company || payload.associateCompanyName || "Company unavailable";
|
|
932
|
+
let fit: number | undefined;
|
|
933
|
+
const item = operation.evidenceIds?.[0] ? evidence.get(operation.evidenceIds[0]) : undefined;
|
|
934
|
+
try { fit = Number((JSON.parse(item?.text ?? "{}") as { fitScore?: unknown }).fitScore); } catch { /* malformed evidence remains display-only */ }
|
|
935
|
+
const lines = [
|
|
936
|
+
truncateToWidth(name, 84),
|
|
937
|
+
truncateToWidth(`${title} · ${company}`, 84),
|
|
938
|
+
truncateToWidth([payload.associateCompanyDomain, props.hs_linkedin_url].filter(Boolean).join(" · "), 84),
|
|
939
|
+
];
|
|
940
|
+
return box(lines, p, `Lead ${index + 1}/${result.plan.operations.length}${Number.isFinite(fit) ? ` · ${Math.round((fit ?? 0) * 100)}% fit` : ""}`).join("\n");
|
|
941
|
+
}).join("\n");
|
|
923
942
|
}
|
|
924
943
|
|
|
925
944
|
function printAcquireOutput(options: {
|
|
@@ -959,12 +978,11 @@ function printAcquireOutput(options: {
|
|
|
959
978
|
console.log(` Budget ${gaugeLine ?? meterLine}`);
|
|
960
979
|
if (hostedPlanUrl) console.log(` Plan ${hostedPlanUrl}`);
|
|
961
980
|
} else if (planSaved || !saved) {
|
|
981
|
+
if (result.plan.operations.length > 0) console.log(renderAcquireLeadCards(result));
|
|
962
982
|
console.log(compactPlan(result.plan, { saved: planSaved }));
|
|
963
983
|
console.log(meterLine);
|
|
964
|
-
if (gaugeLine) console.log(gaugeLine);
|
|
965
984
|
} else {
|
|
966
985
|
console.log(meterLine);
|
|
967
|
-
if (gaugeLine) console.log(gaugeLine);
|
|
968
986
|
console.log("No net-new leads to create (everything sourced already matched, or was withheld by the meter).");
|
|
969
987
|
}
|
|
970
988
|
|
package/src/cli/ui.ts
CHANGED
|
@@ -328,6 +328,12 @@ export function createChecklist(
|
|
|
328
328
|
// newline on every repaint can scroll the old top row into permanent
|
|
329
329
|
// history when the board sits at the bottom of the terminal, producing
|
|
330
330
|
// apparent duplicate/errored frames in terminal captures.
|
|
331
|
+
// Reserve the board's rows before the first paint. Writing populated rows
|
|
332
|
+
// directly at the terminal bottom can scroll the first rows into history,
|
|
333
|
+
// after which cursor-up repaints target the wrong lines and leave fragments.
|
|
334
|
+
if (painted === 0 && lines.length > 1) {
|
|
335
|
+
stream.write(`${"\n".repeat(lines.length - 1)}\u001b[${lines.length - 1}A`);
|
|
336
|
+
}
|
|
331
337
|
const up = painted > 1 ? `\u001b[${painted - 1}A` : "";
|
|
332
338
|
stream.write(`${up}${lines.map((line) => `\u001b[2K${line}`).join("\n")}`);
|
|
333
339
|
painted = lines.length;
|