fullstackgtm 0.44.0 → 0.46.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 +293 -1
- package/INSTALL_FOR_AGENTS.md +13 -11
- package/README.md +45 -24
- package/dist/audit.d.ts +3 -1
- package/dist/audit.js +29 -2
- package/dist/backfill.d.ts +86 -0
- package/dist/backfill.js +251 -0
- package/dist/bin.js +4 -1
- package/dist/cli/audit.d.ts +15 -0
- package/dist/cli/audit.js +392 -0
- package/dist/cli/auth.d.ts +82 -0
- package/dist/cli/auth.js +607 -0
- 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.d.ts +1 -0
- package/dist/cli/call.js +387 -0
- package/dist/cli/capabilities.d.ts +30 -0
- package/dist/cli/capabilities.js +197 -0
- package/dist/cli/draft.d.ts +7 -0
- package/dist/cli/draft.js +87 -0
- package/dist/cli/enrich.d.ts +8 -0
- package/dist/cli/enrich.js +806 -0
- package/dist/cli/fix.d.ts +33 -0
- package/dist/cli/fix.js +346 -0
- package/dist/cli/help.d.ts +25 -0
- package/dist/cli/help.js +646 -0
- package/dist/cli/icp.d.ts +7 -0
- package/dist/cli/icp.js +229 -0
- package/dist/cli/init.d.ts +7 -0
- package/dist/cli/init.js +58 -0
- package/dist/cli/market.d.ts +1 -0
- package/dist/cli/market.js +391 -0
- package/dist/cli/plans.d.ts +5 -0
- package/dist/cli/plans.js +456 -0
- package/dist/cli/schedule.d.ts +8 -0
- package/dist/cli/schedule.js +479 -0
- package/dist/cli/shared.d.ts +71 -0
- package/dist/cli/shared.js +342 -0
- package/dist/cli/signals.d.ts +7 -0
- package/dist/cli/signals.js +412 -0
- package/dist/cli/suggest.d.ts +49 -0
- package/dist/cli/suggest.js +135 -0
- package/dist/cli/tam.d.ts +9 -0
- package/dist/cli/tam.js +387 -0
- package/dist/cli/ui.d.ts +142 -0
- package/dist/cli/ui.js +427 -0
- package/dist/cli.d.ts +1 -57
- package/dist/cli.js +123 -5157
- package/dist/connector.d.ts +23 -0
- package/dist/connector.js +47 -0
- package/dist/connectors/hubspot.d.ts +10 -1
- package/dist/connectors/hubspot.js +244 -10
- package/dist/connectors/hubspotAuth.js +5 -1
- package/dist/connectors/linkedin.js +14 -14
- package/dist/connectors/salesforce.d.ts +10 -1
- package/dist/connectors/salesforce.js +39 -6
- package/dist/connectors/signalSources.js +7 -59
- 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/icp.d.ts +15 -11
- package/dist/icp.js +19 -36
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/judge.d.ts +2 -0
- package/dist/judge.js +6 -0
- 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 +11 -1
- package/dist/marketClassify.js +17 -2
- package/dist/marketTaxonomy.d.ts +6 -1
- package/dist/marketTaxonomy.js +4 -2
- package/dist/mcp-bin.js +31 -2
- package/dist/mcp.js +372 -166
- 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/schedule.d.ts +80 -2
- package/dist/schedule.js +254 -1
- package/dist/spoolFiles.d.ts +44 -0
- package/dist/spoolFiles.js +114 -0
- package/dist/types.d.ts +29 -1
- package/docs/api.md +75 -8
- package/docs/architecture.md +2 -0
- package/docs/linkedin-connector-spec.md +1 -1
- package/docs/signal-spool-format.md +13 -0
- package/llms.txt +18 -9
- package/package.json +10 -3
- package/skills/fullstackgtm/SKILL.md +2 -1
- package/src/audit.ts +27 -1
- package/src/backfill.ts +340 -0
- package/src/bin.ts +5 -1
- package/src/cli/audit.ts +447 -0
- package/src/cli/auth.ts +669 -0
- package/src/cli/backfill.ts +156 -0
- package/src/cli/backfillRuns.ts +198 -0
- package/src/cli/call.ts +414 -0
- package/src/cli/capabilities.ts +215 -0
- package/src/cli/draft.ts +101 -0
- package/src/cli/enrich.ts +908 -0
- package/src/cli/fix.ts +373 -0
- package/src/cli/help.ts +702 -0
- package/src/cli/icp.ts +265 -0
- package/src/cli/init.ts +65 -0
- package/src/cli/market.ts +423 -0
- package/src/cli/plans.ts +524 -0
- package/src/cli/schedule.ts +526 -0
- package/src/cli/shared.ts +392 -0
- package/src/cli/signals.ts +434 -0
- package/src/cli/suggest.ts +151 -0
- package/src/cli/tam.ts +435 -0
- package/src/cli/ui.ts +497 -0
- package/src/cli.ts +122 -5855
- package/src/connector.ts +66 -0
- package/src/connectors/hubspot.ts +273 -9
- package/src/connectors/hubspotAuth.ts +5 -1
- package/src/connectors/linkedin.ts +14 -14
- package/src/connectors/salesforce.ts +51 -3
- package/src/connectors/signalSources.ts +7 -56
- 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/icp.ts +19 -35
- package/src/index.ts +28 -1
- package/src/judge.ts +7 -0
- package/src/llm.ts +239 -0
- package/src/market.ts +157 -44
- package/src/marketClassify.ts +26 -2
- package/src/marketTaxonomy.ts +10 -2
- package/src/mcp-bin.ts +34 -2
- package/src/mcp.ts +270 -63
- package/src/progress.ts +197 -0
- package/src/runReport.ts +159 -4
- package/src/schedule.ts +310 -3
- package/src/spoolFiles.ts +116 -0
- package/src/types.ts +32 -2
- package/docs/dx-punch-list.md +0 -87
- package/docs/roadmap-to-1.0.md +0 -211
package/dist/schedule.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { mkdirSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { credentialsDir, ensureSecureHomeDir, writeSecureFile } from "./credentials.js";
|
|
5
6
|
// Mirrors stableHash in rules.ts (FNV-1a); duplicated to keep schedule.ts
|
|
@@ -592,3 +593,255 @@ export function replaceManagedBlock(existing, profile, block) {
|
|
|
592
593
|
return "";
|
|
593
594
|
return `${merged.join("\n")}\n`;
|
|
594
595
|
}
|
|
596
|
+
/**
|
|
597
|
+
* Compile a cron expression to StartCalendarInterval dicts. Full-range fields
|
|
598
|
+
* (`*`, but also spellings like `0-59`) are omitted (launchd wildcard); the
|
|
599
|
+
* rest cross-product, capped so a dense expression cannot render a megabyte
|
|
600
|
+
* plist. Vixie day semantics: when day-of-month AND day-of-week are both
|
|
601
|
+
* restricted, either may match — launchd ANDs keys within one dict, so that
|
|
602
|
+
* becomes the union of a Day dict set and a Weekday dict set. A date matching
|
|
603
|
+
* both sets makes launchd trigger twice in the same minute; the run entry
|
|
604
|
+
* point drops the second via isDuplicateCronFiring.
|
|
605
|
+
*/
|
|
606
|
+
export function cronToCalendarIntervals(cron, cap = 512) {
|
|
607
|
+
const full = (values, min, max) => values.length === max - min + 1;
|
|
608
|
+
const minutes = full(cron.minute, 0, 59) ? [undefined] : cron.minute;
|
|
609
|
+
const hours = full(cron.hour, 0, 23) ? [undefined] : cron.hour;
|
|
610
|
+
const months = full(cron.month, 1, 12) ? [undefined] : cron.month;
|
|
611
|
+
const domRestricted = cron.dayOfMonthRestricted && !full(cron.dayOfMonth, 1, 31);
|
|
612
|
+
const dowRestricted = cron.dayOfWeekRestricted && !full(cron.dayOfWeek, 0, 6);
|
|
613
|
+
const daySlots = [];
|
|
614
|
+
if (domRestricted)
|
|
615
|
+
for (const day of cron.dayOfMonth)
|
|
616
|
+
daySlots.push({ Day: day });
|
|
617
|
+
if (dowRestricted)
|
|
618
|
+
for (const weekday of cron.dayOfWeek)
|
|
619
|
+
daySlots.push({ Weekday: weekday });
|
|
620
|
+
if (daySlots.length === 0)
|
|
621
|
+
daySlots.push({});
|
|
622
|
+
const count = minutes.length * hours.length * daySlots.length * months.length;
|
|
623
|
+
if (count > cap) {
|
|
624
|
+
throw new Error(`Cron expression "${cron.source}" expands to ${count} launchd calendar intervals (cap ${cap}). ` +
|
|
625
|
+
"Simplify the expression or split it into separate schedules.");
|
|
626
|
+
}
|
|
627
|
+
const intervals = [];
|
|
628
|
+
for (const month of months) {
|
|
629
|
+
for (const daySlot of daySlots) {
|
|
630
|
+
for (const hour of hours) {
|
|
631
|
+
for (const minute of minutes) {
|
|
632
|
+
const dict = {};
|
|
633
|
+
if (minute !== undefined)
|
|
634
|
+
dict.Minute = minute;
|
|
635
|
+
if (hour !== undefined)
|
|
636
|
+
dict.Hour = hour;
|
|
637
|
+
if (daySlot.Day !== undefined)
|
|
638
|
+
dict.Day = daySlot.Day;
|
|
639
|
+
if (daySlot.Weekday !== undefined)
|
|
640
|
+
dict.Weekday = daySlot.Weekday;
|
|
641
|
+
if (month !== undefined)
|
|
642
|
+
dict.Month = month;
|
|
643
|
+
intervals.push(dict);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return intervals;
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* True when a cron-triggered run was already recorded in `firedAt`'s minute.
|
|
652
|
+
* The `schedule run` entry point checks this for trigger:cron so a launchd
|
|
653
|
+
* double-trigger (dom+dow OR corner above) records a duplicate_firing no-op
|
|
654
|
+
* instead of running the command twice. Manual runs are never deduplicated.
|
|
655
|
+
*/
|
|
656
|
+
export function isDuplicateCronFiring(runs, firedAt) {
|
|
657
|
+
const minute = new Date(firedAt);
|
|
658
|
+
minute.setSeconds(0, 0);
|
|
659
|
+
return runs.some((run) => {
|
|
660
|
+
if (run.trigger !== "cron")
|
|
661
|
+
return false;
|
|
662
|
+
const fired = new Date(run.firedAt);
|
|
663
|
+
fired.setSeconds(0, 0);
|
|
664
|
+
return fired.getTime() === minute.getTime();
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
/** Reverse-DNS namespace for one profile's agents; install/uninstall own this prefix wholesale. */
|
|
668
|
+
export function launchdAgentPrefix(profile) {
|
|
669
|
+
if (!/^[A-Za-z0-9._-]+$/.test(profile)) {
|
|
670
|
+
throw new Error(`Cannot build a launchd label from profile "${profile}" — profile names in LaunchAgent labels ` +
|
|
671
|
+
"must be alphanumeric with dot/dash/underscore.");
|
|
672
|
+
}
|
|
673
|
+
return `com.fullstackgtm.${profile}.`;
|
|
674
|
+
}
|
|
675
|
+
export function launchdLabel(profile, scheduleEntryId) {
|
|
676
|
+
if (!/^[A-Za-z0-9._-]+$/.test(scheduleEntryId)) {
|
|
677
|
+
throw new Error(`Cannot build a launchd label from schedule id "${scheduleEntryId}".`);
|
|
678
|
+
}
|
|
679
|
+
return `${launchdAgentPrefix(profile)}${scheduleEntryId}`;
|
|
680
|
+
}
|
|
681
|
+
function xmlEscape(value) {
|
|
682
|
+
return value.replace(/[&<>"']/g, (char) => {
|
|
683
|
+
switch (char) {
|
|
684
|
+
case "&":
|
|
685
|
+
return "&";
|
|
686
|
+
case "<":
|
|
687
|
+
return "<";
|
|
688
|
+
case ">":
|
|
689
|
+
return ">";
|
|
690
|
+
case '"':
|
|
691
|
+
return """;
|
|
692
|
+
default:
|
|
693
|
+
return "'";
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Render one LaunchAgent plist. ProgramArguments is an argv array — launchd
|
|
699
|
+
* execs it directly, no shell, so there is no quoting/injection surface at
|
|
700
|
+
* all (values are XML-escaped for the document, nothing more).
|
|
701
|
+
*/
|
|
702
|
+
export function renderLaunchdPlist(options) {
|
|
703
|
+
const lines = [
|
|
704
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
705
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
706
|
+
'<plist version="1.0">',
|
|
707
|
+
"<dict>",
|
|
708
|
+
"\t<key>Label</key>",
|
|
709
|
+
`\t<string>${xmlEscape(options.label)}</string>`,
|
|
710
|
+
"\t<key>ProgramArguments</key>",
|
|
711
|
+
"\t<array>",
|
|
712
|
+
...options.programArguments.map((arg) => `\t\t<string>${xmlEscape(arg)}</string>`),
|
|
713
|
+
"\t</array>",
|
|
714
|
+
"\t<key>StartCalendarInterval</key>",
|
|
715
|
+
"\t<array>",
|
|
716
|
+
];
|
|
717
|
+
const keyOrder = ["Minute", "Hour", "Day", "Weekday", "Month"];
|
|
718
|
+
for (const interval of options.calendarIntervals) {
|
|
719
|
+
lines.push("\t\t<dict>");
|
|
720
|
+
for (const key of keyOrder) {
|
|
721
|
+
const value = interval[key];
|
|
722
|
+
if (value === undefined)
|
|
723
|
+
continue;
|
|
724
|
+
lines.push(`\t\t\t<key>${key}</key>`, `\t\t\t<integer>${value}</integer>`);
|
|
725
|
+
}
|
|
726
|
+
lines.push("\t\t</dict>");
|
|
727
|
+
}
|
|
728
|
+
lines.push("\t</array>");
|
|
729
|
+
if (options.environment && Object.keys(options.environment).length > 0) {
|
|
730
|
+
lines.push("\t<key>EnvironmentVariables</key>", "\t<dict>");
|
|
731
|
+
for (const [name, value] of Object.entries(options.environment)) {
|
|
732
|
+
lines.push(`\t\t<key>${xmlEscape(name)}</key>`, `\t\t<string>${xmlEscape(value)}</string>`);
|
|
733
|
+
}
|
|
734
|
+
lines.push("\t</dict>");
|
|
735
|
+
}
|
|
736
|
+
if (options.standardOutPath) {
|
|
737
|
+
lines.push("\t<key>StandardOutPath</key>", `\t<string>${xmlEscape(options.standardOutPath)}</string>`);
|
|
738
|
+
}
|
|
739
|
+
if (options.standardErrorPath) {
|
|
740
|
+
lines.push("\t<key>StandardErrorPath</key>", `\t<string>${xmlEscape(options.standardErrorPath)}</string>`);
|
|
741
|
+
}
|
|
742
|
+
lines.push("</dict>", "</plist>");
|
|
743
|
+
return `${lines.join("\n")}\n`;
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* The real ~/Library/LaunchAgents + launchctl. Everything above takes a
|
|
747
|
+
* LaunchdIo so tests inject fakes and never touch the user's agents.
|
|
748
|
+
*/
|
|
749
|
+
export function systemLaunchdIo(agentsDirOverride) {
|
|
750
|
+
const agentsDir = agentsDirOverride ?? join(homedir(), "Library", "LaunchAgents");
|
|
751
|
+
const domain = () => {
|
|
752
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : null;
|
|
753
|
+
if (uid === null)
|
|
754
|
+
throw new Error("launchd scheduling requires a POSIX user id (macOS only).");
|
|
755
|
+
return `gui/${uid}`;
|
|
756
|
+
};
|
|
757
|
+
const launchctl = (args) => {
|
|
758
|
+
const result = spawnSync("launchctl", args, { encoding: "utf8" });
|
|
759
|
+
if (result.error)
|
|
760
|
+
throw new Error(`Cannot run \`launchctl\`: ${result.error.message}`);
|
|
761
|
+
return result;
|
|
762
|
+
};
|
|
763
|
+
return {
|
|
764
|
+
list() {
|
|
765
|
+
try {
|
|
766
|
+
return readdirSync(agentsDir).filter((name) => name.endsWith(".plist"));
|
|
767
|
+
}
|
|
768
|
+
catch {
|
|
769
|
+
return [];
|
|
770
|
+
}
|
|
771
|
+
},
|
|
772
|
+
write(fileName, content) {
|
|
773
|
+
mkdirSync(agentsDir, { recursive: true });
|
|
774
|
+
// 0644 like every LaunchAgent: launchd refuses group/world-WRITABLE
|
|
775
|
+
// plists, and this file holds a dispatch line, not a secret.
|
|
776
|
+
writeFileSync(join(agentsDir, fileName), content, { mode: 0o644 });
|
|
777
|
+
},
|
|
778
|
+
remove(fileName) {
|
|
779
|
+
rmSync(join(agentsDir, fileName), { force: true });
|
|
780
|
+
},
|
|
781
|
+
bootstrap(fileName) {
|
|
782
|
+
const result = launchctl(["bootstrap", domain(), join(agentsDir, fileName)]);
|
|
783
|
+
if (result.status !== 0) {
|
|
784
|
+
throw new Error(`\`launchctl bootstrap\` failed for ${fileName}: ${(result.stderr ?? "").trim() || `exit ${result.status}`}`);
|
|
785
|
+
}
|
|
786
|
+
},
|
|
787
|
+
bootout(label) {
|
|
788
|
+
launchctl(["bootout", `${domain()}/${label}`]);
|
|
789
|
+
},
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Materialize enabled entries as one LaunchAgent each, replacing the
|
|
794
|
+
* profile's com.fullstackgtm.<profile>.* fleet wholesale (stale agents are
|
|
795
|
+
* booted out and deleted; plists outside the prefix are never touched).
|
|
796
|
+
* `cliArgv` is the argv-array analog of the crontab invocation line.
|
|
797
|
+
*/
|
|
798
|
+
export function installLaunchdAgents(profile, entries, cliArgv, io, options = {}) {
|
|
799
|
+
for (const token of cliArgv) {
|
|
800
|
+
if (hasControlChar(token)) {
|
|
801
|
+
throw new Error("Refusing to render LaunchAgents: the resolved CLI invocation (node path or script path) " +
|
|
802
|
+
"contains a newline or control character.");
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
const removed = uninstallLaunchdAgents(profile, io);
|
|
806
|
+
const installed = [];
|
|
807
|
+
for (const entry of entries) {
|
|
808
|
+
assertRenderableEntry(profile, entry);
|
|
809
|
+
let intervals;
|
|
810
|
+
try {
|
|
811
|
+
intervals = cronToCalendarIntervals(parseCron(entry.cron));
|
|
812
|
+
}
|
|
813
|
+
catch (error) {
|
|
814
|
+
throw new Error(`Schedule ${entry.id} ("${entry.label}") cannot materialize as a LaunchAgent: ` +
|
|
815
|
+
`${error instanceof Error ? error.message : String(error)}`);
|
|
816
|
+
}
|
|
817
|
+
const label = launchdLabel(profile, entry.id);
|
|
818
|
+
const logPath = options.logDir ? join(options.logDir, `${label}.log`) : undefined;
|
|
819
|
+
const plist = renderLaunchdPlist({
|
|
820
|
+
label,
|
|
821
|
+
programArguments: [...cliArgv, "schedule", "run", entry.id, "--profile", profile, "--trigger", "cron"],
|
|
822
|
+
calendarIntervals: intervals,
|
|
823
|
+
environment: options.environment,
|
|
824
|
+
standardOutPath: logPath,
|
|
825
|
+
standardErrorPath: logPath,
|
|
826
|
+
});
|
|
827
|
+
const fileName = `${label}.plist`;
|
|
828
|
+
io.write(fileName, plist);
|
|
829
|
+
io.bootstrap(fileName);
|
|
830
|
+
installed.push(label);
|
|
831
|
+
}
|
|
832
|
+
return { installed, removed };
|
|
833
|
+
}
|
|
834
|
+
/** Boot out and delete every agent in the profile's prefix; returns the removed labels. */
|
|
835
|
+
export function uninstallLaunchdAgents(profile, io) {
|
|
836
|
+
const prefix = launchdAgentPrefix(profile);
|
|
837
|
+
const removed = [];
|
|
838
|
+
for (const name of io.list()) {
|
|
839
|
+
if (!name.startsWith(prefix))
|
|
840
|
+
continue;
|
|
841
|
+
const label = name.slice(0, -".plist".length);
|
|
842
|
+
io.bootout(label);
|
|
843
|
+
io.remove(name);
|
|
844
|
+
removed.push(label);
|
|
845
|
+
}
|
|
846
|
+
return removed;
|
|
847
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared reader for the spool container convention (docs/signal-spool-format.md):
|
|
3
|
+
* newline-delimited JSON — one object per line, append-only — with a JSON array
|
|
4
|
+
* accepted in a `.json` file for hand-staged convenience, and a DIRECTORY
|
|
5
|
+
* meaning "every `*.jsonl` / `*.json` file in it, name-sorted" so multiple
|
|
6
|
+
* receivers can each append their own file.
|
|
7
|
+
*
|
|
8
|
+
* The container is the convention; the row SCHEMA stays with each caller:
|
|
9
|
+
* `signals fetch --from` validates staged signal rows, `enrich ingest` stages
|
|
10
|
+
* enrichment rows, `market observe --from` validates observation-set envelopes.
|
|
11
|
+
* This module only parses objects out of files — it never interprets them.
|
|
12
|
+
*
|
|
13
|
+
* Extracted from the `file` signal-source connector (the Phase-2 webhook
|
|
14
|
+
* landing-zone reader), which now delegates here; error-message text is
|
|
15
|
+
* parameterized by `label` so existing connector errors are unchanged.
|
|
16
|
+
*/
|
|
17
|
+
/** Spool files in a landing-zone directory: `*.jsonl` / `*.json`, name-sorted. */
|
|
18
|
+
export declare function spoolFilesIn(dir: string): string[];
|
|
19
|
+
/** Parse a JSON-array spool file into rows. `label` prefixes error messages. */
|
|
20
|
+
export declare function parseSpoolArray(raw: string, path: string, label: string): Record<string, unknown>[];
|
|
21
|
+
/** Parse a JSONL spool file into rows. `label` prefixes error messages. */
|
|
22
|
+
export declare function parseSpoolJsonl(raw: string, path: string, label: string): Record<string, unknown>[];
|
|
23
|
+
/** Parse one spool file's text — JSON array if it opens with `[`, else JSONL. */
|
|
24
|
+
export declare function parseSpoolText(raw: string, path: string, label: string): Record<string, unknown>[];
|
|
25
|
+
/**
|
|
26
|
+
* Should an explicitly named intake path be read with spool semantics?
|
|
27
|
+
* True for a directory (a landing zone) or a `*.jsonl` file. Plain `.json` /
|
|
28
|
+
* `.csv` files keep each verb's original single-file handling, so existing
|
|
29
|
+
* invocations parse exactly as before.
|
|
30
|
+
*/
|
|
31
|
+
export declare function isSpoolPath(absPath: string): boolean;
|
|
32
|
+
/** A parsed spool row plus where it came from (for error labels). */
|
|
33
|
+
export type SpoolRow = {
|
|
34
|
+
file: string;
|
|
35
|
+
index: number;
|
|
36
|
+
row: Record<string, unknown>;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Read an explicitly named spool path — a `.jsonl` file or a directory of
|
|
40
|
+
* `*.jsonl` / `*.json` files. Unlike the webhook `file` connector (where a
|
|
41
|
+
* missing spool is an empty source), the user named this path: a missing or
|
|
42
|
+
* unreadable file is an error, and a malformed row throws with file + line.
|
|
43
|
+
*/
|
|
44
|
+
export declare function readSpoolPath(absPath: string, label: string): SpoolRow[];
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared reader for the spool container convention (docs/signal-spool-format.md):
|
|
3
|
+
* newline-delimited JSON — one object per line, append-only — with a JSON array
|
|
4
|
+
* accepted in a `.json` file for hand-staged convenience, and a DIRECTORY
|
|
5
|
+
* meaning "every `*.jsonl` / `*.json` file in it, name-sorted" so multiple
|
|
6
|
+
* receivers can each append their own file.
|
|
7
|
+
*
|
|
8
|
+
* The container is the convention; the row SCHEMA stays with each caller:
|
|
9
|
+
* `signals fetch --from` validates staged signal rows, `enrich ingest` stages
|
|
10
|
+
* enrichment rows, `market observe --from` validates observation-set envelopes.
|
|
11
|
+
* This module only parses objects out of files — it never interprets them.
|
|
12
|
+
*
|
|
13
|
+
* Extracted from the `file` signal-source connector (the Phase-2 webhook
|
|
14
|
+
* landing-zone reader), which now delegates here; error-message text is
|
|
15
|
+
* parameterized by `label` so existing connector errors are unchanged.
|
|
16
|
+
*/
|
|
17
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
/** Spool files in a landing-zone directory: `*.jsonl` / `*.json`, name-sorted. */
|
|
20
|
+
export function spoolFilesIn(dir) {
|
|
21
|
+
let names;
|
|
22
|
+
try {
|
|
23
|
+
names = readdirSync(dir);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
return names
|
|
29
|
+
.filter((name) => name.endsWith(".jsonl") || name.endsWith(".json"))
|
|
30
|
+
.sort()
|
|
31
|
+
.map((name) => join(dir, name));
|
|
32
|
+
}
|
|
33
|
+
/** Parse a JSON-array spool file into rows. `label` prefixes error messages. */
|
|
34
|
+
export function parseSpoolArray(raw, path, label) {
|
|
35
|
+
let parsed;
|
|
36
|
+
try {
|
|
37
|
+
parsed = JSON.parse(raw);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
throw new Error(`${label} ${path}: not valid JSON (${error instanceof Error ? error.message : String(error)}).`);
|
|
41
|
+
}
|
|
42
|
+
if (!Array.isArray(parsed))
|
|
43
|
+
throw new Error(`${label} ${path}: expected a JSON array of staged signal rows.`);
|
|
44
|
+
return parsed.map((entry, index) => {
|
|
45
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
46
|
+
throw new Error(`${label} ${path}: row ${index} is not an object.`);
|
|
47
|
+
}
|
|
48
|
+
return entry;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/** Parse a JSONL spool file into rows. `label` prefixes error messages. */
|
|
52
|
+
export function parseSpoolJsonl(raw, path, label) {
|
|
53
|
+
const out = [];
|
|
54
|
+
const lines = raw.split("\n");
|
|
55
|
+
lines.forEach((line, index) => {
|
|
56
|
+
const t = line.trim();
|
|
57
|
+
if (!t)
|
|
58
|
+
return;
|
|
59
|
+
let parsed;
|
|
60
|
+
try {
|
|
61
|
+
parsed = JSON.parse(t);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
throw new Error(`${label} ${path}: line ${index} is not valid JSON (${error instanceof Error ? error.message : String(error)}).`);
|
|
65
|
+
}
|
|
66
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
67
|
+
throw new Error(`${label} ${path}: line ${index} is not a JSON object.`);
|
|
68
|
+
}
|
|
69
|
+
out.push(parsed);
|
|
70
|
+
});
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
/** Parse one spool file's text — JSON array if it opens with `[`, else JSONL. */
|
|
74
|
+
export function parseSpoolText(raw, path, label) {
|
|
75
|
+
const trimmed = raw.trim();
|
|
76
|
+
if (!trimmed)
|
|
77
|
+
return [];
|
|
78
|
+
return trimmed.startsWith("[") ? parseSpoolArray(trimmed, path, label) : parseSpoolJsonl(trimmed, path, label);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Should an explicitly named intake path be read with spool semantics?
|
|
82
|
+
* True for a directory (a landing zone) or a `*.jsonl` file. Plain `.json` /
|
|
83
|
+
* `.csv` files keep each verb's original single-file handling, so existing
|
|
84
|
+
* invocations parse exactly as before.
|
|
85
|
+
*/
|
|
86
|
+
export function isSpoolPath(absPath) {
|
|
87
|
+
try {
|
|
88
|
+
if (statSync(absPath).isDirectory())
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
return absPath.endsWith(".jsonl");
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Read an explicitly named spool path — a `.jsonl` file or a directory of
|
|
98
|
+
* `*.jsonl` / `*.json` files. Unlike the webhook `file` connector (where a
|
|
99
|
+
* missing spool is an empty source), the user named this path: a missing or
|
|
100
|
+
* unreadable file is an error, and a malformed row throws with file + line.
|
|
101
|
+
*/
|
|
102
|
+
export function readSpoolPath(absPath, label) {
|
|
103
|
+
const isDir = statSync(absPath).isDirectory();
|
|
104
|
+
const files = isDir ? spoolFilesIn(absPath) : [absPath];
|
|
105
|
+
if (isDir && files.length === 0) {
|
|
106
|
+
throw new Error(`${label} ${absPath}: directory contains no *.jsonl / *.json spool files.`);
|
|
107
|
+
}
|
|
108
|
+
const rows = [];
|
|
109
|
+
for (const file of files) {
|
|
110
|
+
const raw = readFileSync(file, "utf8");
|
|
111
|
+
parseSpoolText(raw, file, label).forEach((row, index) => rows.push({ file, index, row }));
|
|
112
|
+
}
|
|
113
|
+
return rows;
|
|
114
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -15,7 +15,8 @@ export type PatchOperationType = "set_field" | "clear_field" | "link_record" | "
|
|
|
15
15
|
* The afterValue of a `create_record` operation. The connector re-resolves on
|
|
16
16
|
* `matchKey`/`matchValue` at apply time and creates only on a confirmed miss.
|
|
17
17
|
* `estCostUsd` is the acquire meter's per-record charge, recorded against the
|
|
18
|
-
* budget on a successful create.
|
|
18
|
+
* budget on a successful create. Emitted by `enrich acquire` (metered) and
|
|
19
|
+
* `backfill stripe` (closed-won deals from paid invoices, unmetered).
|
|
19
20
|
*/
|
|
20
21
|
export type CreateRecordPayload = {
|
|
21
22
|
properties: Record<string, string>;
|
|
@@ -39,6 +40,22 @@ export type CreateRecordPayload = {
|
|
|
39
40
|
ownerId?: string;
|
|
40
41
|
/** Audit label for how the owner was chosen (e.g. "fixed", "territory:0"). */
|
|
41
42
|
assignedBy?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Deal creates only: provider-neutral stage sentinel. "closed_won" tells the
|
|
45
|
+
* connector to resolve the target pipeline's REAL closed-won stage id from
|
|
46
|
+
* the provider's pipeline metadata at apply time (HubSpot: stage with
|
|
47
|
+
* `metadata.isClosed` true and probability 1) — never by substring-guessing
|
|
48
|
+
* a stage name. If no such stage is resolvable the operation is skipped.
|
|
49
|
+
* Plain deal properties (amount, closedate, dealname, …) travel in
|
|
50
|
+
* `properties` as provider property names, like every other create.
|
|
51
|
+
*/
|
|
52
|
+
dealStage?: "closed_won";
|
|
53
|
+
/**
|
|
54
|
+
* Deal creates only: which pipeline to create the deal in — a pipeline id
|
|
55
|
+
* or a case-insensitive pipeline label. Absent = the portal's default
|
|
56
|
+
* pipeline (lowest displayOrder).
|
|
57
|
+
*/
|
|
58
|
+
dealPipeline?: string;
|
|
42
59
|
};
|
|
43
60
|
export type AuditFindingSeverity = "info" | "warning" | "critical";
|
|
44
61
|
/**
|
|
@@ -387,6 +404,17 @@ export type PatchPlanRun = {
|
|
|
387
404
|
status: PatchPlanRunStatus;
|
|
388
405
|
results: PatchOperationResult[];
|
|
389
406
|
};
|
|
407
|
+
/**
|
|
408
|
+
* Per-page progress during a snapshot pull, emitted by connectors that accept
|
|
409
|
+
* an `onProgress` option. `fetched` is the running total for the object type
|
|
410
|
+
* currently streaming (it resets when the pull moves on to the next type).
|
|
411
|
+
* Presentation-only: a throwing callback must never fail a pull, so emitters
|
|
412
|
+
* call it best-effort.
|
|
413
|
+
*/
|
|
414
|
+
export type SnapshotProgress = {
|
|
415
|
+
objectType: "user" | "account" | "contact" | "deal";
|
|
416
|
+
fetched: number;
|
|
417
|
+
};
|
|
390
418
|
/**
|
|
391
419
|
* The provider contract. Reads produce a canonical snapshot; writes accept a
|
|
392
420
|
* single patch operation and report the outcome. Connectors without
|
package/docs/api.md
CHANGED
|
@@ -47,9 +47,10 @@ release.
|
|
|
47
47
|
for any op that isn't batch-safe (grouped, value-overridden, company-
|
|
48
48
|
associated, conflicted). HubSpot: `search IN` + `batch/create`; Salesforce:
|
|
49
49
|
SOQL `IN` + Composite sObject Collections (`allOrNone:false`).
|
|
50
|
-
- `createHubspotConnector(options)` — read/write/readField/fetchChanges. `applyOperation` implements every `PatchOperationType`: `set_field`, `clear_field`, `link_record`, `create_task`, `create_record` (resolve-first net-new contact/company create — re-checks the dedupe key at apply, never double-creates), `archive_record`, `merge_records` (HubSpot v3 merge — pairwise, irreversible; survivor must belong to the duplicate group). (The Salesforce connector implements the same operation set, with the platform-specific constraints noted below.)
|
|
50
|
+
- `createHubspotConnector(options)` — read/write/readField/fetchChanges. `applyOperation` implements every `PatchOperationType`: `set_field`, `clear_field`, `link_record`, `create_task`, `create_record` (resolve-first net-new contact/company/**deal** create — re-checks the dedupe key at apply, never double-creates; a deal create requires `dealStage: "closed_won"`, resolved to the target pipeline's real closed-won stage id from pipeline metadata, and its dedupe key is a custom deal property ensured on demand), `archive_record`, `merge_records` (HubSpot v3 merge — pairwise, irreversible; survivor must belong to the duplicate group). (The Salesforce connector implements the same operation set, with the platform-specific constraints noted below.)
|
|
51
51
|
- `createSalesforceConnector(options)` — read/write/readField/fetchChanges; probabilities normalized to 0..1. `applyOperation` implements every operation type, with two platform constraints: `merge_records` covers **Accounts and Contacts** only (Salesforce exposes no Opportunity merge), and `create_record` resolve-first creates **contacts/accounts** (SOQL search on the match key, create only on a confirmed miss; stamps the canonical `ownerId` onto `OwnerId`).
|
|
52
52
|
- `createStripeConnector(options)` — read-only billing by design (`applyOperation` returns `skipped`); email domains are the cross-system merge keys. Implements `fetchChanges` (incremental via `created[gte]`).
|
|
53
|
+
- `fetchStripePaidInvoices(options, { sinceIso? })` — paginated `status=paid` invoice reads (`created[gte]` incremental, cents→major, paid-date extraction); feeds `buildStripeBackfillPlan(invoices, snapshot, opts)` (`src/backfill.ts`), which proposes one closed-won deal `create_record` per paid invoice, matched to CRM accounts by billing-email domain then exact name — an unmatched customer gets an explicit proposed account `create_record` in the same plan (freemail domains never used as company domain; `createMissingAccounts: false` restores report-only). Surfaced as the `backfill stripe` verb (plan-only; `--save` → approve → `apply`).
|
|
53
54
|
|
|
54
55
|
## Multi-system operations
|
|
55
56
|
|
|
@@ -75,10 +76,41 @@ Commands: `init`, `login` / `logout`, `snapshot`, `audit`, `report`, `diff`, `me
|
|
|
75
76
|
`icp` (`interview` / `set` / `show` / `judge` / `eval`),
|
|
76
77
|
`signals` (`fetch` / `list` / `outcome` / `weights`), `draft`,
|
|
77
78
|
`schedule` (`add` / `list` / `remove` / `enable` / `disable` / `run` /
|
|
78
|
-
`install` / `uninstall` / `status`), `rules`, `profiles`, `doctor
|
|
79
|
+
`install` / `uninstall` / `status`), `rules`, `profiles`, `doctor`,
|
|
80
|
+
`capabilities`, `robot-docs`.
|
|
79
81
|
Exit codes: `0` success · `1` error · `2` findings/regressions at the requested gate
|
|
80
82
|
(`--fail-on`, `--fail-on-new-findings`). `--json` everywhere; JSON output shapes are stable.
|
|
81
83
|
|
|
84
|
+
Agent discovery: `capabilities` prints the machine-readable CLI contract —
|
|
85
|
+
the command inventory with read-only vs write-shaped access (derived from the
|
|
86
|
+
help table, not a parallel list), this exit-code contract, and safety
|
|
87
|
+
defaults. `<command> --help --json` (and `help <command> --json`) prints
|
|
88
|
+
per-command help as JSON. Unknown commands with `--json` return a structured
|
|
89
|
+
`UNKNOWN_COMMAND` envelope with a did-you-mean hint; the plain-text path
|
|
90
|
+
prints the same hint plus pointers to `--help` and `capabilities --json`.
|
|
91
|
+
`robot-docs` prints the packaged agent guide (skills/fullstackgtm/SKILL.md).
|
|
92
|
+
|
|
93
|
+
Intent inference: a `--flag` that is documented nowhere in the help reference
|
|
94
|
+
but is within one edit of a documented flag (lowercased, `_`→`-`) is treated
|
|
95
|
+
as a typo — exit 1 with `Did you mean` and the exact corrected command
|
|
96
|
+
(structured `UNKNOWN_FLAG` envelope under `--json`). GNU-style `--flag=value`
|
|
97
|
+
spellings of documented flags get the space-separated correction this CLI
|
|
98
|
+
parses. Corrections are suggest-only, never auto-executed. Unknown flags with
|
|
99
|
+
no near-miss are ignored, as before. Unknown `--provider` values and subverb
|
|
100
|
+
typos on the multi-verb commands (`enrich apend` → `enrich append`) get
|
|
101
|
+
nearest-match suggestions; a flag-shaped first token is diagnosed as
|
|
102
|
+
flag-before-command.
|
|
103
|
+
|
|
104
|
+
Triage: `doctor --json` returns environment status plus a `workspace` slice
|
|
105
|
+
(`healthScore`, `scoreDelta`, `lastAuditAt`, `auditCount`, `pendingPlans`)
|
|
106
|
+
and state-aware `nextSteps` — when a plan awaits approval, the exact
|
|
107
|
+
`plans show <id>` → `plans approve <id> --operations <ids|all>` →
|
|
108
|
+
`apply --plan-id <id> --provider <name>` chain. One call orients an agent.
|
|
109
|
+
|
|
110
|
+
Reproducibility: with `SOURCE_DATE_EPOCH` set (seconds since the epoch), the
|
|
111
|
+
audit plan's `createdAt` is pinned and `audit --demo --json` is
|
|
112
|
+
byte-deterministic across re-runs.
|
|
113
|
+
|
|
82
114
|
### Flag lexicon: `--provider` vs `--source` vs `--connector` vs `--channel`
|
|
83
115
|
|
|
84
116
|
Four flags name "where data comes from / goes to"; they are not interchangeable:
|
|
@@ -90,6 +122,25 @@ Four flags name "where data comes from / goes to"; they are not interchangeable:
|
|
|
90
122
|
| `--connector` | A signal-intake source connector on `signals fetch`: pulls candidate signals from a connected platform or the local webhook spool into the signal ledger. | `file`, `serpapi-news`, `hubspot-forms` |
|
|
91
123
|
| `--channel` | Where a plan's output is delivered. On `apply`, the delivery terminus — a plan applies to `--provider <crm>` *or* `--channel outbox` (render approved openers to a local outbox file; transmits nothing), never both. On `draft`, which outreach channel the opener is drafted for (shapes the emitted op). | `apply`: `outbox` · `draft`: `email`, `linkedin`, `task` |
|
|
92
124
|
|
|
125
|
+
### Standardized flag aliases
|
|
126
|
+
|
|
127
|
+
Additive; every legacy flag keeps working:
|
|
128
|
+
|
|
129
|
+
- `--dry-run` — accepted on the plan-spine verbs (`audit`, `suggest`,
|
|
130
|
+
`bulk-update`, `dedupe`, `reassign`, `fix`, `call plan`,
|
|
131
|
+
`enrich append`/`refresh`/`acquire`, `signals fetch`, `icp judge`, `draft`,
|
|
132
|
+
`tam status`, `market overlay`). These verbs are already preview-by-default
|
|
133
|
+
(plans are dry-run; nothing writes to a CRM outside approve → apply), so the
|
|
134
|
+
flag asserts that default rather than changing it: it additionally suppresses
|
|
135
|
+
`--save` (nothing is persisted to the plan store / run ledgers; a stderr note
|
|
136
|
+
is printed when both are passed) and locks `fix` to its stop-before-apply
|
|
137
|
+
path even when `--yes` is present. Omitting `--dry-run` changes nothing.
|
|
138
|
+
(`tam accounts` had `--dry-run` before this convention — there it prices the
|
|
139
|
+
credit pull; same spirit, spend nothing.)
|
|
140
|
+
- `--confirm` — accepted wherever an ad-hoc confirmation flag already gates a
|
|
141
|
+
write stage: alias for `fix --yes` (`tam accounts` already used `--confirm`
|
|
142
|
+
for its spend guard). `--dry-run` beats `--confirm`/`--yes`.
|
|
143
|
+
|
|
93
144
|
Credential resolution ladder: explicit `--token-env` → ambient env
|
|
94
145
|
(`HUBSPOT_ACCESS_TOKEN`, `SALESFORCE_ACCESS_TOKEN`+`SALESFORCE_INSTANCE_URL`,
|
|
95
146
|
`STRIPE_SECRET_KEY`) → stored login (`~/.fullstackgtm`, `FSGTM_HOME` override)
|
|
@@ -145,6 +196,13 @@ dependency-free CSV intake; the Apollo client (`createApolloClient`,
|
|
|
145
196
|
`pullApolloRecords`, 429-aware with `Retry-After`) is the first `api`-kind
|
|
146
197
|
source.
|
|
147
198
|
|
|
199
|
+
Staged-file intake follows one container convention (`spoolFiles.ts`, shared
|
|
200
|
+
with `signals fetch --from` and `market observe --from`): `enrich ingest`
|
|
201
|
+
accepts — in addition to its original `.csv` / `.json` forms, which parse
|
|
202
|
+
exactly as before — a `*.jsonl` spool file (one JSON row per line) or a
|
|
203
|
+
directory of `*.jsonl` / `*.json` spool files (name-sorted), the Phase-2
|
|
204
|
+
webhook landing-zone format (docs/signal-spool-format.md).
|
|
205
|
+
|
|
148
206
|
## Acquire (net-new lead generation)
|
|
149
207
|
|
|
150
208
|
`buildAcquirePlan` turns sourced-but-unmatched prospects into `create_record`
|
|
@@ -226,10 +284,17 @@ firing — an unapproved plan records a `plan_not_approved` no-op run
|
|
|
226
284
|
- Cron: `parseCron` → `CronExpression`, `cronMatches`, `nextCronFiring`,
|
|
227
285
|
`expectedFirings`, `computeMissedFirings` (status and missed-firing
|
|
228
286
|
visibility — local cron has no catch-up).
|
|
229
|
-
- Local provider: `schedule install`
|
|
230
|
-
|
|
287
|
+
- Local provider: `schedule install` materializes enabled entries into the
|
|
288
|
+
platform timer. crontab (Linux default, `--timer crontab`): a
|
|
289
|
+
sentinel-managed block — `crontabSentinels`, `renderManagedBlock`,
|
|
231
290
|
`replaceManagedBlock`, and `systemCrontabIo` behind the injectable
|
|
232
|
-
`CrontabIo` seam (tests never touch a real crontab).
|
|
291
|
+
`CrontabIo` seam (tests never touch a real crontab). launchd (macOS
|
|
292
|
+
default, no Full Disk Access needed): one LaunchAgent per entry —
|
|
293
|
+
`cronToCalendarIntervals` (→ `LaunchdCalendarInterval[]`),
|
|
294
|
+
`renderLaunchdPlist`, `launchdLabel` / `launchdAgentPrefix`,
|
|
295
|
+
`installLaunchdAgents` / `uninstallLaunchdAgents`, and `systemLaunchdIo`
|
|
296
|
+
behind the injectable `LaunchdIo` seam (tests never touch launchctl);
|
|
297
|
+
`isDuplicateCronFiring` backs the `duplicate_firing` run no-op.
|
|
233
298
|
|
|
234
299
|
## Market map
|
|
235
300
|
|
|
@@ -261,9 +326,11 @@ the stored capture it cites before a set is accepted; failed captures read as
|
|
|
261
326
|
|
|
262
327
|
## MCP
|
|
263
328
|
|
|
264
|
-
Tools: `
|
|
265
|
-
|
|
266
|
-
`
|
|
329
|
+
Tools: `fullstackgtm_capabilities` (the machine-readable server contract —
|
|
330
|
+
tool inventory with per-tool CRM-write flags, derived from the server's own
|
|
331
|
+
registration table), `fullstackgtm_audit`, `fullstackgtm_rules`,
|
|
332
|
+
`fullstackgtm_apply` (requires explicit `approvedOperationIds`),
|
|
333
|
+
`fullstackgtm_suggest`, `fullstackgtm_call_parse`, `fullstackgtm_resolve`,
|
|
267
334
|
`fullstackgtm_market_worksheet`, `fullstackgtm_market_observe` (validates,
|
|
268
335
|
verifies quoted spans against the stored captures, appends, returns front
|
|
269
336
|
states). Input schemas are stable.
|
package/docs/architecture.md
CHANGED
|
@@ -72,6 +72,8 @@ provider ──fetchSnapshot()──► CanonicalGtmSnapshot
|
|
|
72
72
|
- `market*.ts` (`market.ts`, `marketClassify.ts`, `marketAxes.ts`,
|
|
73
73
|
`marketOverlay.ts`, `marketScale.ts`, `marketReport.ts`) — the competitive
|
|
74
74
|
market-map layer; classifications are verbatim-verified against captures.
|
|
75
|
+
- `backfill.ts` — Stripe paid invoices → proposed closed-won deal creates
|
|
76
|
+
(domain-then-name account matching; unmatched reported, never auto-created).
|
|
75
77
|
- `enrich.ts` + `enrichApollo.ts` — third-party data enrichment (fill-blanks),
|
|
76
78
|
plus `buildAcquirePlan` / `builtinAcquirePreset` for net-new lead generation.
|
|
77
79
|
- `icp.ts` — the Ideal Customer Profile artifact: per-provider discovery-filter
|
|
@@ -75,7 +75,7 @@ unstable base. Therefore:
|
|
|
75
75
|
fake.
|
|
76
76
|
2. **After the acquire workstream commits/lands:** wire `enrich acquire --source
|
|
77
77
|
linkedin` end-to-end and add the dry-run integration test.
|
|
78
|
-
3. **Live validation** needs a HeyReach trial API key
|
|
78
|
+
3. **Live validation** needs a HeyReach trial API key supplied by the operator — the fake
|
|
79
79
|
covers everything up to the real network call.
|
|
80
80
|
|
|
81
81
|
## Phase 2 (future, recorded for continuity)
|
|
@@ -154,6 +154,19 @@ connector produces, so both paths dedup against each other.
|
|
|
154
154
|
| `quote` | `"Submitted \"<form name>\" — <email>"` |
|
|
155
155
|
| `firstSeen` | submission timestamp (ISO 8601) |
|
|
156
156
|
|
|
157
|
+
## One container convention across staged intake
|
|
158
|
+
|
|
159
|
+
The spool CONTAINER (JSONL one-object-per-line; JSON array accepted; a
|
|
160
|
+
directory means every `*.jsonl` / `*.json` file in it, name-sorted) is shared
|
|
161
|
+
by every staged-file intake path, not just `--connector file`:
|
|
162
|
+
|
|
163
|
+
- `signals fetch --from <path>` — rows are staged signal rows (this doc's table);
|
|
164
|
+
- `enrich ingest <path> --source <id>` — rows are that source's enrichment rows;
|
|
165
|
+
- `market observe --from <path>` — each object is one observation-set envelope.
|
|
166
|
+
|
|
167
|
+
Each verb keeps its original single-file `.json` / `.csv` handling unchanged;
|
|
168
|
+
the row schema stays the verb's own. Shared reader: `src/spoolFiles.ts`.
|
|
169
|
+
|
|
157
170
|
## See also
|
|
158
171
|
|
|
159
172
|
- `docs/api.md` — the `signals` verb and the `SignalSourceConnector` contract.
|