fullstackgtm 0.50.0 → 0.51.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 (66) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +7 -0
  3. package/dist/cli/audit.js +6 -1
  4. package/dist/cli/auth.js +49 -4
  5. package/dist/cli/backfill.js +4 -0
  6. package/dist/cli/call.js +28 -6
  7. package/dist/cli/draft.js +11 -1
  8. package/dist/cli/enrich.js +124 -57
  9. package/dist/cli/fix.js +6 -3
  10. package/dist/cli/help.js +31 -28
  11. package/dist/cli/icp.js +15 -1
  12. package/dist/cli/init.js +3 -3
  13. package/dist/cli/market.js +14 -1
  14. package/dist/cli/planOutput.d.ts +5 -0
  15. package/dist/cli/planOutput.js +27 -0
  16. package/dist/cli/plans.js +184 -33
  17. package/dist/cli/schedule.js +22 -11
  18. package/dist/cli/signals.js +22 -3
  19. package/dist/cli/tam.d.ts +1 -1
  20. package/dist/cli/tam.js +14 -2
  21. package/dist/cli/ui.js +14 -6
  22. package/dist/connectors/clay.d.ts +33 -0
  23. package/dist/connectors/clay.js +123 -0
  24. package/dist/connectors/hubspot.d.ts +4 -0
  25. package/dist/connectors/hubspot.js +36 -18
  26. package/dist/connectors/prospectSources.d.ts +12 -0
  27. package/dist/connectors/prospectSources.js +1 -1
  28. package/dist/contactProviders.d.ts +44 -0
  29. package/dist/contactProviders.js +100 -0
  30. package/dist/enrich.d.ts +7 -1
  31. package/dist/enrich.js +46 -1
  32. package/dist/hostedPatchPlan.js +6 -1
  33. package/dist/icp.d.ts +2 -0
  34. package/dist/icp.js +49 -0
  35. package/dist/index.d.ts +2 -0
  36. package/dist/index.js +2 -0
  37. package/dist/init.d.ts +1 -1
  38. package/dist/init.js +1 -1
  39. package/docs/api.md +18 -1
  40. package/package.json +1 -1
  41. package/src/cli/audit.ts +5 -1
  42. package/src/cli/auth.ts +46 -4
  43. package/src/cli/backfill.ts +3 -0
  44. package/src/cli/call.ts +25 -6
  45. package/src/cli/draft.ts +9 -1
  46. package/src/cli/enrich.ts +132 -56
  47. package/src/cli/fix.ts +5 -3
  48. package/src/cli/help.ts +31 -28
  49. package/src/cli/icp.ts +13 -1
  50. package/src/cli/init.ts +3 -3
  51. package/src/cli/market.ts +12 -1
  52. package/src/cli/planOutput.ts +30 -0
  53. package/src/cli/plans.ts +174 -29
  54. package/src/cli/schedule.ts +21 -15
  55. package/src/cli/signals.ts +22 -3
  56. package/src/cli/tam.ts +12 -3
  57. package/src/cli/ui.ts +15 -6
  58. package/src/connectors/clay.ts +155 -0
  59. package/src/connectors/hubspot.ts +41 -17
  60. package/src/connectors/prospectSources.ts +7 -1
  61. package/src/contactProviders.ts +141 -0
  62. package/src/enrich.ts +51 -2
  63. package/src/hostedPatchPlan.ts +6 -1
  64. package/src/icp.ts +51 -0
  65. package/src/index.ts +25 -0
  66. package/src/init.ts +2 -2
package/dist/cli/help.js CHANGED
@@ -16,13 +16,16 @@ Usage:
16
16
  fullstackgtm login salesforce --instance-url <url> [--no-validate] advanced: paste access token
17
17
  fullstackgtm login stripe [--no-validate]
18
18
  fullstackgtm login anthropic | openai store an LLM API key for call parse/score
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>
19
+ fullstackgtm login apollo | clay store a data-provider Public API key
20
+ fullstackgtm login pipe0 | explorium | theirstack store a discovery-provider key (theirstack = technographic TAM)
21
+ fullstackgtm login heyreach store a HeyReach key for enrich acquire --source linkedin
22
+ fullstackgtm logout <hubspot|salesforce|stripe|anthropic|openai|apollo|clay|pipe0|explorium|heyreach|broker>
20
23
 
21
24
  Secrets (tokens, client secrets) are NEVER passed as flags — they leak via
22
25
  the process list and shell history. Pipe them on stdin or enter them at the
23
26
  interactive prompt:
24
27
  echo "$HUBSPOT_TOKEN" | fullstackgtm login hubspot --private-token
25
- fullstackgtm init [--source pipe0|explorium|linkedin] [--provider hubspot|salesforce] [--out <dir>] [--force]
28
+ fullstackgtm init [--source pipe0|explorium|clay|linkedin] [--provider hubspot|salesforce] [--out <dir>] [--force]
26
29
  cold start: scaffold icp.json + enrich.config.json + a
27
30
  PLAYBOOK wired for this workspace (the CLI ships primitives,
28
31
  your agent is the orchestrator — see docs/recipes.md)
@@ -72,7 +75,7 @@ Usage:
72
75
  fullstackgtm tam estimate [--name <n>] [--icp <path>] (--accounts <n> | --source theirstack|explorium) (--acv <annual-usd> | --acv-from-crm --deal-period monthly|quarterly|annual) [--acv-basis account|buyer] [--buyers-per-account <n>] [--cross-checks <file.json>] [source options] [--json]
73
76
  fullstackgtm tam status [--name <n>] <source options> [--save] [--json]
74
77
  fullstackgtm tam report [--name <n>] [--out <path>]
75
- fullstackgtm tam populate [--name <n>] --cron "<expr>" [--source pipe0|explorium|linkedin] [--provider hubspot|salesforce] [--label <l>]
78
+ fullstackgtm tam populate [--name <n>] --cron "<expr>" [--source pipe0|explorium|clay|linkedin] [--provider hubspot|salesforce] [--label <l>]
76
79
  size the reachable market FROM the ICP (a real account count ×
77
80
  ACV; buyers/account = the contact population target), then fill
78
81
  it: populate schedules plan-only enrich acquire --save (apply
@@ -176,11 +179,11 @@ Usage:
176
179
  fullstackgtm suggest --plan-id <id> | --plan <path> [source options] [--json] [--out <path>]
177
180
  derive values for requires_human_* placeholders
178
181
  from snapshot evidence, with confidence + reasons
179
- fullstackgtm plans list [--status <s>] | show <id> | sync | reject <id>
182
+ fullstackgtm plans list [--status <s>] | show <id> [--verbose] | sync | reject <id>
180
183
  fullstackgtm plans approve <id> --operations <ids|all> [--value <opId>=<v>]
181
184
  fullstackgtm plans approve <id> --values-from <suggestions.json> [--min-confidence high|low] [--include-creates]
182
185
  fullstackgtm plans recover <id> --acknowledge-uncertain-writes
183
- fullstackgtm apply --plan-id <id> --provider <name>
186
+ fullstackgtm apply --plan-id <id> --provider <name> [--verbose|--json]
184
187
  fullstackgtm apply --plan-id <id> --channel outbox (render approved openers; transmits nothing)
185
188
  fullstackgtm apply --plan <path> --provider <name> --approve <ids|all> [options]
186
189
  fullstackgtm audit-log export [--out <path>] | verify --in <path> tamper-evident apply-run record
@@ -276,7 +279,7 @@ export const HELP = {
276
279
  summary: "scaffold a workspace (icp.json + enrich.config.json + PLAYBOOK)",
277
280
  phase: "Setup",
278
281
  synopsis: [
279
- "fullstackgtm init [--source pipe0|explorium|linkedin] [--provider hubspot|salesforce] [--out <dir>] [--force]",
282
+ "fullstackgtm init [--source pipe0|explorium|clay|linkedin] [--provider hubspot|salesforce] [--out <dir>] [--force]",
280
283
  ],
281
284
  detail: "Cold start: writes a starter ICP, an acquire-ready enrich.config.json (with a visible assign seam so leads are never ownerless), and a PLAYBOOK wired with the cold-start + outbound-loop recipes for this workspace. Pure file-writer — no network, keeps existing files unless --force. The CLI ships governed primitives; your coding agent is the orchestrator (see docs/recipes.md).",
282
285
  seeAlso: ["icp", "enrich", "signals"],
@@ -289,7 +292,7 @@ export const HELP = {
289
292
  "fullstackgtm login hubspot | salesforce hosted browser OAuth (default)",
290
293
  "fullstackgtm login --via <hosted url> pair with a team deployment",
291
294
  "fullstackgtm login stripe",
292
- "fullstackgtm login anthropic | openai | apollo",
295
+ "fullstackgtm login anthropic | openai | apollo | clay",
293
296
  ],
294
297
  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`.",
295
298
  seeAlso: ["doctor", "logout", "profiles"],
@@ -297,7 +300,7 @@ export const HELP = {
297
300
  logout: {
298
301
  summary: "remove stored credentials for a provider",
299
302
  phase: "Setup",
300
- synopsis: ["fullstackgtm logout <hubspot|salesforce|stripe|anthropic|openai|apollo|pipe0|explorium|broker>"],
303
+ synopsis: ["fullstackgtm logout <hubspot|salesforce|stripe|anthropic|openai|apollo|clay|pipe0|explorium|broker>"],
301
304
  seeAlso: ["login", "doctor"],
302
305
  },
303
306
  doctor: {
@@ -502,7 +505,7 @@ export const HELP = {
502
505
  summary: "replicated plan lifecycle: list / show / sync / approve / reject",
503
506
  phase: "Govern",
504
507
  synopsis: [
505
- "fullstackgtm plans list [--status <s>] | show <id> | sync | reject <id>",
508
+ "fullstackgtm plans list [--status <s>] | show <id> [--verbose] | sync | reject <id>",
506
509
  "fullstackgtm plans approve <id> --operations <ids|all> [--value <opId>=<v>]",
507
510
  "fullstackgtm plans approve <id> --values-from <suggestions.json> [--min-confidence high|low]",
508
511
  "fullstackgtm plans recover <id> --acknowledge-uncertain-writes",
@@ -514,11 +517,11 @@ export const HELP = {
514
517
  summary: "write ONLY explicitly approved operations to a provider",
515
518
  phase: "Govern / Verify",
516
519
  synopsis: [
517
- "fullstackgtm apply --plan-id <id> --provider <name>",
520
+ "fullstackgtm apply --plan-id <id> --provider <name> [--verbose|--json]",
518
521
  "fullstackgtm apply --plan-id <id> --channel outbox (render approved drafted openers to the outbox; transmits nothing)",
519
522
  "fullstackgtm apply --plan <path> --provider <name> --approve <ids|all> [--value <opId>=<v>]",
520
523
  ],
521
- detail: "The CLI CRM-write verb. Writes only operations approved locally or imported from the shared hosted replica, with an online execution claim when available, stable operation ids, compare-and-set, resolve-before-create, and readback. Hosted may execute the same synchronized plan when it has a compatible CRM connection; its exact operation receipts are imported on the next CLI check-in. Never writes requires_human_* placeholders without a --value override.",
524
+ detail: "The CLI CRM-write verb. Writes only operations approved locally or imported from the shared hosted replica, with an online execution claim when available, stable operation ids, compare-and-set, resolve-before-create, and readback. Default output is a compact outcome card; --verbose prints the full run document and --json preserves structured output. Hosted may execute the same synchronized plan when it has a compatible CRM connection; its exact operation receipts are imported on the next CLI check-in. Never writes requires_human_* placeholders without a --value override.",
522
525
  seeAlso: ["plans", "suggest", "audit-log"],
523
526
  },
524
527
  "audit-log": {
@@ -597,7 +600,7 @@ export const BESPOKE_HELP = ["init", "call", "market", "tam", "enrich", "schedul
597
600
  export const GLOBAL_FLAGS = ["--help", "--full"];
598
601
  export const GLOBAL_SHORT_FLAGS = ["-h"];
599
602
  export const SOURCE_FLAGS = ["--provider", "--token-env", "--input", "--demo", "--sample", "--seed", "--today"];
600
- export const AUDIT_FLAGS = ["--config", "--allow-plugins", "--no-plugins", "--rules", "--stale-days", "--fail-on", "--save", "--dry-run", "--json", "--out", "--full"];
603
+ export const AUDIT_FLAGS = ["--config", "--allow-plugins", "--no-plugins", "--rules", "--stale-days", "--fail-on", "--save", "--dry-run", "--json", "--out", "--full", "--verbose"];
601
604
  // Complete per-command flag registry used by runCli's fail-closed flag
602
605
  // validation. Keep this next to HELP so focused help, machine capabilities,
603
606
  // and the parser safety gate have one command inventory to reconcile against.
@@ -605,7 +608,7 @@ export const COMMAND_FLAGS = {
605
608
  init: ["--source", "--provider", "--out", "--force"],
606
609
  login: ["--via", "--hosted", "--private-token", "--token", "--key", "--api-key", "--client-id", "--client-secret", "--scopes", "--port", "--oauth", "--device", "--instance-url", "--login-url", "--no-validate"],
607
610
  logout: [],
608
- doctor: ["--json"],
611
+ doctor: ["--verbose", "--json"],
609
612
  capabilities: ["--json"],
610
613
  "robot-docs": [],
611
614
  profiles: ["--json"],
@@ -619,24 +622,24 @@ export const COMMAND_FLAGS = {
619
622
  hierarchy: [...SOURCE_FLAGS, "--json", "--out"],
620
623
  relationships: [...SOURCE_FLAGS, "--account-id", "--domain", "--json", "--out"],
621
624
  route: [...SOURCE_FLAGS, "--match", "--no-inherit-owner", "--reassign-owned", "--policy", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
622
- fix: [...SOURCE_FLAGS, "--config", "--allow-plugins", "--no-plugins", "--rule", "--provider", "--min-confidence", "--include-creates", "--yes", "--confirm", "--dry-run"],
623
- "bulk-update": [...SOURCE_FLAGS, "--where", "--set", "--guard", "--require", "--create-task", "--archive", "--force-archive-duplicates", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
624
- dedupe: [...SOURCE_FLAGS, "--key", "--keep", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
625
- reassign: [...SOURCE_FLAGS, "--from", "--assign-unowned", "--to", "--objects", "--where", "--except-deal-stage", "--include-closed-deals", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
626
- backfill: [...SOURCE_FLAGS, "--since", "--pipeline", "--match-property", "--skip-unmatched", "--save", "--dry-run", "--json"],
627
- enrich: [...SOURCE_FLAGS, "--config", "--source", "--icp", "--input", "--provider", "--list", "--stale-days", "--assign-owner", "--objects", "--max", "--scan-limit", "--staged-run", "--run-label", "--label", "--runs", "--save", "--dry-run", "--json", "--out"],
628
- call: [...SOURCE_FLAGS, "--transcript", "--title", "--source", "--model", "--deterministic", "--heuristics", "--llm", "--json", "--ndjson", "--out", "--call", "--call-type", "--rubric", "--list", "--list-rubrics", "--attendees", "--domain", "--deal", "--save", "--dry-run"],
625
+ fix: [...SOURCE_FLAGS, "--config", "--allow-plugins", "--no-plugins", "--rule", "--provider", "--min-confidence", "--include-creates", "--yes", "--confirm", "--dry-run", "--verbose"],
626
+ "bulk-update": [...SOURCE_FLAGS, "--where", "--set", "--guard", "--require", "--create-task", "--archive", "--force-archive-duplicates", "--reason", "--max-operations", "--save", "--dry-run", "--verbose", "--json", "--out"],
627
+ dedupe: [...SOURCE_FLAGS, "--key", "--keep", "--reason", "--max-operations", "--save", "--dry-run", "--verbose", "--json", "--out"],
628
+ reassign: [...SOURCE_FLAGS, "--from", "--assign-unowned", "--to", "--objects", "--where", "--except-deal-stage", "--include-closed-deals", "--reason", "--max-operations", "--save", "--dry-run", "--verbose", "--json", "--out"],
629
+ backfill: [...SOURCE_FLAGS, "--since", "--pipeline", "--match-property", "--skip-unmatched", "--save", "--dry-run", "--verbose", "--json"],
630
+ enrich: [...SOURCE_FLAGS, "--config", "--source", "--icp", "--input", "--provider", "--list", "--stale-days", "--assign-owner", "--objects", "--max", "--scan-limit", "--staged-run", "--run-label", "--label", "--runs", "--save", "--dry-run", "--verbose", "--json", "--out"],
631
+ call: [...SOURCE_FLAGS, "--transcript", "--title", "--source", "--model", "--deterministic", "--heuristics", "--llm", "--verbose", "--json", "--ndjson", "--out", "--call", "--call-type", "--rubric", "--list", "--list-rubrics", "--attendees", "--domain", "--deal", "--save", "--dry-run"],
629
632
  suggest: [...SOURCE_FLAGS, "--plan-id", "--plan", "--json", "--out"],
630
- plans: ["--status", "--operations", "--value", "--values-from", "--min-confidence", "--include-creates", "--acknowledge-uncertain-writes", "--json"],
631
- apply: ["--plan", "--plan-id", "--provider", "--channel", "--token-env", "--approve", "--value", "--json", "--config"],
633
+ plans: ["--status", "--operations", "--value", "--values-from", "--min-confidence", "--include-creates", "--acknowledge-uncertain-writes", "--verbose", "--json"],
634
+ apply: ["--plan", "--plan-id", "--provider", "--channel", "--token-env", "--approve", "--value", "--verbose", "--json", "--config"],
632
635
  "audit-log": ["--in", "--out", "--json"],
633
636
  merge: ["--input", "--out", "--json"],
634
- market: ["--category", "--config", "--vendor", "--snapshot", "--task-account", "--task-deal", "--capture-run", "--run", "--prior-run", "--diff", "--from", "--calls", "--anchor", "--min-mentions", "--max-claims", "--promote-lift", "--model", "--format", "--auto", "--unverified", "--save", "--dry-run", "--json", "--out"],
635
- tam: [...SOURCE_FLAGS, "--source", "--name", "--icp", "--accounts", "--acv", "--acv-basis", "--acv-from-crm", "--deal-period", "--buyers-per-account", "--cross-checks", "--max", "--max-credits", "--usd-per-credit", "--dry-run", "--confirm", "--cron", "--label", "--save", "--json", "--out"],
636
- icp: [...SOURCE_FLAGS, "--name", "--signals-from", "--with-history", "--prompt", "--min-score", "--from-judge", "--golden", "--against-outcomes", "--min-accuracy", "--model", "--label", "--save", "--json", "--out"],
637
- signals: ["--bucket", "--source", "--connector", "--connector-opt", "--watchlist", "--keywords", "--from", "--label", "--since", "--account", "--contact", "--unjudged", "--touch", "--result", "--save", "--json", "--explain"],
638
- draft: ["--from-judge", "--min-score", "--prompt", "--model", "--channel", "--save", "--dry-run", "--json", "--out"],
639
- schedule: ["--cron", "--label", "--provider", "--trigger", "--timer", "--runs", "--json"],
637
+ market: ["--category", "--config", "--vendor", "--snapshot", "--task-account", "--task-deal", "--capture-run", "--run", "--prior-run", "--diff", "--from", "--calls", "--anchor", "--min-mentions", "--max-claims", "--promote-lift", "--model", "--format", "--auto", "--unverified", "--save", "--dry-run", "--verbose", "--json", "--out"],
638
+ tam: [...SOURCE_FLAGS, "--source", "--name", "--icp", "--accounts", "--acv", "--acv-basis", "--acv-from-crm", "--deal-period", "--buyers-per-account", "--cross-checks", "--max", "--max-credits", "--usd-per-credit", "--dry-run", "--confirm", "--cron", "--label", "--save", "--verbose", "--json", "--out"],
639
+ icp: [...SOURCE_FLAGS, "--name", "--signals-from", "--with-history", "--prompt", "--min-score", "--from-judge", "--golden", "--against-outcomes", "--min-accuracy", "--model", "--label", "--save", "--verbose", "--json", "--out"],
640
+ signals: ["--bucket", "--source", "--connector", "--connector-opt", "--watchlist", "--keywords", "--from", "--label", "--since", "--account", "--contact", "--unjudged", "--touch", "--result", "--save", "--verbose", "--json", "--explain"],
641
+ draft: ["--from-judge", "--min-score", "--prompt", "--model", "--channel", "--save", "--dry-run", "--verbose", "--json", "--out"],
642
+ schedule: ["--cron", "--label", "--provider", "--trigger", "--timer", "--runs", "--verbose", "--json"],
640
643
  };
641
644
  export const FLAGS_WITH_VALUES = new Set([
642
645
  "--account", "--account-id", "--accounts", "--acv", "--acv-basis", "--after", "--anchor", "--api-key", "--approve", "--archive", "--assign-owner", "--attendees", "--before", "--bucket", "--buyers-per-account", "--call", "--call-type", "--calls", "--capture-run", "--category", "--channel", "--client", "--client-id", "--client-secret", "--config", "--connector", "--connector-opt", "--contact", "--cron", "--cross-checks", "--deal", "--deal-period", "--diff", "--domain", "--email", "--except-deal-stage", "--format", "--from", "--from-judge", "--golden", "--guard", "--icp", "--in", "--input", "--instance-url", "--keep", "--key", "--keywords", "--label", "--list", "--login-url", "--match", "--match-property", "--max", "--max-claims", "--max-credits", "--max-examples", "--max-operations", "--min-accuracy", "--min-confidence", "--min-mentions", "--min-score", "--model", "--name", "--objects", "--operations", "--out", "--pipeline", "--plan", "--plan-id", "--policy", "--port", "--prepared-by", "--prior-run", "--profile", "--promote-lift", "--prompt", "--provider", "--reason", "--require", "--result", "--rubric", "--rule", "--rules", "--run", "--run-label", "--runs", "--scan-limit", "--scopes", "--seed", "--set", "--signals-from", "--since", "--snapshot", "--source", "--staged-run", "--stale-days", "--status", "--task-account", "--task-deal", "--timer", "--title", "--to", "--today", "--token", "--token-env", "--touch", "--transcript", "--trigger", "--usd-per-credit", "--value", "--values-from", "--vendor", "--via", "--watchlist", "--where",
package/dist/cli/icp.js CHANGED
@@ -9,6 +9,20 @@ import { DEFAULT_GOLDEN_NOW_ISO, DEFAULT_GOLDEN_SET, DEFAULT_MIN_ACCURACY, defau
9
9
  import { loadIcp, numericOption, option, readSnapshot, resolveLlmBaseUrls, saveRequested } from "./shared.js";
10
10
  import { createStatusLine } from "./ui.js";
11
11
  import { unknownSubcommandError } from "./suggest.js";
12
+ function renderJudgeDecisions(decisions) {
13
+ if (decisions.length === 0)
14
+ return "No accounts cleared the score threshold.";
15
+ const lines = [`ICP decisions (${decisions.length})`, ""];
16
+ for (const decision of decisions) {
17
+ const target = decision.contact?.email || decision.contact?.title;
18
+ lines.push(`${decision.decision.toUpperCase().padEnd(7)} ${String(decision.score).padStart(3)} ${decision.accountDomain}${target ? ` · ${target}` : ""}`);
19
+ if (decision.whyNow)
20
+ lines.push(` ${decision.whyNow.replace(/\s+/g, " ").trim()}`);
21
+ if (decision.play)
22
+ lines.push(` Next: ${decision.play.replace(/\s+/g, " ").trim()}`);
23
+ }
24
+ return lines.join("\n");
25
+ }
12
26
  /**
13
27
  * `icp` — develop and inspect the Ideal Customer Profile that targets acquire.
14
28
  * The CLI can't run AskUserQuestion itself; `icp interview` emits the question
@@ -133,7 +147,7 @@ ops. Develop one by interview, then \`enrich acquire\` picks up ./icp.json.
133
147
  }
134
148
  decisions = decisions.filter((d) => d.score >= minScore);
135
149
  // 5) Ranked decisions to stdout (JSON), guidance to stderr.
136
- console.log(JSON.stringify(decisions, null, 2));
150
+ console.log(rest.includes("--json") || rest.includes("--verbose") ? JSON.stringify(decisions, null, 2) : renderJudgeDecisions(decisions));
137
151
  console.error(`Judged ${unjudged.length} signal(s) across ${new Set(decisions.map((d) => d.accountDomain)).size} account(s): ` +
138
152
  `${decisions.filter((d) => d.decision === "send").length} send, ` +
139
153
  `${decisions.filter((d) => d.decision === "nurture").length} nurture, ` +
package/dist/cli/init.js CHANGED
@@ -13,7 +13,7 @@ import { option } from "./shared.js";
13
13
  export function initCommand(args) {
14
14
  if (args.includes("--help") || args.includes("-h")) {
15
15
  console.log(`Usage:
16
- fullstackgtm init [--source pipe0|explorium|linkedin] [--provider hubspot|salesforce] [--out <dir>] [--force]
16
+ fullstackgtm init [--source pipe0|explorium|clay|linkedin] [--provider hubspot|salesforce] [--out <dir>] [--force]
17
17
 
18
18
  Scaffolds a workspace so the first acquire/signals/judge/draft commands work:
19
19
  icp.json starter ICP (edit, or rebuild with \`icp interview\`)
@@ -25,8 +25,8 @@ docs/recipes.md for the full play set. Existing files are kept unless --force.`)
25
25
  return;
26
26
  }
27
27
  const source = (option(args, "--source") ?? "pipe0");
28
- if (!["pipe0", "explorium", "linkedin"].includes(source)) {
29
- throw new Error(`init: --source must be pipe0|explorium|linkedin (got "${source}")`);
28
+ if (!["pipe0", "explorium", "clay", "linkedin"].includes(source)) {
29
+ throw new Error(`init: --source must be pipe0|explorium|clay|linkedin (got "${source}")`);
30
30
  }
31
31
  const provider = (option(args, "--provider") ?? "hubspot");
32
32
  if (!["hubspot", "salesforce"].includes(provider)) {
@@ -481,9 +481,22 @@ Rules:
481
481
  writeFileSync(resolve(process.cwd(), outPath), output);
482
482
  console.log(`Wrote ${outPath}`);
483
483
  }
484
- else {
484
+ else if (rest.includes("--verbose") || option(rest, "--format")) {
485
485
  console.log(output);
486
486
  }
487
+ else {
488
+ const fronts = computeFrontStates(config, set);
489
+ const counts = new Map();
490
+ for (const front of fronts)
491
+ counts.set(front.state, (counts.get(front.state) ?? 0) + 1);
492
+ console.log(`${config.category} market · ${set.runLabel}`);
493
+ console.log(`${fronts.length} claim fronts · ${["open", "contested", "owned", "saturated", "vacant"].filter((state) => counts.has(state)).map((state) => `${counts.get(state)} ${state}`).join(" · ")}`);
494
+ for (const front of fronts) {
495
+ const owner = front.loudVendorIds.length ? ` · ${front.loudVendorIds.join(", ")}` : "";
496
+ console.log(`\n${front.state.toUpperCase()} ${front.claimId}${owner}`);
497
+ }
498
+ console.log("\nUse --verbose for the full evidence report, or --out <path> to write it.");
499
+ }
487
500
  return;
488
501
  }
489
502
  if (subcommand === "axes") {
@@ -0,0 +1,5 @@
1
+ import type { PatchPlan } from "../types.ts";
2
+ export declare function compactPlan(plan: PatchPlan, options?: {
3
+ saved?: boolean;
4
+ }): string;
5
+ export declare function verbosePlanRequested(args: readonly string[]): boolean;
@@ -0,0 +1,27 @@
1
+ import { box, colorEnabled, paint, truncateToWidth } from "./ui.js";
2
+ export function compactPlan(plan, options = {}) {
3
+ const counts = new Map();
4
+ for (const operation of plan.operations) {
5
+ const action = operation.operation || operation.field || "change";
6
+ counts.set(action, (counts.get(action) ?? 0) + 1);
7
+ }
8
+ const mix = [...counts]
9
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
10
+ .slice(0, 4)
11
+ .map(([action, count]) => `${count} ${action}`)
12
+ .join(" · ");
13
+ const lines = [
14
+ truncateToWidth(plan.title, 100),
15
+ `Status ${options.saved ? "SAVED · NEEDS APPROVAL" : "DRY RUN · NOTHING WRITTEN"}`,
16
+ truncateToWidth(`Scope ${plan.operations.length} operation${plan.operations.length === 1 ? "" : "s"}${mix ? ` · ${mix}` : ""}`, 100),
17
+ truncateToWidth(`Effect ${plan.summary}`, 100),
18
+ options.saved ? `Next fullstackgtm plans show ${plan.id}` : "Next re-run with --save to stage this plan for approval",
19
+ ];
20
+ return [
21
+ ...box(lines, paint(colorEnabled(process.stdout)), "Plan preview"),
22
+ `Details: re-run with --verbose${options.saved ? `; JSON: fullstackgtm plans show ${plan.id} --json` : "; machine output: --json"}`,
23
+ ].join("\n");
24
+ }
25
+ export function verbosePlanRequested(args) {
26
+ return args.includes("--verbose") || args.includes("--full");
27
+ }
package/dist/cli/plans.js CHANGED
@@ -17,7 +17,7 @@ import { progressReporter, reportCounts } from "../runReport.js";
17
17
  import { claimHostedPlanApply, reconcileHostedPatchPlan, releaseHostedPlanApply, reportHostedPlanLifecycle, uploadHostedPatchPlan } from "../hostedPatchPlan.js";
18
18
  import { APPLY_STAGES, composeListeners, createProgressEmitter } from "../progress.js";
19
19
  import { connectorFor, isOptionValue, numericOption, option, repeatedOption, selectedRules } from "./shared.js";
20
- import { colorEnabled, createProgressRenderer, paint, planStatusWord, stylizePlanMarkdown, table, truncateToWidth } from "./ui.js";
20
+ import { box, colorEnabled, createProgressRenderer, createStatusLine, formatDuration, paint, planStatusWord, stylizePlanMarkdown, table, truncateToWidth } from "./ui.js";
21
21
  import { unknownSubcommandError } from "./suggest.js";
22
22
  function parseValueOverrides(args) {
23
23
  const valueOverrides = {};
@@ -29,6 +29,119 @@ function parseValueOverrides(args) {
29
29
  }
30
30
  return valueOverrides;
31
31
  }
32
+ function recordHeadline(operation) {
33
+ const after = operation.afterValue && typeof operation.afterValue === "object" && !Array.isArray(operation.afterValue)
34
+ ? operation.afterValue : {};
35
+ const properties = after.properties && typeof after.properties === "object" && !Array.isArray(after.properties)
36
+ ? after.properties : {};
37
+ const first = typeof properties.firstname === "string" ? properties.firstname : "";
38
+ const last = typeof properties.lastname === "string" ? properties.lastname : "";
39
+ const company = typeof properties.company === "string" ? properties.company
40
+ : typeof after.associateCompanyName === "string" ? after.associateCompanyName : undefined;
41
+ const title = [first, last].filter(Boolean).join(" ") || company || operation.objectId || operation.id;
42
+ const role = typeof properties.jobtitle === "string" ? properties.jobtitle : undefined;
43
+ return { title, ...(role ? { subtitle: role } : {}), ...(company ? { company } : {}) };
44
+ }
45
+ function planEffect(stored) {
46
+ const total = stored.plan.operations.length;
47
+ const approved = stored.approvedOperationIds.length;
48
+ if (stored.status === "applied") {
49
+ const results = stored.runs.at(-1)?.results ?? [];
50
+ const applied = results.filter((result) => result.status === "applied").length;
51
+ const skipped = results.filter((result) => result.status === "skipped").length;
52
+ return `Completed ${applied} operation${applied === 1 ? "" : "s"}${skipped ? `; ${skipped} skipped` : ""}. No further writes will run.`;
53
+ }
54
+ if (stored.status === "approved")
55
+ return `Apply will execute ${approved} selected operation${approved === 1 ? "" : "s"}; ${total - approved} will not run.`;
56
+ if (stored.status === "rejected")
57
+ return "Rejected. No provider writes can run.";
58
+ return `No provider writes can run until operations are approved (0 of ${total} selected).`;
59
+ }
60
+ function printPlanCards(stored, hostedUrl) {
61
+ const p = paint(colorEnabled(process.stdout));
62
+ const total = stored.plan.operations.length;
63
+ const approved = new Set(stored.approvedOperationIds);
64
+ const width = Math.max(56, Math.min(100, (process.stdout.columns ?? 100) - 4));
65
+ const fit = (value) => truncateToWidth(value, width);
66
+ const summary = [
67
+ fit(stored.plan.title),
68
+ `Status ${stored.status.toUpperCase().replaceAll("_", " ")}`,
69
+ `Selection ${approved.size} of ${total} operation${total === 1 ? "" : "s"} approved`,
70
+ `Runs ${stored.runs.length}`,
71
+ fit(`Effect ${planEffect(stored)}`),
72
+ ];
73
+ console.log(box(summary, p, "Plan").join("\n"));
74
+ if (hostedUrl)
75
+ console.log(`Hosted: ${hostedUrl}`);
76
+ console.log("\nOperations");
77
+ let operationIndex = 0;
78
+ const latestResults = new Map((stored.runs.at(-1)?.results ?? []).map((result) => [result.operationId, result.status]));
79
+ for (const operation of stored.plan.operations) {
80
+ const selected = approved.has(operation.id);
81
+ const resultStatus = latestResults.get(operation.id);
82
+ const headline = recordHeadline(operation);
83
+ const action = operation.operation === "create_record"
84
+ ? `Create ${operation.objectType}${headline.company ? ` and resolve/link ${headline.company}` : ""}`
85
+ : `${operation.operation.replaceAll("_", " ")} ${operation.objectType}`;
86
+ const lines = [
87
+ fit(headline.title),
88
+ ...(headline.subtitle || headline.company
89
+ ? [fit([headline.subtitle, headline.company].filter(Boolean).join(" · "))]
90
+ : []),
91
+ fit(`Action ${action}`),
92
+ `Operation ${operation.id}`,
93
+ ];
94
+ if (operationIndex++ > 0)
95
+ console.log("");
96
+ const state = resultStatus === "applied" ? "✓ APPLIED"
97
+ : resultStatus && selected ? `! ${resultStatus.toUpperCase()}`
98
+ : selected ? "✓ APPROVED" : stored.status === "applied" ? "○ EXCLUDED" : "○ NOT APPROVED";
99
+ console.log(box(lines, p, state).join("\n"));
100
+ }
101
+ if (stored.status === "approved") {
102
+ console.log(`\nNext: fullstackgtm apply --plan-id ${stored.plan.id} --provider <hubspot|salesforce>`);
103
+ }
104
+ console.log(`Details: fullstackgtm plans show ${stored.plan.id} --verbose`);
105
+ }
106
+ function printApplyCards(run, plan, approvedOperationIds, planIdStored) {
107
+ const p = paint(colorEnabled(process.stdout));
108
+ const approved = new Set(approvedOperationIds);
109
+ const operations = new Map(plan.operations.map((operation) => [operation.id, operation]));
110
+ const counts = { applied: 0, skipped: 0, failed: 0, conflict: 0 };
111
+ for (const result of run.results)
112
+ counts[result.status] += 1;
113
+ const elapsed = Math.max(0, Date.parse(run.finishedAt) - Date.parse(run.startedAt));
114
+ const summary = [
115
+ `Status ${run.status.toUpperCase()}`,
116
+ `Provider ${run.provider}`,
117
+ `Outcome ${counts.applied} applied · ${run.results.length - approved.size} excluded · ${counts.failed + counts.conflict} failed/conflicted`,
118
+ `Duration ${formatDuration(elapsed)}`,
119
+ ...(planIdStored ? ["Receipt recorded locally; hosted reconciliation requested"] : []),
120
+ ];
121
+ console.log(box(summary, p, "Apply result").join("\n"));
122
+ console.log("\nOperations");
123
+ let index = 0;
124
+ for (const result of run.results) {
125
+ const operation = operations.get(result.operationId);
126
+ if (!operation)
127
+ continue;
128
+ const selected = approved.has(result.operationId);
129
+ const headline = recordHeadline(operation);
130
+ const state = !selected ? "○ EXCLUDED" : result.status === "applied" ? "✓ APPLIED" : `! ${result.status.toUpperCase()}`;
131
+ const lines = [
132
+ headline.title,
133
+ ...(headline.subtitle || headline.company ? [[headline.subtitle, headline.company].filter(Boolean).join(" · ")] : []),
134
+ `Operation ${result.operationId}`,
135
+ ...(result.detail ? [truncateToWidth(`Result ${result.detail}`, 100)] : []),
136
+ ];
137
+ if (index++ > 0)
138
+ console.log("");
139
+ console.log(box(lines, p, state).join("\n"));
140
+ }
141
+ if (planIdStored)
142
+ console.log(`\nInspect: fullstackgtm plans show ${run.planId}`);
143
+ console.log("Details: rerun with --verbose; machine output: --json");
144
+ }
32
145
  function tryLoadAcquireConfig(args) {
33
146
  try {
34
147
  const path = resolve(process.cwd(), option(args, "--config") ?? ENRICH_CONFIG_FILE_NAME);
@@ -322,9 +435,12 @@ export async function apply(args) {
322
435
  if (args.includes("--json")) {
323
436
  console.log(JSON.stringify(run, null, 2));
324
437
  }
325
- else {
438
+ else if (args.includes("--verbose")) {
326
439
  console.log(formatPatchPlanRun(run));
327
440
  }
441
+ else {
442
+ printApplyCards(run, plan, approvedOperationIds, Boolean(planId && store));
443
+ }
328
444
  if (run.status === "failed")
329
445
  process.exitCode = 1;
330
446
  }
@@ -422,31 +538,48 @@ export async function plansCommand(args) {
422
538
  }
423
539
  const p = paint(colorEnabled(process.stdout));
424
540
  if (p.enabled) {
425
- // Interactive terminals get an aligned table with status color-banding;
426
- // the plain per-line format below is unchanged for pipes.
541
+ const statusCounts = new Map();
542
+ for (const stored of plans)
543
+ statusCounts.set(stored.status, (statusCounts.get(stored.status) ?? 0) + 1);
544
+ const awaiting = plans.filter((stored) => stored.status === "needs_approval").length;
545
+ const approved = plans.filter((stored) => stored.status === "approved").length;
546
+ console.log(box([
547
+ `${plans.length} plan${plans.length === 1 ? "" : "s"} · ${awaiting} awaiting approval · ${approved} ready to apply`,
548
+ [...statusCounts.entries()].map(([name, count]) => `${name.replaceAll("_", " ")}: ${count}`).join(" · "),
549
+ ], p, "Change queue").join("\n"));
550
+ console.log("");
551
+ // Interactive terminals get an aligned decision table; the plain
552
+ // per-line format below remains stable for pipes and scripts.
427
553
  const rows = plans.map((stored) => [
428
- stored.plan.id,
429
554
  stored.status,
430
- `${stored.approvedOperationIds.length} approved`,
555
+ stored.plan.title,
556
+ `${stored.approvedOperationIds.length}/${stored.plan.operations.length} selected`,
431
557
  `${stored.runs.length} run${stored.runs.length === 1 ? "" : "s"}`,
432
- stored.plan.summary,
558
+ stored.plan.id,
433
559
  ]);
434
560
  // Long summaries would wrap and break the table's alignment. The summary
435
561
  // is the LAST column: cap it to what's left of the terminal width after
436
562
  // the fixed columns and their two-space gutters (floor of 24 so narrow
437
563
  // terminals still show something useful).
438
564
  const columns = process.stdout.columns ?? 80;
439
- const fixedWidth = [0, 1, 2, 3].reduce((sum, index) => sum + Math.max(...rows.map((row) => row[index].length)), 0) + 2 * 4;
440
- const summaryWidth = Math.max(24, columns - fixedWidth);
565
+ const fixedWidth = [0, 2, 3, 4].reduce((sum, index) => sum + Math.max(...rows.map((row) => row[index].length)), 0) + 2 * 4;
566
+ const titleWidth = Math.max(24, columns - fixedWidth);
441
567
  for (const row of rows)
442
- row[4] = truncateToWidth(row[4], summaryWidth);
568
+ row[1] = truncateToWidth(row[1], titleWidth);
443
569
  const statusPainter = (cell) => {
444
570
  const painted = planStatusWord(cell.trimEnd(), p);
445
571
  return painted + cell.slice(cell.trimEnd().length);
446
572
  };
447
- for (const line of table(rows, [null, statusPainter, p.dim, p.dim, null])) {
573
+ for (const line of table(rows, [statusPainter, null, p.dim, p.dim, p.dim])) {
448
574
  console.log(line);
449
575
  }
576
+ const next = plans.find((stored) => stored.status === "approved") ?? plans.find((stored) => stored.status === "needs_approval");
577
+ if (next) {
578
+ const command = next.status === "approved"
579
+ ? `fullstackgtm apply --plan-id ${next.plan.id} --provider <name>`
580
+ : `fullstackgtm plans show ${next.plan.id}`;
581
+ console.log(`\nNext: ${command}`);
582
+ }
450
583
  return;
451
584
  }
452
585
  for (const stored of plans) {
@@ -473,16 +606,11 @@ export async function plansCommand(args) {
473
606
  console.log(JSON.stringify(stored, null, 2));
474
607
  return;
475
608
  }
476
- const showPaint = paint(colorEnabled(process.stdout));
477
- console.log(`Status: ${planStatusWord(stored.status, showPaint)}`);
478
- console.log(`Approved operations: ${stored.approvedOperationIds.join(", ") || "none"}`);
479
- console.log(`Runs: ${stored.runs.length}`);
480
609
  const mirror = await uploadHostedPatchPlan(stored);
481
- if (mirror.status === "saved")
482
- console.log(`Hosted review: ${mirror.state.url}`);
483
- else if (mirror.status === "unavailable" || mirror.status === "conflict")
484
- console.log(`Hosted review: sync pending (${mirror.reason})`);
485
- if (stored.applyAttempts?.length) {
610
+ printPlanCards(stored, mirror.status === "saved" ? mirror.state.url : undefined);
611
+ if (mirror.status === "unavailable" || mirror.status === "conflict")
612
+ console.log(`Hosted sync pending: ${mirror.reason}`);
613
+ if (rest.includes("--verbose") && stored.applyAttempts?.length) {
486
614
  console.log("Apply attempts:");
487
615
  for (const attempt of stored.applyAttempts) {
488
616
  console.log(` ${attempt.id} ${attempt.status} ${attempt.provider} via ${attempt.source} ${attempt.claimedAt}`);
@@ -490,8 +618,11 @@ export async function plansCommand(args) {
490
618
  console.log(` ${attempt.note}`);
491
619
  }
492
620
  }
493
- console.log("");
494
- console.log(stylizePlanMarkdown(patchPlanToMarkdown(stored.plan), showPaint));
621
+ if (rest.includes("--verbose")) {
622
+ const showPaint = paint(colorEnabled(process.stdout));
623
+ console.log("\nFull plan document\n");
624
+ console.log(stylizePlanMarkdown(patchPlanToMarkdown(stored.plan), showPaint));
625
+ }
495
626
  return;
496
627
  }
497
628
  if (subcommand === "approve") {
@@ -578,19 +709,39 @@ export async function plansCommand(args) {
578
709
  if (subcommand === "sync") {
579
710
  const plans = await store.list();
580
711
  let updated = 0;
712
+ let completed = 0;
581
713
  const warnings = [];
582
- for (const stored of plans) {
583
- const result = await reconcileHostedPatchPlan(store, stored);
584
- if (result.status === "updated")
585
- updated += 1;
586
- else if (result.status === "conflict" || result.status === "unavailable")
587
- warnings.push(`${stored.plan.id}: ${result.reason}`);
588
- if (stored.status === "applied") {
589
- const hostedClaimId = [...(stored.applyAttempts ?? [])].reverse().find((attempt) => attempt.hostedClaimId)?.hostedClaimId;
590
- const pushed = await reportHostedPlanLifecycle(stored, { claimId: hostedClaimId });
591
- if (pushed.status === "conflict" || pushed.status === "unavailable")
592
- warnings.push(`${stored.plan.id} receipt: ${pushed.reason}`);
714
+ const status = createStatusLine();
715
+ status.set(`Synchronizing plan replicas 0/${plans.length}`);
716
+ // A workspace can accumulate hundreds of local plans. Reconcile with a
717
+ // small worker pool so one network round-trip per plan does not turn into
718
+ // a minutes-long silent serial wait, without stampeding the hosted API.
719
+ let next = 0;
720
+ const worker = async () => {
721
+ while (next < plans.length) {
722
+ const stored = plans[next++];
723
+ const result = await reconcileHostedPatchPlan(store, stored);
724
+ if (result.status === "updated")
725
+ updated += 1;
726
+ else if (result.status === "conflict" || result.status === "unavailable")
727
+ warnings.push(`${stored.plan.id}: ${result.reason}`);
728
+ // Historical local-only plans have no remote row to receive a receipt.
729
+ // `backfill runs` is the explicit import path for that old history.
730
+ if (stored.status === "applied" && result.status !== "missing" && (stored.applyAttempts?.length ?? 0) > 0) {
731
+ const hostedClaimId = [...(stored.applyAttempts ?? [])].reverse().find((attempt) => attempt.hostedClaimId)?.hostedClaimId;
732
+ const pushed = await reportHostedPlanLifecycle(stored, { claimId: hostedClaimId });
733
+ if (pushed.status === "conflict" || pushed.status === "unavailable")
734
+ warnings.push(`${stored.plan.id} receipt: ${pushed.reason}`);
735
+ }
736
+ completed += 1;
737
+ status.set(`Synchronizing plan replicas ${completed}/${plans.length}`);
593
738
  }
739
+ };
740
+ try {
741
+ await Promise.all(Array.from({ length: Math.min(6, plans.length) }, () => worker()));
742
+ }
743
+ finally {
744
+ status.done();
594
745
  }
595
746
  console.log(`Plan replicas synchronized: ${updated} updated, ${plans.length - updated} unchanged.`);
596
747
  for (const warning of warnings)