@hubspot/cli 8.2.0-experimental.0 → 8.2.0-experimental.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/commands/account/auth.js +2 -0
  2. package/commands/app/logs.js +4 -0
  3. package/commands/getStarted.js +2 -0
  4. package/commands/init.js +2 -0
  5. package/commands/mcp/setup.js +2 -2
  6. package/commands/project/create.js +2 -0
  7. package/commands/project/installApp.d.ts +7 -0
  8. package/commands/project/installApp.js +181 -0
  9. package/commands/project/installDeps.js +2 -0
  10. package/commands/project/upload.js +4 -0
  11. package/commands/project.js +2 -0
  12. package/commands/upgrade.js +2 -0
  13. package/lang/en.d.ts +58 -0
  14. package/lang/en.js +71 -2
  15. package/lib/accountTargetDiscovery.d.ts +15 -0
  16. package/lib/accountTargetDiscovery.js +175 -0
  17. package/lib/app/install.d.ts +22 -0
  18. package/lib/app/install.js +42 -0
  19. package/lib/app/logs.js +1 -1
  20. package/lib/auth/awaitPersonalAccessKeyOverWebsocket.js +3 -2
  21. package/lib/doctor/Doctor.d.ts +1 -0
  22. package/lib/doctor/Doctor.js +26 -0
  23. package/lib/mcp/clients.d.ts +13 -0
  24. package/lib/mcp/clients.js +55 -0
  25. package/lib/mcp/promotion.d.ts +3 -0
  26. package/lib/mcp/promotion.js +109 -0
  27. package/lib/mcp/setup.d.ts +3 -2
  28. package/lib/mcp/setup.js +17 -12
  29. package/lib/projects/installApp.d.ts +58 -0
  30. package/lib/projects/installApp.js +278 -0
  31. package/lib/projects/localDev/AppDevModeInterface.d.ts +0 -3
  32. package/lib/projects/localDev/AppDevModeInterface.js +22 -44
  33. package/lib/projects/localDev/helpers/project.d.ts +4 -3
  34. package/lib/projects/localDev/helpers/project.js +46 -12
  35. package/lib/prompts/projectDevTargetAccountPrompt.js +3 -2
  36. package/lib/theme/cmsDevServerProcess.js +1 -1
  37. package/lib/usageTracking.d.ts +1 -0
  38. package/lib/usageTracking.js +9 -0
  39. package/mcp-server/tools/project/CreateTestAccountTool.js +1 -1
  40. package/package.json +4 -4
  41. package/types/AccountTargets.d.ts +37 -0
  42. package/types/AccountTargets.js +12 -0
  43. package/types/LocalDev.d.ts +3 -2
  44. package/types/ProjectComponents.d.ts +2 -1
  45. package/types/Projects.d.ts +5 -0
@@ -0,0 +1,175 @@
1
+ import path from 'path';
2
+ import { getConfigAccountIfExists, getConfigDefaultAccountIfExists, } from '@hubspot/local-dev-lib/config';
3
+ import { getHsSettingsFileIfExists } from '@hubspot/local-dev-lib/config/hsSettings';
4
+ import { ENVIRONMENT_VARIABLES } from '@hubspot/local-dev-lib/constants/config';
5
+ import { getAllHsProfiles, loadHsProfileFile, } from '@hubspot/project-parsing-lib/profiles';
6
+ import { isTestAccountOrSandbox } from './accountTypes.js';
7
+ import { isDirectoryLinked } from './link/linkUtils.js';
8
+ import { lib } from '../lang/en.js';
9
+ import { ACCOUNT_TARGET_CATEGORIES, ACCOUNT_TARGET_SELECTION_SOURCES, } from '../types/AccountTargets.js';
10
+ function categorizeAccount(account) {
11
+ if (isTestAccountOrSandbox(account)) {
12
+ return ACCOUNT_TARGET_CATEGORIES.RECOMMENDED_TESTING;
13
+ }
14
+ if (!account.accountType) {
15
+ return ACCOUNT_TARGET_CATEGORIES.UNKNOWN;
16
+ }
17
+ return ACCOUNT_TARGET_CATEGORIES.PRODUCTION_WITH_CARE;
18
+ }
19
+ function candidateFromAccount(account, source, profileName) {
20
+ return {
21
+ accountId: account.accountId,
22
+ accountName: account.name,
23
+ accountType: account.accountType,
24
+ environment: account.env || undefined,
25
+ source,
26
+ category: categorizeAccount(account),
27
+ ...(profileName ? { profileName } : {}),
28
+ };
29
+ }
30
+ function candidateFromAccountId(accountId, source, profileName) {
31
+ const account = getConfigAccountIfExists(accountId);
32
+ if (account) {
33
+ return candidateFromAccount(account, source, profileName);
34
+ }
35
+ return {
36
+ accountId,
37
+ source,
38
+ category: ACCOUNT_TARGET_CATEGORIES.UNKNOWN,
39
+ ...(profileName ? { profileName } : {}),
40
+ };
41
+ }
42
+ function dedupeByAccountId(candidates) {
43
+ const seen = new Set();
44
+ return candidates.filter(candidate => {
45
+ if (seen.has(candidate.accountId)) {
46
+ return false;
47
+ }
48
+ seen.add(candidate.accountId);
49
+ return true;
50
+ });
51
+ }
52
+ async function gatherProfileCandidates(projectDir, projectConfig) {
53
+ const projectSourceDir = path.join(projectDir, projectConfig.srcDir);
54
+ const profileNames = await getAllHsProfiles(projectSourceDir);
55
+ const candidates = [];
56
+ for (const name of profileNames) {
57
+ let profile;
58
+ try {
59
+ profile = loadHsProfileFile(projectSourceDir, name);
60
+ }
61
+ catch {
62
+ continue;
63
+ }
64
+ if (profile && profile.accountId) {
65
+ candidates.push(candidateFromAccountId(profile.accountId, ACCOUNT_TARGET_SELECTION_SOURCES.PROFILE, name));
66
+ }
67
+ }
68
+ return candidates;
69
+ }
70
+ function resolveProfileRecommendation(profileCandidates, explicitProfileName) {
71
+ if (explicitProfileName) {
72
+ return profileCandidates.find(candidate => candidate.profileName === explicitProfileName);
73
+ }
74
+ if (profileCandidates.length === 1) {
75
+ return profileCandidates[0];
76
+ }
77
+ return undefined;
78
+ }
79
+ function resolveNonProfileRecommendation({ explicitCandidate, envCandidate, linkedCandidates, linkedDefaultAccountId, defaultCandidate, }) {
80
+ if (explicitCandidate) {
81
+ return explicitCandidate;
82
+ }
83
+ if (envCandidate) {
84
+ return envCandidate;
85
+ }
86
+ if (linkedCandidates.length > 0) {
87
+ if (linkedDefaultAccountId !== undefined) {
88
+ const linkedDefault = linkedCandidates.find(candidate => candidate.accountId === linkedDefaultAccountId);
89
+ if (linkedDefault) {
90
+ return linkedDefault;
91
+ }
92
+ }
93
+ if (linkedCandidates.length === 1) {
94
+ return linkedCandidates[0];
95
+ }
96
+ return undefined;
97
+ }
98
+ return defaultCandidate;
99
+ }
100
+ /**
101
+ * Discovers the candidate target accounts for a command, plus the recommended
102
+ * one to use when a single account is the obvious choice.
103
+ *
104
+ * Profiles take precedence: when the project defines profiles, the candidates
105
+ * are the profile accounts only. Otherwise they are ranked explicit account,
106
+ * then env config, then linked directory, then global default.
107
+ *
108
+ * Takes plain options (not yargs argv) and never prompts or exits, so both the
109
+ * CLI and MCP can call it. `recommended` is omitted when no account is the
110
+ * obvious choice (for example, several profiles). Throws when an explicit
111
+ * account is provided but not found in the config.
112
+ */
113
+ export async function discoverAccountTargets(options = {}) {
114
+ const { explicitAccount, useEnv, profileName, projectDir, projectConfig } = options;
115
+ if (projectDir && projectConfig) {
116
+ const profileCandidates = await gatherProfileCandidates(projectDir, projectConfig);
117
+ if (profileCandidates.length > 0) {
118
+ return {
119
+ candidates: dedupeByAccountId(profileCandidates),
120
+ recommended: resolveProfileRecommendation(profileCandidates, profileName),
121
+ };
122
+ }
123
+ }
124
+ const envConfigActive = Boolean(useEnv) ||
125
+ process.env[ENVIRONMENT_VARIABLES.USE_ENVIRONMENT_HUBSPOT_CONFIG] ===
126
+ 'true';
127
+ const ordered = [];
128
+ let explicitCandidate;
129
+ if (explicitAccount !== undefined && explicitAccount !== '') {
130
+ const account = getConfigAccountIfExists(explicitAccount);
131
+ if (!account) {
132
+ throw new Error(lib.accountTargetDiscovery.errors.explicitAccountNotFound(explicitAccount));
133
+ }
134
+ explicitCandidate = candidateFromAccount(account, ACCOUNT_TARGET_SELECTION_SOURCES.EXPLICIT_ACCOUNT);
135
+ ordered.push(explicitCandidate);
136
+ }
137
+ let envCandidate;
138
+ if (envConfigActive) {
139
+ const envAccount = getConfigDefaultAccountIfExists();
140
+ if (envAccount) {
141
+ envCandidate = candidateFromAccount(envAccount, ACCOUNT_TARGET_SELECTION_SOURCES.ENV_CONFIG);
142
+ ordered.push(envCandidate);
143
+ }
144
+ }
145
+ const settings = getHsSettingsFileIfExists();
146
+ const directoryIsLinked = isDirectoryLinked(settings);
147
+ const linkedCandidates = [];
148
+ if (directoryIsLinked) {
149
+ for (const accountId of settings.accounts) {
150
+ const candidate = candidateFromAccountId(accountId, ACCOUNT_TARGET_SELECTION_SOURCES.LINKED_DIRECTORY);
151
+ linkedCandidates.push(candidate);
152
+ ordered.push(candidate);
153
+ }
154
+ }
155
+ let defaultCandidate;
156
+ if (!envConfigActive) {
157
+ const defaultAccount = getConfigDefaultAccountIfExists();
158
+ if (defaultAccount) {
159
+ defaultCandidate = candidateFromAccount(defaultAccount, ACCOUNT_TARGET_SELECTION_SOURCES.GLOBAL_DEFAULT);
160
+ ordered.push(defaultCandidate);
161
+ }
162
+ }
163
+ return {
164
+ candidates: dedupeByAccountId(ordered),
165
+ recommended: resolveNonProfileRecommendation({
166
+ explicitCandidate,
167
+ envCandidate,
168
+ linkedCandidates,
169
+ linkedDefaultAccountId: directoryIsLinked
170
+ ? settings.localDefaultAccount
171
+ : undefined,
172
+ defaultCandidate,
173
+ }),
174
+ };
175
+ }
@@ -0,0 +1,22 @@
1
+ import type { HubSpotConfigAccount } from '@hubspot/local-dev-lib/types/Accounts';
2
+ import type { IntermediateRepresentationNodeLocalDev } from '@hubspot/project-parsing-lib/translate';
3
+ import type { AppInstallationState, AppIRNode } from '../../types/ProjectComponents.js';
4
+ export type ScopeGroup = {
5
+ id: number;
6
+ name: string;
7
+ };
8
+ type CanUseStaticAuthTestAccountInstallOptions = {
9
+ accountConfig?: HubSpotConfigAccount;
10
+ appNode?: AppIRNode | null;
11
+ parentAccountId?: number;
12
+ };
13
+ export declare function getAppNodeFromProjectNodes(projectNodes: {
14
+ [key: string]: IntermediateRepresentationNodeLocalDev;
15
+ }): AppIRNode | null;
16
+ export declare function isStaticAuthApp(appNode?: AppIRNode | null): boolean;
17
+ export declare function isOAuthApp(appNode?: AppIRNode | null): boolean;
18
+ export declare function isPrivateApp(appNode?: AppIRNode | null): boolean;
19
+ export declare function isMarketplaceApp(appNode?: AppIRNode | null): boolean;
20
+ export declare function getAppInstallationState(isInstalledWithScopeGroups: boolean, previouslyAuthorizedScopeGroups: ScopeGroup[]): AppInstallationState;
21
+ export declare function canUseStaticAuthTestAccountInstall({ accountConfig, appNode, parentAccountId, }: CanUseStaticAuthTestAccountInstallOptions): boolean;
22
+ export {};
@@ -0,0 +1,42 @@
1
+ import { APP_AUTH_TYPES, APP_DISTRIBUTION_TYPES, APP_INSTALLATION_STATES, } from '../constants.js';
2
+ import { isDeveloperTestAccount, isSandbox } from '../accountTypes.js';
3
+ import { isAppIRNode } from '../projects/structure.js';
4
+ export function getAppNodeFromProjectNodes(projectNodes) {
5
+ return Object.values(projectNodes).find(isAppIRNode) || null;
6
+ }
7
+ export function isStaticAuthApp(appNode) {
8
+ return appNode?.config.auth.type.toLowerCase() === APP_AUTH_TYPES.STATIC;
9
+ }
10
+ export function isOAuthApp(appNode) {
11
+ return appNode?.config.auth.type.toLowerCase() === APP_AUTH_TYPES.OAUTH;
12
+ }
13
+ export function isPrivateApp(appNode) {
14
+ return (appNode?.config.distribution?.toLowerCase() ===
15
+ APP_DISTRIBUTION_TYPES.PRIVATE);
16
+ }
17
+ export function isMarketplaceApp(appNode) {
18
+ return (appNode?.config.distribution?.toLowerCase() ===
19
+ APP_DISTRIBUTION_TYPES.MARKETPLACE);
20
+ }
21
+ export function getAppInstallationState(isInstalledWithScopeGroups, previouslyAuthorizedScopeGroups) {
22
+ if (isInstalledWithScopeGroups) {
23
+ return APP_INSTALLATION_STATES.INSTALLED;
24
+ }
25
+ if (previouslyAuthorizedScopeGroups.length > 0) {
26
+ return APP_INSTALLATION_STATES.INSTALLED_WITH_OUTDATED_SCOPES;
27
+ }
28
+ return APP_INSTALLATION_STATES.NOT_INSTALLED;
29
+ }
30
+ export function canUseStaticAuthTestAccountInstall({ accountConfig, appNode, parentAccountId, }) {
31
+ if (!accountConfig || !isStaticAuthApp(appNode)) {
32
+ return false;
33
+ }
34
+ const accountCanUseTestInstall = isDeveloperTestAccount(accountConfig) || isSandbox(accountConfig);
35
+ if (!accountCanUseTestInstall) {
36
+ return false;
37
+ }
38
+ if (parentAccountId === undefined) {
39
+ return true;
40
+ }
41
+ return accountConfig.parentAccountId === parentAccountId;
42
+ }
package/lib/app/logs.js CHANGED
@@ -46,7 +46,7 @@ export function parseSinceTime(sinceInput) {
46
46
  endTime: now.valueOf(),
47
47
  };
48
48
  }
49
- const isoTime = moment(sinceInput);
49
+ const isoTime = moment(sinceInput, moment.ISO_8601, true);
50
50
  if (isoTime.isValid()) {
51
51
  return {
52
52
  startTime: isoTime.valueOf(),
@@ -3,7 +3,7 @@ import { randomUUID } from 'crypto';
3
3
  import open from 'open';
4
4
  import { getHubSpotWebsiteOrigin } from '@hubspot/local-dev-lib/urls';
5
5
  import CLIWebSocketServer from '../CLIWebSocketServer.js';
6
- import { ACCOUNT_AUTH_UI_MESSAGE_SEND_TYPES, ACCOUNT_AUTH_UI_MESSAGE_RECEIVE_TYPES, ACCOUNT_AUTH_WEBSOCKET_SERVER_VERSION, } from '../constants.js';
6
+ import { ACCOUNT_AUTH_UI_MESSAGE_RECEIVE_TYPES, ACCOUNT_AUTH_UI_MESSAGE_SEND_TYPES, ACCOUNT_AUTH_WEBSOCKET_SERVER_VERSION, } from '../constants.js';
7
7
  import { personalAccessKeyPrompt } from '../prompts/personalAccessKeyPrompt.js';
8
8
  import SpinniesManager from '../ui/SpinniesManager.js';
9
9
  import { uiLogger } from '../ui/logger.js';
@@ -55,7 +55,8 @@ function createMessageHandler(server, cliCallbackToken, resolveWebsocketPak) {
55
55
  }
56
56
  async function awaitKeypressOrWebsocketPak(websocketPakPromise) {
57
57
  if (!process.stdin.isTTY) {
58
- return { type: 'keypress' };
58
+ const pak = await websocketPakPromise;
59
+ return { type: 'pak', pak };
59
60
  }
60
61
  SpinniesManager.init();
61
62
  SpinniesManager.add(SPINNER_ID, {
@@ -17,6 +17,7 @@ export declare class Doctor {
17
17
  private checkIfNodeIsInstalled;
18
18
  private checkIfNpmIsInstalled;
19
19
  private checkCLIVersion;
20
+ private checkMcpReadiness;
20
21
  private checkIfNpmInstallRequired;
21
22
  private isValidJsonFile;
22
23
  private checkProjectConfigJsonFiles;
@@ -23,6 +23,8 @@ import { pkg } from '../jsonLoader.js';
23
23
  import { lib } from '../../lang/en.js';
24
24
  import { uiLink } from '../ui/index.js';
25
25
  import { isServerRunningAtUrl } from '../http.js';
26
+ import { detectConfiguredMcpClients } from '../mcp/promotion.js';
27
+ import { MCP_CLIENTS } from '../mcp/clients.js';
26
28
  import { WEBHOOKS_KEY, APP_KEY } from '@hubspot/project-parsing-lib/constants';
27
29
  import { validateProjectConfig } from '../projects/config.js';
28
30
  import { validateSourceDirectory, handleTranslate, } from '../projects/upload.js';
@@ -73,6 +75,7 @@ export class Doctor {
73
75
  this.checkIfNodeIsInstalled(),
74
76
  this.checkIfNpmIsInstalled(),
75
77
  this.checkCLIVersion(),
78
+ this.checkMcpReadiness(),
76
79
  ];
77
80
  }
78
81
  performNetworkingChecks() {
@@ -252,6 +255,29 @@ export class Doctor {
252
255
  });
253
256
  }
254
257
  }
258
+ async checkMcpReadiness() {
259
+ try {
260
+ const configuredClients = detectConfiguredMcpClients();
261
+ const allClientIds = MCP_CLIENTS.map(c => c.id);
262
+ const unconfiguredClients = allClientIds.filter(id => !configuredClients.includes(id));
263
+ if (configuredClients.length > 0) {
264
+ this.diagnosis?.addCliSection({
265
+ type: 'success',
266
+ message: lib.doctor.mcpChecks.configured(configuredClients),
267
+ });
268
+ }
269
+ if (unconfiguredClients.length > 0) {
270
+ this.diagnosis?.addCliSection({
271
+ type: 'warning',
272
+ message: lib.doctor.mcpChecks.notConfigured(unconfiguredClients),
273
+ secondaryMessaging: lib.doctor.mcpChecks.notConfiguredSecondary,
274
+ });
275
+ }
276
+ }
277
+ catch (e) {
278
+ uiLogger.debug(getErrorMessage(e));
279
+ }
280
+ }
255
281
  async checkIfNpmInstallRequired() {
256
282
  let foundError = false;
257
283
  for (const packageFile of this.diagnosticInfo?.packageFiles || []) {
@@ -0,0 +1,13 @@
1
+ export declare const MCP_SERVER_NAME = "HubSpotDev";
2
+ export type McpClientId = 'codex' | 'claude' | 'cursor' | 'gemini' | 'vscode' | 'windsurf';
3
+ type McpClientDetection = {
4
+ type: 'json' | 'text';
5
+ pathSegments: string[];
6
+ };
7
+ export type McpClient = {
8
+ id: McpClientId;
9
+ detection: McpClientDetection;
10
+ };
11
+ export declare const MCP_CLIENTS: McpClient[];
12
+ export declare function getMcpClientPathSegments(id: McpClientId): string[];
13
+ export {};
@@ -0,0 +1,55 @@
1
+ export const MCP_SERVER_NAME = 'HubSpotDev';
2
+ function getVsCodeMcpConfigPathSegments() {
3
+ if (process.platform === 'darwin') {
4
+ return ['Library', 'Application Support', 'Code', 'User', 'mcp.json'];
5
+ }
6
+ if (process.platform === 'win32') {
7
+ return ['AppData', 'Roaming', 'Code', 'User', 'mcp.json'];
8
+ }
9
+ return ['.config', 'Code', 'User', 'mcp.json'];
10
+ }
11
+ export const MCP_CLIENTS = [
12
+ {
13
+ id: 'codex',
14
+ detection: {
15
+ type: 'text',
16
+ pathSegments: ['.codex', 'config.toml'],
17
+ },
18
+ },
19
+ {
20
+ id: 'claude',
21
+ detection: { type: 'json', pathSegments: ['.claude.json'] },
22
+ },
23
+ {
24
+ id: 'cursor',
25
+ detection: { type: 'json', pathSegments: ['.cursor', 'mcp.json'] },
26
+ },
27
+ {
28
+ id: 'gemini',
29
+ detection: {
30
+ type: 'json',
31
+ pathSegments: ['.gemini', 'settings.json'],
32
+ },
33
+ },
34
+ {
35
+ id: 'vscode',
36
+ detection: {
37
+ type: 'json',
38
+ pathSegments: getVsCodeMcpConfigPathSegments(),
39
+ },
40
+ },
41
+ {
42
+ id: 'windsurf',
43
+ detection: {
44
+ type: 'json',
45
+ pathSegments: ['.codeium', 'windsurf', 'mcp_config.json'],
46
+ },
47
+ },
48
+ ];
49
+ export function getMcpClientPathSegments(id) {
50
+ const client = MCP_CLIENTS.find(c => c.id === id);
51
+ if (!client) {
52
+ return [];
53
+ }
54
+ return client.detection.pathSegments;
55
+ }
@@ -0,0 +1,3 @@
1
+ export declare function detectConfiguredMcpClients(): string[];
2
+ export declare function shouldShowMcpPromotion(): Promise<boolean>;
3
+ export declare function showMcpPromotionNudge(commandName: string): Promise<void>;
@@ -0,0 +1,109 @@
1
+ import fs from 'fs-extra';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { getStateValue, setStateValue, } from '@hubspot/local-dev-lib/config/state';
5
+ import { STATE_FLAGS } from '@hubspot/local-dev-lib/constants/config';
6
+ import { commands } from '../../lang/en.js';
7
+ import { debugError } from '../errorHandlers/index.js';
8
+ import { uiLogger } from '../ui/logger.js';
9
+ import { trackMcpPromotionShown } from '../usageTracking.js';
10
+ import { MCP_CLIENTS, MCP_SERVER_NAME } from './clients.js';
11
+ const ONE_WEEK_IN_MS = 7 * 24 * 60 * 60 * 1000;
12
+ function isRecord(value) {
13
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
14
+ }
15
+ function configContainsMcpServer(value) {
16
+ if (typeof value === 'string') {
17
+ return value === MCP_SERVER_NAME;
18
+ }
19
+ if (Array.isArray(value)) {
20
+ return value.some(configContainsMcpServer);
21
+ }
22
+ if (!isRecord(value)) {
23
+ return false;
24
+ }
25
+ return Object.entries(value).some(([key, childValue]) => {
26
+ return key === MCP_SERVER_NAME || configContainsMcpServer(childValue);
27
+ });
28
+ }
29
+ function textConfigContainsMcpServer(configContent) {
30
+ return configContent.includes(MCP_SERVER_NAME);
31
+ }
32
+ function jsonConfigContainsMcpServer(configContent) {
33
+ const parsedConfig = JSON.parse(configContent);
34
+ return configContainsMcpServer(parsedConfig);
35
+ }
36
+ export function detectConfiguredMcpClients() {
37
+ const homeDirectory = os.homedir();
38
+ return MCP_CLIENTS.reduce((clients, client) => {
39
+ try {
40
+ const configPath = path.join(homeDirectory, ...client.detection.pathSegments);
41
+ if (!fs.existsSync(configPath)) {
42
+ return clients;
43
+ }
44
+ const configContent = fs.readFileSync(configPath, 'utf8');
45
+ const hasMcpServer = client.detection.type === 'json'
46
+ ? jsonConfigContainsMcpServer(configContent)
47
+ : textConfigContainsMcpServer(configContent);
48
+ if (hasMcpServer) {
49
+ clients.push(client.id);
50
+ }
51
+ }
52
+ catch (e) {
53
+ debugError(e);
54
+ return clients;
55
+ }
56
+ return clients;
57
+ }, []);
58
+ }
59
+ function isScriptSafeSuppressed() {
60
+ return Boolean(process.env.CI || process.env.HUBSPOT_MCP_AI_AGENT || !process.stdout.isTTY);
61
+ }
62
+ function hasRecentPromotion(lastShownAt, now, cooldownMs) {
63
+ if (!lastShownAt) {
64
+ return false;
65
+ }
66
+ const lastShownTime = new Date(lastShownAt).getTime();
67
+ if (Number.isNaN(lastShownTime)) {
68
+ return false;
69
+ }
70
+ return now.getTime() - lastShownTime < cooldownMs;
71
+ }
72
+ function getMcpPromotionLastShownAt() {
73
+ return getStateValue(STATE_FLAGS.MCP_PROMOTION_LAST_SHOWN_AT);
74
+ }
75
+ export async function shouldShowMcpPromotion() {
76
+ if (isScriptSafeSuppressed()) {
77
+ return false;
78
+ }
79
+ try {
80
+ const mcpTotalToolCalls = getStateValue(STATE_FLAGS.MCP_TOTAL_TOOL_CALLS);
81
+ if (typeof mcpTotalToolCalls === 'number' && mcpTotalToolCalls > 0) {
82
+ return false;
83
+ }
84
+ if (detectConfiguredMcpClients().length > 0) {
85
+ return false;
86
+ }
87
+ return !hasRecentPromotion(getMcpPromotionLastShownAt(), new Date(), ONE_WEEK_IN_MS);
88
+ }
89
+ catch (e) {
90
+ debugError(e);
91
+ return false;
92
+ }
93
+ }
94
+ function setMcpPromotionShownAt() {
95
+ setStateValue(STATE_FLAGS.MCP_PROMOTION_LAST_SHOWN_AT, new Date().toISOString());
96
+ }
97
+ export async function showMcpPromotionNudge(commandName) {
98
+ try {
99
+ if (!(await shouldShowMcpPromotion())) {
100
+ return;
101
+ }
102
+ setMcpPromotionShownAt();
103
+ uiLogger.info(commands.mcp.promotion.activeNudge);
104
+ await trackMcpPromotionShown(commandName || undefined);
105
+ }
106
+ catch (e) {
107
+ debugError(e);
108
+ }
109
+ }
@@ -1,13 +1,14 @@
1
+ import { McpClientId } from './clients.js';
1
2
  export declare const supportedTools: {
2
3
  name: string;
3
- value: string;
4
+ value: McpClientId;
4
5
  }[];
5
6
  interface McpCommand {
6
7
  command: string;
7
8
  args: string[];
8
9
  env?: Record<string, string>;
9
10
  }
10
- export declare function addMcpServerToConfig(targets: string[] | undefined): Promise<string[]>;
11
+ export declare function configureMcpServer(targets: string[] | undefined): Promise<string[]>;
11
12
  export declare function setupVsCode(mcpCommand?: McpCommand): Promise<boolean>;
12
13
  export declare function setupClaudeCode(mcpCommand?: McpCommand): Promise<boolean>;
13
14
  export declare function setupCursor(mcpCommand?: McpCommand): boolean;
package/lib/mcp/setup.js CHANGED
@@ -4,30 +4,35 @@ import { promptUser } from '../prompts/promptUtils.js';
4
4
  import SpinniesManager from '../ui/SpinniesManager.js';
5
5
  import { logError, getErrorMessage } from '../errorHandlers/index.js';
6
6
  import { execAsync } from '../../mcp-server/utils/command.js';
7
+ import { MCP_CLIENTS, MCP_SERVER_NAME, getMcpClientPathSegments, } from './clients.js';
7
8
  import path from 'path';
8
9
  import os from 'os';
9
10
  import fs from 'fs-extra';
10
11
  import { existsSync } from 'fs';
11
- const mcpServerName = 'HubSpotDev';
12
+ const mcpServerName = MCP_SERVER_NAME;
12
13
  const claudeCode = 'claude';
13
14
  const windsurf = 'windsurf';
14
15
  const cursor = 'cursor';
15
16
  const vscode = 'vscode';
16
17
  const codex = 'codex';
17
18
  const gemini = 'gemini';
18
- export const supportedTools = [
19
- { name: commands.mcp.setup.codex, value: codex },
20
- { name: commands.mcp.setup.claudeCode, value: claudeCode },
21
- { name: commands.mcp.setup.cursor, value: cursor },
22
- { name: commands.mcp.setup.gemini, value: gemini },
23
- { name: commands.mcp.setup.vsCode, value: vscode },
24
- { name: commands.mcp.setup.windsurf, value: windsurf },
25
- ];
19
+ const clientLabels = {
20
+ codex: commands.mcp.setup.codex,
21
+ claude: commands.mcp.setup.claudeCode,
22
+ cursor: commands.mcp.setup.cursor,
23
+ gemini: commands.mcp.setup.gemini,
24
+ vscode: commands.mcp.setup.vsCode,
25
+ windsurf: commands.mcp.setup.windsurf,
26
+ };
27
+ export const supportedTools = MCP_CLIENTS.map(client => ({
28
+ name: clientLabels[client.id],
29
+ value: client.id,
30
+ }));
26
31
  const defaultMcpCommand = {
27
32
  command: 'hs',
28
33
  args: ['mcp', 'start'],
29
34
  };
30
- export async function addMcpServerToConfig(targets) {
35
+ export async function configureMcpServer(targets) {
31
36
  try {
32
37
  let derivedTargets = [];
33
38
  if (!targets || targets.length === 0) {
@@ -247,7 +252,7 @@ export async function setupClaudeCode(mcpCommand = defaultMcpCommand) {
247
252
  }
248
253
  }
249
254
  export function setupCursor(mcpCommand = defaultMcpCommand) {
250
- const cursorConfigPath = path.join(os.homedir(), '.cursor', 'mcp.json');
255
+ const cursorConfigPath = path.join(os.homedir(), ...getMcpClientPathSegments(cursor));
251
256
  return setupMcpConfigFile({
252
257
  configPath: cursorConfigPath,
253
258
  configuringMessage: commands.mcp.setup.spinners.configuringCursor,
@@ -257,7 +262,7 @@ export function setupCursor(mcpCommand = defaultMcpCommand) {
257
262
  });
258
263
  }
259
264
  export function setupWindsurf(mcpCommand = defaultMcpCommand) {
260
- const windsurfConfigPath = path.join(os.homedir(), '.codeium', 'windsurf', 'mcp_config.json');
265
+ const windsurfConfigPath = path.join(os.homedir(), ...getMcpClientPathSegments(windsurf));
261
266
  return setupMcpConfigFile({
262
267
  configPath: windsurfConfigPath,
263
268
  configuringMessage: commands.mcp.setup.spinners.configuringWindsurf,
@@ -0,0 +1,58 @@
1
+ import type { HubSpotConfigAccount } from '@hubspot/local-dev-lib/types/Accounts';
2
+ import type { AppInstallationState, AppIRNode } from '../../types/ProjectComponents.js';
3
+ import type { ProjectConfig } from '../../types/Projects.js';
4
+ import { type ScopeGroup } from '../app/install.js';
5
+ export type AppInstallationData = {
6
+ appId?: number;
7
+ isInstalledWithScopeGroups: boolean;
8
+ previouslyAuthorizedScopeGroups: ScopeGroup[];
9
+ };
10
+ export type AppMetadata = {
11
+ appId: number;
12
+ scopeGroupIds: number[];
13
+ ownerPortalId: number;
14
+ };
15
+ type ConfirmInstallAppOptions = {
16
+ appName: string;
17
+ targetAccountId: number;
18
+ force: boolean;
19
+ needsReinstall: boolean;
20
+ };
21
+ type InstallStaticAuthAppOptions = {
22
+ appId: number;
23
+ appNode: AppIRNode;
24
+ appName: string;
25
+ targetAccountId: number;
26
+ targetAccountConfig?: HubSpotConfigAccount;
27
+ projectId: number;
28
+ projectName: string;
29
+ scopeGroupIds: number[];
30
+ ownerPortalId: number;
31
+ installationState: AppInstallationState;
32
+ formatOutputAsJson: boolean;
33
+ };
34
+ type UploadAndDeployOptions = {
35
+ accountId: number;
36
+ projectConfig: ProjectConfig;
37
+ projectDir: string;
38
+ profile: string | undefined;
39
+ force: boolean;
40
+ formatOutputAsJson: boolean;
41
+ };
42
+ export declare function resolveProjectAccountId(derivedAccountId: number, projectConfig: ProjectConfig, projectDir: string, profileOption: string | undefined, useEnvOption: boolean): Promise<{
43
+ accountId: number;
44
+ profileName?: string;
45
+ } | null>;
46
+ export declare function loadInstallableAppNode(projectConfig: ProjectConfig, projectDir: string, accountId: number): Promise<AppIRNode | null>;
47
+ export declare function resolveValidInstallAccount(targetAccountId: number, force: boolean, formatOutputAsJson: boolean): Promise<number | null>;
48
+ export declare function resolveProjectId(options: UploadAndDeployOptions): Promise<number | null>;
49
+ export declare function resolveAppMetadata(options: UploadAndDeployOptions & {
50
+ appId: number | undefined;
51
+ projectId: number;
52
+ appUid: string;
53
+ appName: string;
54
+ }): Promise<AppMetadata | null>;
55
+ export declare function fetchProjectAppInstallationData(targetAccountId: number, projectId: number, appNode: AppIRNode, projectConfig: ProjectConfig): Promise<AppInstallationData | null>;
56
+ export declare function confirmInstallAppAction({ appName, targetAccountId, force, needsReinstall, }: ConfirmInstallAppOptions): Promise<boolean>;
57
+ export declare function installStaticAuthAppForAccount({ appId, appNode, appName, targetAccountId, targetAccountConfig, projectId, projectName, scopeGroupIds, ownerPortalId, installationState, formatOutputAsJson, }: InstallStaticAuthAppOptions): Promise<boolean>;
58
+ export {};