workos 0.17.0 → 0.18.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.
Files changed (81) hide show
  1. package/README.md +3 -4
  2. package/dist/bin.js +54 -1
  3. package/dist/bin.js.map +1 -1
  4. package/dist/cli.config.d.ts +0 -2
  5. package/dist/cli.config.js +3 -3
  6. package/dist/cli.config.js.map +1 -1
  7. package/dist/commands/debug.d.ts +11 -0
  8. package/dist/commands/debug.js +24 -8
  9. package/dist/commands/debug.js.map +1 -1
  10. package/dist/commands/install.js +4 -0
  11. package/dist/commands/install.js.map +1 -1
  12. package/dist/commands/mcp.d.ts +14 -0
  13. package/dist/commands/mcp.js +108 -0
  14. package/dist/commands/mcp.js.map +1 -0
  15. package/dist/doctor/checks/ai-analysis.js +2 -1
  16. package/dist/doctor/checks/ai-analysis.js.map +1 -1
  17. package/dist/doctor/checks/auth-patterns.js +1 -1
  18. package/dist/doctor/checks/auth-patterns.js.map +1 -1
  19. package/dist/doctor/checks/mcp.d.ts +15 -0
  20. package/dist/doctor/checks/mcp.js +34 -0
  21. package/dist/doctor/checks/mcp.js.map +1 -0
  22. package/dist/doctor/index.js +7 -1
  23. package/dist/doctor/index.js.map +1 -1
  24. package/dist/doctor/issues.js +16 -0
  25. package/dist/doctor/issues.js.map +1 -1
  26. package/dist/doctor/output.js +16 -2
  27. package/dist/doctor/output.js.map +1 -1
  28. package/dist/doctor/types.d.ts +17 -0
  29. package/dist/doctor/types.js.map +1 -1
  30. package/dist/emulate/workos/index.js.map +1 -1
  31. package/dist/lib/agent-interface.js +4 -4
  32. package/dist/lib/agent-interface.js.map +1 -1
  33. package/dist/lib/agent-runner.js +55 -16
  34. package/dist/lib/agent-runner.js.map +1 -1
  35. package/dist/lib/ai-content.js +2 -1
  36. package/dist/lib/ai-content.js.map +1 -1
  37. package/dist/lib/constants.d.ts +9 -0
  38. package/dist/lib/constants.js +9 -0
  39. package/dist/lib/constants.js.map +1 -1
  40. package/dist/lib/installer-core.d.ts +20 -20
  41. package/dist/lib/mcp-clients.d.ts +39 -0
  42. package/dist/lib/mcp-clients.js +284 -0
  43. package/dist/lib/mcp-clients.js.map +1 -0
  44. package/dist/lib/mcp-notice.d.ts +72 -0
  45. package/dist/lib/mcp-notice.js +275 -0
  46. package/dist/lib/mcp-notice.js.map +1 -0
  47. package/dist/lib/preferences.d.ts +14 -0
  48. package/dist/lib/preferences.js +3 -0
  49. package/dist/lib/preferences.js.map +1 -1
  50. package/dist/lib/run-with-core.js +2 -1
  51. package/dist/lib/run-with-core.js.map +1 -1
  52. package/dist/lib/settings.d.ts +5 -14
  53. package/dist/lib/settings.js +5 -16
  54. package/dist/lib/settings.js.map +1 -1
  55. package/dist/lib/startup-notice-gate.d.ts +21 -0
  56. package/dist/lib/startup-notice-gate.js +29 -0
  57. package/dist/lib/startup-notice-gate.js.map +1 -0
  58. package/dist/lib/telemetry-notice.js +3 -0
  59. package/dist/lib/telemetry-notice.js.map +1 -1
  60. package/dist/lib/unclaimed-warning.js +3 -0
  61. package/dist/lib/unclaimed-warning.js.map +1 -1
  62. package/dist/lib/validation/security-checks.d.ts +35 -0
  63. package/dist/lib/validation/security-checks.js +105 -0
  64. package/dist/lib/validation/security-checks.js.map +1 -0
  65. package/dist/utils/analytics.d.ts +1 -0
  66. package/dist/utils/analytics.js +5 -1
  67. package/dist/utils/analytics.js.map +1 -1
  68. package/dist/utils/help-json.js +38 -0
  69. package/dist/utils/help-json.js.map +1 -1
  70. package/dist/utils/interaction-mode.d.ts +1 -1
  71. package/dist/utils/interaction-mode.js +0 -3
  72. package/dist/utils/interaction-mode.js.map +1 -1
  73. package/dist/utils/output.d.ts +5 -3
  74. package/dist/utils/output.js +5 -5
  75. package/dist/utils/output.js.map +1 -1
  76. package/dist/utils/telemetry-types.d.ts +1 -1
  77. package/dist/utils/telemetry-types.js.map +1 -1
  78. package/dist/utils/urls.d.ts +9 -3
  79. package/dist/utils/urls.js +10 -4
  80. package/dist/utils/urls.js.map +1 -1
  81. package/package.json +12 -11
@@ -1 +1 @@
1
- {"version":3,"file":"constants.js","sourceRoot":"","sources":["../../src/lib/constants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAS1C;;;GAGG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,MAAM,EAAE,QAAQ;IAChB,KAAK,EAAE,OAAO;IACd,aAAa,EAAE,gBAAgB;IAC/B,WAAW,EAAE,cAAc;IAC3B,SAAS,EAAE,YAAY;CACf,CAAC;AAOX,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC;AAEnF,MAAM,QAAQ,GAAG,SAAS,EAAE,CAAC;AAE7B,MAAM,CAAC,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAC,SAAS,CAAC;AAChD,MAAM,CAAC,MAAM,eAAe,GAAG,QAAQ,CAAC,aAAa,CAAC,aAAa,CAAC;AACpE,MAAM,CAAC,MAAM,oBAAoB,GAAG,QAAQ,CAAC,aAAa,CAAC,YAAY,CAAC;AACxE,MAAM,CAAC,MAAM,UAAU,GAAG,QAAQ,CAAC,aAAa,CAAC,SAAS,CAAC;AAC3D,MAAM,CAAC,MAAM,iBAAiB,GAAG,QAAQ,CAAC,SAAS,CAAC,OAAO,CAAC;AAC5D,MAAM,CAAC,MAAM,gCAAgC,GAAG,QAAQ,CAAC,SAAS,CAAC,SAAS,CAAC;AAC7E,MAAM,CAAC,MAAM,UAAU,GAAG,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC;AAEpD;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAa;IACvC,oBAAoB;IACpB,YAAY;IACZ,aAAa;IACb,cAAc;IACd,aAAa;CACd,CAAC","sourcesContent":["import { getConfig } from './settings.js';\n\n/**\n * Integration identifier type.\n * No longer an enum — each integration self-registers via the auto-discovery registry.\n * The string value matches the integration directory name (e.g., 'nextjs', 'react-router').\n */\nexport type Integration = string;\n\n/**\n * Well-known integration names for backwards compatibility.\n * New integrations do NOT need to be added here — they're auto-discovered.\n */\nexport const KNOWN_INTEGRATIONS = {\n nextjs: 'nextjs',\n react: 'react',\n tanstackStart: 'tanstack-start',\n reactRouter: 'react-router',\n vanillaJs: 'vanilla-js',\n} as const;\n\nexport interface Args {\n debug: boolean;\n integration: Integration;\n}\n\nexport const IS_DEV = ['test', 'development'].includes(process.env.NODE_ENV ?? '');\n\nconst settings = getConfig();\n\nexport const DEBUG = settings.logging.debugMode;\nexport const WORKOS_DOCS_URL = settings.documentation.workosDocsUrl;\nexport const WORKOS_DASHBOARD_URL = settings.documentation.dashboardUrl;\nexport const ISSUES_URL = settings.documentation.issuesUrl;\nexport const ANALYTICS_ENABLED = settings.telemetry.enabled;\nexport const INSTALLER_INTERACTION_EVENT_NAME = settings.telemetry.eventName;\nexport const OAUTH_PORT = settings.legacy.oauthPort;\n\n/**\n * Common glob patterns to ignore when searching for files.\n * Used by multiple integrations.\n */\nexport const IGNORE_PATTERNS: string[] = [\n '**/node_modules/**',\n '**/dist/**',\n '**/build/**',\n '**/public/**',\n '**/.next/**',\n];\n"]}
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../../src/lib/constants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAS1C;;;GAGG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,MAAM,EAAE,QAAQ;IAChB,KAAK,EAAE,OAAO;IACd,aAAa,EAAE,gBAAgB;IAC/B,WAAW,EAAE,cAAc;IAC3B,SAAS,EAAE,YAAY;CACf,CAAC;AAOX,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC;AAEnF,MAAM,QAAQ,GAAG,SAAS,EAAE,CAAC;AAE7B,MAAM,CAAC,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAC,SAAS,CAAC;AAChD,MAAM,CAAC,MAAM,eAAe,GAAG,QAAQ,CAAC,aAAa,CAAC,aAAa,CAAC;AACpE,MAAM,CAAC,MAAM,oBAAoB,GAAG,QAAQ,CAAC,aAAa,CAAC,YAAY,CAAC;AACxE,MAAM,CAAC,MAAM,UAAU,GAAG,QAAQ,CAAC,aAAa,CAAC,SAAS,CAAC;AAC3D,MAAM,CAAC,MAAM,iBAAiB,GAAG,QAAQ,CAAC,SAAS,CAAC,OAAO,CAAC;AAC5D,MAAM,CAAC,MAAM,gCAAgC,GAAG,QAAQ,CAAC,SAAS,CAAC,SAAS,CAAC;AAC7E,MAAM,CAAC,MAAM,UAAU,GAAG,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC;AAEpD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,QAAQ,CAAC;AACxC,MAAM,CAAC,MAAM,cAAc,GAAG,4BAA4B,CAAC;AAE3D;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAa;IACvC,oBAAoB;IACpB,YAAY;IACZ,aAAa;IACb,cAAc;IACd,aAAa;CACd,CAAC","sourcesContent":["import { getConfig } from './settings.js';\n\n/**\n * Integration identifier type.\n * No longer an enum — each integration self-registers via the auto-discovery registry.\n * The string value matches the integration directory name (e.g., 'nextjs', 'react-router').\n */\nexport type Integration = string;\n\n/**\n * Well-known integration names for backwards compatibility.\n * New integrations do NOT need to be added here — they're auto-discovered.\n */\nexport const KNOWN_INTEGRATIONS = {\n nextjs: 'nextjs',\n react: 'react',\n tanstackStart: 'tanstack-start',\n reactRouter: 'react-router',\n vanillaJs: 'vanilla-js',\n} as const;\n\nexport interface Args {\n debug: boolean;\n integration: Integration;\n}\n\nexport const IS_DEV = ['test', 'development'].includes(process.env.NODE_ENV ?? '');\n\nconst settings = getConfig();\n\nexport const DEBUG = settings.logging.debugMode;\nexport const WORKOS_DOCS_URL = settings.documentation.workosDocsUrl;\nexport const WORKOS_DASHBOARD_URL = settings.documentation.dashboardUrl;\nexport const ISSUES_URL = settings.documentation.issuesUrl;\nexport const ANALYTICS_ENABLED = settings.telemetry.enabled;\nexport const INSTALLER_INTERACTION_EVENT_NAME = settings.telemetry.eventName;\nexport const OAUTH_PORT = settings.legacy.oauthPort;\n\n/**\n * WorkOS MCP server identity, shared by the `mcp` command group and the\n * per-client writers in `lib/mcp-clients.ts`.\n *\n * The server is secret-free: HTTP transport with OAuth handled by each client\n * on first connect, so configuring it never requires CLI auth.\n */\nexport const MCP_SERVER_NAME = 'workos';\nexport const MCP_SERVER_URL = 'https://mcp.workos.com/mcp';\n\n/**\n * Common glob patterns to ignore when searching for files.\n * Used by multiple integrations.\n */\nexport const IGNORE_PATTERNS: string[] = [\n '**/node_modules/**',\n '**/dist/**',\n '**/build/**',\n '**/public/**',\n '**/.next/**',\n];\n"]}
@@ -42,54 +42,54 @@ export declare const installerMachine: import("xstate").StateMachine<InstallerMa
42
42
  } | {
43
43
  type: "PR_DECLINED";
44
44
  }, {
45
- [x: string]: import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<string, {
46
- title: string;
47
- body: string;
48
- cwd: string;
49
- }, import("xstate").EventObject>> | import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<boolean, {
45
+ [x: string]: import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<boolean, {
50
46
  options: InstallerOptions;
51
- }, import("xstate").EventObject>> | import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<WorkspaceCheckOutput, {
47
+ }, import("xstate").EventObject> | import("xstate").PromiseActorLogic<WorkspaceCheckOutput, {
52
48
  options: InstallerOptions;
53
- }, import("xstate").EventObject>> | import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<void, {
49
+ }, import("xstate").EventObject> | import("xstate").PromiseActorLogic<void, {
54
50
  context: InstallerMachineContext;
55
- }, import("xstate").EventObject>> | import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<DetectionOutput, {
51
+ }, import("xstate").EventObject> | import("xstate").PromiseActorLogic<DetectionOutput, {
56
52
  options: InstallerOptions;
57
- }, import("xstate").EventObject>> | import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<GitCheckOutput, {
53
+ }, import("xstate").EventObject> | import("xstate").PromiseActorLogic<GitCheckOutput, {
58
54
  installDir: string;
59
- }, import("xstate").EventObject>> | import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<void, {
55
+ }, import("xstate").EventObject> | import("xstate").PromiseActorLogic<void, {
60
56
  context: InstallerMachineContext;
61
- }, import("xstate").EventObject>> | import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<AgentOutput, {
57
+ }, import("xstate").EventObject> | import("xstate").PromiseActorLogic<AgentOutput, {
62
58
  context: InstallerMachineContext;
63
- }, import("xstate").EventObject>> | import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<EnvFileInfo, {
59
+ }, import("xstate").EventObject> | import("xstate").PromiseActorLogic<EnvFileInfo, {
64
60
  installDir: string;
65
- }, import("xstate").EventObject>> | import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<DiscoveryResult, {
61
+ }, import("xstate").EventObject> | import("xstate").PromiseActorLogic<DiscoveryResult, {
66
62
  installDir: string;
67
- }, import("xstate").EventObject>> | import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<boolean, void, import("xstate").EventObject>> | import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<{
63
+ }, import("xstate").EventObject> | import("xstate").PromiseActorLogic<boolean, void, import("xstate").EventObject> | import("xstate").PromiseActorLogic<{
68
64
  result: DeviceAuthResult;
69
65
  deviceAuth: DeviceAuthResponse;
70
66
  }, {
71
67
  emitter: InstallerMachineContext["emitter"];
72
- }, import("xstate").EventObject>> | import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<StagingCredentials, void, import("xstate").EventObject>> | import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<BranchCheckOutput, void, import("xstate").EventObject>> | import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<{
68
+ }, import("xstate").EventObject> | import("xstate").PromiseActorLogic<StagingCredentials, void, import("xstate").EventObject> | import("xstate").PromiseActorLogic<BranchCheckOutput, void, import("xstate").EventObject> | import("xstate").PromiseActorLogic<{
73
69
  branch: string;
74
70
  }, {
75
71
  name: string;
76
72
  fallbackName: string;
77
- }, import("xstate").EventObject>> | import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<{
73
+ }, import("xstate").EventObject> | import("xstate").PromiseActorLogic<{
78
74
  hasChanges: boolean;
79
75
  files: string[];
80
- }, void, import("xstate").EventObject>> | import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<string, {
76
+ }, void, import("xstate").EventObject> | import("xstate").PromiseActorLogic<string, {
81
77
  integration: string;
82
78
  files: string[];
83
79
  direct?: boolean;
84
- }, import("xstate").EventObject>> | import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<void, {
80
+ }, import("xstate").EventObject> | import("xstate").PromiseActorLogic<void, {
85
81
  message: string;
86
82
  cwd: string;
87
- }, import("xstate").EventObject>> | import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<string, {
83
+ }, import("xstate").EventObject> | import("xstate").PromiseActorLogic<string, {
88
84
  integration: string;
89
85
  files: string[];
90
86
  commitMessage: string;
91
87
  direct?: boolean;
92
- }, import("xstate").EventObject>> | import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<void, {
88
+ }, import("xstate").EventObject> | import("xstate").PromiseActorLogic<void, {
89
+ cwd: string;
90
+ }, import("xstate").EventObject> | import("xstate").PromiseActorLogic<string, {
91
+ title: string;
92
+ body: string;
93
93
  cwd: string;
94
94
  }, import("xstate").EventObject>> | undefined;
95
95
  }, {
@@ -0,0 +1,39 @@
1
+ export type McpAgentKey = 'claude-code' | 'codex' | 'cursor';
2
+ export type McpOutcome = 'installed' | 'already-installed' | 'removed' | 'not-installed' | 'skipped' | 'failed';
3
+ export interface McpClientResult {
4
+ agent: McpAgentKey;
5
+ displayName: string;
6
+ outcome: McpOutcome;
7
+ /** stderr/message excerpt when `outcome === 'failed'`. */
8
+ error?: string;
9
+ }
10
+ export interface McpClientTarget {
11
+ key: McpAgentKey;
12
+ displayName: string;
13
+ /** Agent is usable on this machine (config dir present AND, for CLI clients, binary runnable). */
14
+ isAvailable(): Promise<boolean>;
15
+ /** The WorkOS server is present in this client's config. */
16
+ isInstalled(): Promise<boolean>;
17
+ add(): Promise<McpClientResult>;
18
+ remove(): Promise<McpClientResult>;
19
+ }
20
+ /** Stable list of known agent keys, for `--agent` validation by callers. */
21
+ export declare const MCP_AGENT_KEYS: McpAgentKey[];
22
+ /** The three client targets, in a stable order. */
23
+ export declare function createMcpClients(): McpClientTarget[];
24
+ /**
25
+ * The URL the WorkOS server is configured with in Cursor's `~/.cursor/mcp.json`,
26
+ * or null when the file/entry is absent, unreadable, or unparseable.
27
+ *
28
+ * Cursor is the only client whose config we read directly (the CLI clients don't
29
+ * expose per-entry URLs), so this powers doctor's URL-drift ("misconfigured")
30
+ * check without a second jsonc reader duplicating the config schema. Read-only
31
+ * and never throws — a problem reading just yields null.
32
+ */
33
+ export declare function getCursorConfiguredUrl(): Promise<string | null>;
34
+ /**
35
+ * Return only the clients available on this machine, optionally narrowed to
36
+ * `agentFilter` keys. Availability is probed in parallel. An unknown filter key
37
+ * simply matches nothing here — callers validate keys and surface the error.
38
+ */
39
+ export declare function detectMcpClients(agentFilter?: string[]): Promise<McpClientTarget[]>;
@@ -0,0 +1,284 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+ import { access, mkdir, readFile, writeFile } from 'fs/promises';
4
+ import * as jsonc from 'jsonc-parser';
5
+ import { execFileNoThrow } from '../utils/exec-file.js';
6
+ import { MCP_SERVER_NAME, MCP_SERVER_URL } from './constants.js';
7
+ /**
8
+ * Client-writer library for the WorkOS MCP server.
9
+ *
10
+ * One `McpClientTarget` interface, three implementations. Claude Code and Codex
11
+ * are configured by shelling out to their own CLIs (robust to their config
12
+ * format changes); Cursor has no CLI, so we merge `~/.cursor/mcp.json` directly
13
+ * with `jsonc-parser` (hand-edited files may contain comments / trailing
14
+ * commas). Every operation degrades to a reported outcome — a missing binary,
15
+ * an old client without HTTP-transport support, or an unparseable config is a
16
+ * `failed`/`skipped`/`not-installed` result, never a thrown error.
17
+ *
18
+ * The library owns detection and per-client config writing; callers (the `mcp`
19
+ * command today; install-flow / doctor in Phase 2) stay thin. Nothing here may
20
+ * assume it runs inside the `mcp` command.
21
+ */
22
+ /** 10s ceiling on every CLI shell-out so a hung client can't wedge the caller. */
23
+ const EXEC_TIMEOUT_MS = 10_000;
24
+ /** jsonc parse options: tolerate the comments / trailing commas real users
25
+ * hand-edit into `~/.cursor/mcp.json`, and an empty file, so only genuinely
26
+ * malformed JSON is flagged. */
27
+ const JSONC_PARSE_OPTIONS = {
28
+ allowTrailingComma: true,
29
+ allowEmptyContent: true,
30
+ };
31
+ const JSONC_MODIFY_OPTIONS = {
32
+ formattingOptions: { tabSize: 2, insertSpaces: true },
33
+ };
34
+ /** Stable list of known agent keys, for `--agent` validation by callers. */
35
+ export const MCP_AGENT_KEYS = ['claude-code', 'codex', 'cursor'];
36
+ /** Async existence check — `access` rejects with ENOENT when the path is missing. */
37
+ async function pathExists(p) {
38
+ try {
39
+ await access(p);
40
+ return true;
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ }
46
+ /** Trim a shell-out message to a single-line excerpt for the `error` field. */
47
+ function excerpt(raw) {
48
+ const text = raw.trim();
49
+ if (!text)
50
+ return 'Unknown error';
51
+ const firstLine = text.split('\n').find((l) => l.trim().length > 0) ?? text;
52
+ return firstLine.trim().slice(0, 300);
53
+ }
54
+ /**
55
+ * Does a CLI `mcp list` output contain our server?
56
+ *
57
+ * Matches the first whitespace-delimited token of any line (with an optional
58
+ * trailing colon stripped), which covers both list shapes we support:
59
+ * - Claude Code: `workos: https://mcp.workos.com/mcp (HTTP) - ✔ Connected`
60
+ * - Codex table: `workos https://mcp.workos.com/mcp ...`
61
+ *
62
+ * First-token matching avoids the substring collision a naive
63
+ * `output.includes('workos')` would hit on an unrelated `workos-docs` entry.
64
+ */
65
+ function listHasServer(stdout, name) {
66
+ return stdout.split('\n').some((line) => {
67
+ const firstToken = line.trim().split(/\s+/)[0] ?? '';
68
+ return firstToken.replace(/:$/, '') === name;
69
+ });
70
+ }
71
+ /**
72
+ * Shared shell-out client for Claude Code and Codex. The two differ only in
73
+ * binary name, config dir, and the `add` arg shape; every outcome-mapping rule
74
+ * is identical, so they share one implementation.
75
+ */
76
+ function createCliClient(config) {
77
+ const { key, displayName, binary, configDir, addArgs, removeArgs } = config;
78
+ const result = (outcome, error) => ({
79
+ agent: key,
80
+ displayName,
81
+ outcome,
82
+ ...(error ? { error } : {}),
83
+ });
84
+ async function checkInstalled() {
85
+ const res = await execFileNoThrow(binary, ['mcp', 'list'], { timeout: EXEC_TIMEOUT_MS });
86
+ if (res.status !== 0)
87
+ return false;
88
+ return listHasServer(res.stdout, MCP_SERVER_NAME);
89
+ }
90
+ return {
91
+ key,
92
+ displayName,
93
+ async isAvailable() {
94
+ // Two layers: the config dir can exist while the binary isn't on PATH,
95
+ // and vice versa — require both before we try to shell out.
96
+ if (!(await pathExists(join(homedir(), configDir))))
97
+ return false;
98
+ const res = await execFileNoThrow(binary, ['--version'], { timeout: EXEC_TIMEOUT_MS });
99
+ return res.status === 0;
100
+ },
101
+ isInstalled: checkInstalled,
102
+ async add() {
103
+ const res = await execFileNoThrow(binary, addArgs, { timeout: EXEC_TIMEOUT_MS });
104
+ if (res.status === 0)
105
+ return result('installed');
106
+ // Idempotent: an "already exists" collision is a success, not a failure.
107
+ const combined = `${res.stdout}\n${res.stderr}`.toLowerCase();
108
+ if (combined.includes('already exists') || combined.includes('already configured')) {
109
+ return result('already-installed');
110
+ }
111
+ // A non-zero exit doesn't always mean the write failed: Codex persists the
112
+ // server config and THEN starts an OAuth flow that blocks (no browser /
113
+ // callback available here) until our timeout kills it with a non-zero
114
+ // status. Confirm against the actual config before declaring failure —
115
+ // more robust than matching each client's success wording.
116
+ if (await checkInstalled())
117
+ return result('installed');
118
+ // Otherwise a real failure — an old client lacking `--transport http` /
119
+ // `--url`, a bad invocation, etc. Reported, never thrown.
120
+ return result('failed', excerpt(res.stderr || res.stdout));
121
+ },
122
+ async remove() {
123
+ const res = await execFileNoThrow(binary, removeArgs, { timeout: EXEC_TIMEOUT_MS });
124
+ const combined = `${res.stdout}\n${res.stderr}`.toLowerCase();
125
+ // Claude exits 1 and Codex exits 0 when the server is absent; both print
126
+ // "No MCP server named ...". Treat either as an idempotent no-op.
127
+ if (combined.includes('no mcp server'))
128
+ return result('not-installed');
129
+ if (res.status === 0)
130
+ return result('removed');
131
+ return result('failed', excerpt(res.stderr || res.stdout));
132
+ },
133
+ };
134
+ }
135
+ function createClaudeCodeClient() {
136
+ return createCliClient({
137
+ key: 'claude-code',
138
+ displayName: 'Claude Code',
139
+ binary: 'claude',
140
+ configDir: '.claude',
141
+ // User scope: the management MCP is account-level, not per-repo — project
142
+ // scope would nag teammates via checked-in config.
143
+ addArgs: ['mcp', 'add', '--transport', 'http', '--scope', 'user', MCP_SERVER_NAME, MCP_SERVER_URL],
144
+ removeArgs: ['mcp', 'remove', '--scope', 'user', MCP_SERVER_NAME],
145
+ });
146
+ }
147
+ function createCodexClient() {
148
+ return createCliClient({
149
+ key: 'codex',
150
+ displayName: 'Codex',
151
+ binary: 'codex',
152
+ configDir: '.codex',
153
+ addArgs: ['mcp', 'add', MCP_SERVER_NAME, '--url', MCP_SERVER_URL],
154
+ removeArgs: ['mcp', 'remove', MCP_SERVER_NAME],
155
+ });
156
+ }
157
+ /**
158
+ * Cursor has no CLI, so we read-modify-write `~/.cursor/mcp.json` directly with
159
+ * `jsonc-parser`, preserving existing servers, comments, and formatting.
160
+ */
161
+ function createCursorClient() {
162
+ const key = 'cursor';
163
+ const displayName = 'Cursor';
164
+ const cursorDir = () => join(homedir(), '.cursor');
165
+ const configPath = () => join(cursorDir(), 'mcp.json');
166
+ const result = (outcome, error) => ({
167
+ agent: key,
168
+ displayName,
169
+ outcome,
170
+ ...(error ? { error } : {}),
171
+ });
172
+ /** Read + parse the config. `parseError` is true only for genuinely malformed
173
+ * JSON (not comments / trailing commas / empty). */
174
+ async function readConfig() {
175
+ const path = configPath();
176
+ if (!(await pathExists(path))) {
177
+ return { content: '', parsed: undefined, parseError: false, exists: false };
178
+ }
179
+ const content = await readFile(path, 'utf8');
180
+ const errors = [];
181
+ const parsed = jsonc.parse(content, errors, JSONC_PARSE_OPTIONS);
182
+ return { content, parsed, parseError: errors.length > 0, exists: true };
183
+ }
184
+ function hasWorkosServer(parsed) {
185
+ const servers = parsed?.mcpServers;
186
+ return Boolean(servers && typeof servers === 'object' && MCP_SERVER_NAME in servers);
187
+ }
188
+ return {
189
+ key,
190
+ displayName,
191
+ async isAvailable() {
192
+ return pathExists(cursorDir());
193
+ },
194
+ async isInstalled() {
195
+ try {
196
+ const { parsed, parseError } = await readConfig();
197
+ if (parseError)
198
+ return false;
199
+ return hasWorkosServer(parsed);
200
+ }
201
+ catch {
202
+ return false;
203
+ }
204
+ },
205
+ async add() {
206
+ const path = configPath();
207
+ try {
208
+ const { content, parseError, exists } = await readConfig();
209
+ // Never overwrite a file we couldn't parse — the user must fix it first.
210
+ if (exists && parseError) {
211
+ return result('failed', `Could not parse ${path}; fix it manually and retry.`);
212
+ }
213
+ await mkdir(cursorDir(), { recursive: true });
214
+ // Converge to our URL — an existing `workos` key with a different URL is
215
+ // overwritten (reported as `installed`).
216
+ const edits = jsonc.modify(content, ['mcpServers', MCP_SERVER_NAME], { url: MCP_SERVER_URL }, JSONC_MODIFY_OPTIONS);
217
+ await writeFile(path, jsonc.applyEdits(content, edits), 'utf8');
218
+ return result('installed');
219
+ }
220
+ catch (error) {
221
+ return result('failed', error instanceof Error ? error.message : 'Unknown error');
222
+ }
223
+ },
224
+ async remove() {
225
+ const path = configPath();
226
+ try {
227
+ const { content, parsed, parseError, exists } = await readConfig();
228
+ if (!exists)
229
+ return result('not-installed');
230
+ if (parseError) {
231
+ return result('failed', `Could not parse ${path}; fix it manually and retry.`);
232
+ }
233
+ if (!hasWorkosServer(parsed))
234
+ return result('not-installed');
235
+ const edits = jsonc.modify(content, ['mcpServers', MCP_SERVER_NAME], undefined, JSONC_MODIFY_OPTIONS);
236
+ await writeFile(path, jsonc.applyEdits(content, edits), 'utf8');
237
+ return result('removed');
238
+ }
239
+ catch (error) {
240
+ return result('failed', error instanceof Error ? error.message : 'Unknown error');
241
+ }
242
+ },
243
+ };
244
+ }
245
+ /** The three client targets, in a stable order. */
246
+ export function createMcpClients() {
247
+ return [createClaudeCodeClient(), createCodexClient(), createCursorClient()];
248
+ }
249
+ /**
250
+ * The URL the WorkOS server is configured with in Cursor's `~/.cursor/mcp.json`,
251
+ * or null when the file/entry is absent, unreadable, or unparseable.
252
+ *
253
+ * Cursor is the only client whose config we read directly (the CLI clients don't
254
+ * expose per-entry URLs), so this powers doctor's URL-drift ("misconfigured")
255
+ * check without a second jsonc reader duplicating the config schema. Read-only
256
+ * and never throws — a problem reading just yields null.
257
+ */
258
+ export async function getCursorConfiguredUrl() {
259
+ try {
260
+ const path = join(homedir(), '.cursor', 'mcp.json');
261
+ if (!(await pathExists(path)))
262
+ return null;
263
+ const content = await readFile(path, 'utf8');
264
+ const parsed = jsonc.parse(content, [], JSONC_PARSE_OPTIONS);
265
+ const servers = parsed?.mcpServers;
266
+ const entry = servers?.[MCP_SERVER_NAME];
267
+ return typeof entry?.url === 'string' ? entry.url : null;
268
+ }
269
+ catch {
270
+ return null;
271
+ }
272
+ }
273
+ /**
274
+ * Return only the clients available on this machine, optionally narrowed to
275
+ * `agentFilter` keys. Availability is probed in parallel. An unknown filter key
276
+ * simply matches nothing here — callers validate keys and surface the error.
277
+ */
278
+ export async function detectMcpClients(agentFilter) {
279
+ const clients = createMcpClients();
280
+ const filtered = agentFilter ? clients.filter((c) => agentFilter.includes(c.key)) : clients;
281
+ const availability = await Promise.all(filtered.map((c) => c.isAvailable()));
282
+ return filtered.filter((_, i) => availability[i]);
283
+ }
284
+ //# sourceMappingURL=mcp-clients.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-clients.js","sourceRoot":"","sources":["../../src/lib/mcp-clients.ts"],"names":[],"mappings":"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","sourcesContent":["import { homedir } from 'os';\nimport { join } from 'path';\nimport { access, mkdir, readFile, writeFile } from 'fs/promises';\nimport * as jsonc from 'jsonc-parser';\nimport { execFileNoThrow } from '../utils/exec-file.js';\nimport { MCP_SERVER_NAME, MCP_SERVER_URL } from './constants.js';\n\n/**\n * Client-writer library for the WorkOS MCP server.\n *\n * One `McpClientTarget` interface, three implementations. Claude Code and Codex\n * are configured by shelling out to their own CLIs (robust to their config\n * format changes); Cursor has no CLI, so we merge `~/.cursor/mcp.json` directly\n * with `jsonc-parser` (hand-edited files may contain comments / trailing\n * commas). Every operation degrades to a reported outcome — a missing binary,\n * an old client without HTTP-transport support, or an unparseable config is a\n * `failed`/`skipped`/`not-installed` result, never a thrown error.\n *\n * The library owns detection and per-client config writing; callers (the `mcp`\n * command today; install-flow / doctor in Phase 2) stay thin. Nothing here may\n * assume it runs inside the `mcp` command.\n */\n\n/** 10s ceiling on every CLI shell-out so a hung client can't wedge the caller. */\nconst EXEC_TIMEOUT_MS = 10_000;\n\n/** jsonc parse options: tolerate the comments / trailing commas real users\n * hand-edit into `~/.cursor/mcp.json`, and an empty file, so only genuinely\n * malformed JSON is flagged. */\nconst JSONC_PARSE_OPTIONS: jsonc.ParseOptions = {\n allowTrailingComma: true,\n allowEmptyContent: true,\n};\n\nconst JSONC_MODIFY_OPTIONS: jsonc.ModificationOptions = {\n formattingOptions: { tabSize: 2, insertSpaces: true },\n};\n\nexport type McpAgentKey = 'claude-code' | 'codex' | 'cursor';\n\nexport type McpOutcome = 'installed' | 'already-installed' | 'removed' | 'not-installed' | 'skipped' | 'failed';\n\nexport interface McpClientResult {\n agent: McpAgentKey;\n displayName: string;\n outcome: McpOutcome;\n /** stderr/message excerpt when `outcome === 'failed'`. */\n error?: string;\n}\n\nexport interface McpClientTarget {\n key: McpAgentKey;\n displayName: string;\n /** Agent is usable on this machine (config dir present AND, for CLI clients, binary runnable). */\n isAvailable(): Promise<boolean>;\n /** The WorkOS server is present in this client's config. */\n isInstalled(): Promise<boolean>;\n add(): Promise<McpClientResult>;\n remove(): Promise<McpClientResult>;\n}\n\n/** Stable list of known agent keys, for `--agent` validation by callers. */\nexport const MCP_AGENT_KEYS: McpAgentKey[] = ['claude-code', 'codex', 'cursor'];\n\n/** Async existence check — `access` rejects with ENOENT when the path is missing. */\nasync function pathExists(p: string): Promise<boolean> {\n try {\n await access(p);\n return true;\n } catch {\n return false;\n }\n}\n\n/** Trim a shell-out message to a single-line excerpt for the `error` field. */\nfunction excerpt(raw: string): string {\n const text = raw.trim();\n if (!text) return 'Unknown error';\n const firstLine = text.split('\\n').find((l) => l.trim().length > 0) ?? text;\n return firstLine.trim().slice(0, 300);\n}\n\n/**\n * Does a CLI `mcp list` output contain our server?\n *\n * Matches the first whitespace-delimited token of any line (with an optional\n * trailing colon stripped), which covers both list shapes we support:\n * - Claude Code: `workos: https://mcp.workos.com/mcp (HTTP) - ✔ Connected`\n * - Codex table: `workos https://mcp.workos.com/mcp ...`\n *\n * First-token matching avoids the substring collision a naive\n * `output.includes('workos')` would hit on an unrelated `workos-docs` entry.\n */\nfunction listHasServer(stdout: string, name: string): boolean {\n return stdout.split('\\n').some((line) => {\n const firstToken = line.trim().split(/\\s+/)[0] ?? '';\n return firstToken.replace(/:$/, '') === name;\n });\n}\n\n/**\n * Shared shell-out client for Claude Code and Codex. The two differ only in\n * binary name, config dir, and the `add` arg shape; every outcome-mapping rule\n * is identical, so they share one implementation.\n */\nfunction createCliClient(config: {\n key: McpAgentKey;\n displayName: string;\n binary: string;\n configDir: string;\n addArgs: string[];\n removeArgs: string[];\n}): McpClientTarget {\n const { key, displayName, binary, configDir, addArgs, removeArgs } = config;\n const result = (outcome: McpOutcome, error?: string): McpClientResult => ({\n agent: key,\n displayName,\n outcome,\n ...(error ? { error } : {}),\n });\n\n async function checkInstalled(): Promise<boolean> {\n const res = await execFileNoThrow(binary, ['mcp', 'list'], { timeout: EXEC_TIMEOUT_MS });\n if (res.status !== 0) return false;\n return listHasServer(res.stdout, MCP_SERVER_NAME);\n }\n\n return {\n key,\n displayName,\n async isAvailable() {\n // Two layers: the config dir can exist while the binary isn't on PATH,\n // and vice versa — require both before we try to shell out.\n if (!(await pathExists(join(homedir(), configDir)))) return false;\n const res = await execFileNoThrow(binary, ['--version'], { timeout: EXEC_TIMEOUT_MS });\n return res.status === 0;\n },\n isInstalled: checkInstalled,\n async add() {\n const res = await execFileNoThrow(binary, addArgs, { timeout: EXEC_TIMEOUT_MS });\n if (res.status === 0) return result('installed');\n // Idempotent: an \"already exists\" collision is a success, not a failure.\n const combined = `${res.stdout}\\n${res.stderr}`.toLowerCase();\n if (combined.includes('already exists') || combined.includes('already configured')) {\n return result('already-installed');\n }\n // A non-zero exit doesn't always mean the write failed: Codex persists the\n // server config and THEN starts an OAuth flow that blocks (no browser /\n // callback available here) until our timeout kills it with a non-zero\n // status. Confirm against the actual config before declaring failure —\n // more robust than matching each client's success wording.\n if (await checkInstalled()) return result('installed');\n // Otherwise a real failure — an old client lacking `--transport http` /\n // `--url`, a bad invocation, etc. Reported, never thrown.\n return result('failed', excerpt(res.stderr || res.stdout));\n },\n async remove() {\n const res = await execFileNoThrow(binary, removeArgs, { timeout: EXEC_TIMEOUT_MS });\n const combined = `${res.stdout}\\n${res.stderr}`.toLowerCase();\n // Claude exits 1 and Codex exits 0 when the server is absent; both print\n // \"No MCP server named ...\". Treat either as an idempotent no-op.\n if (combined.includes('no mcp server')) return result('not-installed');\n if (res.status === 0) return result('removed');\n return result('failed', excerpt(res.stderr || res.stdout));\n },\n };\n}\n\nfunction createClaudeCodeClient(): McpClientTarget {\n return createCliClient({\n key: 'claude-code',\n displayName: 'Claude Code',\n binary: 'claude',\n configDir: '.claude',\n // User scope: the management MCP is account-level, not per-repo — project\n // scope would nag teammates via checked-in config.\n addArgs: ['mcp', 'add', '--transport', 'http', '--scope', 'user', MCP_SERVER_NAME, MCP_SERVER_URL],\n removeArgs: ['mcp', 'remove', '--scope', 'user', MCP_SERVER_NAME],\n });\n}\n\nfunction createCodexClient(): McpClientTarget {\n return createCliClient({\n key: 'codex',\n displayName: 'Codex',\n binary: 'codex',\n configDir: '.codex',\n addArgs: ['mcp', 'add', MCP_SERVER_NAME, '--url', MCP_SERVER_URL],\n removeArgs: ['mcp', 'remove', MCP_SERVER_NAME],\n });\n}\n\n/**\n * Cursor has no CLI, so we read-modify-write `~/.cursor/mcp.json` directly with\n * `jsonc-parser`, preserving existing servers, comments, and formatting.\n */\nfunction createCursorClient(): McpClientTarget {\n const key: McpAgentKey = 'cursor';\n const displayName = 'Cursor';\n const cursorDir = () => join(homedir(), '.cursor');\n const configPath = () => join(cursorDir(), 'mcp.json');\n const result = (outcome: McpOutcome, error?: string): McpClientResult => ({\n agent: key,\n displayName,\n outcome,\n ...(error ? { error } : {}),\n });\n\n /** Read + parse the config. `parseError` is true only for genuinely malformed\n * JSON (not comments / trailing commas / empty). */\n async function readConfig(): Promise<{\n content: string;\n parsed: Record<string, unknown> | undefined;\n parseError: boolean;\n exists: boolean;\n }> {\n const path = configPath();\n if (!(await pathExists(path))) {\n return { content: '', parsed: undefined, parseError: false, exists: false };\n }\n const content = await readFile(path, 'utf8');\n const errors: jsonc.ParseError[] = [];\n const parsed = jsonc.parse(content, errors, JSONC_PARSE_OPTIONS) as Record<string, unknown> | undefined;\n return { content, parsed, parseError: errors.length > 0, exists: true };\n }\n\n function hasWorkosServer(parsed: Record<string, unknown> | undefined): boolean {\n const servers = parsed?.mcpServers;\n return Boolean(servers && typeof servers === 'object' && MCP_SERVER_NAME in (servers as object));\n }\n\n return {\n key,\n displayName,\n async isAvailable() {\n return pathExists(cursorDir());\n },\n async isInstalled() {\n try {\n const { parsed, parseError } = await readConfig();\n if (parseError) return false;\n return hasWorkosServer(parsed);\n } catch {\n return false;\n }\n },\n async add() {\n const path = configPath();\n try {\n const { content, parseError, exists } = await readConfig();\n // Never overwrite a file we couldn't parse — the user must fix it first.\n if (exists && parseError) {\n return result('failed', `Could not parse ${path}; fix it manually and retry.`);\n }\n await mkdir(cursorDir(), { recursive: true });\n // Converge to our URL — an existing `workos` key with a different URL is\n // overwritten (reported as `installed`).\n const edits = jsonc.modify(\n content,\n ['mcpServers', MCP_SERVER_NAME],\n { url: MCP_SERVER_URL },\n JSONC_MODIFY_OPTIONS,\n );\n await writeFile(path, jsonc.applyEdits(content, edits), 'utf8');\n return result('installed');\n } catch (error) {\n return result('failed', error instanceof Error ? error.message : 'Unknown error');\n }\n },\n async remove() {\n const path = configPath();\n try {\n const { content, parsed, parseError, exists } = await readConfig();\n if (!exists) return result('not-installed');\n if (parseError) {\n return result('failed', `Could not parse ${path}; fix it manually and retry.`);\n }\n if (!hasWorkosServer(parsed)) return result('not-installed');\n const edits = jsonc.modify(content, ['mcpServers', MCP_SERVER_NAME], undefined, JSONC_MODIFY_OPTIONS);\n await writeFile(path, jsonc.applyEdits(content, edits), 'utf8');\n return result('removed');\n } catch (error) {\n return result('failed', error instanceof Error ? error.message : 'Unknown error');\n }\n },\n };\n}\n\n/** The three client targets, in a stable order. */\nexport function createMcpClients(): McpClientTarget[] {\n return [createClaudeCodeClient(), createCodexClient(), createCursorClient()];\n}\n\n/**\n * The URL the WorkOS server is configured with in Cursor's `~/.cursor/mcp.json`,\n * or null when the file/entry is absent, unreadable, or unparseable.\n *\n * Cursor is the only client whose config we read directly (the CLI clients don't\n * expose per-entry URLs), so this powers doctor's URL-drift (\"misconfigured\")\n * check without a second jsonc reader duplicating the config schema. Read-only\n * and never throws — a problem reading just yields null.\n */\nexport async function getCursorConfiguredUrl(): Promise<string | null> {\n try {\n const path = join(homedir(), '.cursor', 'mcp.json');\n if (!(await pathExists(path))) return null;\n const content = await readFile(path, 'utf8');\n const parsed = jsonc.parse(content, [], JSONC_PARSE_OPTIONS) as Record<string, unknown> | undefined;\n const servers = parsed?.mcpServers as Record<string, unknown> | undefined;\n const entry = servers?.[MCP_SERVER_NAME] as { url?: unknown } | undefined;\n return typeof entry?.url === 'string' ? entry.url : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Return only the clients available on this machine, optionally narrowed to\n * `agentFilter` keys. Availability is probed in parallel. An unknown filter key\n * simply matches nothing here — callers validate keys and surface the error.\n */\nexport async function detectMcpClients(agentFilter?: string[]): Promise<McpClientTarget[]> {\n const clients = createMcpClients();\n const filtered = agentFilter ? clients.filter((c) => agentFilter.includes(c.key)) : clients;\n const availability = await Promise.all(filtered.map((c) => c.isAvailable()));\n return filtered.filter((_, i) => availability[i]);\n}\n"]}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * MCP-install onboarding surfaces + their shared gate state.
3
+ *
4
+ * One module owns all automatic-ask state so the install-flow prompt and the
5
+ * one-time banner can't drift apart. The decline contract is absolute: any
6
+ * explicit "no" records `promptDeclined` and no automatic surface ever asks
7
+ * again — `workos mcp install` stays available manually.
8
+ *
9
+ * Mirrors telemetry-notice.ts: persisted shown/declined state in the plain
10
+ * prefs store, mode-gated display, and a never-throws contract so a notice can
11
+ * never block or fail a command. The mode gate is the CLI's interaction-mode
12
+ * system (isPromptAllowed / isHumanMode), which already folds in CI markers,
13
+ * agent markers, and TTY state — non-TTY safety comes from using it.
14
+ */
15
+ export type McpAskState = {
16
+ declined: boolean;
17
+ bannerShown: boolean;
18
+ };
19
+ /**
20
+ * Ceiling on the whole install-flow offer so a wedged prompt can never hold up
21
+ * `workos install`. The deadline actively aborts the pending clack prompt (via
22
+ * AbortSignal) — clack's cancel path releases stdin and restores raw mode, so
23
+ * the process can actually exit; a race alone would leave the prompt's stdin
24
+ * handle keeping the event loop alive. Mirrors login.ts's
25
+ * SKILL_INSTALL_TIMEOUT_MS.
26
+ */
27
+ export declare const MCP_OFFER_TIMEOUT_MS: number;
28
+ /**
29
+ * Read the persisted automatic-ask state. Never throws — a missing/corrupt
30
+ * prefs file degrades to "nothing recorded" so gating still works in-memory.
31
+ */
32
+ export declare function getMcpAskState(): Promise<McpAskState>;
33
+ /**
34
+ * Record an explicit decline. Absolute and permanent for automatic surfaces.
35
+ * Write failures are swallowed: gating degrades to per-run memory, never
36
+ * crashes a command.
37
+ */
38
+ export declare function recordMcpDeclined(): Promise<void>;
39
+ /** Record that the one-time banner was shown, stamping the current time. */
40
+ export declare function recordMcpBannerShown(): Promise<void>;
41
+ /**
42
+ * Would an automatic ask be appropriate right now? True only when the user has
43
+ * not declined AND at least one detected agent lacks the server. Does NOT gate
44
+ * on interaction mode — callers add that (isPromptAllowed for the prompt,
45
+ * isHumanMode for the banner). Checks `declined` first so declined machines
46
+ * never pay for the client shell-outs.
47
+ */
48
+ export declare function isAutoAskEligible(): Promise<boolean>;
49
+ /**
50
+ * One-time stderr banner nudging the user toward `workos mcp install`. Shown at
51
+ * most once per machine, only on a normal human-mode run, and never when an
52
+ * earlier startup notice already fired this run (telemetry notice and unclaimed
53
+ * warning take precedence). Records `bannerShownAt` before printing —
54
+ * shown-once beats seen-once (a lost banner is fine; a nag loop is not). Never
55
+ * throws.
56
+ */
57
+ export declare function maybeShowMcpNotice(): Promise<void>;
58
+ /**
59
+ * Offer to install the WorkOS MCP server at the end of `workos install`.
60
+ *
61
+ * Wraps offerMcpInstall best-effort: a try/catch AND a 30s deadline so the
62
+ * offer can never throw into, wedge, or fail the install flow. The deadline
63
+ * aborts the pending prompt (AbortSignal → clack cancel, which releases stdin);
64
+ * the race is a backstop in case the flow is wedged somewhere that can't be
65
+ * aborted (client shell-outs are separately bounded by their exec timeouts).
66
+ * `workos install` has already succeeded by the time this runs.
67
+ */
68
+ export declare function maybeOfferMcpInstall(_opts: {
69
+ entryPoint: 'install-flow';
70
+ }): Promise<void>;
71
+ /** Reset per-session banner state (for testing). */
72
+ export declare function resetMcpNoticeState(): void;