fullstackgtm 0.10.1 → 0.11.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 +60 -0
- package/INSTALL_FOR_AGENTS.md +14 -0
- package/README.md +33 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +244 -13
- package/dist/connectors/hubspot.js +21 -3
- package/dist/connectors/hubspotAuth.js +6 -2
- package/dist/connectors/salesforce.js +19 -1
- package/dist/connectors/salesforceAuth.js +17 -3
- package/dist/credentials.d.ts +19 -0
- package/dist/credentials.js +69 -8
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/mcp.js +16 -0
- package/dist/report.d.ts +61 -0
- package/dist/report.js +331 -0
- package/dist/rules.d.ts +1 -0
- package/dist/rules.js +47 -0
- package/dist/suggest.d.ts +31 -0
- package/dist/suggest.js +148 -0
- package/docs/api.md +13 -1
- package/package.json +1 -1
- package/src/cli.ts +264 -11
- package/src/connectors/hubspot.ts +21 -3
- package/src/connectors/hubspotAuth.ts +6 -2
- package/src/connectors/salesforce.ts +19 -1
- package/src/connectors/salesforceAuth.ts +19 -6
- package/src/credentials.ts +71 -6
- package/src/index.ts +7 -0
- package/src/mcp.ts +22 -0
- package/src/report.ts +502 -0
- package/src/rules.ts +50 -0
- package/src/suggest.ts +202 -0
|
@@ -155,13 +155,26 @@ export async function validateSalesforceToken(
|
|
|
155
155
|
instanceUrl: string,
|
|
156
156
|
fetchImpl: typeof fetch = fetch,
|
|
157
157
|
): Promise<{ ok: boolean; detail: string }> {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
158
|
+
let response: Response;
|
|
159
|
+
try {
|
|
160
|
+
response = await fetchImpl(
|
|
161
|
+
`${instanceUrl.replace(/\/$/, "")}/services/oauth2/userinfo`,
|
|
162
|
+
{ headers: { Authorization: `Bearer ${accessToken}` } },
|
|
163
|
+
);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
const cause = error instanceof Error && error.cause instanceof Error ? `: ${error.cause.message}` : "";
|
|
166
|
+
return {
|
|
167
|
+
ok: false,
|
|
168
|
+
detail: `Cannot reach Salesforce at ${instanceUrl}${cause}. Check the --instance-url (your My Domain URL, e.g. https://yourco.my.salesforce.com) and network access.`,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
162
171
|
if (response.ok) {
|
|
163
172
|
return { ok: true, detail: "Token accepted by the Salesforce API." };
|
|
164
173
|
}
|
|
165
|
-
|
|
166
|
-
|
|
174
|
+
// Never echo the response body: provider error payloads can reflect request
|
|
175
|
+
// details and end up in logs or shell scrollback.
|
|
176
|
+
return {
|
|
177
|
+
ok: false,
|
|
178
|
+
detail: `Salesforce rejected the token: HTTP ${response.status} ${response.statusText}`.trim(),
|
|
179
|
+
};
|
|
167
180
|
}
|
package/src/credentials.ts
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
chmodSync,
|
|
3
3
|
existsSync,
|
|
4
4
|
mkdirSync,
|
|
5
|
+
readdirSync,
|
|
5
6
|
readFileSync,
|
|
6
7
|
unlinkSync,
|
|
7
8
|
writeFileSync,
|
|
@@ -15,8 +16,63 @@ import { refreshSalesforceToken } from "./connectors/salesforceAuth.ts";
|
|
|
15
16
|
* Local CLI credential store: ~/.fullstackgtm/credentials.json (0600), or
|
|
16
17
|
* $FSGTM_HOME/credentials.json when set. Environment tokens always win over
|
|
17
18
|
* stored credentials so CI and agent sandboxes never touch the filesystem.
|
|
19
|
+
*
|
|
20
|
+
* Profiles let one operator hold credentials for several organizations at
|
|
21
|
+
* once (a consultant working across client CRMs). The default profile keeps
|
|
22
|
+
* the historical layout; a named profile scopes the entire home — credentials
|
|
23
|
+
* AND stored plans — under `profiles/<name>/`, so a patch plan proposed
|
|
24
|
+
* against one client's CRM can never be applied through another client's
|
|
25
|
+
* credentials.
|
|
18
26
|
*/
|
|
19
27
|
|
|
28
|
+
export const DEFAULT_PROFILE = "default";
|
|
29
|
+
|
|
30
|
+
const PROFILE_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
31
|
+
|
|
32
|
+
let explicitProfile: string | null = null;
|
|
33
|
+
|
|
34
|
+
export function validateProfileName(name: string): string {
|
|
35
|
+
if (!PROFILE_NAME_PATTERN.test(name) || name === "." || name === "..") {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Invalid profile name: ${JSON.stringify(name)}. Use letters, numbers, dots, dashes, ` +
|
|
38
|
+
"or underscores (must start with a letter or number, max 64 characters).",
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
return name;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Select the profile for this process; wins over $FULLSTACKGTM_PROFILE. */
|
|
45
|
+
export function setActiveProfile(name: string) {
|
|
46
|
+
explicitProfile = validateProfileName(name);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function activeProfile(): string {
|
|
50
|
+
if (explicitProfile) return explicitProfile;
|
|
51
|
+
const fromEnv = process.env.FULLSTACKGTM_PROFILE;
|
|
52
|
+
return fromEnv ? validateProfileName(fromEnv) : DEFAULT_PROFILE;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Base home directory, shared by every profile. */
|
|
56
|
+
export function baseHomeDir(): string {
|
|
57
|
+
return process.env.FSGTM_HOME ?? join(homedir(), ".fullstackgtm");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Profiles that exist on disk (have a directory), always including the
|
|
62
|
+
* default profile. Existence does not imply stored credentials.
|
|
63
|
+
*/
|
|
64
|
+
export function listProfiles(): string[] {
|
|
65
|
+
const names = new Set([DEFAULT_PROFILE]);
|
|
66
|
+
try {
|
|
67
|
+
for (const entry of readdirSync(join(baseHomeDir(), "profiles"), { withFileTypes: true })) {
|
|
68
|
+
if (entry.isDirectory() && PROFILE_NAME_PATTERN.test(entry.name)) names.add(entry.name);
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
// No profiles directory yet.
|
|
72
|
+
}
|
|
73
|
+
return Array.from(names).sort();
|
|
74
|
+
}
|
|
75
|
+
|
|
20
76
|
export type StoredCredential = {
|
|
21
77
|
kind: "private_app" | "oauth" | "broker";
|
|
22
78
|
accessToken: string;
|
|
@@ -43,7 +99,9 @@ type CredentialsFile = {
|
|
|
43
99
|
};
|
|
44
100
|
|
|
45
101
|
export function credentialsDir(): string {
|
|
46
|
-
|
|
102
|
+
const base = baseHomeDir();
|
|
103
|
+
const profile = activeProfile();
|
|
104
|
+
return profile === DEFAULT_PROFILE ? base : join(base, "profiles", profile);
|
|
47
105
|
}
|
|
48
106
|
|
|
49
107
|
export function credentialsPath(): string {
|
|
@@ -59,11 +117,18 @@ export function credentialsPath(): string {
|
|
|
59
117
|
*/
|
|
60
118
|
export function ensureSecureHomeDir(): string {
|
|
61
119
|
const dir = credentialsDir();
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
120
|
+
// A named profile nests under base/profiles/<name>; lock down every level
|
|
121
|
+
// we create, not just the leaf — recursive mkdir applies `mode` (less
|
|
122
|
+
// umask) only to directories it creates, and never to pre-existing ones.
|
|
123
|
+
const levels =
|
|
124
|
+
dir === baseHomeDir() ? [dir] : [baseHomeDir(), join(baseHomeDir(), "profiles"), dir];
|
|
125
|
+
for (const level of levels) {
|
|
126
|
+
mkdirSync(level, { recursive: true, mode: 0o700 });
|
|
127
|
+
try {
|
|
128
|
+
chmodSync(level, 0o700);
|
|
129
|
+
} catch {
|
|
130
|
+
// Non-POSIX filesystems (e.g. Windows) ignore chmod; nothing to enforce.
|
|
131
|
+
}
|
|
67
132
|
}
|
|
68
133
|
return dir;
|
|
69
134
|
}
|
package/src/index.ts
CHANGED
|
@@ -34,12 +34,16 @@ export {
|
|
|
34
34
|
} from "./connectors/salesforceAuth.ts";
|
|
35
35
|
export { createStripeConnector, type StripeConnectorOptions } from "./connectors/stripe.ts";
|
|
36
36
|
export {
|
|
37
|
+
activeProfile,
|
|
37
38
|
credentialsDir,
|
|
38
39
|
credentialsPath,
|
|
40
|
+
DEFAULT_PROFILE,
|
|
39
41
|
deleteCredential,
|
|
40
42
|
getCredential,
|
|
43
|
+
listProfiles,
|
|
41
44
|
resolveHubspotAccessToken,
|
|
42
45
|
resolveHubspotConnection,
|
|
46
|
+
setActiveProfile,
|
|
43
47
|
storeCredential,
|
|
44
48
|
type HubspotConnection,
|
|
45
49
|
type StoredCredential,
|
|
@@ -64,6 +68,7 @@ export {
|
|
|
64
68
|
} from "./merge.ts";
|
|
65
69
|
export { createFilePlanStore, type PlanStore, type StoredPlan } from "./planStore.ts";
|
|
66
70
|
export { formatPatchPlanRun, patchPlanToMarkdown } from "./format.ts";
|
|
71
|
+
export { auditReportToHtml, auditReportToMarkdown, type ReportOptions } from "./report.ts";
|
|
67
72
|
export {
|
|
68
73
|
HUBSPOT_DEFAULT_FIELD_MAPPINGS,
|
|
69
74
|
SALESFORCE_DEFAULT_FIELD_MAPPINGS,
|
|
@@ -83,6 +88,7 @@ export {
|
|
|
83
88
|
closingSoonInactiveRule,
|
|
84
89
|
duplicateAccountDomainRule,
|
|
85
90
|
duplicateContactEmailRule,
|
|
91
|
+
duplicateOpenDealRule,
|
|
86
92
|
missingDealAccountRule,
|
|
87
93
|
missingDealAmountRule,
|
|
88
94
|
missingDealOwnerRule,
|
|
@@ -93,6 +99,7 @@ export {
|
|
|
93
99
|
staleDealRule,
|
|
94
100
|
} from "./rules.ts";
|
|
95
101
|
export { sampleSnapshot } from "./sampleData.ts";
|
|
102
|
+
export { suggestValues, type SuggestionConfidence, type ValueSuggestion } from "./suggest.ts";
|
|
96
103
|
export type {
|
|
97
104
|
ApprovalStatus,
|
|
98
105
|
AuditFinding,
|
package/src/mcp.ts
CHANGED
|
@@ -46,6 +46,7 @@ import type { FieldMappings } from "./mappings.ts";
|
|
|
46
46
|
import { formatPatchPlanRun, patchPlanToMarkdown } from "./format.ts";
|
|
47
47
|
import { builtinAuditRules } from "./rules.ts";
|
|
48
48
|
import { sampleSnapshot } from "./sampleData.ts";
|
|
49
|
+
import { suggestValues } from "./suggest.ts";
|
|
49
50
|
import type { CanonicalGtmSnapshot, GtmConnector, PatchPlan } from "./types.ts";
|
|
50
51
|
|
|
51
52
|
function content(value: unknown) {
|
|
@@ -166,6 +167,27 @@ export async function startMcpServer() {
|
|
|
166
167
|
},
|
|
167
168
|
);
|
|
168
169
|
|
|
170
|
+
server.registerTool(
|
|
171
|
+
"fullstackgtm_suggest",
|
|
172
|
+
{
|
|
173
|
+
title: "Suggest Placeholder Values",
|
|
174
|
+
description:
|
|
175
|
+
"Derive values for a plan's requires_human_* placeholder operations from snapshot " +
|
|
176
|
+
"evidence (account-name matching, contact associations), with confidence levels and " +
|
|
177
|
+
"reasons. Read-only; feed accepted values into fullstackgtm_apply's valueOverrides.",
|
|
178
|
+
inputSchema: {
|
|
179
|
+
planPath: z.string(),
|
|
180
|
+
provider: z.enum(["sample", "demo", "hubspot", "salesforce", "stripe"]).optional(),
|
|
181
|
+
inputPath: z.string().optional(),
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
async ({ planPath, provider, inputPath }) => {
|
|
185
|
+
const plan = JSON.parse(readFileSync(resolve(process.cwd(), planPath), "utf8")) as PatchPlan;
|
|
186
|
+
const snapshot = await readSnapshot(provider, inputPath);
|
|
187
|
+
return content({ suggestions: suggestValues(plan, snapshot) });
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
|
|
169
191
|
server.registerTool(
|
|
170
192
|
"fullstackgtm_rules",
|
|
171
193
|
{
|