fullstackgtm 0.45.0 → 0.47.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 +115 -1
- package/INSTALL_FOR_AGENTS.md +13 -11
- package/README.md +27 -18
- package/dist/backfill.d.ts +86 -0
- package/dist/backfill.js +251 -0
- package/dist/bin.js +4 -1
- package/dist/cli/auth.d.ts +2 -0
- package/dist/cli/auth.js +119 -12
- package/dist/cli/backfill.d.ts +1 -0
- package/dist/cli/backfill.js +125 -0
- package/dist/cli/backfillRuns.d.ts +1 -0
- package/dist/cli/backfillRuns.js +187 -0
- package/dist/cli/call.js +23 -9
- package/dist/cli/enrich.js +28 -10
- package/dist/cli/fix.js +9 -7
- package/dist/cli/help.js +60 -23
- package/dist/cli/plans.js +13 -11
- package/dist/cli/shared.d.ts +4 -3
- package/dist/cli/shared.js +31 -20
- package/dist/cli/ui.d.ts +21 -0
- package/dist/cli/ui.js +53 -1
- package/dist/cli.js +37 -41
- package/dist/connector.d.ts +8 -0
- package/dist/connector.js +104 -21
- package/dist/connectors/hubspot.d.ts +8 -1
- package/dist/connectors/hubspot.js +406 -13
- package/dist/connectors/hubspotAuth.js +5 -1
- package/dist/connectors/linkedin.js +14 -14
- package/dist/connectors/salesforce.d.ts +8 -1
- package/dist/connectors/salesforce.js +163 -2
- package/dist/connectors/stripe.d.ts +37 -0
- package/dist/connectors/stripe.js +103 -31
- package/dist/enrich.d.ts +7 -0
- package/dist/enrich.js +7 -0
- package/dist/health.d.ts +11 -69
- package/dist/health.js +4 -134
- package/dist/healthScore.d.ts +71 -0
- package/dist/healthScore.js +143 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/llm.d.ts +29 -0
- package/dist/llm.js +206 -0
- package/dist/market.d.ts +39 -1
- package/dist/market.js +116 -44
- package/dist/marketClassify.d.ts +9 -1
- package/dist/marketClassify.js +10 -1
- package/dist/marketTaxonomy.d.ts +6 -1
- package/dist/marketTaxonomy.js +4 -2
- package/dist/mcp-bin.js +29 -0
- package/dist/mcp.js +117 -4
- package/dist/progress.d.ts +96 -0
- package/dist/progress.js +142 -0
- package/dist/runReport.d.ts +24 -0
- package/dist/runReport.js +139 -4
- package/dist/types.d.ts +33 -1
- package/docs/api.md +4 -2
- package/docs/architecture.md +2 -0
- package/docs/linkedin-connector-spec.md +1 -1
- package/llms.txt +3 -3
- package/package.json +10 -3
- package/skills/fullstackgtm/SKILL.md +1 -0
- package/src/backfill.ts +340 -0
- package/src/bin.ts +5 -1
- package/src/cli/auth.ts +135 -15
- package/src/cli/backfill.ts +156 -0
- package/src/cli/backfillRuns.ts +198 -0
- package/src/cli/call.ts +26 -10
- package/src/cli/enrich.ts +33 -10
- package/src/cli/fix.ts +11 -10
- package/src/cli/help.ts +61 -23
- package/src/cli/plans.ts +15 -14
- package/src/cli/shared.ts +44 -27
- package/src/cli/ui.ts +72 -1
- package/src/cli.ts +38 -41
- package/src/connector.ts +110 -16
- package/src/connectors/hubspot.ts +423 -14
- package/src/connectors/hubspotAuth.ts +5 -1
- package/src/connectors/linkedin.ts +14 -14
- package/src/connectors/salesforce.ts +168 -3
- package/src/connectors/stripe.ts +154 -34
- package/src/enrich.ts +13 -0
- package/src/health.ts +14 -213
- package/src/healthScore.ts +223 -0
- package/src/index.ts +28 -1
- package/src/llm.ts +239 -0
- package/src/market.ts +157 -44
- package/src/marketClassify.ts +18 -1
- package/src/marketTaxonomy.ts +10 -2
- package/src/mcp-bin.ts +32 -0
- package/src/mcp.ts +140 -6
- package/src/progress.ts +197 -0
- package/src/runReport.ts +159 -4
- package/src/types.ts +35 -2
- package/docs/dx-punch-list.md +0 -87
- package/docs/roadmap-to-1.0.md +0 -211
package/dist/cli/enrich.js
CHANGED
|
@@ -8,7 +8,8 @@ import { buildAcquirePlan, buildEnrichPlan, createFileEnrichRunStore, DEFAULT_ST
|
|
|
8
8
|
import { loadMeter, remaining } from "../acquireMeter.js";
|
|
9
9
|
import { crmContactKeys, fetchExploriumProspects, fetchPipe0CrustdataProspects, partitionFreshProspects, pipe0ResolveCompanyDomains, pipe0ResolveWorkEmails, prospectIdentityKeys } from "../connectors/prospectSources.js";
|
|
10
10
|
import { loadSeen, recordSeen } from "../acquireSeen.js";
|
|
11
|
-
import { reportCounts, reportEvent } from "../runReport.js";
|
|
11
|
+
import { progressReporter, reportCounts, reportEvent } from "../runReport.js";
|
|
12
|
+
import { ACQUIRE_STAGES, composeListeners, createProgressEmitter } from "../progress.js";
|
|
12
13
|
import { createLinkedInProvider, discoverLinkedInProspects } from "../acquireLinkedIn.js";
|
|
13
14
|
import { fitThreshold, icpToCrustdataFilters, icpToExploriumFilters, scoreProspectAgainstIcp } from "../icp.js";
|
|
14
15
|
import { apolloPullKeysForAppend, apolloPullKeysForRefresh, createApolloClient, pullApolloRecords } from "../enrichApollo.js";
|
|
@@ -16,7 +17,7 @@ import { isSpoolPath, readSpoolPath } from "../spoolFiles.js";
|
|
|
16
17
|
import { isOptionValue, loadIcp, numericOption, option, readSnapshot, saveRequested } from "./shared.js";
|
|
17
18
|
import { providerKey } from "./tam.js";
|
|
18
19
|
import { unknownSubcommandError } from "./suggest.js";
|
|
19
|
-
import { colorEnabled, createStatusLine, formatBar, formatDuration, paint } from "./ui.js";
|
|
20
|
+
import { colorEnabled, createProgressRenderer, createStatusLine, formatBar, formatDuration, paint } from "./ui.js";
|
|
20
21
|
/**
|
|
21
22
|
* The enrich layer: governed append/refresh of third-party data (Apollo pull,
|
|
22
23
|
* Clay ingest) into the CRM through the normal dry-run → approval → apply
|
|
@@ -197,14 +198,31 @@ are phase 2. Recurring execution is the scheduler's job; enrich has no cron.`);
|
|
|
197
198
|
}
|
|
198
199
|
}
|
|
199
200
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
201
|
+
// Progress: candidate rows tick on an interactive stderr board (inert when
|
|
202
|
+
// piped) and, via the composed reporter, heartbeat to a paired hosted app.
|
|
203
|
+
// The meter reading (creates vs headroom + budget burn) rides the same
|
|
204
|
+
// emitter, feeding the dashboard's gauge without printing anything new.
|
|
205
|
+
const renderer = createProgressRenderer(ACQUIRE_STAGES);
|
|
206
|
+
const acquireProgress = createProgressEmitter(composeListeners(renderer.listener, progressReporter()));
|
|
207
|
+
let result;
|
|
208
|
+
try {
|
|
209
|
+
acquireProgress.stage(ACQUIRE_STAGES[0], 0, ACQUIRE_STAGES.length);
|
|
210
|
+
if (config.acquire.budget?.records?.perDay && headroom.records.day !== null) {
|
|
211
|
+
acquireProgress.meter(config.acquire.budget.records.perDay - headroom.records.day, config.acquire.budget.records.perDay, "records/day");
|
|
212
|
+
}
|
|
213
|
+
result = buildAcquirePlan({
|
|
214
|
+
config,
|
|
215
|
+
source,
|
|
216
|
+
snapshot,
|
|
217
|
+
records,
|
|
218
|
+
runLabel: option(rest, "--run-label") ?? `acquire-${source}-${today}`,
|
|
219
|
+
maxRecords: cap,
|
|
220
|
+
progress: acquireProgress,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
renderer.done();
|
|
225
|
+
}
|
|
208
226
|
if (result.counts.unassigned > 0 && result.counts.created > 0) {
|
|
209
227
|
console.error(`⚠ ${result.counts.unassigned}/${result.counts.created} proposed lead(s) have no owner ` +
|
|
210
228
|
`(policy could not place them). They will be created ownerless.`);
|
package/dist/cli/fix.js
CHANGED
|
@@ -11,8 +11,10 @@ import { buildBulkUpdatePlan } from "../bulkUpdate.js";
|
|
|
11
11
|
import { buildDedupePlan } from "../dedupe.js";
|
|
12
12
|
import { buildReassignPlans } from "../reassign.js";
|
|
13
13
|
import { suggestValues } from "../suggest.js";
|
|
14
|
+
import { APPLY_STAGES, composeListeners, createProgressEmitter } from "../progress.js";
|
|
15
|
+
import { progressReporter } from "../runReport.js";
|
|
14
16
|
import { confirmRequested, connectorFor, isOptionValue, numericOption, option, readSnapshot, repeatedOption, saveRequested } from "./shared.js";
|
|
15
|
-
import { box, colorEnabled,
|
|
17
|
+
import { box, colorEnabled, createProgressRenderer, paint } from "./ui.js";
|
|
16
18
|
/**
|
|
17
19
|
* The resolve gate: exit 0 = safe to create, exit 2 = match found (exists or
|
|
18
20
|
* ambiguous — do NOT blind-create), exit 1 = error. Built for sync jobs and
|
|
@@ -260,20 +262,20 @@ export async function fixCommand(args) {
|
|
|
260
262
|
return;
|
|
261
263
|
}
|
|
262
264
|
const connector = await connectorFor(provider, args);
|
|
263
|
-
// Live
|
|
264
|
-
|
|
265
|
+
// Live apply board on interactive terminals (stderr; inert otherwise); the
|
|
266
|
+
// same emitter streams heartbeats to the paired hosted app on long runs.
|
|
267
|
+
const renderer = createProgressRenderer(APPLY_STAGES);
|
|
268
|
+
const progress = createProgressEmitter(composeListeners(renderer.listener, progressReporter()));
|
|
265
269
|
let run;
|
|
266
270
|
try {
|
|
267
271
|
run = await applyPatchPlan(connector, plan, {
|
|
268
272
|
approvedOperationIds: approvedIds,
|
|
269
273
|
valueOverrides: overrides,
|
|
270
|
-
|
|
271
|
-
? (progress) => ticker.set(`Applying ${progress.completed}/${progress.total} · ✓ ${progress.applied} applied · ${progress.failed} failed · ${progress.conflicts} conflict${progress.conflicts === 1 ? "" : "s"}`)
|
|
272
|
-
: undefined,
|
|
274
|
+
progress,
|
|
273
275
|
});
|
|
274
276
|
}
|
|
275
277
|
finally {
|
|
276
|
-
|
|
278
|
+
renderer.done();
|
|
277
279
|
}
|
|
278
280
|
await store.recordRun(plan.id, run);
|
|
279
281
|
const counts = { applied: 0, conflict: 0, skipped: 0, failed: 0 };
|
package/dist/cli/help.js
CHANGED
|
@@ -5,11 +5,15 @@ export function usage() {
|
|
|
5
5
|
and apply only explicitly approved operations.
|
|
6
6
|
|
|
7
7
|
Usage:
|
|
8
|
-
fullstackgtm login
|
|
9
|
-
fullstackgtm login
|
|
10
|
-
fullstackgtm login hubspot --
|
|
11
|
-
fullstackgtm login salesforce --
|
|
12
|
-
fullstackgtm login
|
|
8
|
+
fullstackgtm login hubspot hosted browser OAuth (default; no app needed)
|
|
9
|
+
fullstackgtm login salesforce hosted browser OAuth (default; no app needed)
|
|
10
|
+
fullstackgtm login hubspot --hosted [--via <url>]
|
|
11
|
+
fullstackgtm login salesforce --hosted [--via <url>]
|
|
12
|
+
fullstackgtm login --via <hosted url> pair with a team deployment (broker only)
|
|
13
|
+
fullstackgtm login hubspot --private-token [--no-validate] advanced: paste private app token
|
|
14
|
+
fullstackgtm login hubspot --oauth --client-id <id> [--port ${DEFAULT_LOOPBACK_PORT}] [--scopes a,b] advanced: BYO HubSpot app
|
|
15
|
+
fullstackgtm login salesforce --device --client-id <consumer key> [--login-url <url>] advanced: BYO Connected App
|
|
16
|
+
fullstackgtm login salesforce --instance-url <url> [--no-validate] advanced: paste access token
|
|
13
17
|
fullstackgtm login stripe [--no-validate]
|
|
14
18
|
fullstackgtm login anthropic | openai store an LLM API key for call parse/score
|
|
15
19
|
fullstackgtm login apollo store an Apollo API key for enrich pulls\n fullstackgtm login pipe0 | explorium | theirstack store a discovery-provider key (theirstack = technographic TAM)\n fullstackgtm login heyreach store a HeyReach key for enrich acquire --source linkedin\n fullstackgtm logout <hubspot|salesforce|stripe|anthropic|openai|apollo|pipe0|explorium|heyreach|broker>
|
|
@@ -17,7 +21,7 @@ Usage:
|
|
|
17
21
|
Secrets (tokens, client secrets) are NEVER passed as flags — they leak via
|
|
18
22
|
the process list and shell history. Pipe them on stdin or enter them at the
|
|
19
23
|
interactive prompt:
|
|
20
|
-
echo "$HUBSPOT_TOKEN" | fullstackgtm login hubspot
|
|
24
|
+
echo "$HUBSPOT_TOKEN" | fullstackgtm login hubspot --private-token
|
|
21
25
|
fullstackgtm init [--source pipe0|explorium|linkedin] [--provider hubspot|salesforce] [--out <dir>] [--force]
|
|
22
26
|
cold start: scaffold icp.json + enrich.config.json + a
|
|
23
27
|
PLAYBOOK wired for this workspace (the CLI ships primitives,
|
|
@@ -27,16 +31,18 @@ Usage:
|
|
|
27
31
|
fullstackgtm report [source options] [audit options] [report options]
|
|
28
32
|
fullstackgtm diff --before <a.json> --after <b.json> [--json] [--fail-on-new-findings]
|
|
29
33
|
fullstackgtm merge --input <a.json> --input <b.json> [...] --out <merged.json> [--json]
|
|
30
|
-
fullstackgtm call parse --transcript <file> [--title t] [--source fathom|granola|...] [--model m] [--
|
|
34
|
+
fullstackgtm call parse --transcript <file> [--title t] [--source fathom|granola|...] [--model m] [--llm] [--heuristics] [--json|--ndjson] [--out <path>]
|
|
31
35
|
fullstackgtm call classify --transcript <file>|--call <parsed.json> [--llm] [--deterministic] [--json]
|
|
32
36
|
fullstackgtm call score --transcript <file>|--call <parsed.json> [--call-type <t>] [--rubric <rubric.json>] [--model m] [--json|--out <path>]
|
|
33
37
|
fullstackgtm call link --attendees <a@x.com,...> | --domain <x.com> [source options] [--json]
|
|
34
38
|
fullstackgtm call plan --transcript <file>|--call <parsed.json> --deal <id> [source options] [--save|--json]
|
|
35
|
-
calls become evidence: LLM extraction by default
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
--
|
|
39
|
-
|
|
39
|
+
calls become evidence: chunked LLM extraction by default when a
|
|
40
|
+
key resolves (ANTHROPIC_API_KEY/OPENAI_API_KEY or \`login
|
|
41
|
+
anthropic|openai\`), free deterministic fallback when none does;
|
|
42
|
+
--heuristics (alias --deterministic) forces the deterministic
|
|
43
|
+
engine, --llm forces the LLM path. Model: --model, else env
|
|
44
|
+
FSGTM_INSIGHTS_MODEL. Then link the call to its deal and propose
|
|
45
|
+
governed next-step writes.
|
|
40
46
|
fullstackgtm resolve <account|contact|deal> [--name N] [--domain D] [--email E] [--account-id A] [source options] [--json]
|
|
41
47
|
the create gate: exit 0 = safe to create, exit 2 = match
|
|
42
48
|
found (exists/ambiguous) — call before ANY record creation
|
|
@@ -76,6 +82,15 @@ Usage:
|
|
|
76
82
|
fill-blanks-only patch plan through the normal dry-run →
|
|
77
83
|
approve → apply gate. refresh re-checks stale stamped fields
|
|
78
84
|
and proposes updates only where the source value changed.
|
|
85
|
+
fullstackgtm backfill stripe|runs [--since <iso>] [--pipeline <id|label>] [--match-property <name>] [--skip-unmatched] [source options] [--save] [--dry-run] [--json]
|
|
86
|
+
revenue truth backfill: one closed-won deal per PAID Stripe
|
|
87
|
+
invoice (amount = invoice total, close date = paid date,
|
|
88
|
+
associated to the matched customer account, deduped by
|
|
89
|
+
invoice id). Reads Stripe + the CRM snapshot, emits a
|
|
90
|
+
dry-run create_record plan through the normal approve →
|
|
91
|
+
apply gate; unmatched customers get a proposed NEW account
|
|
92
|
+
in the same plan (freemail domains never used;
|
|
93
|
+
--skip-unmatched = report-only). HubSpot-only for now.
|
|
79
94
|
fullstackgtm signals fetch [--bucket job,…] [--source greenhouse,lever,ashby] [--watchlist <path|crm:seg>] [--keywords …] [--from <file.json|spool.jsonl|spool-dir>] [--save]
|
|
80
95
|
fullstackgtm signals list [--since 7d] [--bucket b] [--account d] [--unjudged]
|
|
81
96
|
fullstackgtm signals outcome --account <d> [--touch <id>] --result replied|meeting|bounced|no_reply
|
|
@@ -184,13 +199,14 @@ Authentication (checked in order):
|
|
|
184
199
|
1. --token-env <name> explicit env var for this invocation (hubspot)
|
|
185
200
|
2. ambient env HUBSPOT_ACCESS_TOKEN, or SALESFORCE_ACCESS_TOKEN +
|
|
186
201
|
SALESFORCE_INSTANCE_URL (CI, agent sandboxes)
|
|
187
|
-
3. stored provider login
|
|
188
|
-
(hubspot: private
|
|
189
|
-
salesforce:
|
|
190
|
-
|
|
191
|
-
4. broker
|
|
192
|
-
|
|
193
|
-
|
|
202
|
+
3. stored provider login BYO direct credentials kept in ~/.fullstackgtm
|
|
203
|
+
(hubspot: --private-token or loopback OAuth;
|
|
204
|
+
salesforce: pasted token or device flow). These
|
|
205
|
+
direct credentials override broker-hosted tokens.
|
|
206
|
+
4. hosted/broker login fullstackgtm login hubspot|salesforce (or --hosted):
|
|
207
|
+
hosted browser OAuth stores a broker credential;
|
|
208
|
+
provider tokens are minted server-side. \`login --via
|
|
209
|
+
<url>\` pairs with an existing team deployment broker.
|
|
194
210
|
|
|
195
211
|
Source options (snapshot and audit):
|
|
196
212
|
--sample Built-in minimal mock CRM data (default)
|
|
@@ -257,11 +273,12 @@ export const HELP = {
|
|
|
257
273
|
summary: "connect a provider or LLM key (secrets via stdin/env, never argv)",
|
|
258
274
|
phase: "Setup",
|
|
259
275
|
synopsis: [
|
|
276
|
+
"fullstackgtm login hubspot | salesforce hosted browser OAuth (default)",
|
|
260
277
|
"fullstackgtm login --via <hosted url> pair with a team deployment",
|
|
261
|
-
"fullstackgtm login
|
|
278
|
+
"fullstackgtm login stripe",
|
|
262
279
|
"fullstackgtm login anthropic | openai | apollo",
|
|
263
280
|
],
|
|
264
|
-
detail: "
|
|
281
|
+
detail: "HubSpot/Salesforce use hosted browser OAuth by default; BYO app/token paths are advanced. Secrets are NEVER passed as flags — pipe on stdin or enter at the prompt: `echo \"$TOKEN\" | fullstackgtm login hubspot --private-token`.",
|
|
265
282
|
seeAlso: ["doctor", "logout", "profiles"],
|
|
266
283
|
},
|
|
267
284
|
logout: {
|
|
@@ -404,6 +421,26 @@ export const HELP = {
|
|
|
404
421
|
detail: "Extra `--where` scoping is account-lifted for deals/contacts. `--except-deal-stage <stage>` excludes that stage and any record whose account has an open deal in it, re-verified per record at apply.",
|
|
405
422
|
seeAlso: ["bulk-update", "plans", "apply"],
|
|
406
423
|
},
|
|
424
|
+
backfill: {
|
|
425
|
+
summary: "Stripe paid invoices → closed-won deal proposals (governed)",
|
|
426
|
+
phase: "Remediate",
|
|
427
|
+
synopsis: [
|
|
428
|
+
"fullstackgtm backfill stripe|runs [--since <iso>] [--pipeline <id|label>] [--match-property <name>] [--skip-unmatched] [source options] [--save] [--dry-run] [--json]",
|
|
429
|
+
],
|
|
430
|
+
detail: "`backfill runs` replays LOCAL execution history to the paired hosted app (idempotent: deterministic run ids + server-side dedupe): every stored plan's apply runs and the health.jsonl timeline, so the hosted feed and Home health trend cover the engagement from its CLI-only beginning. Requires `login --via`; only what live reporting sends leaves the machine (statuses, counts, timestamps, scores — never CRM field values). `backfill stripe`: billing is the revenue source of truth — proposes ONE closed-won deal per paid Stripe invoice (amount = invoice total, close date = paid date, associated to the matched customer account, deduped by invoice id). Customers are matched to CRM accounts by domain, then exact name; a customer the CRM doesn't know gets an explicit proposed ACCOUNT create in the same plan (billing-email domain used unless it is freemail; a customer with no usable name/domain stays report-only; `--skip-unmatched` restores report-only for all unmatched). Emits a dry-run create_record plan — nothing is written until `plans approve` → `apply`, where the connector re-resolves each invoice id (and each account by domain/name) and creates only on a confirmed miss. Deal creation is HubSpot-only for now.",
|
|
431
|
+
options: [
|
|
432
|
+
["--since <iso>", "only invoices created on/after this timestamp"],
|
|
433
|
+
["--pipeline <id|label>", "target deal pipeline (default: the portal's default pipeline)"],
|
|
434
|
+
["--match-property <name>", "deal dedupe property stamped with the invoice id (default stripe_invoice_id)"],
|
|
435
|
+
["--skip-unmatched", "do not propose accounts for unmatched customers (report-only, the pre-1.3 behavior)"],
|
|
436
|
+
["--provider <name>", "CRM snapshot to match against: hubspot | salesforce"],
|
|
437
|
+
["--input <path>", "match against a saved snapshot JSON instead of a live pull"],
|
|
438
|
+
["--save", "persist the dry-run plan for approve → apply"],
|
|
439
|
+
["--json", "machine-readable {plan, counts, unmatched, proposedAccounts} (stripe) or replay summary (runs)"],
|
|
440
|
+
["--dry-run", "(runs) count what would be replayed without sending anything"],
|
|
441
|
+
],
|
|
442
|
+
seeAlso: ["enrich", "plans", "apply", "resolve"],
|
|
443
|
+
},
|
|
407
444
|
enrich: {
|
|
408
445
|
summary: "governed third-party enrichment (Apollo/Clay), fill-blanks-only",
|
|
409
446
|
phase: "Remediate",
|
|
@@ -416,7 +453,7 @@ export const HELP = {
|
|
|
416
453
|
summary: "transcripts → evidence, rubric scores, deal links, governed writes",
|
|
417
454
|
phase: "Remediate",
|
|
418
455
|
synopsis: ["fullstackgtm call parse|classify|score|link|plan … (run `call --help` for full options)"],
|
|
419
|
-
detail: "`parse` normalizes any transcript into canonical segments + evidence
|
|
456
|
+
detail: "`parse` normalizes any transcript into canonical segments + evidence — chunked LLM extraction by default when a key resolves (ANTHROPIC_API_KEY/OPENAI_API_KEY or `login anthropic|openai`), free deterministic fallback when none does; `--heuristics` forces the deterministic engine, `--llm` forces the LLM path, model via `--model` or env FSGTM_INSIGHTS_MODEL. `classify` picks the call type, `score` rates it against the type's rubric, `link` finds the deal, `plan` proposes governed next-step writes.",
|
|
420
457
|
seeAlso: ["plans", "apply"],
|
|
421
458
|
},
|
|
422
459
|
// Govern — the plan/apply spine
|
|
@@ -530,7 +567,7 @@ export function shortUsage() {
|
|
|
530
567
|
["Setup & health", ["login", "logout", "doctor", "capabilities", "robot-docs", "profiles", "health"]],
|
|
531
568
|
["Detect — read-only", ["audit", "report", "snapshot", "diff", "rules"]],
|
|
532
569
|
["Prevent — gate writes", ["resolve"]],
|
|
533
|
-
["Remediate — governed writes", ["fix", "bulk-update", "dedupe", "reassign", "enrich"]],
|
|
570
|
+
["Remediate — governed writes", ["fix", "bulk-update", "dedupe", "reassign", "enrich", "backfill"]],
|
|
534
571
|
["Calls → evidence", ["call"]],
|
|
535
572
|
["Govern — the plan/apply spine", ["suggest", "plans", "apply", "audit-log", "merge"]],
|
|
536
573
|
["Market intelligence", ["market", "tam"]],
|
package/dist/cli/plans.js
CHANGED
|
@@ -13,9 +13,10 @@ import { buildAuditLog, verifyAuditLog } from "../auditLog.js";
|
|
|
13
13
|
import { createFilePlanStore } from "../planStore.js";
|
|
14
14
|
import { ENRICH_CONFIG_FILE_NAME, loadEnrichConfig } from "../enrich.js";
|
|
15
15
|
import { loadMeter, recordConsumption, remaining } from "../acquireMeter.js";
|
|
16
|
-
import { reportCounts } from "../runReport.js";
|
|
16
|
+
import { progressReporter, reportCounts } from "../runReport.js";
|
|
17
|
+
import { APPLY_STAGES, composeListeners, createProgressEmitter } from "../progress.js";
|
|
17
18
|
import { connectorFor, isOptionValue, numericOption, option, repeatedOption, selectedRules } from "./shared.js";
|
|
18
|
-
import { colorEnabled,
|
|
19
|
+
import { colorEnabled, createProgressRenderer, paint, planStatusWord, stylizePlanMarkdown, table, truncateToWidth } from "./ui.js";
|
|
19
20
|
import { unknownSubcommandError } from "./suggest.js";
|
|
20
21
|
function parseValueOverrides(args) {
|
|
21
22
|
const valueOverrides = {};
|
|
@@ -209,21 +210,22 @@ export async function apply(args) {
|
|
|
209
210
|
// A channel (e.g. outbox) renders approved ops to a local artifact and
|
|
210
211
|
// transmits nothing; a CRM provider writes records. Same governed apply path.
|
|
211
212
|
const connector = channel ? createChannelConnector(channel) : await connectorFor(provider, args);
|
|
212
|
-
// Interactive terminals get a live
|
|
213
|
-
// executes
|
|
214
|
-
|
|
213
|
+
// Interactive terminals get a live apply board on stderr while the run
|
|
214
|
+
// executes (preflight → operations → results, with a per-op safety ticker);
|
|
215
|
+
// piped runs render nothing. Either way the emitter streams heartbeats to
|
|
216
|
+
// the paired hosted app when a long run is in flight.
|
|
217
|
+
const renderer = createProgressRenderer(APPLY_STAGES);
|
|
218
|
+
const progress = createProgressEmitter(composeListeners(renderer.listener, progressReporter()));
|
|
215
219
|
let run;
|
|
216
220
|
try {
|
|
217
221
|
run = await applyPatchPlan(connector, plan, {
|
|
218
222
|
approvedOperationIds,
|
|
219
223
|
valueOverrides,
|
|
220
|
-
|
|
221
|
-
? (progress) => ticker.set(`Applying ${progress.completed}/${progress.total} · ✓ ${progress.applied} applied · ${progress.failed} failed · ${progress.conflicts} conflict${progress.conflicts === 1 ? "" : "s"} · ${progress.skipped} skipped`)
|
|
222
|
-
: undefined,
|
|
224
|
+
progress,
|
|
223
225
|
});
|
|
224
226
|
}
|
|
225
227
|
finally {
|
|
226
|
-
|
|
228
|
+
renderer.done();
|
|
227
229
|
}
|
|
228
230
|
if (planId && store) {
|
|
229
231
|
await store.recordRun(planId, run);
|
|
@@ -242,9 +244,9 @@ export async function apply(args) {
|
|
|
242
244
|
for (const result of run.results)
|
|
243
245
|
applyTally[result.status] = (applyTally[result.status] ?? 0) + 1;
|
|
244
246
|
reportCounts(applyTally);
|
|
245
|
-
// Rich-only closing line on stderr (
|
|
247
|
+
// Rich-only closing line on stderr (renderer.active implies an interactive,
|
|
246
248
|
// color-safe stderr): the one-glance outcome of the run.
|
|
247
|
-
if (
|
|
249
|
+
if (renderer.active) {
|
|
248
250
|
const pe = paint(true);
|
|
249
251
|
const badge = run.status === "applied"
|
|
250
252
|
? pe.green(`✓ ${applyTally.applied} operation(s) applied`)
|
package/dist/cli/shared.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type LlmProvider } from "../llm.ts";
|
|
2
2
|
import { type Icp } from "../icp.ts";
|
|
3
|
-
import type { CanonicalGtmSnapshot, GtmConnector
|
|
3
|
+
import type { CanonicalGtmSnapshot, GtmConnector } from "../types.ts";
|
|
4
|
+
import { type ProgressEmitter } from "../progress.ts";
|
|
4
5
|
export declare function option(args: string[], name: string): string | null;
|
|
5
6
|
export declare function repeatedOption(args: string[], name: string): string[];
|
|
6
7
|
/**
|
|
@@ -21,8 +22,8 @@ export declare function saveRequested(args: string[]): boolean;
|
|
|
21
22
|
*/
|
|
22
23
|
export declare function confirmRequested(args: string[], ...legacyAliases: string[]): boolean;
|
|
23
24
|
export declare function numericOption(args: string[], name: string): number | undefined;
|
|
24
|
-
export declare function connectorFor(provider: string, args: string[],
|
|
25
|
-
export declare function readSnapshot(args: string[]): Promise<CanonicalGtmSnapshot>;
|
|
25
|
+
export declare function connectorFor(provider: string, args: string[], progress?: ProgressEmitter): Promise<GtmConnector>;
|
|
26
|
+
export declare function readSnapshot(args: string[], progress?: ProgressEmitter): Promise<CanonicalGtmSnapshot>;
|
|
26
27
|
/**
|
|
27
28
|
* Validate that an --input file actually has the canonical snapshot shape
|
|
28
29
|
* (the JSON `snapshot --out` writes) instead of blindly casting — a plan or
|
package/dist/cli/shared.js
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
// Extracted verbatim from src/cli.ts (mechanical split, no behavior change).
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
|
-
import { createHubspotConnector } from "../connectors/hubspot.js";
|
|
5
|
-
import { createSalesforceConnector } from "../connectors/salesforce.js";
|
|
6
|
-
import { createStripeConnector } from "../connectors/stripe.js";
|
|
7
4
|
import { credentialsPath, getCredential, resolveHubspotConnection, resolveSalesforceConnection, storeCredential } from "../credentials.js";
|
|
8
|
-
import { generateDemoSnapshot } from "../demo.js";
|
|
9
5
|
import { builtinAuditRules } from "../rules.js";
|
|
10
|
-
import { sampleSnapshot } from "../sampleData.js";
|
|
11
6
|
import { detectProviderFromKey, resolveLlmCredential, validateLlmKey } from "../llm.js";
|
|
12
7
|
import { parseIcp } from "../icp.js";
|
|
13
8
|
import { nearest } from "./suggest.js";
|
|
14
|
-
import {
|
|
9
|
+
import { composeListeners, createProgressEmitter, SNAPSHOT_PULL_STAGES, STRIPE_SNAPSHOT_STAGES, } from "../progress.js";
|
|
10
|
+
import { progressReporter } from "../runReport.js";
|
|
11
|
+
import { createProgressRenderer } from "./ui.js";
|
|
15
12
|
export function option(args, name) {
|
|
16
13
|
const index = args.indexOf(name);
|
|
17
14
|
if (index === -1)
|
|
@@ -98,23 +95,28 @@ async function salesforceConnection() {
|
|
|
98
95
|
return stored;
|
|
99
96
|
throw new Error("No Salesforce credentials. Run `fullstackgtm login salesforce`, pair with a hosted deployment via `fullstackgtm login --via <url>`, or set SALESFORCE_ACCESS_TOKEN and SALESFORCE_INSTANCE_URL. (`fullstackgtm doctor` shows credential status; device-flow login needs an admin-created Connected App — see the README's \"Connect your CRM\" section.)");
|
|
100
97
|
}
|
|
101
|
-
export async function connectorFor(provider, args,
|
|
98
|
+
export async function connectorFor(provider, args, progress) {
|
|
99
|
+
// Connector modules are the two largest files in dist (~100KB together) and
|
|
100
|
+
// load lazily here: only live `--provider` runs pay for them, not --demo/
|
|
101
|
+
// --sample/--input runs or the always-eager help/version paths.
|
|
102
102
|
if (provider === "hubspot") {
|
|
103
103
|
const connection = await hubspotConnection(args);
|
|
104
|
+
const { createHubspotConnector } = await import("../connectors/hubspot.js");
|
|
104
105
|
return createHubspotConnector({
|
|
105
106
|
getAccessToken: () => connection.accessToken,
|
|
106
107
|
fieldMappings: connection.fieldMappings ?? undefined,
|
|
107
108
|
// Point at a mock/proxy HubSpot (tests, evals, request-recording).
|
|
108
109
|
apiBaseUrl: process.env.HUBSPOT_API_BASE_URL,
|
|
109
|
-
|
|
110
|
+
progress,
|
|
110
111
|
});
|
|
111
112
|
}
|
|
112
113
|
if (provider === "salesforce") {
|
|
113
114
|
const connection = await salesforceConnection();
|
|
115
|
+
const { createSalesforceConnector } = await import("../connectors/salesforce.js");
|
|
114
116
|
return createSalesforceConnector({
|
|
115
117
|
getConnection: () => connection,
|
|
116
118
|
fieldMappings: connection.fieldMappings ?? undefined,
|
|
117
|
-
|
|
119
|
+
progress,
|
|
118
120
|
});
|
|
119
121
|
}
|
|
120
122
|
if (provider === "stripe") {
|
|
@@ -122,38 +124,47 @@ export async function connectorFor(provider, args, onProgress) {
|
|
|
122
124
|
if (!key) {
|
|
123
125
|
throw new Error("No Stripe credentials. Run `echo \"$STRIPE_KEY\" | fullstackgtm login stripe` or set STRIPE_SECRET_KEY. A restricted key with read access to Customers and Subscriptions is enough. (`fullstackgtm doctor` shows credential status.)");
|
|
124
126
|
}
|
|
125
|
-
|
|
127
|
+
const { createStripeConnector } = await import("../connectors/stripe.js");
|
|
128
|
+
return createStripeConnector({ getApiKey: () => key, progress });
|
|
126
129
|
}
|
|
127
130
|
const providerSuggestion = nearest(provider.toLowerCase(), ["hubspot", "salesforce", "stripe"], 3);
|
|
128
131
|
throw new Error(`Unknown provider: ${provider}.${providerSuggestion ? ` Did you mean ${providerSuggestion}?` : ""} ` +
|
|
129
132
|
"Supported providers: hubspot, salesforce, stripe");
|
|
130
133
|
}
|
|
131
|
-
export async function readSnapshot(args) {
|
|
134
|
+
export async function readSnapshot(args, progress) {
|
|
132
135
|
const provider = option(args, "--provider");
|
|
133
136
|
if (provider) {
|
|
137
|
+
// A verb driving its own progress board (e.g. `backfill stripe`) passes
|
|
138
|
+
// its emitter through; the pull's stage/items events flow into it and no
|
|
139
|
+
// second board is painted.
|
|
140
|
+
if (progress) {
|
|
141
|
+
const connector = await connectorFor(provider, args, progress);
|
|
142
|
+
return connector.fetchSnapshot();
|
|
143
|
+
}
|
|
134
144
|
// Live pulls can page for minutes with nothing on screen. On an
|
|
135
|
-
// interactive terminal, a stderr
|
|
136
|
-
// piped/CI/agent runs
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
145
|
+
// interactive terminal, a stderr checklist ticks through the pull stages
|
|
146
|
+
// with running tallies; piped/CI/agent runs render nothing (zero bytes
|
|
147
|
+
// written). Either way the emitter feeds the broker heartbeat when the
|
|
148
|
+
// CLI is paired, so long pulls tick live on the hosted dashboard too.
|
|
149
|
+
const renderer = createProgressRenderer(provider === "stripe" ? STRIPE_SNAPSHOT_STAGES : SNAPSHOT_PULL_STAGES);
|
|
150
|
+
const emitter = createProgressEmitter(composeListeners(renderer.listener, progressReporter()));
|
|
151
|
+
const connector = await connectorFor(provider, args, emitter);
|
|
142
152
|
try {
|
|
143
153
|
return await connector.fetchSnapshot();
|
|
144
154
|
}
|
|
145
155
|
finally {
|
|
146
|
-
|
|
156
|
+
renderer.done();
|
|
147
157
|
}
|
|
148
158
|
}
|
|
149
159
|
if (args.includes("--demo")) {
|
|
160
|
+
const { generateDemoSnapshot } = await import("../demo.js");
|
|
150
161
|
return generateDemoSnapshot({
|
|
151
162
|
seed: numericOption(args, "--seed"),
|
|
152
163
|
today: option(args, "--today") ?? undefined,
|
|
153
164
|
});
|
|
154
165
|
}
|
|
155
166
|
if (args.includes("--sample"))
|
|
156
|
-
return sampleSnapshot;
|
|
167
|
+
return (await import("../sampleData.js")).sampleSnapshot;
|
|
157
168
|
const input = option(args, "--input");
|
|
158
169
|
if (!input) {
|
|
159
170
|
throw new Error("No data source. Pass one of: --provider <hubspot|salesforce|stripe> (live, read-only), " +
|
package/dist/cli/ui.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ProgressListener } from "../progress.ts";
|
|
1
2
|
type StreamLike = {
|
|
2
3
|
isTTY?: boolean;
|
|
3
4
|
columns?: number;
|
|
@@ -100,6 +101,26 @@ export declare function createChecklist(items: Array<{
|
|
|
100
101
|
id: string;
|
|
101
102
|
label: string;
|
|
102
103
|
}>, stream?: StreamLike, env?: UiEnv): Checklist;
|
|
104
|
+
export type ProgressRenderer = {
|
|
105
|
+
/**
|
|
106
|
+
* Feed to `createProgressEmitter` (compose with the broker streaming
|
|
107
|
+
* listener via `composeListeners` when the run should also heartbeat).
|
|
108
|
+
*/
|
|
109
|
+
listener: ProgressListener;
|
|
110
|
+
/** Stop animating and erase the board (callers print their own summary). */
|
|
111
|
+
done(): void;
|
|
112
|
+
readonly active: boolean;
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Adapter from the shared progress vocabulary (src/progress.ts) to the
|
|
116
|
+
* existing TTY primitives: `stage` events advance a live checklist (○ → ⠹ →
|
|
117
|
+
* ✓), while `items` / `note` / `opResult` / `meter` events annotate the
|
|
118
|
+
* running stage ("contacts 1,200/4,800", the apply ticker tallies, a fuel
|
|
119
|
+
* gauge). Outside an interactive TTY this is an inert no-op — piped/CI/agent
|
|
120
|
+
* output stays byte-identical (streaming listeners composed alongside still
|
|
121
|
+
* run; rendering is presentation only).
|
|
122
|
+
*/
|
|
123
|
+
export declare function createProgressRenderer(stages: readonly string[], stream?: StreamLike, env?: UiEnv): ProgressRenderer;
|
|
103
124
|
/** Truncate to `max` display characters, ending in "…" when cut. */
|
|
104
125
|
export declare function truncateToWidth(text: string, max: number): string;
|
|
105
126
|
/** critical → red, warning → yellow, info → dim. */
|
package/dist/cli/ui.js
CHANGED
|
@@ -297,6 +297,59 @@ export function createChecklist(items, stream = process.stderr, env = process.en
|
|
|
297
297
|
active: true,
|
|
298
298
|
};
|
|
299
299
|
}
|
|
300
|
+
const NOOP_RENDERER = { listener: () => { }, done() { }, active: false };
|
|
301
|
+
/**
|
|
302
|
+
* Adapter from the shared progress vocabulary (src/progress.ts) to the
|
|
303
|
+
* existing TTY primitives: `stage` events advance a live checklist (○ → ⠹ →
|
|
304
|
+
* ✓), while `items` / `note` / `opResult` / `meter` events annotate the
|
|
305
|
+
* running stage ("contacts 1,200/4,800", the apply ticker tallies, a fuel
|
|
306
|
+
* gauge). Outside an interactive TTY this is an inert no-op — piped/CI/agent
|
|
307
|
+
* output stays byte-identical (streaming listeners composed alongside still
|
|
308
|
+
* run; rendering is presentation only).
|
|
309
|
+
*/
|
|
310
|
+
export function createProgressRenderer(stages, stream = process.stderr, env = process.env) {
|
|
311
|
+
const board = createChecklist(stages.map((stage) => ({ id: stage, label: stage })), stream, env);
|
|
312
|
+
if (!board.active)
|
|
313
|
+
return NOOP_RENDERER;
|
|
314
|
+
let current = null;
|
|
315
|
+
const noteFor = (event, snapshot) => {
|
|
316
|
+
if (event.kind === "items") {
|
|
317
|
+
return event.total !== undefined
|
|
318
|
+
? `${formatCount(event.done)}/${formatCount(event.total)}`
|
|
319
|
+
: formatCount(event.done);
|
|
320
|
+
}
|
|
321
|
+
if (event.kind === "note")
|
|
322
|
+
return event.note;
|
|
323
|
+
if (event.kind === "opResult") {
|
|
324
|
+
return `${GLYPH.ok} ${formatCount(snapshot.opsApplied)} applied · ${formatCount(snapshot.opsFailed)} failed · ${formatCount(snapshot.opsSkipped)} skipped`;
|
|
325
|
+
}
|
|
326
|
+
if (event.kind === "meter") {
|
|
327
|
+
const fraction = event.budget > 0 ? event.spent / event.budget : 0;
|
|
328
|
+
return `${formatBar(fraction, 10)} ${formatCount(event.spent)}/${formatCount(event.budget)} ${event.unit}`;
|
|
329
|
+
}
|
|
330
|
+
return undefined;
|
|
331
|
+
};
|
|
332
|
+
return {
|
|
333
|
+
listener(event, snapshot) {
|
|
334
|
+
if (event.kind === "stage") {
|
|
335
|
+
// Advance the board: close out the previous stage, spin the new one.
|
|
336
|
+
if (current && current !== event.stage)
|
|
337
|
+
board.update(current, "ok");
|
|
338
|
+
current = event.stage;
|
|
339
|
+
board.update(current, "running");
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
// Events for a stage the board doesn't know (e.g. a nested pull inside a
|
|
343
|
+
// larger flow) are ignored by createChecklist's unknown-id no-op.
|
|
344
|
+
if (current)
|
|
345
|
+
board.update(current, "running", noteFor(event, snapshot));
|
|
346
|
+
},
|
|
347
|
+
done() {
|
|
348
|
+
board.done();
|
|
349
|
+
},
|
|
350
|
+
active: true,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
300
353
|
/** Truncate to `max` display characters, ending in "…" when cut. */
|
|
301
354
|
export function truncateToWidth(text, max) {
|
|
302
355
|
if (max <= 0)
|
|
@@ -370,6 +423,5 @@ export function stylizePlanMarkdown(text, p) {
|
|
|
370
423
|
}
|
|
371
424
|
/** Strip ANSI escape sequences (test helper + safety net for width math). */
|
|
372
425
|
export function stripAnsi(text) {
|
|
373
|
-
// eslint-disable-next-line no-control-regex
|
|
374
426
|
return text.replace(/\u001b\[[0-9;]*[A-Za-z]/g, "");
|
|
375
427
|
}
|