fullstackgtm 0.54.0 → 0.55.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/DATA-FLOWS.md +1 -0
- package/README.md +3 -1
- package/dist/cli/auth.js +4 -3
- package/dist/cli/enrich.js +7 -5
- package/dist/cli/help.js +10 -7
- package/dist/cli/shared.d.ts +9 -0
- package/dist/cli/shared.js +37 -18
- package/dist/cli/signals.js +77 -4
- package/dist/cli/ui.js +3 -3
- package/dist/icp.d.ts +18 -0
- package/dist/icp.js +40 -7
- package/dist/icpDerive.js +34 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/schedule.js +3 -3
- package/dist/signalDiscovery.d.ts +52 -0
- package/dist/signalDiscovery.js +235 -0
- package/dist/signals.d.ts +4 -0
- package/docs/api.md +2 -2
- package/llms.txt +5 -2
- package/package.json +1 -1
- package/skills/fullstackgtm/SKILL.md +1 -1
- package/src/cli/auth.ts +4 -3
- package/src/cli/enrich.ts +7 -5
- package/src/cli/help.ts +10 -7
- package/src/cli/shared.ts +37 -14
- package/src/cli/signals.ts +72 -4
- package/src/cli/ui.ts +4 -4
- package/src/icp.ts +57 -8
- package/src/icpDerive.ts +32 -2
- package/src/index.ts +11 -0
- package/src/schedule.ts +3 -3
- package/src/signalDiscovery.ts +298 -0
- package/src/signals.ts +4 -0
package/dist/index.d.ts
CHANGED
|
@@ -54,6 +54,7 @@ export { registrableDomain, categoryKeywords, pickCategoryPage, extractLogoUrl,
|
|
|
54
54
|
export { computeMissedFirings, createFileScheduleRunStore, createFileScheduleStore, cronMatches, crontabSentinels, expectedFirings, nextCronFiring, parseCron, renderManagedBlock, replaceManagedBlock, scheduleId, scheduleRunsDir, schedulesPath, systemCrontabIo, tokenizeCommand, validateSchedulableArgv, type CronExpression, type CrontabIo, type ScheduleEntry, type ScheduleProvider, type ScheduleRunRecord, type ScheduleRunStore, type ScheduleRunTrigger, type ScheduleStore, } from "./schedule.ts";
|
|
55
55
|
export { suggestValues, type SuggestionConfidence, type ValueSuggestion } from "./suggest.ts";
|
|
56
56
|
export { buildSignalsFromAts, computeWeights, createFileSignalStore, DEFAULT_SIGNALS_CONFIG, dedupKey, dedupeSignals, loadSignalsConfig, makeOutcome, normalizeAccountDomain, parseSignalsConfig, recencyFactor, SIGNAL_BUCKETS, SIGNALS_CONFIG_FILE_NAME, signalId, signalRunId, signalsDir, signalWeight, type Signal, type SignalBucket, type SignalBucketConfig, type SignalOutcome, type SignalOutcomeResult, type SignalRun, type SignalsConfig, type SignalStore, } from "./signals.ts";
|
|
57
|
+
export { DEFAULT_DISCOVERY_ATS_DOMAINS, discoverSignalsWithExa, exaQueryForHypothesis, parseJobIdentity, triggerHypothesesForIcp, type DiscoverSignalsOptions, type EvidenceCandidate, type SignalDiscoveryResult, type SignalDiscoverySummary, } from "./signalDiscovery.ts";
|
|
57
58
|
export { fetchAtsJobs, snippetFor, type AtsBoardSource, type AtsJob, } from "./connectors/atsBoards.ts";
|
|
58
59
|
export { scoreBand, judgeRunId, normalizeSpan, isGroundedSpan, scoreAccount, accountRecentlyTouched, bestContactForAccount, deterministicDecision, deterministicWhyNow, JUDGE_SCHEMA, DEFAULT_JUDGE_PROMPT, judgeDecisionLlm, judgeSignals, judgeDir, createFileJudgeStore, type JudgeDecisionKind, type JudgeDecision, type JudgeRun, type JudgeBand, type AccountScore, type JudgeStore, } from "./judge.ts";
|
|
59
60
|
export { draft, authorOpeners, draftOpenerLlm, deterministicOpener, creditedTriggerSignal, draftEvidence, sanitizeOpener, firstLine, firstLineGroundedInTrigger, hasBannedGreeting, isStaleTrigger, draftOperationId, draftEvidenceId, DRAFT_CHANNELS, DRAFT_SCHEMA, DEFAULT_DRAFT_PROMPT, DEFAULT_FRESHNESS_DAYS, type Draft, type RejectedDraft, type DraftResult, type DraftChannel, } from "./draft.ts";
|
package/dist/index.js
CHANGED
|
@@ -54,6 +54,7 @@ export { registrableDomain, categoryKeywords, pickCategoryPage, extractLogoUrl,
|
|
|
54
54
|
export { computeMissedFirings, createFileScheduleRunStore, createFileScheduleStore, cronMatches, crontabSentinels, expectedFirings, nextCronFiring, parseCron, renderManagedBlock, replaceManagedBlock, scheduleId, scheduleRunsDir, schedulesPath, systemCrontabIo, tokenizeCommand, validateSchedulableArgv, } from "./schedule.js";
|
|
55
55
|
export { suggestValues } from "./suggest.js";
|
|
56
56
|
export { buildSignalsFromAts, computeWeights, createFileSignalStore, DEFAULT_SIGNALS_CONFIG, dedupKey, dedupeSignals, loadSignalsConfig, makeOutcome, normalizeAccountDomain, parseSignalsConfig, recencyFactor, SIGNAL_BUCKETS, SIGNALS_CONFIG_FILE_NAME, signalId, signalRunId, signalsDir, signalWeight, } from "./signals.js";
|
|
57
|
+
export { DEFAULT_DISCOVERY_ATS_DOMAINS, discoverSignalsWithExa, exaQueryForHypothesis, parseJobIdentity, triggerHypothesesForIcp, } from "./signalDiscovery.js";
|
|
57
58
|
export { fetchAtsJobs, snippetFor, } from "./connectors/atsBoards.js";
|
|
58
59
|
export { scoreBand, judgeRunId, normalizeSpan, isGroundedSpan, scoreAccount, accountRecentlyTouched, bestContactForAccount, deterministicDecision, deterministicWhyNow, JUDGE_SCHEMA, DEFAULT_JUDGE_PROMPT, judgeDecisionLlm, judgeSignals, judgeDir, createFileJudgeStore, } from "./judge.js";
|
|
59
60
|
export { draft, authorOpeners, draftOpenerLlm, deterministicOpener, creditedTriggerSignal, draftEvidence, sanitizeOpener, firstLine, firstLineGroundedInTrigger, hasBannedGreeting, isStaleTrigger, draftOperationId, draftEvidenceId, DRAFT_CHANNELS, DRAFT_SCHEMA, DEFAULT_DRAFT_PROMPT, DEFAULT_FRESHNESS_DAYS, } from "./draft.js";
|
package/dist/schedule.js
CHANGED
|
@@ -38,18 +38,18 @@ const SCHEDULABLE = {
|
|
|
38
38
|
// approved). So scheduled acquire accumulates proposals, never surprise leads.
|
|
39
39
|
enrich: ["append", "refresh", "acquire"],
|
|
40
40
|
market: ["capture", "refresh"],
|
|
41
|
-
// The GTM brain. `signals fetch`
|
|
41
|
+
// The GTM brain. `signals fetch|discover` are read-only re: CRM (--save persists only
|
|
42
42
|
// the local signal ledger). `icp judge`/`icp eval` are read-only/grade-only
|
|
43
43
|
// (--save writes only the local judge store, never a plan). `draft` is
|
|
44
44
|
// plan-side — it only stages a needs_approval plan, never applies — so the
|
|
45
45
|
// whole verb is safely schedulable (apply stays `apply --plan-id` only and
|
|
46
46
|
// re-checks `approved` at run time, so a scheduled draft still cannot send).
|
|
47
|
-
signals: ["fetch"],
|
|
47
|
+
signals: ["fetch", "discover"],
|
|
48
48
|
icp: ["judge", "eval"],
|
|
49
49
|
draft: null,
|
|
50
50
|
};
|
|
51
51
|
const ALLOWLIST_SUMMARY = "audit, snapshot, enrich append|refresh, enrich acquire --save (stages a lead plan), " +
|
|
52
|
-
"market capture|refresh, signals fetch, icp judge|eval, draft (stages a plan), " +
|
|
52
|
+
"market capture|refresh, signals fetch|discover, icp judge|eval, draft (stages a plan), " +
|
|
53
53
|
"suggest, report, doctor — plus apply --plan-id <id> (re-checked approved at every firing)";
|
|
54
54
|
/**
|
|
55
55
|
* Validate that an argv resolves to a schedulable fullstackgtm command.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Icp, TriggerHypothesis } from "./icp.ts";
|
|
2
|
+
import { type Signal, type SignalsConfig } from "./signals.ts";
|
|
3
|
+
export declare const DEFAULT_DISCOVERY_ATS_DOMAINS: string[];
|
|
4
|
+
export type EvidenceCandidate = {
|
|
5
|
+
hypothesisId: string;
|
|
6
|
+
hypothesisLabel: string;
|
|
7
|
+
companyName: string;
|
|
8
|
+
jobTitle: string;
|
|
9
|
+
sourceUrl: string;
|
|
10
|
+
quote: string;
|
|
11
|
+
publishedAt?: string;
|
|
12
|
+
accountDomain?: string;
|
|
13
|
+
};
|
|
14
|
+
export type SignalDiscoverySummary = {
|
|
15
|
+
provider: "exa";
|
|
16
|
+
readOnly: true;
|
|
17
|
+
searchesUsed: number;
|
|
18
|
+
searchLimit: number;
|
|
19
|
+
costUsd: number;
|
|
20
|
+
costLimitUsd: number;
|
|
21
|
+
rawResults: number;
|
|
22
|
+
matchedEvidence: number;
|
|
23
|
+
resolvedAccounts: number;
|
|
24
|
+
unresolvedAccounts: number;
|
|
25
|
+
warnings: string[];
|
|
26
|
+
};
|
|
27
|
+
export type SignalDiscoveryResult = {
|
|
28
|
+
summary: SignalDiscoverySummary;
|
|
29
|
+
candidates: EvidenceCandidate[];
|
|
30
|
+
signals: Signal[];
|
|
31
|
+
};
|
|
32
|
+
export type DiscoverSignalsOptions = {
|
|
33
|
+
icp: Icp;
|
|
34
|
+
apiKey: string;
|
|
35
|
+
since: Date;
|
|
36
|
+
maxAccounts: number;
|
|
37
|
+
maxResults: number;
|
|
38
|
+
maxSearches: number;
|
|
39
|
+
maxUsd: number;
|
|
40
|
+
now?: Date;
|
|
41
|
+
config?: SignalsConfig;
|
|
42
|
+
fetch?: typeof globalThis.fetch;
|
|
43
|
+
apiBaseUrl?: string;
|
|
44
|
+
};
|
|
45
|
+
/** Explicit hypotheses win; intent topics remain a backwards-compatible fallback. */
|
|
46
|
+
export declare function triggerHypothesesForIcp(icp: Icp): TriggerHypothesis[];
|
|
47
|
+
export declare function exaQueryForHypothesis(hypothesis: TriggerHypothesis): string;
|
|
48
|
+
export declare function parseJobIdentity(rawTitle: string, sourceUrl?: string): {
|
|
49
|
+
companyName: string;
|
|
50
|
+
jobTitle: string;
|
|
51
|
+
} | null;
|
|
52
|
+
export declare function discoverSignalsWithExa(options: DiscoverSignalsOptions): Promise<SignalDiscoveryResult>;
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { DEFAULT_SIGNALS_CONFIG, normalizeAccountDomain, signalId, signalWeight, } from "./signals.js";
|
|
2
|
+
export const DEFAULT_DISCOVERY_ATS_DOMAINS = [
|
|
3
|
+
"job-boards.greenhouse.io",
|
|
4
|
+
"boards.greenhouse.io",
|
|
5
|
+
"jobs.lever.co",
|
|
6
|
+
"jobs.ashbyhq.com",
|
|
7
|
+
"jobs.smartrecruiters.com",
|
|
8
|
+
"apply.workable.com",
|
|
9
|
+
];
|
|
10
|
+
const NON_COMPANY_DOMAINS = new Set([
|
|
11
|
+
...DEFAULT_DISCOVERY_ATS_DOMAINS,
|
|
12
|
+
"linkedin.com", "facebook.com", "instagram.com", "x.com", "twitter.com",
|
|
13
|
+
"crunchbase.com", "wikipedia.org", "glassdoor.com", "indeed.com",
|
|
14
|
+
]);
|
|
15
|
+
const STOP_WORDS = new Set(["about", "after", "before", "company", "could", "from", "hiring", "into", "their", "there", "these", "they", "this", "with", "your"]);
|
|
16
|
+
function strings(value, max = 12) {
|
|
17
|
+
return [...new Set((Array.isArray(value) ? value : []).filter((v) => typeof v === "string")
|
|
18
|
+
.map((v) => v.replace(/\s+/g, " ").trim()).filter(Boolean))].slice(0, max);
|
|
19
|
+
}
|
|
20
|
+
function slug(value, fallback) {
|
|
21
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 48) || fallback;
|
|
22
|
+
}
|
|
23
|
+
/** Explicit hypotheses win; intent topics remain a backwards-compatible fallback. */
|
|
24
|
+
export function triggerHypothesesForIcp(icp) {
|
|
25
|
+
const explicit = icp.signals?.triggerHypotheses ?? [];
|
|
26
|
+
const valid = explicit.map((item, index) => ({
|
|
27
|
+
id: slug(item.id || item.label, `trigger-${index + 1}`),
|
|
28
|
+
label: item.label?.trim(),
|
|
29
|
+
positiveEvidence: strings(item.positiveEvidence),
|
|
30
|
+
activeProjects: strings(item.activeProjects),
|
|
31
|
+
buyerFunctions: strings(item.buyerFunctions),
|
|
32
|
+
negativeEvidence: strings(item.negativeEvidence),
|
|
33
|
+
preferredSources: item.preferredSources,
|
|
34
|
+
})).filter((item) => item.label && (item.positiveEvidence.length || item.activeProjects.length));
|
|
35
|
+
if (valid.length)
|
|
36
|
+
return valid;
|
|
37
|
+
const topics = strings(icp.signals?.intentTopics);
|
|
38
|
+
if (topics.length) {
|
|
39
|
+
return [{ id: "intent-topics", label: topics.join(" or "), positiveEvidence: topics, preferredSources: ["job"] }];
|
|
40
|
+
}
|
|
41
|
+
throw new Error("signals discover: the ICP has no behavioral trigger hypotheses. Add signals.triggerHypotheses (or intentTopics), or regenerate it with `fullstackgtm icp derive --domain <domain>`.");
|
|
42
|
+
}
|
|
43
|
+
function evidenceTerms(hypothesis) {
|
|
44
|
+
return strings([...(hypothesis.positiveEvidence ?? []), ...(hypothesis.activeProjects ?? []), ...(hypothesis.buyerFunctions ?? [])], 20);
|
|
45
|
+
}
|
|
46
|
+
export function exaQueryForHypothesis(hypothesis) {
|
|
47
|
+
const terms = evidenceTerms(hypothesis).slice(0, 8).map((v) => `"${v.replace(/"/g, "")}"`);
|
|
48
|
+
return `recent company job posting showing ${hypothesis.label}${terms.length ? ` (${terms.join(" OR ")})` : ""}`;
|
|
49
|
+
}
|
|
50
|
+
function words(value) {
|
|
51
|
+
return value.toLowerCase().match(/[a-z0-9]+/g)?.filter((word) => word.length >= 4 && !STOP_WORDS.has(word)) ?? [];
|
|
52
|
+
}
|
|
53
|
+
function evidenceMatches(text, hypothesis) {
|
|
54
|
+
const hay = text.toLowerCase();
|
|
55
|
+
if ((hypothesis.negativeEvidence ?? []).some((term) => term && hay.includes(term.toLowerCase())))
|
|
56
|
+
return false;
|
|
57
|
+
const terms = evidenceTerms(hypothesis);
|
|
58
|
+
if (terms.some((term) => term && hay.includes(term.toLowerCase())))
|
|
59
|
+
return true;
|
|
60
|
+
const significant = [...new Set(terms.flatMap(words))];
|
|
61
|
+
return significant.filter((term) => hay.includes(term)).length >= 2;
|
|
62
|
+
}
|
|
63
|
+
function cleanTitle(value) {
|
|
64
|
+
return value.replace(/\s+/g, " ").replace(/\s*[|–—-]\s*(Greenhouse|Lever|Ashby|SmartRecruiters|Workable).*$/i, "").trim();
|
|
65
|
+
}
|
|
66
|
+
export function parseJobIdentity(rawTitle, sourceUrl) {
|
|
67
|
+
const title = cleanTitle(rawTitle);
|
|
68
|
+
const application = /^Job Application for (.+?) at (.+)$/i.exec(title);
|
|
69
|
+
if (application)
|
|
70
|
+
return { jobTitle: application[1].trim(), companyName: application[2].trim() };
|
|
71
|
+
const at = /^(.+?)\s+(?:at|@)\s+(.+)$/i.exec(title);
|
|
72
|
+
if (at)
|
|
73
|
+
return { jobTitle: at[1].trim(), companyName: at[2].trim() };
|
|
74
|
+
const dash = /^(.+?)\s+[–—-]\s+(.+)$/.exec(title);
|
|
75
|
+
if (dash)
|
|
76
|
+
return { companyName: dash[1].trim(), jobTitle: dash[2].trim() };
|
|
77
|
+
if (sourceUrl && title) {
|
|
78
|
+
try {
|
|
79
|
+
const parts = new URL(sourceUrl).pathname.split("/").filter(Boolean);
|
|
80
|
+
const boardSlug = parts.find((part) => !["jobs", "job", "positions", "posting"].includes(part.toLowerCase()) && !/^\d+$/.test(part));
|
|
81
|
+
if (boardSlug)
|
|
82
|
+
return { companyName: decodeURIComponent(boardSlug).replace(/[-_]+/g, " ").trim(), jobTitle: title };
|
|
83
|
+
}
|
|
84
|
+
catch { /* source URL validation happens separately */ }
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
function safeHostname(raw) {
|
|
89
|
+
try {
|
|
90
|
+
return normalizeAccountDomain(new URL(raw).hostname);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return "";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function isExcludedDomain(domain) {
|
|
97
|
+
return [...NON_COMPANY_DOMAINS].some((excluded) => domain === excluded || domain.endsWith(`.${excluded}`));
|
|
98
|
+
}
|
|
99
|
+
function companyMatches(companyName, resultTitle, url) {
|
|
100
|
+
const companyWords = words(companyName);
|
|
101
|
+
const candidate = `${resultTitle} ${safeHostname(url).split(".")[0]}`.toLowerCase();
|
|
102
|
+
return companyWords.length > 0 && companyWords.some((word) => candidate.includes(word));
|
|
103
|
+
}
|
|
104
|
+
function finiteCost(response) {
|
|
105
|
+
const total = response.costDollars?.total;
|
|
106
|
+
return typeof total === "number" && Number.isFinite(total) && total >= 0 ? total : 0;
|
|
107
|
+
}
|
|
108
|
+
async function exaSearch(args) {
|
|
109
|
+
let response;
|
|
110
|
+
try {
|
|
111
|
+
response = await args.fetch(`${args.apiBaseUrl.replace(/\/$/, "")}/search`, {
|
|
112
|
+
method: "POST",
|
|
113
|
+
headers: { "Content-Type": "application/json", "x-api-key": args.apiKey },
|
|
114
|
+
body: JSON.stringify(args.body),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
throw new Error(`signals discover: could not reach Exa (${error instanceof Error ? error.message : String(error)}).`);
|
|
119
|
+
}
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
if (response.status === 401 || response.status === 403) {
|
|
122
|
+
throw new Error("signals discover: Exa rejected the API key. Run `fullstackgtm login exa` and try again.");
|
|
123
|
+
}
|
|
124
|
+
if (response.status === 429)
|
|
125
|
+
throw new Error("signals discover: Exa rate limit reached; retry later or lower --max-searches.");
|
|
126
|
+
throw new Error(`signals discover: Exa search failed (HTTP ${response.status}).`);
|
|
127
|
+
}
|
|
128
|
+
return await response.json();
|
|
129
|
+
}
|
|
130
|
+
function resultRows(response) {
|
|
131
|
+
return Array.isArray(response.results) ? response.results.filter((row) => Boolean(row && typeof row === "object")) : [];
|
|
132
|
+
}
|
|
133
|
+
function firstQuote(result, title) {
|
|
134
|
+
const highlights = strings(result.highlights, 4);
|
|
135
|
+
return (highlights.find((value) => value.length >= 20) ?? highlights[0] ?? title).slice(0, 600);
|
|
136
|
+
}
|
|
137
|
+
export async function discoverSignalsWithExa(options) {
|
|
138
|
+
if (!options.apiKey.trim())
|
|
139
|
+
throw new Error("signals discover: missing Exa API key. Set EXA_API_KEY or run `fullstackgtm login exa`.");
|
|
140
|
+
if (!Number.isInteger(options.maxSearches) || options.maxSearches < 1)
|
|
141
|
+
throw new Error("signals discover: --max-searches must be a positive integer.");
|
|
142
|
+
if (!Number.isInteger(options.maxAccounts) || options.maxAccounts < 1)
|
|
143
|
+
throw new Error("signals discover: --max-accounts must be a positive integer.");
|
|
144
|
+
if (!Number.isInteger(options.maxResults) || options.maxResults < 1)
|
|
145
|
+
throw new Error("signals discover: --max-results must be a positive integer.");
|
|
146
|
+
if (!Number.isFinite(options.maxUsd) || options.maxUsd <= 0)
|
|
147
|
+
throw new Error("signals discover: --max-usd must be greater than zero.");
|
|
148
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
149
|
+
const apiBaseUrl = options.apiBaseUrl ?? process.env.EXA_API_BASE_URL ?? "https://api.exa.ai";
|
|
150
|
+
const hypotheses = triggerHypothesesForIcp(options.icp).filter((h) => !h.preferredSources?.length || h.preferredSources.includes("job"));
|
|
151
|
+
if (!hypotheses.length)
|
|
152
|
+
throw new Error("signals discover: no trigger hypothesis allows the currently supported `job` source.");
|
|
153
|
+
const now = options.now ?? new Date();
|
|
154
|
+
const config = options.config ?? DEFAULT_SIGNALS_CONFIG;
|
|
155
|
+
const warnings = [];
|
|
156
|
+
let searchesUsed = 0;
|
|
157
|
+
let costUsd = 0;
|
|
158
|
+
let rawResults = 0;
|
|
159
|
+
const found = [];
|
|
160
|
+
const seenUrls = new Set();
|
|
161
|
+
const canSearch = () => searchesUsed < options.maxSearches && costUsd < options.maxUsd;
|
|
162
|
+
for (const hypothesis of hypotheses) {
|
|
163
|
+
if (!canSearch() || rawResults >= options.maxResults)
|
|
164
|
+
break;
|
|
165
|
+
const remaining = options.maxResults - rawResults;
|
|
166
|
+
const response = await exaSearch({ apiKey: options.apiKey, fetch: fetchImpl, apiBaseUrl, body: {
|
|
167
|
+
query: exaQueryForHypothesis(hypothesis),
|
|
168
|
+
includeDomains: DEFAULT_DISCOVERY_ATS_DOMAINS,
|
|
169
|
+
startCrawlDate: options.since.toISOString(),
|
|
170
|
+
numResults: Math.min(25, remaining),
|
|
171
|
+
contents: { highlights: { maxCharacters: 1200 } },
|
|
172
|
+
} });
|
|
173
|
+
searchesUsed += 1;
|
|
174
|
+
costUsd += finiteCost(response);
|
|
175
|
+
const rows = resultRows(response);
|
|
176
|
+
rawResults += rows.length;
|
|
177
|
+
for (const row of rows) {
|
|
178
|
+
const title = typeof row.title === "string" ? row.title.trim() : "";
|
|
179
|
+
const sourceUrl = typeof row.url === "string" ? row.url.trim() : "";
|
|
180
|
+
if (!title || !sourceUrl || seenUrls.has(sourceUrl) || !DEFAULT_DISCOVERY_ATS_DOMAINS.some((domain) => safeHostname(sourceUrl).endsWith(domain)))
|
|
181
|
+
continue;
|
|
182
|
+
const identity = parseJobIdentity(title, sourceUrl);
|
|
183
|
+
if (!identity)
|
|
184
|
+
continue;
|
|
185
|
+
const quote = firstQuote(row, title);
|
|
186
|
+
if (!evidenceMatches(`${title} ${quote}`, hypothesis))
|
|
187
|
+
continue;
|
|
188
|
+
seenUrls.add(sourceUrl);
|
|
189
|
+
found.push({ hypothesisId: hypothesis.id, hypothesisLabel: hypothesis.label, companyName: identity.companyName,
|
|
190
|
+
jobTitle: identity.jobTitle, sourceUrl, quote,
|
|
191
|
+
...(typeof row.publishedDate === "string" && Number.isFinite(Date.parse(row.publishedDate)) ? { publishedAt: new Date(row.publishedDate).toISOString() } : {}) });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const companyNames = [...new Set(found.map((item) => item.companyName))].slice(0, options.maxAccounts);
|
|
195
|
+
const domainByCompany = new Map();
|
|
196
|
+
for (const companyName of companyNames) {
|
|
197
|
+
if (!canSearch())
|
|
198
|
+
break;
|
|
199
|
+
const response = await exaSearch({ apiKey: options.apiKey, fetch: fetchImpl, apiBaseUrl, body: {
|
|
200
|
+
query: `${companyName} official company website`, category: "company", numResults: 3,
|
|
201
|
+
} });
|
|
202
|
+
searchesUsed += 1;
|
|
203
|
+
costUsd += finiteCost(response);
|
|
204
|
+
const match = resultRows(response).find((row) => {
|
|
205
|
+
const url = typeof row.url === "string" ? row.url : "";
|
|
206
|
+
const domain = safeHostname(url);
|
|
207
|
+
return domain && !isExcludedDomain(domain) && companyMatches(companyName, typeof row.title === "string" ? row.title : "", url);
|
|
208
|
+
});
|
|
209
|
+
if (match && typeof match.url === "string")
|
|
210
|
+
domainByCompany.set(companyName, safeHostname(match.url));
|
|
211
|
+
}
|
|
212
|
+
for (const item of found)
|
|
213
|
+
item.accountDomain = domainByCompany.get(item.companyName);
|
|
214
|
+
const resolved = found.filter((item) => item.accountDomain).slice(0, options.maxAccounts);
|
|
215
|
+
if (searchesUsed >= options.maxSearches && (hypotheses.length > 1 || domainByCompany.size < companyNames.length)) {
|
|
216
|
+
warnings.push(`Search limit reached (${options.maxSearches}); some evidence or company domains were not resolved.`);
|
|
217
|
+
}
|
|
218
|
+
if (costUsd >= options.maxUsd)
|
|
219
|
+
warnings.push(`Reported Exa cost reached the $${options.maxUsd.toFixed(2)} budget; no further requests were made.`);
|
|
220
|
+
const nowIso = now.toISOString();
|
|
221
|
+
const signals = resolved.map((item) => {
|
|
222
|
+
const base = { accountDomain: item.accountDomain, bucket: "job", trigger: `evidence: ${item.hypothesisLabel} — ${item.jobTitle}` };
|
|
223
|
+
return { ...base, id: signalId(base), quote: item.quote, sourceUrl: item.sourceUrl, firstSeen: nowIso,
|
|
224
|
+
weight: signalWeight({ bucketWeight: config.buckets.job.weight, firstSeen: nowIso, now,
|
|
225
|
+
windowDays: config.dedupWindowDays, repostedRoleBoost: config.repostedRoleBoost }),
|
|
226
|
+
source: "exa", judgedBy: null, roleKey: item.sourceUrl, hypothesisId: item.hypothesisId,
|
|
227
|
+
companyName: item.companyName, ...(item.publishedAt ? { publishedAt: item.publishedAt } : {}) };
|
|
228
|
+
});
|
|
229
|
+
const roundedCost = Math.round(costUsd * 1_000_000) / 1_000_000;
|
|
230
|
+
return { summary: { provider: "exa", readOnly: true, searchesUsed, searchLimit: options.maxSearches,
|
|
231
|
+
costUsd: roundedCost, costLimitUsd: options.maxUsd, rawResults, matchedEvidence: found.length,
|
|
232
|
+
resolvedAccounts: new Set(signals.map((s) => s.accountDomain)).size,
|
|
233
|
+
unresolvedAccounts: new Set(found.filter((item) => !item.accountDomain).map((item) => item.companyName)).size, warnings },
|
|
234
|
+
candidates: found, signals };
|
|
235
|
+
}
|
package/dist/signals.d.ts
CHANGED
|
@@ -47,6 +47,10 @@ export type Signal = {
|
|
|
47
47
|
* id under the same title) from one continuously-open req. job-bucket only.
|
|
48
48
|
*/
|
|
49
49
|
roleKey?: string;
|
|
50
|
+
/** Optional provenance carried by evidence-first discovery. */
|
|
51
|
+
hypothesisId?: string;
|
|
52
|
+
companyName?: string;
|
|
53
|
+
publishedAt?: string;
|
|
50
54
|
};
|
|
51
55
|
export type SignalOutcomeResult = "replied" | "meeting" | "bounced" | "no_reply";
|
|
52
56
|
export type SignalOutcome = {
|
package/docs/api.md
CHANGED
|
@@ -126,7 +126,7 @@ Four flags name "where data comes from / goes to"; they are not interchangeable:
|
|
|
126
126
|
| Flag | Meaning | Values |
|
|
127
127
|
|---|---|---|
|
|
128
128
|
| `--provider` | The CRM the data lives in — the system a snapshot is read from and an approved plan is applied to. | `hubspot`, `salesforce` (plus `stripe`, read-only) |
|
|
129
|
-
| `--source` | The external data source feeding a verb: an enrichment/discovery vendor on `enrich`/`tam` (`--source apollo`, `acquire --source pipe0`), a staged-ingest label on `enrich ingest`,
|
|
129
|
+
| `--source` | The external data source feeding a verb: an enrichment/discovery vendor on `enrich`/`tam` (`--source apollo`, `acquire --source pipe0`), a staged-ingest label on `enrich ingest`, on `signals fetch` which no-auth ATS board adapters to scan, or `exa` on `signals discover`. | `apollo`, `clay`, `pipe0`, `explorium`, `theirstack`, `linkedin` (HeyReach), `exa`; `greenhouse` / `lever` / `ashby` on `signals fetch` |
|
|
130
130
|
| `--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` |
|
|
131
131
|
| `--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` |
|
|
132
132
|
|
|
@@ -136,7 +136,7 @@ Additive; every legacy flag keeps working:
|
|
|
136
136
|
|
|
137
137
|
- `--dry-run` — accepted on the plan-spine verbs (`audit`, `suggest`,
|
|
138
138
|
`bulk-update`, `dedupe`, `reassign`, `fix`, `call plan`,
|
|
139
|
-
`enrich append`/`refresh`/`acquire`, `signals fetch`, `icp judge`, `draft`,
|
|
139
|
+
`enrich append`/`refresh`/`acquire`, `signals fetch`/`discover`, `icp judge`, `draft`,
|
|
140
140
|
`tam status`, `market overlay`). These verbs are already preview-by-default
|
|
141
141
|
(plans are dry-run; nothing writes to a CRM outside approve → apply), so the
|
|
142
142
|
flag asserts that default rather than changing it: it additionally suppresses
|
package/llms.txt
CHANGED
|
@@ -190,7 +190,10 @@ file/serpapi-news/hubspot-forms with secrets through the credential ladder; and
|
|
|
190
190
|
push platforms via the webhook spool — `--connector file` reads the conventional
|
|
191
191
|
landing zone `<home>/signals/spool/*.jsonl`, see docs/signal-spool-format.md),
|
|
192
192
|
keyed by **account domain**, ranked by learned `weights`; it writes NOTHING to
|
|
193
|
-
the CRM. `
|
|
193
|
+
the CRM. `signals discover` starts from editable ICP behavioral hypotheses,
|
|
194
|
+
searches canonical public ATS evidence through Exa with explicit request and
|
|
195
|
+
reported-cost limits, resolves official employer domains, and writes NOTHING
|
|
196
|
+
unless `--save` persists the local signal ledger. `icp interview|set|show`
|
|
194
197
|
builds `icp.json`; `icp judge` ranks fresh signals into send/nurture/skip — pass
|
|
195
198
|
a snapshot (`--provider`/`--input`/`--demo`) and each decision resolves its CRM
|
|
196
199
|
target: `accountId`, the best `contact {id,email,title}`, and `contacts[]` (all
|
|
@@ -216,7 +219,7 @@ surfaced at every hop, so the loop is contact-coherent end to end — see the
|
|
|
216
219
|
`fullstackgtm schedule` is the horizontal scheduler — no feature namespace
|
|
217
220
|
owns cron logic. `add "<command>" --cron "<expr>"` validates the command
|
|
218
221
|
against the read/plan-side allowlist (audit, snapshot, enrich append|refresh,
|
|
219
|
-
enrich acquire --save, market capture|refresh, signals fetch, icp judge|eval,
|
|
222
|
+
enrich acquire --save, market capture|refresh, signals fetch|discover, icp judge|eval,
|
|
220
223
|
draft, suggest, report, doctor) and the 5-field cron
|
|
221
224
|
expression, but touches no system timer; `install` materializes enabled
|
|
222
225
|
entries into the platform timer (`--timer` defaults launchd on macOS,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fullstackgtm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.55.1",
|
|
4
4
|
"description": "Open-source agentic GTM ops framework: canonical GTM data model, pluggable deterministic audits, reviewable dry-run patch plans, approval-gated write-back with conflict detection, and cross-system entity resolution. HubSpot, Salesforce, and Stripe connectors included.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Full Stack GTM LLC <ryan@fullstackgtm.com> (https://fullstackgtm.com)",
|
|
@@ -64,7 +64,7 @@ credentials AND stored plans per client org.
|
|
|
64
64
|
| `call parse\|score\|link\|plan` | Transcripts → evidence-quoted insights, rubric scorecards, deal linking, governed next-step writes |
|
|
65
65
|
| `enrich append\|refresh\|ingest\|status` | Governed enrichment (Apollo pull / Clay ingest), fill-blanks-only plans |
|
|
66
66
|
| `enrich acquire [--source explorium\|pipe0\|linkedin] [--max <new>] [--scan-limit <raw>]` | Net-new ICP-targeted lead gen: independent per-provider/list/query checkpoints traverse full audiences instead of rereading page one; paired CLIs CAS-sync opaque, non-PII continuation to the hosted org; resolve-first deduped `create_record` plans, meter-capped and owner-stamped; `enrich status --runs` exposes funnel + continuation; never auto-writes |
|
|
67
|
-
| `signals fetch\|list\|outcome\|weights` | Detect-side timing layer: capture fresh buying triggers into a profile-scoped ledger (free Greenhouse/Lever/Ashby
|
|
67
|
+
| `signals fetch\|discover\|list\|outcome\|weights` | Detect-side timing layer: capture fresh buying triggers into a profile-scoped ledger (free Greenhouse/Lever/Ashby watchlist scans; Exa-backed ICP behavioral evidence discovery; `--from` staged files; `--connector file\|serpapi-news\|hubspot-forms` to pull connected platforms / read the webhook spool `<home>/signals/spool`, secrets via the credential ladder), ranked by learned weights. Writes NOTHING to the CRM; `outcome --account <domain> --contact <id> --result …` re-weights which triggers earn a touch |
|
|
68
68
|
| `icp interview\|set\|show\|judge\|eval` | Build the ICP, then `judge` ranks fresh signals into send/nurture/skip — pass a snapshot (`--provider`/`--input`) and each decision resolves its CRM target (`accountId` + the `contact`/`contacts` to reach). `eval` is the calibration gate (exit 2 below the bar) |
|
|
69
69
|
| `draft [--from-judge latest] [--channel email\|linkedin\|task]` | One trigger-grounded opener per hot account → a governed `create_task` targeting the resolved `contact.id` (or `accountId`); rejects a domain-only decision ("acquire it first"). **Never sends** — after `plans approve`, `apply --provider <crm>` logs it as a CRM task, or `apply --channel outbox` renders it to `<home>/signals/outbox/<channel>.jsonl` for a downstream sender to drain; the CLI transmits nothing |
|
|
70
70
|
| `market init\|capture\|classify\|worksheet\|observe\|fronts\|axes\|overlay\|scale\|report\|refresh` | Competitive category map; evidence quotes verified verbatim against stored captures |
|
package/src/cli/auth.ts
CHANGED
|
@@ -399,7 +399,7 @@ export async function login(args: string[]) {
|
|
|
399
399
|
console.log(`Stored Clay Public API key in ${credentialsPath()}. Clay connector commands resolve it automatically.`);
|
|
400
400
|
return;
|
|
401
401
|
}
|
|
402
|
-
if (provider === "pipe0" || provider === "explorium" || provider === "heyreach" || provider === "theirstack") {
|
|
402
|
+
if (provider === "pipe0" || provider === "explorium" || provider === "heyreach" || provider === "theirstack" || provider === "exa") {
|
|
403
403
|
rejectArgvSecret(args, "--token", "--key", "--api-key");
|
|
404
404
|
const key = await readSecret(`${provider} API key`);
|
|
405
405
|
if (!key) throw new Error(`No ${provider} key provided.`);
|
|
@@ -408,13 +408,14 @@ export async function login(args: string[]) {
|
|
|
408
408
|
const stamp = new Date().toISOString();
|
|
409
409
|
storeCredential(provider, { kind: "api_key", accessToken: key, createdAt: stamp, updatedAt: stamp });
|
|
410
410
|
const usedBy =
|
|
411
|
-
provider === "theirstack" ? "`fullstackgtm tam estimate --source theirstack`" :
|
|
411
|
+
provider === "theirstack" ? "`fullstackgtm tam estimate --source theirstack`" :
|
|
412
|
+
provider === "exa" ? "`fullstackgtm signals discover`" : "`fullstackgtm enrich acquire`";
|
|
412
413
|
console.log(`Stored ${provider} API key in ${credentialsPath()}. ${usedBy} uses it automatically (validated on first pull).`);
|
|
413
414
|
return;
|
|
414
415
|
}
|
|
415
416
|
if (provider !== "hubspot") {
|
|
416
417
|
throw new Error(
|
|
417
|
-
"login supports: hubspot, salesforce, stripe, anthropic, openai, openrouter, apollo, clay, pipe0, explorium, heyreach, theirstack, or --via <hosted url>. Usage: fullstackgtm login <provider> | fullstackgtm login --via https://gtm.example.com",
|
|
418
|
+
"login supports: hubspot, salesforce, stripe, anthropic, openai, openrouter, apollo, clay, pipe0, explorium, heyreach, theirstack, exa, or --via <hosted url>. Usage: fullstackgtm login <provider> | fullstackgtm login --via https://gtm.example.com",
|
|
418
419
|
);
|
|
419
420
|
}
|
|
420
421
|
const now = new Date().toISOString();
|
package/src/cli/enrich.ts
CHANGED
|
@@ -25,7 +25,7 @@ import { isSpoolPath, readSpoolPath } from "../spoolFiles.ts";
|
|
|
25
25
|
import { isOptionValue, loadIcp, numericOption, option, readSnapshot, saveRequested } from "./shared.ts";
|
|
26
26
|
import { providerKey } from "./tam.ts";
|
|
27
27
|
import { unknownSubcommandError } from "./suggest.ts";
|
|
28
|
-
import { box, colorEnabled, createProgressRenderer, createStatusLine, formatBar, formatDuration, paint, truncateToWidth, type Paint } from "./ui.ts";
|
|
28
|
+
import { box, colorEnabled, createProgressRenderer, createStatusLine, formatBar, formatDuration, paint, scoreColor, truncateToWidth, type Paint } from "./ui.ts";
|
|
29
29
|
import { compactPlan, verbosePlanRequested } from "./planOutput.ts";
|
|
30
30
|
import type { AcquireBudget } from "../acquireMeter.ts";
|
|
31
31
|
|
|
@@ -946,12 +946,14 @@ function renderAcquireLeadCards(result: ReturnType<typeof buildAcquirePlan>): st
|
|
|
946
946
|
let fit: number | undefined;
|
|
947
947
|
const item = operation.evidenceIds?.[0] ? evidence.get(operation.evidenceIds[0]) : undefined;
|
|
948
948
|
try { fit = Number((JSON.parse(item?.text ?? "{}") as { fitScore?: unknown }).fitScore); } catch { /* malformed evidence remains display-only */ }
|
|
949
|
+
const fitPercent = Number.isFinite(fit) ? Math.round((fit ?? 0) * 100) : undefined;
|
|
949
950
|
const lines = [
|
|
950
|
-
truncateToWidth(name, 84),
|
|
951
|
-
truncateToWidth(`${title} · ${company}`, 84),
|
|
952
|
-
truncateToWidth([payload.associateCompanyDomain, props.hs_linkedin_url].filter(Boolean).join(" · "), 84),
|
|
951
|
+
p.bold(truncateToWidth(name, 84)),
|
|
952
|
+
p.cyan(truncateToWidth(`${title} · ${company}`, 84)),
|
|
953
|
+
p.blue(truncateToWidth([payload.associateCompanyDomain, props.hs_linkedin_url].filter(Boolean).join(" · "), 84)),
|
|
953
954
|
];
|
|
954
|
-
|
|
955
|
+
const fitLabel = fitPercent === undefined ? "" : ` · ${scoreColor(fitPercent, p)}% fit`;
|
|
956
|
+
return box(lines, p, `Lead ${index + 1}/${result.plan.operations.length}${fitLabel}`).join("\n");
|
|
955
957
|
}).join("\n");
|
|
956
958
|
}
|
|
957
959
|
|
package/src/cli/help.ts
CHANGED
|
@@ -21,9 +21,9 @@ Usage:
|
|
|
21
21
|
fullstackgtm login stripe [--no-validate]
|
|
22
22
|
fullstackgtm login anthropic | openai store an LLM API key for call parse/score
|
|
23
23
|
fullstackgtm login apollo | clay store a data-provider Public API key
|
|
24
|
-
fullstackgtm login pipe0 | explorium | theirstack store a discovery-provider key
|
|
24
|
+
fullstackgtm login pipe0 | explorium | theirstack | exa store a discovery-provider key
|
|
25
25
|
fullstackgtm login heyreach store a HeyReach key for enrich acquire --source linkedin
|
|
26
|
-
fullstackgtm logout <hubspot|salesforce|stripe|anthropic|openai|apollo|clay|pipe0|explorium|heyreach|broker>
|
|
26
|
+
fullstackgtm logout <hubspot|salesforce|stripe|anthropic|openai|apollo|clay|pipe0|explorium|theirstack|exa|heyreach|broker>
|
|
27
27
|
|
|
28
28
|
Secrets (tokens, client secrets) are NEVER passed as flags — they leak via
|
|
29
29
|
the process list and shell history. Pipe them on stdin or enter them at the
|
|
@@ -105,10 +105,12 @@ Usage:
|
|
|
105
105
|
in the same plan (freemail domains never used;
|
|
106
106
|
--skip-unmatched = report-only). HubSpot-only for now.
|
|
107
107
|
fullstackgtm signals fetch [--bucket job,…] [--source greenhouse,lever,ashby] [--watchlist <path|crm:seg>] [--keywords …] [--from <file.json|spool.jsonl|spool-dir>] [--save]
|
|
108
|
+
fullstackgtm signals discover [--icp <path>] [--source exa] [--since 30d] [--max-accounts 10] [--max-results 50] [--max-searches 12] [--max-usd 0.25] [--save]
|
|
108
109
|
fullstackgtm signals list [--since 7d] [--bucket b] [--account d] [--unjudged]
|
|
109
110
|
fullstackgtm signals outcome --account <d> [--touch <id>] --result replied|meeting|bounced|no_reply
|
|
110
111
|
fullstackgtm signals weights [--explain]
|
|
111
|
-
detect fresh buying triggers (
|
|
112
|
+
detect fresh buying triggers (ICP-driven evidence discovery,
|
|
113
|
+
ATS hiring scans, and staged
|
|
112
114
|
funding/company/social ingest), rank them, persist a local
|
|
113
115
|
signal ledger. READ-ONLY re: CRM — fetch NEVER emits a plan;
|
|
114
116
|
--save persists only the signal ledger. outcome feeds the
|
|
@@ -324,7 +326,7 @@ export const HELP: Record<string, HelpEntry> = {
|
|
|
324
326
|
logout: {
|
|
325
327
|
summary: "remove stored credentials for a provider",
|
|
326
328
|
phase: "Setup",
|
|
327
|
-
synopsis: ["fullstackgtm logout <hubspot|salesforce|stripe|anthropic|openai|apollo|clay|pipe0|explorium|broker>"],
|
|
329
|
+
synopsis: ["fullstackgtm logout <hubspot|salesforce|stripe|anthropic|openai|apollo|clay|pipe0|explorium|theirstack|exa|broker>"],
|
|
328
330
|
seeAlso: ["login", "doctor"],
|
|
329
331
|
},
|
|
330
332
|
doctor: {
|
|
@@ -622,12 +624,13 @@ export const HELP: Record<string, HelpEntry> = {
|
|
|
622
624
|
phase: "Detect",
|
|
623
625
|
synopsis: [
|
|
624
626
|
"fullstackgtm signals fetch [--bucket job,…] [--source greenhouse,lever,ashby] [--connector file,serpapi-news,hubspot-forms] [--connector-opt k=v] [--watchlist <path|crm:seg>] [--keywords …] [--from <file.json|spool.jsonl|spool-dir>] [--save]",
|
|
627
|
+
"fullstackgtm signals discover [--icp <path>] [--source exa] [--since 30d] [--max-accounts 10] [--max-results 50] [--max-searches 12] [--max-usd 0.25] [--save]",
|
|
625
628
|
"fullstackgtm signals list [--since 7d] [--bucket b] [--account d] [--unjudged]",
|
|
626
629
|
"fullstackgtm signals outcome --account <d> [--touch <id>] --result replied|meeting|bounced|no_reply",
|
|
627
630
|
"fullstackgtm signals weights [--explain]",
|
|
628
631
|
],
|
|
629
632
|
detail:
|
|
630
|
-
"Read-only re: CRM — `fetch` NEVER
|
|
633
|
+
"Read-only re: CRM — `fetch` and `discover` NEVER emit a patch plan; `--save` persists only the local signal ledger. `discover` matches ICP behavioral hypotheses against canonical public ATS evidence through Exa, with explicit request and reported-cost budgets. Secrets resolve through env/login, never argv. `outcome` feeds the learned per-bucket `weights`.",
|
|
631
634
|
seeAlso: ["icp", "draft"],
|
|
632
635
|
},
|
|
633
636
|
icp: {
|
|
@@ -699,13 +702,13 @@ export const COMMAND_FLAGS: Record<string, string[]> = {
|
|
|
699
702
|
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"],
|
|
700
703
|
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"],
|
|
701
704
|
icp: [...SOURCE_FLAGS, "--name", "--icp", "--domain", "--no-interactive", "--signals-from", "--with-history", "--prompt", "--min-score", "--from-judge", "--golden", "--against-outcomes", "--min-accuracy", "--model", "--label", "--save", "--verbose", "--json", "--out"],
|
|
702
|
-
signals: ["--bucket", "--source", "--connector", "--connector-opt", "--watchlist", "--keywords", "--from", "--label", "--since", "--account", "--contact", "--unjudged", "--touch", "--result", "--save", "--verbose", "--json", "--explain"],
|
|
705
|
+
signals: ["--bucket", "--source", "--connector", "--connector-opt", "--watchlist", "--keywords", "--from", "--label", "--since", "--icp", "--max-accounts", "--max-results", "--max-searches", "--max-usd", "--account", "--contact", "--unjudged", "--touch", "--result", "--save", "--verbose", "--json", "--explain"],
|
|
703
706
|
draft: ["--from-judge", "--min-score", "--prompt", "--model", "--channel", "--save", "--dry-run", "--verbose", "--json", "--out"],
|
|
704
707
|
schedule: ["--cron", "--label", "--provider", "--trigger", "--timer", "--runs", "--verbose", "--json"],
|
|
705
708
|
};
|
|
706
709
|
|
|
707
710
|
export const FLAGS_WITH_VALUES = new Set([
|
|
708
|
-
"--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",
|
|
711
|
+
"--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-accounts", "--max-claims", "--max-credits", "--max-examples", "--max-operations", "--max-results", "--max-searches", "--max-usd", "--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",
|
|
709
712
|
]);
|
|
710
713
|
|
|
711
714
|
// Lifecycle-grouped front door. One line per verb, organized by the
|
package/src/cli/shared.ts
CHANGED
|
@@ -336,6 +336,35 @@ export function loadIcp(args: string[]): Icp | undefined {
|
|
|
336
336
|
* e.g. `echo "$TOKEN" | fullstackgtm login hubspot`
|
|
337
337
|
* - interactive: prompted on the TTY with the input muted
|
|
338
338
|
*/
|
|
339
|
+
export function createSecretMaskRenderer(options: {
|
|
340
|
+
label: string;
|
|
341
|
+
lineLength: () => number;
|
|
342
|
+
write: (value: string) => void;
|
|
343
|
+
}) {
|
|
344
|
+
let muted = false;
|
|
345
|
+
let renderQueued = false;
|
|
346
|
+
let settled = false;
|
|
347
|
+
return {
|
|
348
|
+
mute() { muted = true; },
|
|
349
|
+
settle() { settled = true; },
|
|
350
|
+
write(value: string) {
|
|
351
|
+
if (!muted) { options.write(value); return; }
|
|
352
|
+
if (settled || renderQueued) return;
|
|
353
|
+
renderQueued = true;
|
|
354
|
+
queueMicrotask(() => {
|
|
355
|
+
renderQueued = false;
|
|
356
|
+
// readline clears `line` during close. Submission settles this renderer
|
|
357
|
+
// first so an Enter-triggered repaint cannot redraw an empty prompt
|
|
358
|
+
// after the final mask/newline ("0 characters received").
|
|
359
|
+
if (settled) return;
|
|
360
|
+
const length = options.lineLength();
|
|
361
|
+
const mask = `${"•".repeat(Math.min(length, 12))}${length > 12 ? "…" : ""}`;
|
|
362
|
+
options.write(`\r\u001b[2K${options.label}: ${mask} (${length} character${length === 1 ? "" : "s"} received)`);
|
|
363
|
+
});
|
|
364
|
+
},
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
339
368
|
export async function readSecret(label: string): Promise<string> {
|
|
340
369
|
if (!process.stdin.isTTY) {
|
|
341
370
|
const chunks: Buffer[] = [];
|
|
@@ -362,26 +391,20 @@ export async function readSecret(label: string): Promise<string> {
|
|
|
362
391
|
output: process.stderr,
|
|
363
392
|
terminal: true,
|
|
364
393
|
});
|
|
365
|
-
let muted = false;
|
|
366
394
|
const mutable = rl as unknown as { _writeToOutput?: (value: string) => void; line?: string };
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
renderQueued = false;
|
|
374
|
-
const length = mutable.line?.length ?? 0;
|
|
375
|
-
const mask = `${"•".repeat(Math.min(length, 12))}${length > 12 ? "…" : ""}`;
|
|
376
|
-
process.stderr.write(`\r\u001b[2K${label}: ${mask} (${length} character${length === 1 ? "" : "s"} received)`);
|
|
377
|
-
});
|
|
378
|
-
};
|
|
395
|
+
const renderer = createSecretMaskRenderer({
|
|
396
|
+
label,
|
|
397
|
+
lineLength: () => mutable.line?.length ?? 0,
|
|
398
|
+
write: (value) => process.stderr.write(value),
|
|
399
|
+
});
|
|
400
|
+
mutable._writeToOutput = (value: string) => renderer.write(value);
|
|
379
401
|
rl.question(`${label}: `, (answer) => {
|
|
402
|
+
renderer.settle();
|
|
380
403
|
rl.close();
|
|
381
404
|
process.stderr.write("\n");
|
|
382
405
|
resolveSecret(answer.trim());
|
|
383
406
|
});
|
|
384
|
-
|
|
407
|
+
renderer.mute();
|
|
385
408
|
});
|
|
386
409
|
}
|
|
387
410
|
|