fullstackgtm 0.42.0 → 0.44.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 (68) hide show
  1. package/CHANGELOG.md +273 -0
  2. package/README.md +18 -7
  3. package/dist/calls.d.ts +13 -0
  4. package/dist/calls.js +14 -3
  5. package/dist/cli.js +718 -60
  6. package/dist/connectors/hubspot.js +58 -24
  7. package/dist/connectors/outboxChannel.d.ts +64 -0
  8. package/dist/connectors/outboxChannel.js +170 -0
  9. package/dist/connectors/prospectSources.d.ts +22 -0
  10. package/dist/connectors/prospectSources.js +43 -0
  11. package/dist/connectors/salesforce.js +40 -15
  12. package/dist/connectors/signalSources.d.ts +105 -0
  13. package/dist/connectors/signalSources.js +316 -0
  14. package/dist/connectors/theirstack.d.ts +84 -0
  15. package/dist/connectors/theirstack.js +125 -0
  16. package/dist/draft.js +27 -5
  17. package/dist/enrich.d.ts +6 -0
  18. package/dist/enrich.js +47 -1
  19. package/dist/health.d.ts +12 -0
  20. package/dist/health.js +29 -2
  21. package/dist/icp.d.ts +47 -3
  22. package/dist/icp.js +105 -11
  23. package/dist/index.d.ts +2 -0
  24. package/dist/index.js +2 -0
  25. package/dist/init.d.ts +47 -0
  26. package/dist/init.js +143 -0
  27. package/dist/judge.d.ts +36 -3
  28. package/dist/judge.js +46 -10
  29. package/dist/judgeEval.d.ts +7 -0
  30. package/dist/judgeEval.js +8 -1
  31. package/dist/runReport.d.ts +26 -0
  32. package/dist/runReport.js +15 -0
  33. package/dist/schedule.js +18 -4
  34. package/dist/signals.d.ts +58 -0
  35. package/dist/signals.js +65 -0
  36. package/dist/tam.d.ts +225 -0
  37. package/dist/tam.js +470 -0
  38. package/dist/types.d.ts +12 -0
  39. package/docs/api.md +26 -4
  40. package/docs/outbox-format.md +92 -0
  41. package/docs/recipes.md +195 -0
  42. package/docs/roadmap-to-1.0.md +31 -1
  43. package/docs/signal-spool-format.md +162 -0
  44. package/docs/tam.md +195 -0
  45. package/llms.txt +89 -1
  46. package/package.json +1 -1
  47. package/skills/fullstackgtm/SKILL.md +17 -1
  48. package/src/calls.ts +24 -3
  49. package/src/cli.ts +809 -55
  50. package/src/connectors/hubspot.ts +55 -23
  51. package/src/connectors/outboxChannel.ts +202 -0
  52. package/src/connectors/prospectSources.ts +57 -0
  53. package/src/connectors/salesforce.ts +39 -14
  54. package/src/connectors/signalSources.ts +363 -0
  55. package/src/connectors/theirstack.ts +170 -0
  56. package/src/draft.ts +26 -5
  57. package/src/enrich.ts +51 -1
  58. package/src/health.ts +47 -2
  59. package/src/icp.ts +113 -11
  60. package/src/index.ts +42 -0
  61. package/src/init.ts +166 -0
  62. package/src/judge.ts +85 -11
  63. package/src/judgeEval.ts +8 -1
  64. package/src/runReport.ts +39 -0
  65. package/src/schedule.ts +20 -4
  66. package/src/signals.ts +95 -0
  67. package/src/tam.ts +654 -0
  68. package/src/types.ts +12 -0
package/src/schedule.ts CHANGED
@@ -84,7 +84,13 @@ const SCHEDULABLE: Record<string, string[] | null> = {
84
84
  suggest: null,
85
85
  report: null,
86
86
  doctor: null,
87
- enrich: ["append", "refresh"],
87
+ // `enrich append|refresh` fill blanks; `enrich acquire` creates net-new
88
+ // leads — but ONLY as a needs_approval `create_record` plan (`--save`,
89
+ // required below), never a write. This is what lets `tam populate` chip away
90
+ // at a TAM unattended: each firing queues a fresh lead plan, the meter is
91
+ // charged only at apply, and apply stays `apply --plan-id` (re-checked
92
+ // approved). So scheduled acquire accumulates proposals, never surprise leads.
93
+ enrich: ["append", "refresh", "acquire"],
88
94
  market: ["capture", "refresh"],
89
95
  // The GTM brain. `signals fetch` is read-only re: CRM (--save persists only
90
96
  // the local signal ledger). `icp judge`/`icp eval` are read-only/grade-only
@@ -98,9 +104,9 @@ const SCHEDULABLE: Record<string, string[] | null> = {
98
104
  };
99
105
 
100
106
  const ALLOWLIST_SUMMARY =
101
- "audit, snapshot, enrich append|refresh, market capture|refresh, signals fetch, " +
102
- "icp judge|eval, draft (stages a plan), suggest, report, doctor — " +
103
- "plus apply --plan-id <id> (re-checked approved at every firing)";
107
+ "audit, snapshot, enrich append|refresh, enrich acquire --save (stages a lead plan), " +
108
+ "market capture|refresh, signals fetch, icp judge|eval, draft (stages a plan), " +
109
+ "suggest, report, doctor — plus apply --plan-id <id> (re-checked approved at every firing)";
104
110
 
105
111
  /**
106
112
  * Validate that an argv resolves to a schedulable fullstackgtm command.
@@ -158,6 +164,16 @@ export function validateSchedulableArgv(argv: string[]): void {
158
164
  .join(", ")}.`,
159
165
  );
160
166
  }
167
+ // `enrich acquire` is schedulable ONLY in its plan-producing form: without
168
+ // --save it's a dry-run that writes nothing AND queues nothing, so an
169
+ // unattended firing would silently no-op. Require --save so a scheduled
170
+ // acquire always leaves a needs_approval plan behind (apply stays gated).
171
+ if (head === "enrich" && sub === "acquire" && !argv.includes("--save")) {
172
+ throw new Error(
173
+ "Scheduled `enrich acquire` must include --save so each firing queues a needs_approval lead plan " +
174
+ "(without it the run produces nothing). Apply stays a separate human gate (`apply --plan-id <id>`).",
175
+ );
176
+ }
161
177
  }
162
178
  }
163
179
 
package/src/signals.ts CHANGED
@@ -65,6 +65,9 @@ export type SignalOutcome = {
65
65
  id: string;
66
66
  accountDomain: string;
67
67
  touchId?: string;
68
+ /** The contact the touch went to (from the judge decision), when known — so an
69
+ * outcome credits the person reached, not just the account domain. */
70
+ contactId?: string;
68
71
  result: SignalOutcomeResult;
69
72
  recordedAt: string;
70
73
  /** Signal ids credited (from the judge decision). */
@@ -413,6 +416,71 @@ function withinWindow(iso: string, now: Date, windowDays: number): boolean {
413
416
  return now.getTime() - t <= windowDays * DAY_MS;
414
417
  }
415
418
 
419
+ // ---------------------------------------------------------------------------
420
+ // Staged rows -> signals (shared by `--from` ingest and source connectors)
421
+
422
+ /**
423
+ * One staged signal row: the platform-agnostic intake shape every NON-job source
424
+ * produces (a source connector's output, or a `--from` JSON row). It carries the
425
+ * evidence anchor (`quote`) but none of the derived fields (`id`, `weight`,
426
+ * `source`) — `stagedRowToSignal` fills those so there is ONE place that gates
427
+ * evidence and stamps identity, whether the row came from a file or an API.
428
+ */
429
+ export type StagedSignalRow = {
430
+ bucket: SignalBucket;
431
+ accountDomain: string;
432
+ trigger: string;
433
+ /** VERBATIM evidence — required, non-empty. */
434
+ quote: string;
435
+ sourceUrl?: string;
436
+ /** ISO 8601; defaults to run time when absent. */
437
+ firstSeen?: string;
438
+ /** Optional explicit weight; defaults to the bucket's configured weight. */
439
+ weight?: number;
440
+ };
441
+
442
+ /**
443
+ * Validate one loosely-typed staged row and turn it into a `Signal`, applying
444
+ * the verbatim-evidence gate (a row with no quote is rejected, never faked) and
445
+ * the same id/normalization logic the ATS path uses. `errorLabel` lets the
446
+ * caller produce a precise message ("--from f.json: row 3", "serpapi-news row 0")
447
+ * since both the `--from` ingest and the source-connector registry funnel here.
448
+ *
449
+ * Throws on a malformed row; returns the canonical `Signal` on success. Bucket
450
+ * FILTERING (skip rows outside a `--bucket` selection) stays with the caller —
451
+ * this function is per-row validation only.
452
+ */
453
+ export function stagedRowToSignal(
454
+ entry: Record<string, unknown>,
455
+ opts: { now: Date; source: string; errorLabel: string },
456
+ ): Signal {
457
+ const { now, source, errorLabel } = opts;
458
+ const bucket = String(entry.bucket ?? "");
459
+ if (!SIGNAL_BUCKETS.includes(bucket as SignalBucket)) {
460
+ throw new Error(`${errorLabel} has unknown bucket "${bucket}" (one of ${SIGNAL_BUCKETS.join(", ")}).`);
461
+ }
462
+ const accountDomain = normalizeAccountDomain(String(entry.accountDomain ?? entry.domain ?? ""));
463
+ if (!accountDomain) throw new Error(`${errorLabel} is missing accountDomain.`);
464
+ const trigger = String(entry.trigger ?? "").trim();
465
+ if (!trigger) throw new Error(`${errorLabel} is missing trigger.`);
466
+ const quote = String(entry.quote ?? "").trim();
467
+ if (!quote) throw new Error(`${errorLabel} is missing the verbatim quote (the evidence anchor).`);
468
+ const base = { accountDomain, bucket: bucket as SignalBucket, trigger };
469
+ const firstSeen = typeof entry.firstSeen === "string" && entry.firstSeen ? entry.firstSeen : now.toISOString();
470
+ return {
471
+ id: signalId(base),
472
+ accountDomain,
473
+ bucket: bucket as SignalBucket,
474
+ trigger,
475
+ quote,
476
+ sourceUrl: String(entry.sourceUrl ?? ""),
477
+ firstSeen,
478
+ weight: typeof entry.weight === "number" ? entry.weight : DEFAULT_SIGNALS_CONFIG.buckets[bucket as SignalBucket].weight,
479
+ source,
480
+ judgedBy: null,
481
+ };
482
+ }
483
+
416
484
  // ---------------------------------------------------------------------------
417
485
  // Dedup
418
486
 
@@ -530,6 +598,31 @@ export function signalsDir(baseDir?: string): string {
530
598
  return join(baseDir ?? credentialsDir(), "signals");
531
599
  }
532
600
 
601
+ /**
602
+ * Conventional webhook landing zone: `<signals>/spool`, profile-scoped. A
603
+ * webhook receiver (hosted, or the operator's own glue) appends one JSONL row
604
+ * per event to a `*.jsonl` file here; `signals fetch --connector file` reads the
605
+ * whole directory when given no explicit path. The CLI never writes here — the
606
+ * receiver does — so this is just the agreed-upon location, not a managed store.
607
+ * Per-source files (`rb2b.jsonl`, `hubspot.jsonl`, …) coexist. See
608
+ * docs/signal-spool-format.md.
609
+ */
610
+ export function signalsSpoolDir(baseDir?: string): string {
611
+ return join(signalsDir(baseDir), "spool");
612
+ }
613
+
614
+ /**
615
+ * Conventional outbox: `<signals>/outbox`, profile-scoped — the SEND-side mirror
616
+ * of the spool. `apply --channel outbox` renders each APPROVED drafted opener to
617
+ * a `<channel>.jsonl` file here (one row per touch); a downstream sender (hosted,
618
+ * or the operator's own) drains it. The CLI WRITES governed, approved send
619
+ * intents here but TRANSMITS NOTHING — the "drafts everything, transmits nothing"
620
+ * invariant holds. See docs/outbox-format.md.
621
+ */
622
+ export function signalsOutboxDir(baseDir?: string): string {
623
+ return join(signalsDir(baseDir), "outbox");
624
+ }
625
+
533
626
  export type SignalRun = {
534
627
  id: string;
535
628
  runLabel: string;
@@ -668,6 +761,7 @@ export function createFileSignalStore(directory?: string): SignalStore {
668
761
  export function makeOutcome(input: {
669
762
  accountDomain: string;
670
763
  touchId?: string;
764
+ contactId?: string;
671
765
  result: SignalOutcomeResult;
672
766
  creditedSignals?: string[];
673
767
  recordedAt?: string;
@@ -678,6 +772,7 @@ export function makeOutcome(input: {
678
772
  id: outcomeId({ accountDomain, touchId: input.touchId, result: input.result, recordedAt }),
679
773
  accountDomain,
680
774
  ...(input.touchId !== undefined ? { touchId: input.touchId } : {}),
775
+ ...(input.contactId !== undefined ? { contactId: input.contactId } : {}),
681
776
  result: input.result,
682
777
  recordedAt,
683
778
  creditedSignals: input.creditedSignals ?? [],