fullstackgtm 0.12.0 → 0.13.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 +37 -0
- package/README.md +19 -0
- package/dist/calls.d.ts +72 -0
- package/dist/calls.js +355 -0
- package/dist/cli.js +181 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mcp.js +24 -1
- package/llms.txt +7 -0
- package/package.json +1 -1
- package/src/calls.ts +444 -0
- package/src/cli.ts +195 -0
- package/src/index.ts +13 -0
- package/src/mcp.ts +30 -1
package/dist/cli.js
CHANGED
|
@@ -17,6 +17,7 @@ import { createFilePlanStore } from "./planStore.js";
|
|
|
17
17
|
import { auditReportToHtml, auditReportToMarkdown } from "./report.js";
|
|
18
18
|
import { builtinAuditRules } from "./rules.js";
|
|
19
19
|
import { sampleSnapshot } from "./sampleData.js";
|
|
20
|
+
import { parseCall, suggestCallDeal } from "./calls.js";
|
|
20
21
|
import { suggestValues } from "./suggest.js";
|
|
21
22
|
function usage() {
|
|
22
23
|
return `FullStackGTM — audit GTM data across providers, propose reviewable patch plans,
|
|
@@ -40,6 +41,11 @@ Usage:
|
|
|
40
41
|
fullstackgtm report [source options] [audit options] [report options]
|
|
41
42
|
fullstackgtm diff --before <a.json> --after <b.json> [--json] [--fail-on-new-findings]
|
|
42
43
|
fullstackgtm merge --input <a.json> --input <b.json> [...] --out <merged.json> [--json]
|
|
44
|
+
fullstackgtm call parse --transcript <file> [--title t] [--source fathom|granola|...] [--json|--ndjson] [--out <path>]
|
|
45
|
+
fullstackgtm call link --attendees <a@x.com,...> | --domain <x.com> [source options] [--json]
|
|
46
|
+
fullstackgtm call plan --transcript <file>|--call <parsed.json> --deal <id> [source options] [--save|--json]
|
|
47
|
+
calls become evidence: parse dialects (Speaker:/[Me]/Granola JSON),
|
|
48
|
+
link to the right deal, and propose governed next-step writes
|
|
43
49
|
fullstackgtm suggest --plan-id <id> | --plan <path> [source options] [--json] [--out <path>]
|
|
44
50
|
derive values for requires_human_* placeholders
|
|
45
51
|
from snapshot evidence, with confidence + reasons
|
|
@@ -179,6 +185,8 @@ async function connectorFor(provider, args) {
|
|
|
179
185
|
return createHubspotConnector({
|
|
180
186
|
getAccessToken: () => connection.accessToken,
|
|
181
187
|
fieldMappings: connection.fieldMappings ?? undefined,
|
|
188
|
+
// Point at a mock/proxy HubSpot (tests, evals, request-recording).
|
|
189
|
+
apiBaseUrl: process.env.HUBSPOT_API_BASE_URL,
|
|
182
190
|
});
|
|
183
191
|
}
|
|
184
192
|
if (provider === "salesforce") {
|
|
@@ -403,6 +411,175 @@ function parseValueOverrides(args) {
|
|
|
403
411
|
}
|
|
404
412
|
return valueOverrides;
|
|
405
413
|
}
|
|
414
|
+
async function callCommand(args) {
|
|
415
|
+
const [subcommand, ...rest] = args;
|
|
416
|
+
const loadParsedCall = () => {
|
|
417
|
+
const callPath = option(rest, "--call");
|
|
418
|
+
if (callPath) {
|
|
419
|
+
return JSON.parse(readFileSync(resolve(process.cwd(), callPath), "utf8"));
|
|
420
|
+
}
|
|
421
|
+
const transcriptPath = option(rest, "--transcript");
|
|
422
|
+
if (!transcriptPath)
|
|
423
|
+
throw new Error(`call ${subcommand} requires --transcript <file> or --call <parsed.json>`);
|
|
424
|
+
const raw = readFileSync(resolve(process.cwd(), transcriptPath), "utf8");
|
|
425
|
+
const source = option(rest, "--source");
|
|
426
|
+
return parseCall(raw, {
|
|
427
|
+
title: option(rest, "--title") ?? undefined,
|
|
428
|
+
sourceSystem: source,
|
|
429
|
+
capturedAt: new Date().toISOString(),
|
|
430
|
+
});
|
|
431
|
+
};
|
|
432
|
+
if (subcommand === "parse") {
|
|
433
|
+
const parsed = loadParsedCall();
|
|
434
|
+
const outPath = option(rest, "--out");
|
|
435
|
+
if (outPath)
|
|
436
|
+
writeFileSync(resolve(process.cwd(), outPath), `${JSON.stringify(parsed, null, 2)}\n`);
|
|
437
|
+
if (rest.includes("--ndjson")) {
|
|
438
|
+
// One flat row per insight — warehouse-friendly (e.g. Snowflake COPY).
|
|
439
|
+
for (const insight of parsed.insights) {
|
|
440
|
+
console.log(JSON.stringify({
|
|
441
|
+
call_id: parsed.id,
|
|
442
|
+
call_title: parsed.title ?? null,
|
|
443
|
+
source_system: parsed.sourceSystem,
|
|
444
|
+
type: insight.type,
|
|
445
|
+
title: insight.title,
|
|
446
|
+
text: insight.text,
|
|
447
|
+
evidence: insight.evidence,
|
|
448
|
+
speaker: insight.speaker ?? null,
|
|
449
|
+
confidence: insight.confidence,
|
|
450
|
+
importance: insight.importance,
|
|
451
|
+
}));
|
|
452
|
+
}
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (rest.includes("--json") || outPath) {
|
|
456
|
+
if (!outPath)
|
|
457
|
+
console.log(JSON.stringify(parsed, null, 2));
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
console.log(`Call ${parsed.id}${parsed.title ? ` — ${parsed.title}` : ""} (${parsed.sourceSystem})`);
|
|
461
|
+
console.log(`${parsed.segments.length} segments · ${parsed.insights.length} insights (${parsed.summary.highImportance} high-importance)\n`);
|
|
462
|
+
for (const insight of parsed.insights) {
|
|
463
|
+
console.log(`[${insight.type}] (importance ${insight.importance}) ${insight.text}`);
|
|
464
|
+
}
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (subcommand === "link") {
|
|
468
|
+
const attendees = option(rest, "--attendees");
|
|
469
|
+
const domain = option(rest, "--domain");
|
|
470
|
+
if (!attendees && !domain)
|
|
471
|
+
throw new Error("call link requires --attendees <emails,comma-separated> and/or --domain <example.com>");
|
|
472
|
+
const snapshot = await readSnapshot(rest);
|
|
473
|
+
const suggestion = suggestCallDeal(snapshot, {
|
|
474
|
+
attendeeEmails: attendees?.split(",").map((e) => e.trim()).filter(Boolean),
|
|
475
|
+
domain: domain ?? undefined,
|
|
476
|
+
});
|
|
477
|
+
if (rest.includes("--json")) {
|
|
478
|
+
console.log(JSON.stringify(suggestion, null, 2));
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
const marker = suggestion.confidence === "high" ? "✓" : suggestion.confidence === "low" ? "~" : "✗";
|
|
482
|
+
console.log(`${marker} [${suggestion.confidence}] ${suggestion.dealId ?? "no match"}${suggestion.dealName ? ` — ${suggestion.dealName}` : ""}`);
|
|
483
|
+
console.log(` ${suggestion.reason}`);
|
|
484
|
+
}
|
|
485
|
+
if (suggestion.confidence === "none")
|
|
486
|
+
process.exitCode = 1;
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (subcommand === "plan") {
|
|
490
|
+
const dealId = option(rest, "--deal");
|
|
491
|
+
if (!dealId)
|
|
492
|
+
throw new Error("call plan requires --deal <dealId> (use `call link` to find it)");
|
|
493
|
+
const parsed = loadParsedCall();
|
|
494
|
+
const snapshot = await readSnapshot(rest);
|
|
495
|
+
const deal = snapshot.deals.find((row) => row.id === dealId);
|
|
496
|
+
if (!deal)
|
|
497
|
+
throw new Error(`Deal ${dealId} is not in the snapshot — check the id or the snapshot source.`);
|
|
498
|
+
const nextSteps = parsed.insights.filter((insight) => insight.type === "next_step");
|
|
499
|
+
if (nextSteps.length === 0) {
|
|
500
|
+
console.log("No next-step insights in this call — nothing to plan. (Other insight types are evidence, not writes.)");
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const [top, ...others] = nextSteps;
|
|
504
|
+
const proposed = top.text.trim().slice(0, 255);
|
|
505
|
+
const current = deal.nextStep?.trim() ?? "";
|
|
506
|
+
const plan = buildCallPlan(parsed, deal, proposed, current, others.slice(0, 3));
|
|
507
|
+
if (rest.includes("--save")) {
|
|
508
|
+
await createFilePlanStore().save(plan);
|
|
509
|
+
console.log(`Saved plan ${plan.id}. Review with \`fullstackgtm plans show ${plan.id}\`, approve with \`fullstackgtm plans approve ${plan.id} --operations <ids|all>\`, then \`fullstackgtm apply --plan-id ${plan.id} --provider <name>\`.`);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
console.log(rest.includes("--json") ? JSON.stringify(plan, null, 2) : patchPlanToMarkdown(plan));
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
throw new Error(`call supports: parse, link, plan (got ${subcommand ?? "nothing"})`);
|
|
516
|
+
}
|
|
517
|
+
function buildCallPlan(parsed, deal, proposed, current, extraNextSteps) {
|
|
518
|
+
const findings = [];
|
|
519
|
+
const operations = [];
|
|
520
|
+
const nextStepEvidence = parsed.evidence.filter((item) => item.metadata?.insightType === "next_step");
|
|
521
|
+
const evidenceIds = nextStepEvidence.map((item) => item.id);
|
|
522
|
+
if (current.toLowerCase() !== proposed.toLowerCase()) {
|
|
523
|
+
findings.push({
|
|
524
|
+
id: `finding_${parsed.id.replace(/^call_/, "")}_${deal.id}`,
|
|
525
|
+
objectType: "deal",
|
|
526
|
+
objectId: deal.id,
|
|
527
|
+
ruleId: "call-next-step-not-reflected-in-crm",
|
|
528
|
+
type: "call_next_step_not_reflected_in_crm",
|
|
529
|
+
title: "Call agreed a next step the CRM does not reflect",
|
|
530
|
+
severity: "warning",
|
|
531
|
+
summary: current
|
|
532
|
+
? `The call produced "${proposed}" but ${deal.name}'s next step still reads "${current}".`
|
|
533
|
+
: `The call produced "${proposed}" but ${deal.name} has no next step set.`,
|
|
534
|
+
recommendation: "Review the evidence and approve the next-step update.",
|
|
535
|
+
evidenceIds,
|
|
536
|
+
currentCrmValue: current || null,
|
|
537
|
+
proposedValue: proposed,
|
|
538
|
+
});
|
|
539
|
+
operations.push({
|
|
540
|
+
id: `op_${parsed.id.replace(/^call_/, "")}_next`,
|
|
541
|
+
objectType: "deal",
|
|
542
|
+
objectId: deal.id,
|
|
543
|
+
operation: "set_field",
|
|
544
|
+
field: "nextStep",
|
|
545
|
+
beforeValue: current || null,
|
|
546
|
+
afterValue: proposed,
|
|
547
|
+
reason: `Call evidence: ${nextStepEvidence[0]?.text.slice(0, 200) ?? proposed}`,
|
|
548
|
+
sourceRuleOrPolicy: "call_intelligence.next_step",
|
|
549
|
+
riskLevel: "high",
|
|
550
|
+
approvalRequired: true,
|
|
551
|
+
rollback: "Restore the previous deal next step (the before value) if the update is wrong.",
|
|
552
|
+
evidenceIds,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
for (const [index, extra] of extraNextSteps.entries()) {
|
|
556
|
+
operations.push({
|
|
557
|
+
id: `op_${parsed.id.replace(/^call_/, "")}_task${index}`,
|
|
558
|
+
objectType: "deal",
|
|
559
|
+
objectId: deal.id,
|
|
560
|
+
operation: "create_task",
|
|
561
|
+
field: "follow_up_task",
|
|
562
|
+
beforeValue: null,
|
|
563
|
+
afterValue: extra.text.trim().slice(0, 255),
|
|
564
|
+
reason: `Additional commitment from the call: ${extra.evidence.slice(0, 160)}`,
|
|
565
|
+
sourceRuleOrPolicy: "call_intelligence.follow_up",
|
|
566
|
+
riskLevel: "low",
|
|
567
|
+
approvalRequired: true,
|
|
568
|
+
rollback: "Close or delete the created task.",
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
return {
|
|
572
|
+
id: `patch_plan_${parsed.id.replace(/^call_/, "")}${deal.id.slice(-4)}`,
|
|
573
|
+
title: `Call evidence plan${parsed.title ? ` — ${parsed.title}` : ""} → ${deal.name}`,
|
|
574
|
+
createdAt: new Date().toISOString(),
|
|
575
|
+
status: "needs_approval",
|
|
576
|
+
dryRun: true,
|
|
577
|
+
summary: `${findings.length} finding(s) and ${operations.length} proposed operation(s) from call ${parsed.id}.`,
|
|
578
|
+
findings,
|
|
579
|
+
evidence: parsed.evidence,
|
|
580
|
+
operations,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
406
583
|
async function suggest(args) {
|
|
407
584
|
const planId = option(args, "--plan-id");
|
|
408
585
|
const planPath = option(args, "--plan");
|
|
@@ -1123,6 +1300,10 @@ export async function runCli(argv) {
|
|
|
1123
1300
|
await suggest(args);
|
|
1124
1301
|
return;
|
|
1125
1302
|
}
|
|
1303
|
+
if (command === "call") {
|
|
1304
|
+
await callCommand(args);
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1126
1307
|
if (command === "profiles") {
|
|
1127
1308
|
profilesCommand(args);
|
|
1128
1309
|
return;
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export { formatPatchPlanRun, patchPlanToMarkdown } from "./format.ts";
|
|
|
15
15
|
export { auditReportToHtml, auditReportToMarkdown, type ReportOptions } from "./report.ts";
|
|
16
16
|
export { HUBSPOT_DEFAULT_FIELD_MAPPINGS, SALESFORCE_DEFAULT_FIELD_MAPPINGS, mappedField, mappedFields, normalizeFieldMappings, readMappedValue, type CrmObjectType, type FieldMappings, } from "./mappings.ts";
|
|
17
17
|
export { accountSingleSourceRule, activeDealAccountWithoutContactsRule, auditFindingId, buildSnapshotIndex, builtinAuditRules, closingSoonInactiveRule, duplicateAccountDomainRule, duplicateContactEmailRule, duplicateOpenDealRule, missingDealAccountRule, missingDealAmountRule, missingDealOwnerRule, orphanAccountRule, pastCloseDateRule, patchOperationId, requiresHumanInput, staleDealRule, } from "./rules.ts";
|
|
18
|
+
export { extractCallInsights, normalizeTranscript, parseCall, parseTranscript, suggestCallDeal, summarizeInsights, type CallDealSuggestion, type CallInsightType, type ExtractedCallInsight, type ParsedCall, type ParsedTranscriptSegment, } from "./calls.ts";
|
|
18
19
|
export { sampleSnapshot } from "./sampleData.ts";
|
|
19
20
|
export { suggestValues, type SuggestionConfidence, type ValueSuggestion } from "./suggest.ts";
|
|
20
21
|
export type { ApprovalStatus, AuditFinding, AuditFindingSeverity, CanonicalAccount, CanonicalActivity, CanonicalContact, CanonicalDeal, CanonicalGtmSnapshot, CanonicalUser, CrmProvider, GtmAuditRule, GtmConnector, GtmEvidence, GtmEvidenceSourceSystem, GtmObjectType, GtmPolicy, GtmRuleContext, GtmRuleResult, GtmSnapshotIndex, PatchOperation, PatchOperationResult, PatchOperationType, PatchPlan, PatchPlanRun, PatchPlanRunStatus, PatchVerification, PipelineFinding, PipelineFindingStatus, PipelineFindingType, ProviderIdentity, RiskLevel, SourceFreshness, } from "./types.ts";
|
package/dist/index.js
CHANGED
|
@@ -15,5 +15,6 @@ export { formatPatchPlanRun, patchPlanToMarkdown } from "./format.js";
|
|
|
15
15
|
export { auditReportToHtml, auditReportToMarkdown } from "./report.js";
|
|
16
16
|
export { HUBSPOT_DEFAULT_FIELD_MAPPINGS, SALESFORCE_DEFAULT_FIELD_MAPPINGS, mappedField, mappedFields, normalizeFieldMappings, readMappedValue, } from "./mappings.js";
|
|
17
17
|
export { accountSingleSourceRule, activeDealAccountWithoutContactsRule, auditFindingId, buildSnapshotIndex, builtinAuditRules, closingSoonInactiveRule, duplicateAccountDomainRule, duplicateContactEmailRule, duplicateOpenDealRule, missingDealAccountRule, missingDealAmountRule, missingDealOwnerRule, orphanAccountRule, pastCloseDateRule, patchOperationId, requiresHumanInput, staleDealRule, } from "./rules.js";
|
|
18
|
+
export { extractCallInsights, normalizeTranscript, parseCall, parseTranscript, suggestCallDeal, summarizeInsights, } from "./calls.js";
|
|
18
19
|
export { sampleSnapshot } from "./sampleData.js";
|
|
19
20
|
export { suggestValues } from "./suggest.js";
|
package/dist/mcp.js
CHANGED
|
@@ -45,6 +45,7 @@ import { generateDemoSnapshot } from "./demo.js";
|
|
|
45
45
|
import { formatPatchPlanRun, patchPlanToMarkdown } from "./format.js";
|
|
46
46
|
import { builtinAuditRules } from "./rules.js";
|
|
47
47
|
import { sampleSnapshot } from "./sampleData.js";
|
|
48
|
+
import { parseCall } from "./calls.js";
|
|
48
49
|
import { suggestValues } from "./suggest.js";
|
|
49
50
|
function content(value) {
|
|
50
51
|
return {
|
|
@@ -62,7 +63,10 @@ async function connectorFor(provider) {
|
|
|
62
63
|
if (!token) {
|
|
63
64
|
throw new Error("No HubSpot credentials. Run `fullstackgtm login hubspot` or set HUBSPOT_ACCESS_TOKEN in the MCP server environment.");
|
|
64
65
|
}
|
|
65
|
-
return createHubspotConnector({
|
|
66
|
+
return createHubspotConnector({
|
|
67
|
+
getAccessToken: () => token,
|
|
68
|
+
apiBaseUrl: process.env.HUBSPOT_API_BASE_URL,
|
|
69
|
+
});
|
|
66
70
|
}
|
|
67
71
|
if (provider === "salesforce") {
|
|
68
72
|
const connection = process.env.SALESFORCE_ACCESS_TOKEN && process.env.SALESFORCE_INSTANCE_URL
|
|
@@ -156,6 +160,25 @@ export async function startMcpServer() {
|
|
|
156
160
|
const snapshot = await readSnapshot(provider, inputPath);
|
|
157
161
|
return content({ suggestions: suggestValues(plan, snapshot) });
|
|
158
162
|
});
|
|
163
|
+
server.registerTool("fullstackgtm_call_parse", {
|
|
164
|
+
title: "Parse Call Transcript",
|
|
165
|
+
description: "Deterministically parse a call transcript (Speaker:/[Speaker]: lines or Granola " +
|
|
166
|
+
"utterance JSON) into canonical segments, keyword-derived insights (next steps, " +
|
|
167
|
+
"objections, pricing, risks, competitor mentions...), and GtmEvidence records. " +
|
|
168
|
+
"Read-only and LLM-free; pair with fullstackgtm_audit/apply for governed writes.",
|
|
169
|
+
inputSchema: {
|
|
170
|
+
transcript: z.string().optional(),
|
|
171
|
+
transcriptPath: z.string().optional(),
|
|
172
|
+
title: z.string().optional(),
|
|
173
|
+
source: z.enum(["gong", "chorus", "fathom", "manual", "csv", "unknown"]).optional(),
|
|
174
|
+
},
|
|
175
|
+
}, async ({ transcript, transcriptPath, title, source }) => {
|
|
176
|
+
const raw = transcript ??
|
|
177
|
+
(transcriptPath ? readFileSync(resolve(process.cwd(), transcriptPath), "utf8") : null);
|
|
178
|
+
if (!raw)
|
|
179
|
+
throw new Error("Provide transcript (text) or transcriptPath (file).");
|
|
180
|
+
return content(parseCall(raw, { title, sourceSystem: source }));
|
|
181
|
+
});
|
|
159
182
|
server.registerTool("fullstackgtm_rules", {
|
|
160
183
|
title: "List Audit Rules",
|
|
161
184
|
description: "List the built-in deterministic audit rules with ids and descriptions.",
|
package/llms.txt
CHANGED
|
@@ -18,6 +18,13 @@ at/above `--fail-on`.
|
|
|
18
18
|
- [CRM-health lifecycle](https://github.com/fullstackgtm/core/blob/main/docs/crm-health-lifecycle.md): the Prevent → Detect → Remediate → Verify/Attribute model; no-new-dupes design
|
|
19
19
|
- [CHANGELOG](https://github.com/fullstackgtm/core/blob/main/CHANGELOG.md): release history
|
|
20
20
|
|
|
21
|
+
## Key invariants (calls)
|
|
22
|
+
|
|
23
|
+
`fullstackgtm call parse` (and MCP fullstackgtm_call_parse) is deterministic
|
|
24
|
+
and LLM-free; `call link` suggests the deal with confidence + reason;
|
|
25
|
+
`call plan` proposes governed next-step writes through the standard
|
|
26
|
+
approve/apply lifecycle.
|
|
27
|
+
|
|
21
28
|
## Key invariants
|
|
22
29
|
|
|
23
30
|
- Reads are safe by default; nothing is written without explicit `--approve`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fullstackgtm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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",
|