@mintlify/cli 4.0.1379 → 4.0.1381

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mintlify/cli",
3
- "version": "4.0.1379",
3
+ "version": "4.0.1381",
4
4
  "description": "The Mintlify CLI",
5
5
  "engines": {
6
6
  "node": ">=18.0.0"
@@ -45,12 +45,12 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "@inquirer/prompts": "7.9.0",
48
- "@mintlify/common": "1.0.1071",
49
- "@mintlify/link-rot": "3.0.1268",
48
+ "@mintlify/common": "1.0.1072",
49
+ "@mintlify/link-rot": "3.0.1269",
50
50
  "@mintlify/models": "0.0.344",
51
- "@mintlify/prebuild": "1.0.1222",
52
- "@mintlify/previewing": "4.0.1292",
53
- "@mintlify/validation": "0.1.816",
51
+ "@mintlify/prebuild": "1.0.1223",
52
+ "@mintlify/previewing": "4.0.1293",
53
+ "@mintlify/validation": "0.1.817",
54
54
  "adm-zip": "0.5.16",
55
55
  "chalk": "5.2.0",
56
56
  "color": "4.2.3",
@@ -59,6 +59,7 @@
59
59
  "ink": "6.3.0",
60
60
  "inquirer": "12.3.0",
61
61
  "js-yaml": "4.1.1",
62
+ "jsonc-parser": "^3.3.1",
62
63
  "mdast-util-mdx-jsx": "3.2.0",
63
64
  "open": "8.4.2",
64
65
  "openid-client": "6.8.2",
@@ -81,7 +82,7 @@
81
82
  "keytar": "7.9.0"
82
83
  },
83
84
  "devDependencies": {
84
- "@mintlify/editor": "0.0.281",
85
+ "@mintlify/editor": "0.0.282",
85
86
  "@mintlify/ts-config": "2.0.2",
86
87
  "@tsconfig/recommended": "1.0.2",
87
88
  "@types/adm-zip": "0.5.7",
@@ -101,5 +102,5 @@
101
102
  "vitest": "2.1.9",
102
103
  "vitest-mock-process": "1.0.4"
103
104
  },
104
- "gitHead": "24e02114e244af78c8f5b90759cdfc93052cab28"
105
+ "gitHead": "735d5f6fa89b67ed4919b057d13479ee067af0c4"
105
106
  }
package/src/cli.tsx CHANGED
@@ -38,6 +38,7 @@ import {
38
38
  suppressConsoleWarnings,
39
39
  terminate,
40
40
  } from './helpers.js';
41
+ import { runIndexSetup } from './indexSetup/setup.js';
41
42
  import { init } from './init.js';
42
43
  import { getAccessToken } from './keyring.js';
43
44
  import { login } from './login.js';
@@ -582,6 +583,35 @@ export const cli = ({ packageName = 'mint' }: { packageName?: string }) => {
582
583
  )
583
584
  .demandCommand(1, 'specify a subcommand: set, get, or clear')
584
585
  )
586
+ .command(
587
+ 'index',
588
+ 'Install the Mintlify index MCP server into your coding agents',
589
+ (yargs) =>
590
+ yargs
591
+ .option('claude', { type: 'boolean', description: 'Set up Claude Code' })
592
+ .option('cursor', { type: 'boolean', description: 'Set up Cursor' })
593
+ .option('vscode', { type: 'boolean', description: 'Set up VS Code' })
594
+ .option('codex', { type: 'boolean', description: 'Set up Codex' })
595
+ .option('opencode', { type: 'boolean', description: 'Set up OpenCode' })
596
+ .option('windsurf', { type: 'boolean', description: 'Set up Windsurf' })
597
+ .option('zed', { type: 'boolean', description: 'Set up Zed' })
598
+ .option('project', {
599
+ type: 'boolean',
600
+ description: 'Write project-level config instead of global',
601
+ })
602
+ .option('yes', {
603
+ alias: 'y',
604
+ type: 'boolean',
605
+ description: 'Skip prompts and set up all detected agents',
606
+ })
607
+ .usage('usage: mintlify index [options]')
608
+ .example('mintlify index', 'interactive picker')
609
+ .example('mintlify index --claude --cursor', 'set up specific agents'),
610
+ async (argv) => {
611
+ const code = await runIndexSetup(argv);
612
+ await terminate(code);
613
+ }
614
+ )
585
615
  .command(
586
616
  'update',
587
617
  'Update the CLI to the latest version',
@@ -0,0 +1,206 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+
4
+ import { CURSOR_RULE, RULE_BODY, VSCODE_RULE } from './rule.js';
5
+
6
+ export const INDEX_MCP_URL = 'https://index.mintlify.com/mcp';
7
+ export const SERVER_NAME = 'mintlify-index';
8
+
9
+ export type ClientName = 'claude' | 'cursor' | 'vscode' | 'codex' | 'opencode' | 'windsurf' | 'zed';
10
+
11
+ export type Scope = 'global' | 'project';
12
+
13
+ export interface SetupContext {
14
+ home: string;
15
+ cwd: string;
16
+ platform: NodeJS.Platform;
17
+ env: Record<string, string | undefined>;
18
+ }
19
+
20
+ export interface RuleSpec {
21
+ kind: 'file' | 'append';
22
+ content: string;
23
+ globalPath: (ctx: SetupContext) => string;
24
+ projectPath?: (ctx: SetupContext) => string;
25
+ }
26
+
27
+ export interface ClientDefinition {
28
+ name: ClientName;
29
+ displayName: string;
30
+ format: 'json' | 'toml';
31
+ configKey: string;
32
+ globalConfigPaths: (ctx: SetupContext) => string[];
33
+ projectConfigPaths?: (ctx: SetupContext) => string[];
34
+ buildEntry: () => Record<string, unknown>;
35
+ rule?: RuleSpec;
36
+ detectPaths: (ctx: SetupContext) => string[];
37
+ }
38
+
39
+ function claudeConfigDir(ctx: SetupContext): string {
40
+ const override = ctx.env.CLAUDE_CONFIG_DIR;
41
+ return override !== undefined && override !== '' ? override : path.join(ctx.home, '.claude');
42
+ }
43
+
44
+ function claudeGlobalConfigPath(ctx: SetupContext): string {
45
+ const override = ctx.env.CLAUDE_CONFIG_DIR;
46
+ return override !== undefined && override !== ''
47
+ ? path.join(override, '.claude.json')
48
+ : path.join(ctx.home, '.claude.json');
49
+ }
50
+
51
+ function zedUserDir(ctx: SetupContext): string {
52
+ if (ctx.platform === 'win32') {
53
+ const appData = ctx.env.APPDATA;
54
+ const base =
55
+ appData !== undefined && appData !== '' ? appData : path.join(ctx.home, 'AppData', 'Roaming');
56
+ return path.join(base, 'Zed');
57
+ }
58
+ return path.join(ctx.home, '.config', 'zed');
59
+ }
60
+
61
+ function vscodeUserDir(ctx: SetupContext): string {
62
+ if (ctx.platform === 'darwin') {
63
+ return path.join(ctx.home, 'Library', 'Application Support', 'Code', 'User');
64
+ }
65
+ if (ctx.platform === 'win32') {
66
+ const appData = ctx.env.APPDATA;
67
+ const base =
68
+ appData !== undefined && appData !== '' ? appData : path.join(ctx.home, 'AppData', 'Roaming');
69
+ return path.join(base, 'Code', 'User');
70
+ }
71
+ return path.join(ctx.home, '.config', 'Code', 'User');
72
+ }
73
+
74
+ export const CLIENTS: Record<ClientName, ClientDefinition> = {
75
+ claude: {
76
+ name: 'claude',
77
+ displayName: 'Claude Code',
78
+ format: 'json',
79
+ configKey: 'mcpServers',
80
+ globalConfigPaths: (ctx) => [claudeGlobalConfigPath(ctx)],
81
+ projectConfigPaths: (ctx) => [path.join(ctx.cwd, '.mcp.json')],
82
+ buildEntry: () => ({ type: 'http', url: INDEX_MCP_URL }),
83
+ rule: {
84
+ kind: 'file',
85
+ content: RULE_BODY,
86
+ globalPath: (ctx) => path.join(claudeConfigDir(ctx), 'rules', 'mintlify-index.md'),
87
+ projectPath: (ctx) => path.join(ctx.cwd, '.claude', 'rules', 'mintlify-index.md'),
88
+ },
89
+ detectPaths: (ctx) => [claudeConfigDir(ctx)],
90
+ },
91
+ cursor: {
92
+ name: 'cursor',
93
+ displayName: 'Cursor',
94
+ format: 'json',
95
+ configKey: 'mcpServers',
96
+ globalConfigPaths: (ctx) => [path.join(ctx.home, '.cursor', 'mcp.json')],
97
+ projectConfigPaths: (ctx) => [path.join(ctx.cwd, '.cursor', 'mcp.json')],
98
+ buildEntry: () => ({ url: INDEX_MCP_URL }),
99
+ rule: {
100
+ kind: 'file',
101
+ content: CURSOR_RULE,
102
+ globalPath: (ctx) => path.join(ctx.home, '.cursor', 'rules', 'mintlify-index.mdc'),
103
+ projectPath: (ctx) => path.join(ctx.cwd, '.cursor', 'rules', 'mintlify-index.mdc'),
104
+ },
105
+ detectPaths: (ctx) => [path.join(ctx.home, '.cursor')],
106
+ },
107
+ vscode: {
108
+ name: 'vscode',
109
+ displayName: 'VS Code',
110
+ format: 'json',
111
+ configKey: 'servers',
112
+ globalConfigPaths: (ctx) => [path.join(vscodeUserDir(ctx), 'mcp.json')],
113
+ projectConfigPaths: (ctx) => [path.join(ctx.cwd, '.vscode', 'mcp.json')],
114
+ buildEntry: () => ({ type: 'http', url: INDEX_MCP_URL }),
115
+ rule: {
116
+ kind: 'file',
117
+ content: VSCODE_RULE,
118
+ globalPath: (ctx) =>
119
+ path.join(vscodeUserDir(ctx), 'prompts', 'mintlify-index.instructions.md'),
120
+ projectPath: (ctx) =>
121
+ path.join(ctx.cwd, '.github', 'instructions', 'mintlify-index.instructions.md'),
122
+ },
123
+ detectPaths: (ctx) => [vscodeUserDir(ctx)],
124
+ },
125
+ codex: {
126
+ name: 'codex',
127
+ displayName: 'Codex',
128
+ format: 'toml',
129
+ configKey: 'mcp_servers',
130
+ globalConfigPaths: (ctx) => [path.join(ctx.home, '.codex', 'config.toml')],
131
+ projectConfigPaths: (ctx) => [path.join(ctx.cwd, '.codex', 'config.toml')],
132
+ buildEntry: () => ({ type: 'http', url: INDEX_MCP_URL }),
133
+ rule: {
134
+ kind: 'append',
135
+ content: RULE_BODY,
136
+ globalPath: (ctx) => path.join(ctx.home, '.codex', 'AGENTS.md'),
137
+ projectPath: (ctx) => path.join(ctx.cwd, 'AGENTS.md'),
138
+ },
139
+ detectPaths: (ctx) => [path.join(ctx.home, '.codex')],
140
+ },
141
+ opencode: {
142
+ name: 'opencode',
143
+ displayName: 'OpenCode',
144
+ format: 'json',
145
+ configKey: 'mcp',
146
+ globalConfigPaths: (ctx) => [path.join(ctx.home, '.config', 'opencode', 'opencode.json')],
147
+ projectConfigPaths: (ctx) => [path.join(ctx.cwd, 'opencode.json')],
148
+ buildEntry: () => ({ type: 'remote', url: INDEX_MCP_URL, enabled: true }),
149
+ rule: {
150
+ kind: 'append',
151
+ content: RULE_BODY,
152
+ globalPath: (ctx) => path.join(ctx.home, '.config', 'opencode', 'AGENTS.md'),
153
+ projectPath: (ctx) => path.join(ctx.cwd, 'AGENTS.md'),
154
+ },
155
+ detectPaths: (ctx) => [path.join(ctx.home, '.config', 'opencode')],
156
+ },
157
+ windsurf: {
158
+ name: 'windsurf',
159
+ displayName: 'Windsurf',
160
+ format: 'json',
161
+ configKey: 'mcpServers',
162
+ globalConfigPaths: (ctx) => [path.join(ctx.home, '.codeium', 'windsurf', 'mcp_config.json')],
163
+ buildEntry: () => ({ serverUrl: INDEX_MCP_URL }),
164
+ rule: {
165
+ kind: 'append',
166
+ content: RULE_BODY,
167
+ globalPath: (ctx) =>
168
+ path.join(ctx.home, '.codeium', 'windsurf', 'memories', 'global_rules.md'),
169
+ projectPath: (ctx) => path.join(ctx.cwd, '.windsurf', 'rules', 'mintlify-index.md'),
170
+ },
171
+ detectPaths: (ctx) => [path.join(ctx.home, '.codeium', 'windsurf')],
172
+ },
173
+ zed: {
174
+ name: 'zed',
175
+ displayName: 'Zed',
176
+ format: 'json',
177
+ configKey: 'context_servers',
178
+ globalConfigPaths: (ctx) => [path.join(zedUserDir(ctx), 'settings.json')],
179
+ projectConfigPaths: (ctx) => [path.join(ctx.cwd, '.zed', 'settings.json')],
180
+ buildEntry: () => ({ url: INDEX_MCP_URL }),
181
+ detectPaths: (ctx) => [zedUserDir(ctx)],
182
+ },
183
+ };
184
+
185
+ export const ALL_CLIENT_NAMES: ClientName[] = [
186
+ 'claude',
187
+ 'cursor',
188
+ 'vscode',
189
+ 'codex',
190
+ 'opencode',
191
+ 'windsurf',
192
+ 'zed',
193
+ ];
194
+
195
+ export async function detectClients(ctx: SetupContext): Promise<ClientName[]> {
196
+ const detected: ClientName[] = [];
197
+ for (const name of ALL_CLIENT_NAMES) {
198
+ for (const candidate of CLIENTS[name].detectPaths(ctx)) {
199
+ if (await fs.pathExists(candidate)) {
200
+ detected.push(name);
201
+ break;
202
+ }
203
+ }
204
+ }
205
+ return detected;
206
+ }
@@ -0,0 +1,140 @@
1
+ import fs from 'fs-extra';
2
+ import { applyEdits, modify, parse, type ParseError } from 'jsonc-parser';
3
+ import path from 'path';
4
+
5
+ function isEnoent(error: unknown): boolean {
6
+ return typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT';
7
+ }
8
+
9
+ export async function readJsonConfig(filePath: string): Promise<Record<string, unknown>> {
10
+ let raw: string;
11
+ try {
12
+ raw = await fs.readFile(filePath, 'utf-8');
13
+ } catch (error) {
14
+ if (isEnoent(error)) {
15
+ return {};
16
+ }
17
+ throw error;
18
+ }
19
+ if (raw.trim() === '') {
20
+ return {};
21
+ }
22
+ const errors: ParseError[] = [];
23
+ const parsed = parse(raw, errors, { allowTrailingComma: true });
24
+ if (errors.length > 0) {
25
+ throw new Error(`invalid JSON at ${filePath}`);
26
+ }
27
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
28
+ throw new Error(`expected a JSON object at ${filePath}`);
29
+ }
30
+ return parsed;
31
+ }
32
+
33
+ export function mergeServerEntry(
34
+ config: Record<string, unknown>,
35
+ configKey: string,
36
+ serverName: string,
37
+ entry: Record<string, unknown>
38
+ ): { config: Record<string, unknown>; alreadyExists: boolean } {
39
+ const section = config[configKey];
40
+ const servers: Record<string, unknown> = {};
41
+ if (typeof section === 'object' && section !== null && !Array.isArray(section)) {
42
+ Object.assign(servers, section);
43
+ }
44
+ const alreadyExists = serverName in servers;
45
+ servers[serverName] = entry;
46
+ return { config: { ...config, [configKey]: servers }, alreadyExists };
47
+ }
48
+
49
+ export async function writeJsonConfig(
50
+ filePath: string,
51
+ config: Record<string, unknown>
52
+ ): Promise<void> {
53
+ await fs.mkdirp(path.dirname(filePath));
54
+ await fs.writeFile(filePath, `${JSON.stringify(config, null, 2)}\n`);
55
+ }
56
+
57
+ export async function writeJsonServerEntry(
58
+ filePath: string,
59
+ configKey: string,
60
+ serverName: string,
61
+ entry: Record<string, unknown>
62
+ ): Promise<{ status: 'configured' | 'reconfigured' }> {
63
+ let raw: string | undefined;
64
+ try {
65
+ raw = await fs.readFile(filePath, 'utf-8');
66
+ } catch (error) {
67
+ if (!isEnoent(error)) {
68
+ throw error;
69
+ }
70
+ }
71
+ if (raw === undefined || raw.trim() === '') {
72
+ const { config } = mergeServerEntry({}, configKey, serverName, entry);
73
+ await writeJsonConfig(filePath, config);
74
+ return { status: 'configured' };
75
+ }
76
+ const errors: ParseError[] = [];
77
+ const parsed = parse(raw, errors, { allowTrailingComma: true });
78
+ if (errors.length > 0 || typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
79
+ throw new Error(`unable to parse existing config at ${filePath}`);
80
+ }
81
+ const section = parsed[configKey];
82
+ const sectionIsObject =
83
+ typeof section === 'object' && section !== null && !Array.isArray(section);
84
+ const alreadyExists = sectionIsObject && serverName in section;
85
+ const edits = sectionIsObject
86
+ ? modify(raw, [configKey, serverName], entry, {
87
+ formattingOptions: { insertSpaces: true, tabSize: 2 },
88
+ })
89
+ : modify(
90
+ raw,
91
+ [configKey],
92
+ { [serverName]: entry },
93
+ {
94
+ formattingOptions: { insertSpaces: true, tabSize: 2 },
95
+ }
96
+ );
97
+ await fs.mkdirp(path.dirname(filePath));
98
+ await fs.writeFile(filePath, applyEdits(raw, edits));
99
+ return { status: alreadyExists ? 'reconfigured' : 'configured' };
100
+ }
101
+
102
+ export async function resolveConfigPath(candidates: string[]): Promise<string> {
103
+ for (const candidate of candidates) {
104
+ if (await fs.pathExists(candidate)) {
105
+ return candidate;
106
+ }
107
+ }
108
+ const fallback = candidates[0];
109
+ if (fallback === undefined) {
110
+ throw new Error('no config path candidates');
111
+ }
112
+ return fallback;
113
+ }
114
+
115
+ export function upsertTomlServer(
116
+ content: string,
117
+ serverName: string,
118
+ entry: Record<string, string>
119
+ ): { content: string; alreadyExists: boolean } {
120
+ const header = `[mcp_servers.${serverName}]`;
121
+ const escapedName = serverName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
122
+ const headerPattern = new RegExp(
123
+ `^[ \\t]*\\[mcp_servers\\.${escapedName}\\][ \\t]*(?:#.*)?$`,
124
+ 'm'
125
+ );
126
+ const lines = Object.entries(entry).map(([key, value]) => `${key} = ${JSON.stringify(value)}`);
127
+ const block = `${[header, ...lines].join('\n')}\n`;
128
+ const match = headerPattern.exec(content);
129
+ if (match === null) {
130
+ const separator = content.length === 0 ? '' : content.endsWith('\n') ? '\n' : '\n\n';
131
+ return { content: `${content}${separator}${block}`, alreadyExists: false };
132
+ }
133
+ const headerIndex = match.index + match[0].indexOf('[');
134
+ const rest = content.slice(headerIndex + header.length);
135
+ const nextTable = rest.search(/\n\s*\[/);
136
+ const end = nextTable === -1 ? content.length : headerIndex + header.length + nextTable + 1;
137
+ const before = content.slice(0, headerIndex);
138
+ const after = content.slice(end).replace(/^\n+/, '\n');
139
+ return { content: `${before}${block}${after}`, alreadyExists: true };
140
+ }
@@ -0,0 +1,113 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+
4
+ import {
5
+ SERVER_NAME,
6
+ type ClientDefinition,
7
+ type ClientName,
8
+ type Scope,
9
+ type SetupContext,
10
+ } from './clients.js';
11
+ import { resolveConfigPath, upsertTomlServer, writeJsonServerEntry } from './configFile.js';
12
+ import { appendRuleSection, writeRuleFile } from './rule.js';
13
+
14
+ export interface ClientSetupResult {
15
+ client: ClientName;
16
+ displayName: string;
17
+ mcpStatus: 'configured' | 'reconfigured' | 'failed';
18
+ mcpDetail: string;
19
+ ruleStatus: 'installed' | 'updated' | 'failed' | 'none';
20
+ ruleDetail: string;
21
+ }
22
+
23
+ async function writeMcpEntry(
24
+ definition: ClientDefinition,
25
+ scope: Scope,
26
+ ctx: SetupContext
27
+ ): Promise<{ status: 'configured' | 'reconfigured'; configPath: string }> {
28
+ const projectPaths = definition.projectConfigPaths?.(ctx) ?? [];
29
+ const candidates =
30
+ scope === 'project' && projectPaths.length > 0
31
+ ? projectPaths
32
+ : definition.globalConfigPaths(ctx);
33
+ const configPath = await resolveConfigPath(candidates);
34
+ if (definition.format === 'toml') {
35
+ let existing = '';
36
+ try {
37
+ existing = await fs.readFile(configPath, 'utf-8');
38
+ } catch (error) {
39
+ if (
40
+ !(typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT')
41
+ ) {
42
+ throw error;
43
+ }
44
+ }
45
+ const entry: Record<string, string> = {};
46
+ for (const [key, value] of Object.entries(definition.buildEntry())) {
47
+ if (typeof value === 'string') {
48
+ entry[key] = value;
49
+ }
50
+ }
51
+ const { content, alreadyExists } = upsertTomlServer(existing, SERVER_NAME, entry);
52
+ await fs.mkdirp(path.dirname(configPath));
53
+ await fs.writeFile(configPath, content);
54
+ return { status: alreadyExists ? 'reconfigured' : 'configured', configPath };
55
+ }
56
+ const { status } = await writeJsonServerEntry(
57
+ configPath,
58
+ definition.configKey,
59
+ SERVER_NAME,
60
+ definition.buildEntry()
61
+ );
62
+ return { status, configPath };
63
+ }
64
+
65
+ async function writeRule(
66
+ definition: ClientDefinition,
67
+ scope: Scope,
68
+ ctx: SetupContext
69
+ ): Promise<{ status: 'installed' | 'updated' | 'none'; rulePath: string }> {
70
+ const rule = definition.rule;
71
+ if (rule === undefined) {
72
+ return { status: 'none', rulePath: '' };
73
+ }
74
+ const projectPath = rule.projectPath?.(ctx);
75
+ const rulePath =
76
+ scope === 'project' && projectPath !== undefined ? projectPath : rule.globalPath(ctx);
77
+ const status =
78
+ rule.kind === 'file'
79
+ ? await writeRuleFile(rulePath, rule.content)
80
+ : await appendRuleSection(rulePath, rule.content);
81
+ return { status, rulePath };
82
+ }
83
+
84
+ export async function setupClient(
85
+ definition: ClientDefinition,
86
+ scope: Scope,
87
+ ctx: SetupContext
88
+ ): Promise<ClientSetupResult> {
89
+ const result: ClientSetupResult = {
90
+ client: definition.name,
91
+ displayName: definition.displayName,
92
+ mcpStatus: 'failed',
93
+ mcpDetail: '',
94
+ ruleStatus: 'none',
95
+ ruleDetail: '',
96
+ };
97
+ try {
98
+ const { status, configPath } = await writeMcpEntry(definition, scope, ctx);
99
+ result.mcpStatus = status;
100
+ result.mcpDetail = configPath;
101
+ } catch (error) {
102
+ result.mcpDetail = error instanceof Error ? error.message : String(error);
103
+ }
104
+ try {
105
+ const { status, rulePath } = await writeRule(definition, scope, ctx);
106
+ result.ruleStatus = status;
107
+ result.ruleDetail = rulePath;
108
+ } catch (error) {
109
+ result.ruleStatus = 'failed';
110
+ result.ruleDetail = error instanceof Error ? error.message : String(error);
111
+ }
112
+ return result;
113
+ }
@@ -0,0 +1,56 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+
4
+ export const RULE_MARKER = '<!-- mintlify-index -->';
5
+
6
+ export const RULE_BODY = `Use the Mintlify index \`context\` tool whenever you research how to use a library, framework, SDK, API, or CLI tool, including syntax, configuration, migration, and setup questions. Use it even for well-known libraries, since training data may be stale, and prefer it over web search for developer documentation. Do not use it for general programming concepts or for debugging business logic.
7
+ `;
8
+
9
+ export const CURSOR_RULE = `---
10
+ alwaysApply: true
11
+ ---
12
+
13
+ ${RULE_BODY}`;
14
+
15
+ export const VSCODE_RULE = `---
16
+ applyTo: '**'
17
+ ---
18
+
19
+ ${RULE_BODY}`;
20
+
21
+ export async function writeRuleFile(
22
+ filePath: string,
23
+ content: string
24
+ ): Promise<'installed' | 'updated'> {
25
+ const exists = await fs.pathExists(filePath);
26
+ await fs.mkdirp(path.dirname(filePath));
27
+ await fs.writeFile(filePath, content);
28
+ return exists ? 'updated' : 'installed';
29
+ }
30
+
31
+ export async function appendRuleSection(
32
+ filePath: string,
33
+ content: string
34
+ ): Promise<'installed' | 'updated'> {
35
+ const section = `${RULE_MARKER}\n${content}${RULE_MARKER}`;
36
+ let existing = '';
37
+ try {
38
+ existing = await fs.readFile(filePath, 'utf-8');
39
+ } catch (error) {
40
+ if (
41
+ !(typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT')
42
+ ) {
43
+ throw error;
44
+ }
45
+ }
46
+ const escaped = RULE_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
47
+ const pattern = new RegExp(`${escaped}\\n[\\s\\S]*?${escaped}`);
48
+ if (pattern.test(existing)) {
49
+ await fs.writeFile(filePath, existing.replace(pattern, section));
50
+ return 'updated';
51
+ }
52
+ const separator = existing.length === 0 ? '' : existing.endsWith('\n') ? '\n' : '\n\n';
53
+ await fs.mkdirp(path.dirname(filePath));
54
+ await fs.writeFile(filePath, `${existing}${separator}${section}\n`);
55
+ return 'installed';
56
+ }