fullstackgtm 0.47.0 → 0.49.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 +76 -6
- package/CONTRIBUTING.md +23 -4
- package/DATA-FLOWS.md +7 -2
- package/INSTALL_FOR_AGENTS.md +15 -6
- package/README.md +28 -9
- package/SECURITY.md +27 -3
- package/dist/audit.js +7 -0
- package/dist/backfill.js +2 -16
- package/dist/cli/audit.js +10 -7
- package/dist/cli/auth.js +8 -4
- package/dist/cli/fix.d.ts +3 -0
- package/dist/cli/fix.js +90 -8
- package/dist/cli/help.d.ts +6 -0
- package/dist/cli/help.js +81 -3
- package/dist/cli/market.js +180 -2
- package/dist/cli/plans.js +56 -5
- package/dist/cli/suggest.d.ts +2 -1
- package/dist/cli/suggest.js +49 -3
- package/dist/cli/ui.js +2 -0
- package/dist/cli.js +41 -16
- package/dist/config.d.ts +13 -1
- package/dist/config.js +23 -2
- package/dist/connectors/hubspot.d.ts +2 -1
- package/dist/connectors/prospectSources.js +4 -11
- package/dist/connectors/salesforce.d.ts +2 -1
- package/dist/connectors/salesforce.js +12 -5
- package/dist/connectors/salesforceAuth.d.ts +9 -0
- package/dist/connectors/salesforceAuth.js +47 -5
- package/dist/connectors/signalSources.js +2 -11
- package/dist/connectors/theirstack.d.ts +0 -19
- package/dist/connectors/theirstack.js +3 -10
- package/dist/credentials.js +36 -26
- package/dist/format.js +31 -5
- package/dist/freeEmailDomains.d.ts +1 -0
- package/dist/freeEmailDomains.js +14 -0
- package/dist/hierarchy.d.ts +29 -0
- package/dist/hierarchy.js +164 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.js +8 -3
- package/dist/market.d.ts +1 -1
- package/dist/market.js +7 -127
- package/dist/marketClassify.d.ts +10 -0
- package/dist/marketClassify.js +52 -1
- package/dist/marketSourcing.js +7 -45
- package/dist/mcp.js +86 -130
- package/dist/planStore.d.ts +28 -1
- package/dist/planStore.js +195 -49
- package/dist/providerError.d.ts +21 -0
- package/dist/providerError.js +30 -0
- package/dist/publicHttp.d.ts +28 -0
- package/dist/publicHttp.js +143 -0
- package/dist/relationships.d.ts +39 -0
- package/dist/relationships.js +101 -0
- package/dist/route.d.ts +46 -0
- package/dist/route.js +228 -0
- package/dist/rules.d.ts +1 -0
- package/dist/rules.js +172 -12
- package/dist/secureFile.d.ts +15 -0
- package/dist/secureFile.js +164 -0
- package/dist/types.d.ts +16 -1
- package/docs/api.md +49 -5
- package/docs/architecture.md +18 -4
- package/llms.txt +7 -0
- package/package.json +5 -3
- package/skills/fullstackgtm/SKILL.md +9 -3
- package/src/audit.ts +7 -0
- package/src/backfill.ts +2 -17
- package/src/cli/audit.ts +10 -7
- package/src/cli/auth.ts +8 -4
- package/src/cli/fix.ts +96 -8
- package/src/cli/help.ts +88 -3
- package/src/cli/market.ts +181 -2
- package/src/cli/plans.ts +66 -5
- package/src/cli/suggest.ts +58 -4
- package/src/cli/ui.ts +1 -0
- package/src/cli.ts +39 -14
- package/src/config.ts +39 -1
- package/src/connectors/hubspot.ts +3 -1
- package/src/connectors/prospectSources.ts +4 -11
- package/src/connectors/salesforce.ts +15 -6
- package/src/connectors/salesforceAuth.ts +46 -6
- package/src/connectors/signalSources.ts +2 -12
- package/src/connectors/theirstack.ts +3 -10
- package/src/credentials.ts +47 -28
- package/src/format.ts +33 -5
- package/src/freeEmailDomains.ts +14 -0
- package/src/hierarchy.ts +185 -0
- package/src/index.ts +29 -0
- package/src/market.ts +7 -111
- package/src/marketClassify.ts +61 -1
- package/src/marketSourcing.ts +7 -43
- package/src/mcp.ts +115 -152
- package/src/planStore.ts +235 -57
- package/src/providerError.ts +37 -0
- package/src/publicHttp.ts +147 -0
- package/src/relationships.ts +115 -0
- package/src/route.ts +278 -0
- package/src/rules.ts +192 -12
- package/src/secureFile.ts +169 -0
- package/src/types.ts +18 -0
package/dist/credentials.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { chmodSync, existsSync, mkdirSync, readdirSync,
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readdirSync, unlinkSync, } from "node:fs";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { refreshHubspotToken } from "./connectors/hubspotAuth.js";
|
|
6
|
-
import { refreshSalesforceToken } from "./connectors/salesforceAuth.js";
|
|
6
|
+
import { refreshSalesforceToken, validateSalesforceOrigin } from "./connectors/salesforceAuth.js";
|
|
7
7
|
import { detectKeychainBackend } from "./keychain.js";
|
|
8
|
+
import { assertNoSymlinkComponents, readSecureRegularFile, UnsafeManagedPathError, writeSecureFileAtomic, } from "./secureFile.js";
|
|
8
9
|
/**
|
|
9
10
|
* Local CLI credential store: ~/.fullstackgtm/credentials.json (0600), or
|
|
10
11
|
* $FSGTM_HOME/credentials.json when set. Environment tokens always win over
|
|
@@ -80,7 +81,9 @@ export function ensureSecureHomeDir() {
|
|
|
80
81
|
// umask) only to directories it creates, and never to pre-existing ones.
|
|
81
82
|
const levels = dir === baseHomeDir() ? [dir] : [baseHomeDir(), join(baseHomeDir(), "profiles"), dir];
|
|
82
83
|
for (const level of levels) {
|
|
84
|
+
assertNoSymlinkComponents(level);
|
|
83
85
|
mkdirSync(level, { recursive: true, mode: 0o700 });
|
|
86
|
+
assertNoSymlinkComponents(level);
|
|
84
87
|
try {
|
|
85
88
|
chmodSync(level, 0o700);
|
|
86
89
|
}
|
|
@@ -92,13 +95,7 @@ export function ensureSecureHomeDir() {
|
|
|
92
95
|
}
|
|
93
96
|
/** Write a 0600 file under the home, enforcing the mode even on rewrite. */
|
|
94
97
|
export function writeSecureFile(path, contents) {
|
|
95
|
-
|
|
96
|
-
try {
|
|
97
|
-
chmodSync(path, 0o600);
|
|
98
|
-
}
|
|
99
|
-
catch {
|
|
100
|
-
// Non-POSIX filesystems ignore chmod.
|
|
101
|
-
}
|
|
98
|
+
writeSecureFileAtomic(path, contents);
|
|
102
99
|
}
|
|
103
100
|
/**
|
|
104
101
|
* The 0600/0700 guarantee was write-only: a credentials.json inherited at
|
|
@@ -107,18 +104,12 @@ export function writeSecureFile(path, contents) {
|
|
|
107
104
|
* mode on read too — re-tighten to 0600 and warn once — so a world-readable
|
|
108
105
|
* credential store can't sit there silently leaking the token to other users.
|
|
109
106
|
*/
|
|
110
|
-
function
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
"(it was readable or writable by other users).");
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
catch {
|
|
120
|
-
// Missing file or non-POSIX filesystem: nothing to enforce.
|
|
121
|
-
}
|
|
107
|
+
function readCredentialFile(path) {
|
|
108
|
+
return readSecureRegularFile(path, {
|
|
109
|
+
tightenMode: 0o600,
|
|
110
|
+
onModeTightened: (mode) => console.error(`fullstackgtm: tightened ${path} from ${mode.toString(8).padStart(3, "0")} to 600 ` +
|
|
111
|
+
"(it was readable or writable by other users)."),
|
|
112
|
+
});
|
|
122
113
|
}
|
|
123
114
|
/**
|
|
124
115
|
* Persistence backend for the credential blob: the OS keychain when
|
|
@@ -144,7 +135,7 @@ function migratePlaintextToKeychain(keychain) {
|
|
|
144
135
|
if (!existsSync(credentialsPath()))
|
|
145
136
|
return;
|
|
146
137
|
try {
|
|
147
|
-
const fileParsed = JSON.parse(
|
|
138
|
+
const fileParsed = JSON.parse(readCredentialFile(credentialsPath()));
|
|
148
139
|
if (fileParsed && fileParsed.version === 1 && fileParsed.providers) {
|
|
149
140
|
const current = keychain.backend.get(keychain.account);
|
|
150
141
|
const existing = current ? (JSON.parse(current).providers ?? {}) : {};
|
|
@@ -164,7 +155,7 @@ function readFile() {
|
|
|
164
155
|
if (keychain)
|
|
165
156
|
migratePlaintextToKeychain(keychain);
|
|
166
157
|
try {
|
|
167
|
-
const raw = keychain ? keychain.backend.get(keychain.account) : (
|
|
158
|
+
const raw = keychain ? keychain.backend.get(keychain.account) : readCredentialFile(credentialsPath());
|
|
168
159
|
if (raw) {
|
|
169
160
|
const parsed = JSON.parse(raw);
|
|
170
161
|
if (parsed && typeof parsed === "object" && parsed.version === 1 && parsed.providers) {
|
|
@@ -172,7 +163,9 @@ function readFile() {
|
|
|
172
163
|
}
|
|
173
164
|
}
|
|
174
165
|
}
|
|
175
|
-
catch {
|
|
166
|
+
catch (error) {
|
|
167
|
+
if (error instanceof UnsafeManagedPathError)
|
|
168
|
+
throw error;
|
|
176
169
|
// Missing or unreadable store falls through to an empty one.
|
|
177
170
|
}
|
|
178
171
|
return { version: 1, providers: {} };
|
|
@@ -192,6 +185,18 @@ export function getCredential(provider) {
|
|
|
192
185
|
return readFile().providers[provider] ?? null;
|
|
193
186
|
}
|
|
194
187
|
export function storeCredential(provider, credential) {
|
|
188
|
+
if (provider === "salesforce") {
|
|
189
|
+
if (!credential.instanceUrl) {
|
|
190
|
+
throw new Error("Salesforce credentials require an instance URL.");
|
|
191
|
+
}
|
|
192
|
+
credential = {
|
|
193
|
+
...credential,
|
|
194
|
+
instanceUrl: validateSalesforceOrigin(credential.instanceUrl, "Salesforce credential instance URL"),
|
|
195
|
+
...(credential.loginUrl
|
|
196
|
+
? { loginUrl: validateSalesforceOrigin(credential.loginUrl, "Salesforce credential login URL") }
|
|
197
|
+
: {}),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
195
200
|
const file = readFile();
|
|
196
201
|
file.providers[provider] = credential;
|
|
197
202
|
persist(file);
|
|
@@ -248,11 +253,15 @@ export async function resolveSalesforceConnection(options = {}) {
|
|
|
248
253
|
if (!credential.instanceUrl) {
|
|
249
254
|
throw new Error("Stored Salesforce credential has no instance URL. Run `fullstackgtm login salesforce` again.");
|
|
250
255
|
}
|
|
256
|
+
const instanceUrl = validateSalesforceOrigin(credential.instanceUrl, "Stored Salesforce credential instance URL");
|
|
257
|
+
if (credential.loginUrl) {
|
|
258
|
+
validateSalesforceOrigin(credential.loginUrl, "Stored Salesforce credential login URL");
|
|
259
|
+
}
|
|
251
260
|
const needsRefresh = credential.kind === "oauth" &&
|
|
252
261
|
credential.expiresAt !== undefined &&
|
|
253
262
|
Date.now() > credential.expiresAt - REFRESH_SKEW_MS;
|
|
254
263
|
if (!needsRefresh) {
|
|
255
|
-
return { accessToken: credential.accessToken, instanceUrl
|
|
264
|
+
return { accessToken: credential.accessToken, instanceUrl };
|
|
256
265
|
}
|
|
257
266
|
if (!credential.refreshToken || !credential.clientId) {
|
|
258
267
|
throw new Error("Stored Salesforce token is expired and cannot be refreshed. Run `fullstackgtm login salesforce` again.");
|
|
@@ -279,9 +288,10 @@ export async function resolveSalesforceConnection(options = {}) {
|
|
|
279
288
|
if (!minted.instanceUrl) {
|
|
280
289
|
throw new Error("The hosted deployment returned no Salesforce instance URL.");
|
|
281
290
|
}
|
|
291
|
+
const instanceUrl = validateSalesforceOrigin(minted.instanceUrl, "Hosted Salesforce credential instance URL");
|
|
282
292
|
return {
|
|
283
293
|
accessToken: minted.accessToken,
|
|
284
|
-
instanceUrl
|
|
294
|
+
instanceUrl,
|
|
285
295
|
fieldMappings: minted.fieldMappings,
|
|
286
296
|
};
|
|
287
297
|
}
|
package/dist/format.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
// `summary: true` renders
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// where you approve specific operations — keep every operation's detail.
|
|
1
|
+
// `summary: true` renders the compact audit view: header, findings summary,
|
|
2
|
+
// assumptions/open questions, and an operation count. Defaults to the full view
|
|
3
|
+
// so write-preview callers (bulk-update, fix, dedupe, plans show, MCP) — where
|
|
4
|
+
// you approve specific operations — keep every operation's detail.
|
|
6
5
|
export function patchPlanToMarkdown(plan, opts = {}) {
|
|
7
6
|
const lines = [
|
|
8
7
|
`# ${plan.title}`,
|
|
@@ -25,6 +24,7 @@ export function patchPlanToMarkdown(plan, opts = {}) {
|
|
|
25
24
|
});
|
|
26
25
|
lines.push("");
|
|
27
26
|
}
|
|
27
|
+
appendAssumptionsAndDecisionPoints(lines, plan);
|
|
28
28
|
if (opts.summary) {
|
|
29
29
|
const ops = plan.operations.length;
|
|
30
30
|
lines.push(ops === 0
|
|
@@ -96,6 +96,32 @@ export function formatPatchPlanRun(run) {
|
|
|
96
96
|
}
|
|
97
97
|
return `${lines.join("\n")}\n`;
|
|
98
98
|
}
|
|
99
|
+
function appendAssumptionsAndDecisionPoints(lines, plan) {
|
|
100
|
+
const assumptions = [...(plan.assumptions ?? [])].sort((left, right) => {
|
|
101
|
+
if (left.confidence === "guess" && right.confidence !== "guess")
|
|
102
|
+
return -1;
|
|
103
|
+
if (left.confidence !== "guess" && right.confidence === "guess")
|
|
104
|
+
return 1;
|
|
105
|
+
return 0;
|
|
106
|
+
});
|
|
107
|
+
const openQuestions = plan.openQuestions ?? [];
|
|
108
|
+
if (assumptions.length === 0 && openQuestions.length === 0)
|
|
109
|
+
return;
|
|
110
|
+
lines.push("## Assumptions & decision points", "");
|
|
111
|
+
for (const assumption of assumptions) {
|
|
112
|
+
const warning = assumption.confidence === "guess" ? "⚠️ " : "";
|
|
113
|
+
lines.push(`- ${warning}${assumption.text}${assumption.source ? ` _(source: ${assumption.source})_` : ""}`);
|
|
114
|
+
}
|
|
115
|
+
if (openQuestions.length > 0) {
|
|
116
|
+
if (assumptions.length > 0)
|
|
117
|
+
lines.push("");
|
|
118
|
+
lines.push("### Open questions", "");
|
|
119
|
+
for (const question of openQuestions) {
|
|
120
|
+
lines.push(`- ${question}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
lines.push("");
|
|
124
|
+
}
|
|
99
125
|
function formatValue(value) {
|
|
100
126
|
if (value === undefined || value === null || value === "")
|
|
101
127
|
return "unset";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const FREE_EMAIL_DOMAINS: Set<string>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Shared free-mail domains that should never be treated as company domains.
|
|
2
|
+
export const FREE_EMAIL_DOMAINS = new Set([
|
|
3
|
+
"gmail.com",
|
|
4
|
+
"googlemail.com",
|
|
5
|
+
"yahoo.com",
|
|
6
|
+
"outlook.com",
|
|
7
|
+
"hotmail.com",
|
|
8
|
+
"live.com",
|
|
9
|
+
"icloud.com",
|
|
10
|
+
"me.com",
|
|
11
|
+
"aol.com",
|
|
12
|
+
"proton.me",
|
|
13
|
+
"protonmail.com",
|
|
14
|
+
]);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { CanonicalGtmSnapshot } from "./types.ts";
|
|
2
|
+
export type AccountHierarchyNode = {
|
|
3
|
+
accountId: string;
|
|
4
|
+
name: string;
|
|
5
|
+
domain?: string;
|
|
6
|
+
parentAccountId?: string;
|
|
7
|
+
parentReason?: string;
|
|
8
|
+
children: AccountHierarchyNode[];
|
|
9
|
+
};
|
|
10
|
+
export type AccountHierarchyConflict = {
|
|
11
|
+
type: "duplicate_domain" | "ambiguous_parent" | "cycle";
|
|
12
|
+
accountIds: string[];
|
|
13
|
+
key: string;
|
|
14
|
+
detail: string;
|
|
15
|
+
};
|
|
16
|
+
export type AccountHierarchyReport = {
|
|
17
|
+
generatedAt: string;
|
|
18
|
+
roots: AccountHierarchyNode[];
|
|
19
|
+
orphanAccounts: string[];
|
|
20
|
+
conflicts: AccountHierarchyConflict[];
|
|
21
|
+
counts: {
|
|
22
|
+
accounts: number;
|
|
23
|
+
roots: number;
|
|
24
|
+
linkedChildren: number;
|
|
25
|
+
conflicts: number;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
export declare function buildAccountHierarchy(snapshot: CanonicalGtmSnapshot): AccountHierarchyReport;
|
|
29
|
+
export declare function accountHierarchyToMarkdown(report: AccountHierarchyReport): string;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { normalizeDomain } from "./merge.js";
|
|
2
|
+
function rawString(account, keys) {
|
|
3
|
+
if (!account.raw || typeof account.raw !== "object")
|
|
4
|
+
return undefined;
|
|
5
|
+
const raw = account.raw;
|
|
6
|
+
for (const key of keys) {
|
|
7
|
+
const value = raw[key];
|
|
8
|
+
if (typeof value === "string" && value.trim())
|
|
9
|
+
return value.trim();
|
|
10
|
+
if (value && typeof value === "object") {
|
|
11
|
+
const nested = value;
|
|
12
|
+
for (const nestedKey of ["id", "value", "name"]) {
|
|
13
|
+
if (typeof nested[nestedKey] === "string" && String(nested[nestedKey]).trim())
|
|
14
|
+
return String(nested[nestedKey]).trim();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
function parentHint(account) {
|
|
21
|
+
return rawString(account, ["parentAccountId", "parent_account_id", "ParentId", "parentCompanyId", "hs_parent_company_id"]);
|
|
22
|
+
}
|
|
23
|
+
function cloneNode(account) {
|
|
24
|
+
return { accountId: String(account.id), name: account.name, ...(account.domain ? { domain: account.domain } : {}), children: [] };
|
|
25
|
+
}
|
|
26
|
+
function looksLikePublicSuffix(domain) {
|
|
27
|
+
const parts = domain.split(".");
|
|
28
|
+
if (parts.length !== 2)
|
|
29
|
+
return false;
|
|
30
|
+
const [label, tld] = parts;
|
|
31
|
+
return tld.length === 2 && ["ac", "co", "com", "edu", "gov", "net", "org"].includes(label);
|
|
32
|
+
}
|
|
33
|
+
function domainParent(childDomain, domains) {
|
|
34
|
+
const parts = childDomain.split(".");
|
|
35
|
+
for (let i = 1; i < parts.length - 1; i++) {
|
|
36
|
+
const candidate = parts.slice(i).join(".");
|
|
37
|
+
if (candidate.split(".").length < 2 || looksLikePublicSuffix(candidate))
|
|
38
|
+
continue;
|
|
39
|
+
const unique = new Map((domains.get(candidate) ?? []).map((account) => [String(account.id), account]));
|
|
40
|
+
if (unique.size > 1)
|
|
41
|
+
return "ambiguous";
|
|
42
|
+
if (unique.size === 1)
|
|
43
|
+
return [...unique.values()][0];
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
function detectCycles(parentByChild) {
|
|
48
|
+
const cyclic = new Set();
|
|
49
|
+
const cycles = [];
|
|
50
|
+
const emitted = new Set();
|
|
51
|
+
for (const start of parentByChild.keys()) {
|
|
52
|
+
const path = [];
|
|
53
|
+
const indexById = new Map();
|
|
54
|
+
let cursor = start;
|
|
55
|
+
while (cursor && parentByChild.has(cursor)) {
|
|
56
|
+
const seenAt = indexById.get(cursor);
|
|
57
|
+
if (seenAt !== undefined) {
|
|
58
|
+
const cycle = path.slice(seenAt);
|
|
59
|
+
for (const id of cycle)
|
|
60
|
+
cyclic.add(id);
|
|
61
|
+
const key = [...cycle].sort().join("|");
|
|
62
|
+
if (!emitted.has(key)) {
|
|
63
|
+
emitted.add(key);
|
|
64
|
+
cycles.push(cycle);
|
|
65
|
+
}
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
indexById.set(cursor, path.length);
|
|
69
|
+
path.push(cursor);
|
|
70
|
+
cursor = parentByChild.get(cursor)?.parentId;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { accountIds: cyclic, cycles };
|
|
74
|
+
}
|
|
75
|
+
export function buildAccountHierarchy(snapshot) {
|
|
76
|
+
const byId = new Map(snapshot.accounts.map((account) => [String(account.id), account]));
|
|
77
|
+
const accounts = [...byId.values()];
|
|
78
|
+
const domains = new Map();
|
|
79
|
+
for (const account of accounts) {
|
|
80
|
+
const domain = normalizeDomain(account.domain);
|
|
81
|
+
if (domain)
|
|
82
|
+
domains.set(domain, [...(domains.get(domain) ?? []), account]);
|
|
83
|
+
}
|
|
84
|
+
const conflicts = [];
|
|
85
|
+
for (const [domain, accounts] of domains) {
|
|
86
|
+
const ids = [...new Set(accounts.map((account) => String(account.id)))];
|
|
87
|
+
if (ids.length > 1)
|
|
88
|
+
conflicts.push({ type: "duplicate_domain", accountIds: ids.sort(), key: domain, detail: `${ids.length} accounts share domain ${domain}; hierarchy inference will not choose between them.` });
|
|
89
|
+
}
|
|
90
|
+
const nodes = new Map(accounts.map((account) => [String(account.id), cloneNode(account)]));
|
|
91
|
+
const parentByChild = new Map();
|
|
92
|
+
for (const account of accounts) {
|
|
93
|
+
const id = String(account.id);
|
|
94
|
+
const hint = parentHint(account);
|
|
95
|
+
if (hint && byId.has(hint) && hint !== id) {
|
|
96
|
+
parentByChild.set(id, { parentId: hint, reason: "provider-parent" });
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const domain = normalizeDomain(account.domain);
|
|
100
|
+
if (!domain)
|
|
101
|
+
continue;
|
|
102
|
+
const inferred = domainParent(domain, domains);
|
|
103
|
+
if (inferred === "ambiguous")
|
|
104
|
+
conflicts.push({ type: "ambiguous_parent", accountIds: [id], key: domain, detail: `Multiple possible parent accounts found for ${domain}; leaving account as a root.` });
|
|
105
|
+
else if (inferred && String(inferred.id) !== id)
|
|
106
|
+
parentByChild.set(id, { parentId: String(inferred.id), reason: "domain-subdomain" });
|
|
107
|
+
}
|
|
108
|
+
const cyclicAccounts = detectCycles(parentByChild);
|
|
109
|
+
for (const cycle of cyclicAccounts.cycles) {
|
|
110
|
+
const ordered = [...cycle].sort();
|
|
111
|
+
const key = ordered.join(" -> ");
|
|
112
|
+
conflicts.push({
|
|
113
|
+
type: "cycle",
|
|
114
|
+
accountIds: ordered,
|
|
115
|
+
key,
|
|
116
|
+
detail: `Cyclic parent hints detected among ${ordered.join(", ")}; leaving those accounts as roots instead of hiding them in a loop.`,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
for (const accountId of cyclicAccounts.accountIds)
|
|
120
|
+
parentByChild.delete(accountId);
|
|
121
|
+
const roots = [];
|
|
122
|
+
const orphanAccounts = [];
|
|
123
|
+
const linkedChildren = new Set();
|
|
124
|
+
for (const account of accounts) {
|
|
125
|
+
const id = String(account.id);
|
|
126
|
+
const node = nodes.get(id);
|
|
127
|
+
const parent = parentByChild.get(id);
|
|
128
|
+
if (parent && nodes.has(parent.parentId)) {
|
|
129
|
+
node.parentAccountId = parent.parentId;
|
|
130
|
+
node.parentReason = parent.reason;
|
|
131
|
+
nodes.get(parent.parentId).children.push(node);
|
|
132
|
+
linkedChildren.add(id);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
roots.push(node);
|
|
136
|
+
if (!account.domain && !parentHint(account))
|
|
137
|
+
orphanAccounts.push(id);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const sortTree = (list) => {
|
|
141
|
+
list.sort((a, b) => a.name.localeCompare(b.name) || a.accountId.localeCompare(b.accountId));
|
|
142
|
+
for (const node of list)
|
|
143
|
+
sortTree(node.children);
|
|
144
|
+
};
|
|
145
|
+
sortTree(roots);
|
|
146
|
+
return { generatedAt: snapshot.generatedAt, roots, orphanAccounts: orphanAccounts.sort(), conflicts, counts: { accounts: accounts.length, roots: roots.length, linkedChildren: linkedChildren.size, conflicts: conflicts.length } };
|
|
147
|
+
}
|
|
148
|
+
export function accountHierarchyToMarkdown(report) {
|
|
149
|
+
const lines = ["# Account hierarchy", "", `${report.counts.accounts} account(s), ${report.counts.roots} root(s), ${report.counts.linkedChildren} inferred child link(s), ${report.counts.conflicts} conflict(s).`, ""];
|
|
150
|
+
const walk = (nodes, depth) => {
|
|
151
|
+
for (const node of nodes) {
|
|
152
|
+
const suffix = [node.domain, node.parentReason ? `via ${node.parentReason}` : undefined].filter(Boolean).join("; ");
|
|
153
|
+
lines.push(`${" ".repeat(depth)}- ${node.name} (${node.accountId}${suffix ? ` — ${suffix}` : ""})`);
|
|
154
|
+
walk(node.children, depth + 1);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
walk(report.roots, 0);
|
|
158
|
+
if (report.conflicts.length) {
|
|
159
|
+
lines.push("", "## Conflicts");
|
|
160
|
+
for (const conflict of report.conflicts)
|
|
161
|
+
lines.push(`- ${conflict.type} ${conflict.key}: ${conflict.detail}`);
|
|
162
|
+
}
|
|
163
|
+
return lines.join("\n");
|
|
164
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,13 +3,16 @@ export { matchesTerritory, parseAssignmentPolicy, resolveAssignment, type Assign
|
|
|
3
3
|
export { buildBulkUpdatePlan, isFilterableField, parseWhere, type BulkUpdateOptions } from "./bulkUpdate.ts";
|
|
4
4
|
export { buildDedupePlan, dedupeKey, type DedupeOptions } from "./dedupe.ts";
|
|
5
5
|
export { buildReassignPlans, type ReassignObjectType, type ReassignOptions } from "./reassign.ts";
|
|
6
|
-
export {
|
|
6
|
+
export { buildLeadRoutePlan, type LeadRouteCounts, type LeadRouteOptions, type LeadRouteResult, } from "./route.ts";
|
|
7
|
+
export { accountHierarchyToMarkdown, buildAccountHierarchy, type AccountHierarchyConflict, type AccountHierarchyNode, type AccountHierarchyReport, } from "./hierarchy.ts";
|
|
8
|
+
export { buildRelationshipMap, relationshipMapToMarkdown, type RelationshipMap, type StakeholderNode, type StakeholderRole, type StakeholderSentiment, } from "./relationships.ts";
|
|
9
|
+
export { CONFIG_FILE_NAME, loadConfig, mergePolicy, resolveConfiguredRules, rulePackageTrustFromCli, type FullstackgtmConfig, type LoadedConfig, type RulePackageTrust, } from "./config.ts";
|
|
7
10
|
export { applyPatchPlan, type ApplyPatchPlanOptions } from "./connector.ts";
|
|
8
11
|
export { APPLY_STAGES, BACKFILL_STRIPE_STAGES, composeListeners, createProgressEmitter, CRM_SYNC_STAGES, nullProgressEmitter, SNAPSHOT_PULL_STAGES, STRIPE_SNAPSHOT_STAGES, type ProgressEmitter, type ProgressEvent, type ProgressListener, type ProgressSnapshot, } from "./progress.ts";
|
|
9
12
|
export { createHubspotConnector, type HubspotConnectorOptions } from "./connectors/hubspot.ts";
|
|
10
13
|
export { DEFAULT_LOOPBACK_PORT, DEFAULT_OAUTH_SCOPES, exchangeHubspotCode, refreshHubspotToken, runHubspotLoopbackLogin, validateHubspotToken, type HubspotTokenSet, type LoopbackLoginOptions, } from "./connectors/hubspotAuth.ts";
|
|
11
14
|
export { createSalesforceConnector, type SalesforceConnection, type SalesforceConnectorOptions, } from "./connectors/salesforce.ts";
|
|
12
|
-
export { pollSalesforceDeviceLogin, refreshSalesforceToken, startSalesforceDeviceLogin, validateSalesforceToken, type SalesforceDeviceAuthorization, type SalesforceTokenSet, } from "./connectors/salesforceAuth.ts";
|
|
15
|
+
export { pollSalesforceDeviceLogin, refreshSalesforceToken, startSalesforceDeviceLogin, validateSalesforceOrigin, validateSalesforceToken, type SalesforceDeviceAuthorization, type SalesforceTokenSet, } from "./connectors/salesforceAuth.ts";
|
|
13
16
|
export { createStripeConnector, fetchStripePaidInvoices, type StripeConnectorOptions, type StripePaidInvoice, } from "./connectors/stripe.ts";
|
|
14
17
|
export { buildStripeBackfillPlan, DEFAULT_BACKFILL_MATCH_PROPERTY, type StripeBackfillCounts, type StripeBackfillOptions, type StripeBackfillResult, type StripeBackfillUnmatched, } from "./backfill.ts";
|
|
15
18
|
export { activeProfile, credentialsDir, credentialsPath, DEFAULT_PROFILE, deleteCredential, getCredential, listProfiles, resolveHubspotAccessToken, resolveHubspotConnection, setActiveProfile, storeCredential, type HubspotConnection, type StoredCredential, } from "./credentials.ts";
|
|
@@ -23,11 +26,12 @@ export { mergeSnapshots, type MergeConflict, type MergeMatch, type MergeReport,
|
|
|
23
26
|
export { createFilePlanStore, type PlanStore, type StoredPlan } from "./planStore.ts";
|
|
24
27
|
export { computeApprovalDigests, loadOrCreateSigningKey, loadSigningKey, signApproval, verifyApprovalDigests, type ApprovalVerification, } from "./integrity.ts";
|
|
25
28
|
export { buildAuditLog, verifyAuditLog, type AuditLogEntry, type AuditLogExport, type AuditLogVerification, } from "./auditLog.ts";
|
|
29
|
+
export { FREE_EMAIL_DOMAINS } from "./freeEmailDomains.ts";
|
|
26
30
|
export { formatPatchPlanRun, patchPlanToMarkdown } from "./format.ts";
|
|
27
31
|
export { computeHealth, summarizeHealth, healthToMarkdown, type HealthEntry, type HealthRollup, type HealthRuleDelta, } from "./health.ts";
|
|
28
32
|
export { auditReportToHtml, auditReportToMarkdown, type ReportOptions } from "./report.ts";
|
|
29
33
|
export { HUBSPOT_DEFAULT_FIELD_MAPPINGS, SALESFORCE_DEFAULT_FIELD_MAPPINGS, mappedField, mappedFields, normalizeFieldMappings, readMappedValue, type CrmObjectType, type FieldMappings, } from "./mappings.ts";
|
|
30
|
-
export { accountSingleSourceRule, activeDealAccountWithoutContactsRule, auditFindingId, buildSnapshotIndex, builtinAuditRules, closingSoonInactiveRule, duplicateAccountDomainRule, duplicateContactEmailRule, duplicateOpenDealRule, missingDealAccountRule, missingDealAmountRule, missingDealOwnerRule, orphanAccountRule, pastCloseDateRule, patchOperationId, provenanceSummary, requiresHumanInput, staleDealRule, } from "./rules.ts";
|
|
34
|
+
export { accountSingleSourceRule, activeDealAccountWithoutContactsRule, auditFindingId, buildSnapshotIndex, builtinAuditRules, closingSoonInactiveRule, isFindingsOnlyRule, duplicateAccountDomainRule, duplicateContactEmailRule, duplicateOpenDealRule, missingDealAccountRule, missingDealAmountRule, missingDealOwnerRule, orphanAccountRule, pastCloseDateRule, patchOperationId, provenanceSummary, requiresHumanInput, staleDealRule, } from "./rules.ts";
|
|
31
35
|
export { extractCallInsights, normalizeTranscript, parseCall, parseTranscript, suggestCallDeal, summarizeInsights, type CallDealSuggestion, type CallInsightType, type ExtractedCallInsight, type ParsedCall, type ParsedTranscriptSegment, } from "./calls.ts";
|
|
32
36
|
export { sampleSnapshot } from "./sampleData.ts";
|
|
33
37
|
export { DEFAULT_MODELS, DEFAULT_RUBRIC, classifyCallLlm, detectProviderFromKey, extractInsightsLlm, forcedToolCall, parseRubric, resolveLlmCredential, scoreCallLlm, validateLlmKey, type CallScorecard, type LlmCallClassification, type LlmCredential, type LlmExtractedInsight, type LlmProvider, type Rubric, type RubricDimension, type ScoreBand, type ScoredDimension, } from "./llm.ts";
|
|
@@ -48,4 +52,5 @@ export { fetchAtsJobs, snippetFor, type AtsBoardSource, type AtsJob, } from "./c
|
|
|
48
52
|
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";
|
|
49
53
|
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";
|
|
50
54
|
export { DEFAULT_GOLDEN_SET, DEFAULT_MIN_ACCURACY, HOT_SCORE, defaultJudgeFn, gradeAgainstOutcomes, gradeJudge, parseGoldenSet, type Confusion, type EvalJudgeFn, type GoldenHistory, type GoldenRow, type GradeResult, type OutcomeCalibration, } from "./judgeEval.ts";
|
|
51
|
-
export type { ApprovalStatus, AuditFinding, AuditFindingSeverity, CanonicalAccount, CanonicalActivity, CanonicalContact, CanonicalDeal, CanonicalGtmSnapshot, CanonicalUser, CrmProvider, GtmAuditRule, GtmConnector, GtmEvidence, GtmEvidenceSourceSystem, GtmObjectType, GtmPolicy, GtmRuleContext, GtmRuleResult, GtmSnapshotIndex, PatchOperation, PatchOperationResult, PatchOperationType, PatchPlan, PatchPlanRun, PatchPlanRunStatus, PatchVerification, PipelineFinding, PipelineFindingStatus, PipelineFindingType, ProviderIdentity, RiskLevel, SourceFreshness, } from "./types.ts";
|
|
55
|
+
export type { ApprovalStatus, AuditFinding, AuditFindingSeverity, CanonicalAccount, CanonicalActivity, CanonicalContact, CanonicalDeal, CanonicalGtmSnapshot, CanonicalUser, CrmProvider, GtmAuditRule, GtmConnector, GtmEvidence, GtmEvidenceSourceSystem, GtmObjectType, GtmPolicy, GtmRuleContext, GtmRuleResult, GtmSnapshotIndex, PatchOperation, PatchOperationResult, PatchOperationType, PatchPlan, PatchPlanAssumption, PatchPlanAssumptionConfidence, PatchPlanRun, PatchPlanRunStatus, PatchVerification, PipelineFinding, PipelineFindingStatus, PipelineFindingType, ProviderIdentity, RiskLevel, SourceFreshness, } from "./types.ts";
|
|
56
|
+
export { ProviderHttpError } from "./providerError.ts";
|
package/dist/index.js
CHANGED
|
@@ -3,13 +3,16 @@ export { matchesTerritory, parseAssignmentPolicy, resolveAssignment, } from "./a
|
|
|
3
3
|
export { buildBulkUpdatePlan, isFilterableField, parseWhere } from "./bulkUpdate.js";
|
|
4
4
|
export { buildDedupePlan, dedupeKey } from "./dedupe.js";
|
|
5
5
|
export { buildReassignPlans } from "./reassign.js";
|
|
6
|
-
export {
|
|
6
|
+
export { buildLeadRoutePlan, } from "./route.js";
|
|
7
|
+
export { accountHierarchyToMarkdown, buildAccountHierarchy, } from "./hierarchy.js";
|
|
8
|
+
export { buildRelationshipMap, relationshipMapToMarkdown, } from "./relationships.js";
|
|
9
|
+
export { CONFIG_FILE_NAME, loadConfig, mergePolicy, resolveConfiguredRules, rulePackageTrustFromCli, } from "./config.js";
|
|
7
10
|
export { applyPatchPlan } from "./connector.js";
|
|
8
11
|
export { APPLY_STAGES, BACKFILL_STRIPE_STAGES, composeListeners, createProgressEmitter, CRM_SYNC_STAGES, nullProgressEmitter, SNAPSHOT_PULL_STAGES, STRIPE_SNAPSHOT_STAGES, } from "./progress.js";
|
|
9
12
|
export { createHubspotConnector } from "./connectors/hubspot.js";
|
|
10
13
|
export { DEFAULT_LOOPBACK_PORT, DEFAULT_OAUTH_SCOPES, exchangeHubspotCode, refreshHubspotToken, runHubspotLoopbackLogin, validateHubspotToken, } from "./connectors/hubspotAuth.js";
|
|
11
14
|
export { createSalesforceConnector, } from "./connectors/salesforce.js";
|
|
12
|
-
export { pollSalesforceDeviceLogin, refreshSalesforceToken, startSalesforceDeviceLogin, validateSalesforceToken, } from "./connectors/salesforceAuth.js";
|
|
15
|
+
export { pollSalesforceDeviceLogin, refreshSalesforceToken, startSalesforceDeviceLogin, validateSalesforceOrigin, validateSalesforceToken, } from "./connectors/salesforceAuth.js";
|
|
13
16
|
export { createStripeConnector, fetchStripePaidInvoices, } from "./connectors/stripe.js";
|
|
14
17
|
export { buildStripeBackfillPlan, DEFAULT_BACKFILL_MATCH_PROPERTY, } from "./backfill.js";
|
|
15
18
|
export { activeProfile, credentialsDir, credentialsPath, DEFAULT_PROFILE, deleteCredential, getCredential, listProfiles, resolveHubspotAccessToken, resolveHubspotConnection, setActiveProfile, storeCredential, } from "./credentials.js";
|
|
@@ -23,11 +26,12 @@ export { mergeSnapshots, } from "./merge.js";
|
|
|
23
26
|
export { createFilePlanStore } from "./planStore.js";
|
|
24
27
|
export { computeApprovalDigests, loadOrCreateSigningKey, loadSigningKey, signApproval, verifyApprovalDigests, } from "./integrity.js";
|
|
25
28
|
export { buildAuditLog, verifyAuditLog, } from "./auditLog.js";
|
|
29
|
+
export { FREE_EMAIL_DOMAINS } from "./freeEmailDomains.js";
|
|
26
30
|
export { formatPatchPlanRun, patchPlanToMarkdown } from "./format.js";
|
|
27
31
|
export { computeHealth, summarizeHealth, healthToMarkdown, } from "./health.js";
|
|
28
32
|
export { auditReportToHtml, auditReportToMarkdown } from "./report.js";
|
|
29
33
|
export { HUBSPOT_DEFAULT_FIELD_MAPPINGS, SALESFORCE_DEFAULT_FIELD_MAPPINGS, mappedField, mappedFields, normalizeFieldMappings, readMappedValue, } from "./mappings.js";
|
|
30
|
-
export { accountSingleSourceRule, activeDealAccountWithoutContactsRule, auditFindingId, buildSnapshotIndex, builtinAuditRules, closingSoonInactiveRule, duplicateAccountDomainRule, duplicateContactEmailRule, duplicateOpenDealRule, missingDealAccountRule, missingDealAmountRule, missingDealOwnerRule, orphanAccountRule, pastCloseDateRule, patchOperationId, provenanceSummary, requiresHumanInput, staleDealRule, } from "./rules.js";
|
|
34
|
+
export { accountSingleSourceRule, activeDealAccountWithoutContactsRule, auditFindingId, buildSnapshotIndex, builtinAuditRules, closingSoonInactiveRule, isFindingsOnlyRule, duplicateAccountDomainRule, duplicateContactEmailRule, duplicateOpenDealRule, missingDealAccountRule, missingDealAmountRule, missingDealOwnerRule, orphanAccountRule, pastCloseDateRule, patchOperationId, provenanceSummary, requiresHumanInput, staleDealRule, } from "./rules.js";
|
|
31
35
|
export { extractCallInsights, normalizeTranscript, parseCall, parseTranscript, suggestCallDeal, summarizeInsights, } from "./calls.js";
|
|
32
36
|
export { sampleSnapshot } from "./sampleData.js";
|
|
33
37
|
export { DEFAULT_MODELS, DEFAULT_RUBRIC, classifyCallLlm, detectProviderFromKey, extractInsightsLlm, forcedToolCall, parseRubric, resolveLlmCredential, scoreCallLlm, validateLlmKey, } from "./llm.js";
|
|
@@ -48,3 +52,4 @@ export { fetchAtsJobs, snippetFor, } from "./connectors/atsBoards.js";
|
|
|
48
52
|
export { scoreBand, judgeRunId, normalizeSpan, isGroundedSpan, scoreAccount, accountRecentlyTouched, bestContactForAccount, deterministicDecision, deterministicWhyNow, JUDGE_SCHEMA, DEFAULT_JUDGE_PROMPT, judgeDecisionLlm, judgeSignals, judgeDir, createFileJudgeStore, } from "./judge.js";
|
|
49
53
|
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";
|
|
50
54
|
export { DEFAULT_GOLDEN_SET, DEFAULT_MIN_ACCURACY, HOT_SCORE, defaultJudgeFn, gradeAgainstOutcomes, gradeJudge, parseGoldenSet, } from "./judgeEval.js";
|
|
55
|
+
export { ProviderHttpError } from "./providerError.js";
|
package/dist/market.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ProgressEmitter } from "./progress.ts";
|
|
2
2
|
import type { GtmEvidence } from "./types.ts";
|
|
3
|
+
export { assertPublicUrl } from "./publicHttp.ts";
|
|
3
4
|
/**
|
|
4
5
|
* The Market Map: a live model of the competitive category a company sells
|
|
5
6
|
* into. Vendors publish claims constantly (pricing pages, feature pages,
|
|
@@ -162,7 +163,6 @@ export type FetchPage = (url: string) => Promise<{
|
|
|
162
163
|
status: number;
|
|
163
164
|
body: string;
|
|
164
165
|
}>;
|
|
165
|
-
export declare function assertPublicUrl(rawUrl: string): Promise<URL>;
|
|
166
166
|
export type CaptureOptions = {
|
|
167
167
|
/** Directory for captures; defaults to <marketHome>/captures. Ignored when `store` is given. */
|
|
168
168
|
dir?: string;
|