@xfe-repo/cli-plugin-ai-rules 2.0.4 → 2.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +1 -1
- package/dist/formats/mcp-json.js +7 -4
- package/dist/formats/mcp-toml.d.ts +21 -0
- package/dist/formats/mcp-toml.js +211 -0
- package/dist/index.js +17 -7
- package/dist/pipeline.js +16 -6
- package/dist/targets.d.ts +2 -1
- package/dist/targets.js +7 -1
- package/dist/types.d.ts +5 -1
- package/dist/types.js +5 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
package/dist/formats/mcp-json.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { existsSync } from 'node:fs';
|
|
10
10
|
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
11
11
|
import path from 'node:path';
|
|
12
|
-
import { SyncItemType } from '../types.js';
|
|
12
|
+
import { McpConfigFormat, SyncItemType } from '../types.js';
|
|
13
13
|
import { getTarget } from '../targets.js';
|
|
14
14
|
import { computeHash } from './shared.js';
|
|
15
15
|
// ─── Constants ──────
|
|
@@ -21,7 +21,7 @@ const MCP_SERVER_PREFIX = 'xfe:';
|
|
|
21
21
|
* 可选注入项合并到同一 JSON 文件
|
|
22
22
|
*/
|
|
23
23
|
export function planMcpJsonFiles(target, items, injections) {
|
|
24
|
-
if (!target
|
|
24
|
+
if (!isMcpJsonTarget(target))
|
|
25
25
|
return [];
|
|
26
26
|
const syncServers = buildServerEntries(items.filter((i) => i.type === SyncItemType.McpServer));
|
|
27
27
|
const injectionServers = injections ? buildServerEntries(collectMcpItems(injections)) : {};
|
|
@@ -53,7 +53,7 @@ export async function patchMcpJsonInjections(config, projectRoot, injections) {
|
|
|
53
53
|
let removed = 0;
|
|
54
54
|
for (const targetId of config.targets) {
|
|
55
55
|
const target = getTarget(targetId);
|
|
56
|
-
if (!target
|
|
56
|
+
if (!target || !isMcpJsonTarget(target))
|
|
57
57
|
continue;
|
|
58
58
|
const absPath = path.join(projectRoot, target.mcp);
|
|
59
59
|
if (!existsSync(absPath)) {
|
|
@@ -86,7 +86,7 @@ export async function patchMcpJsonInjections(config, projectRoot, injections) {
|
|
|
86
86
|
}
|
|
87
87
|
// ─── Gitignore ──────
|
|
88
88
|
export function getMcpJsonGitignore(target) {
|
|
89
|
-
return target
|
|
89
|
+
return isMcpJsonTarget(target) ? [target.mcp] : [];
|
|
90
90
|
}
|
|
91
91
|
// ─── Helpers ──────
|
|
92
92
|
function buildServerEntries(items) {
|
|
@@ -129,4 +129,7 @@ async function mergeWithExisting(absPath, newContent) {
|
|
|
129
129
|
existing[rootKey] = { ...userServers, ...newParsed[rootKey] };
|
|
130
130
|
return JSON.stringify(existing, null, 2) + '\n';
|
|
131
131
|
}
|
|
132
|
+
function isMcpJsonTarget(target) {
|
|
133
|
+
return Boolean(target.mcp) && (target.mcpFormat ?? McpConfigFormat.Json) === McpConfigFormat.Json;
|
|
134
|
+
}
|
|
132
135
|
//# sourceMappingURL=mcp-json.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP TOML 格式模块
|
|
3
|
+
*
|
|
4
|
+
* 处理 Codex `.codex/config.toml` 的 MCP 配置写入:
|
|
5
|
+
* - 使用标记块隔离 xfe 管理的 server 条目
|
|
6
|
+
* - 将 server id 规范化为 Codex 可识别的 snake_case 表名
|
|
7
|
+
* - 写入时保留用户自定义 TOML 配置,仅替换 xfe 管理块
|
|
8
|
+
* - 新建文件时生成 Codex 兼容的 `[mcp_servers]` 配置结构
|
|
9
|
+
*/
|
|
10
|
+
import type { AiConfig, InjectionRegistry, PlannedFile, SyncEntry } from '../types.js';
|
|
11
|
+
import type { TargetConfig } from '../targets.js';
|
|
12
|
+
export declare function planMcpTomlFiles(target: TargetConfig, items: SyncEntry[], injections?: InjectionRegistry): PlannedFile[];
|
|
13
|
+
export declare function writeMcpTomlFile(file: PlannedFile, projectRoot: string): Promise<string>;
|
|
14
|
+
export interface PatchResult {
|
|
15
|
+
written: number;
|
|
16
|
+
removed: number;
|
|
17
|
+
hashes: Record<string, string>;
|
|
18
|
+
}
|
|
19
|
+
export declare function patchMcpTomlInjections(config: AiConfig, projectRoot: string, injections: InjectionRegistry): Promise<PatchResult>;
|
|
20
|
+
export declare function getMcpTomlGitignore(target: TargetConfig): string[];
|
|
21
|
+
//# sourceMappingURL=mcp-toml.d.ts.map
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP TOML 格式模块
|
|
3
|
+
*
|
|
4
|
+
* 处理 Codex `.codex/config.toml` 的 MCP 配置写入:
|
|
5
|
+
* - 使用标记块隔离 xfe 管理的 server 条目
|
|
6
|
+
* - 将 server id 规范化为 Codex 可识别的 snake_case 表名
|
|
7
|
+
* - 写入时保留用户自定义 TOML 配置,仅替换 xfe 管理块
|
|
8
|
+
* - 新建文件时生成 Codex 兼容的 `[mcp_servers]` 配置结构
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync } from 'node:fs';
|
|
11
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { McpConfigFormat, SyncItemType } from '../types.js';
|
|
14
|
+
import { getTarget } from '../targets.js';
|
|
15
|
+
import { computeHash, spliceMarkerBlock } from './shared.js';
|
|
16
|
+
// ─── Constants ──────
|
|
17
|
+
const DEFAULT_ROOT_KEY = 'mcp_servers';
|
|
18
|
+
const DEFAULT_SERVER_KEY = 'mcp_server';
|
|
19
|
+
const SERVER_KEY_HASH_LENGTH = 8;
|
|
20
|
+
const MANAGED_BLOCK_BEGIN = '# xfe-ai-rules:mcp:begin';
|
|
21
|
+
const MANAGED_BLOCK_END = '# xfe-ai-rules:mcp:end';
|
|
22
|
+
const TOML_BARE_KEY_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
23
|
+
const SERVER_KEY_INVALID_CHARS_PATTERN = /[^a-z0-9]+/g;
|
|
24
|
+
const SERVER_KEY_EDGE_UNDERSCORES_PATTERN = /^_+|_+$/g;
|
|
25
|
+
const SERVER_KEY_NUMBER_PREFIX_PATTERN = /^[0-9]/;
|
|
26
|
+
// ─── Plan ──────
|
|
27
|
+
export function planMcpTomlFiles(target, items, injections) {
|
|
28
|
+
if (!isMcpTomlTarget(target))
|
|
29
|
+
return [];
|
|
30
|
+
const syncServers = buildServerEntries(items.filter((i) => i.type === SyncItemType.McpServer));
|
|
31
|
+
const injectionServers = injections ? buildServerEntries(collectMcpItems(injections)) : {};
|
|
32
|
+
const servers = { ...syncServers, ...injectionServers };
|
|
33
|
+
if (Object.keys(servers).length === 0)
|
|
34
|
+
return [];
|
|
35
|
+
return [
|
|
36
|
+
{
|
|
37
|
+
relativePath: target.mcp,
|
|
38
|
+
content: buildMcpToml(target, servers, true),
|
|
39
|
+
action: 'write',
|
|
40
|
+
source: Object.keys(syncServers).length > 0 ? 'sync' : 'injection',
|
|
41
|
+
format: 'mcp-toml',
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
// ─── Write ──────
|
|
46
|
+
export async function writeMcpTomlFile(file, projectRoot) {
|
|
47
|
+
const absPath = path.join(projectRoot, file.relativePath);
|
|
48
|
+
await mkdir(path.dirname(absPath), { recursive: true });
|
|
49
|
+
const merged = await mergeWithExisting(absPath, file.content);
|
|
50
|
+
await writeFile(absPath, merged, 'utf-8');
|
|
51
|
+
return computeHash(merged);
|
|
52
|
+
}
|
|
53
|
+
export async function patchMcpTomlInjections(config, projectRoot, injections) {
|
|
54
|
+
const servers = buildServerEntries(collectMcpItems(injections));
|
|
55
|
+
const isEmpty = Object.keys(servers).length === 0;
|
|
56
|
+
let written = 0;
|
|
57
|
+
let removed = 0;
|
|
58
|
+
for (const targetId of config.targets) {
|
|
59
|
+
const target = getTarget(targetId);
|
|
60
|
+
if (!target || !isMcpTomlTarget(target))
|
|
61
|
+
continue;
|
|
62
|
+
const absPath = path.join(projectRoot, target.mcp);
|
|
63
|
+
if (!existsSync(absPath)) {
|
|
64
|
+
if (!isEmpty) {
|
|
65
|
+
await mkdir(path.dirname(absPath), { recursive: true });
|
|
66
|
+
await writeFile(absPath, buildMcpToml(target, servers, true), 'utf-8');
|
|
67
|
+
written++;
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const content = await readFile(absPath, 'utf-8');
|
|
72
|
+
const updated = isEmpty
|
|
73
|
+
? removeManagedBlock(content)
|
|
74
|
+
: mergeTomlContent(content, buildMcpToml(target, servers, true), getRootKey(target));
|
|
75
|
+
if (updated === content)
|
|
76
|
+
continue;
|
|
77
|
+
await writeFile(absPath, updated, 'utf-8');
|
|
78
|
+
isEmpty ? removed++ : written++;
|
|
79
|
+
}
|
|
80
|
+
return { written, removed, hashes: {} };
|
|
81
|
+
}
|
|
82
|
+
// ─── Gitignore ──────
|
|
83
|
+
export function getMcpTomlGitignore(target) {
|
|
84
|
+
return isMcpTomlTarget(target) ? [target.mcp] : [];
|
|
85
|
+
}
|
|
86
|
+
// ─── Helpers ──────
|
|
87
|
+
function buildServerEntries(items) {
|
|
88
|
+
const servers = {};
|
|
89
|
+
for (const item of items) {
|
|
90
|
+
if (!item.serverConfig)
|
|
91
|
+
continue;
|
|
92
|
+
const serverKey = createUniqueServerKey(item.id, servers);
|
|
93
|
+
servers[serverKey] = item.serverConfig;
|
|
94
|
+
}
|
|
95
|
+
return servers;
|
|
96
|
+
}
|
|
97
|
+
function buildMcpToml(target, servers, includeRootTable) {
|
|
98
|
+
const rootKey = getRootKey(target);
|
|
99
|
+
const serverBlocks = Object.entries(servers).map(([name, serverConfig]) => buildServerBlock(rootKey, name, serverConfig));
|
|
100
|
+
const lines = [MANAGED_BLOCK_BEGIN, includeRootTable ? `[${formatTomlKey(rootKey)}]` : '', ...serverBlocks, MANAGED_BLOCK_END];
|
|
101
|
+
return normalizeTomlLines(lines);
|
|
102
|
+
}
|
|
103
|
+
function buildServerBlock(rootKey, name, serverConfig) {
|
|
104
|
+
if (!isTomlTableValue(serverConfig))
|
|
105
|
+
return `[${formatTomlKey(rootKey)}.${formatTomlKey(name)}]`;
|
|
106
|
+
const assignments = Object.entries(serverConfig)
|
|
107
|
+
.filter((entry) => isSerializableTomlValue(entry[1]))
|
|
108
|
+
.map(([key, value]) => `${formatTomlKey(key)} = ${formatTomlValue(value)}`);
|
|
109
|
+
return [`[${formatTomlKey(rootKey)}.${formatTomlKey(name)}]`, ...assignments].join('\n');
|
|
110
|
+
}
|
|
111
|
+
async function mergeWithExisting(absPath, newContent) {
|
|
112
|
+
if (!existsSync(absPath))
|
|
113
|
+
return newContent;
|
|
114
|
+
const content = await readFile(absPath, 'utf-8');
|
|
115
|
+
return mergeTomlContent(content, newContent, DEFAULT_ROOT_KEY);
|
|
116
|
+
}
|
|
117
|
+
function mergeTomlContent(content, newContent, rootKey) {
|
|
118
|
+
const stripped = removeManagedBlock(content);
|
|
119
|
+
const replacement = hasRootScope(stripped, rootKey) ? removeRootTable(newContent, rootKey) : newContent;
|
|
120
|
+
return spliceMarkerBlock(stripped, MANAGED_BLOCK_BEGIN, MANAGED_BLOCK_END, replacement);
|
|
121
|
+
}
|
|
122
|
+
function removeManagedBlock(content) {
|
|
123
|
+
return spliceMarkerBlock(content, MANAGED_BLOCK_BEGIN, MANAGED_BLOCK_END);
|
|
124
|
+
}
|
|
125
|
+
function removeRootTable(content, rootKey) {
|
|
126
|
+
const lines = content.split('\n');
|
|
127
|
+
const rootTableLine = `[${formatTomlKey(rootKey)}]`;
|
|
128
|
+
const filtered = lines.filter((line) => line.trim() !== rootTableLine);
|
|
129
|
+
return filtered.join('\n').replace(`${MANAGED_BLOCK_BEGIN}\n\n\n`, `${MANAGED_BLOCK_BEGIN}\n\n`);
|
|
130
|
+
}
|
|
131
|
+
function hasRootScope(content, rootKey) {
|
|
132
|
+
const formattedRootKey = escapeRegExp(formatTomlKey(rootKey));
|
|
133
|
+
const rootScopePattern = new RegExp(`^\\s*\\[\\s*${formattedRootKey}(?:\\s*\\]|\\s*\\.)`, 'm');
|
|
134
|
+
return rootScopePattern.test(content);
|
|
135
|
+
}
|
|
136
|
+
function collectMcpItems(injections) {
|
|
137
|
+
return [...injections.values()].flat().filter((i) => i.type === SyncItemType.McpServer);
|
|
138
|
+
}
|
|
139
|
+
function createUniqueServerKey(id, servers) {
|
|
140
|
+
const normalizedKey = normalizeServerKey(id);
|
|
141
|
+
if (!hasServerKey(servers, normalizedKey))
|
|
142
|
+
return normalizedKey;
|
|
143
|
+
const hashSuffix = computeHash(id).slice(0, SERVER_KEY_HASH_LENGTH);
|
|
144
|
+
const keyWithHash = `${normalizedKey}_${hashSuffix}`;
|
|
145
|
+
if (!hasServerKey(servers, keyWithHash))
|
|
146
|
+
return keyWithHash;
|
|
147
|
+
let index = 2;
|
|
148
|
+
while (hasServerKey(servers, `${keyWithHash}_${index}`)) {
|
|
149
|
+
index++;
|
|
150
|
+
}
|
|
151
|
+
return `${keyWithHash}_${index}`;
|
|
152
|
+
}
|
|
153
|
+
function normalizeServerKey(id) {
|
|
154
|
+
const snakeCaseKey = id.toLowerCase().replace(SERVER_KEY_INVALID_CHARS_PATTERN, '_').replace(SERVER_KEY_EDGE_UNDERSCORES_PATTERN, '');
|
|
155
|
+
const fallbackKey = snakeCaseKey || `${DEFAULT_SERVER_KEY}_${computeHash(id).slice(0, SERVER_KEY_HASH_LENGTH)}`;
|
|
156
|
+
if (SERVER_KEY_NUMBER_PREFIX_PATTERN.test(fallbackKey))
|
|
157
|
+
return `${DEFAULT_SERVER_KEY}_${fallbackKey}`;
|
|
158
|
+
return fallbackKey;
|
|
159
|
+
}
|
|
160
|
+
function hasServerKey(servers, key) {
|
|
161
|
+
return Object.prototype.hasOwnProperty.call(servers, key);
|
|
162
|
+
}
|
|
163
|
+
function getRootKey(target) {
|
|
164
|
+
return target.mcpRootKey ?? DEFAULT_ROOT_KEY;
|
|
165
|
+
}
|
|
166
|
+
function isMcpTomlTarget(target) {
|
|
167
|
+
return Boolean(target.mcp) && target.mcpFormat === McpConfigFormat.Toml;
|
|
168
|
+
}
|
|
169
|
+
function isTomlTableValue(value) {
|
|
170
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
171
|
+
}
|
|
172
|
+
function isSerializableTomlValue(value) {
|
|
173
|
+
if (value === null || value === undefined)
|
|
174
|
+
return false;
|
|
175
|
+
if (Array.isArray(value))
|
|
176
|
+
return value.every(isSerializableTomlValue);
|
|
177
|
+
if (isTomlTableValue(value))
|
|
178
|
+
return Object.values(value).every(isSerializableTomlValue);
|
|
179
|
+
if (typeof value === 'number')
|
|
180
|
+
return Number.isFinite(value);
|
|
181
|
+
return ['string', 'boolean'].includes(typeof value);
|
|
182
|
+
}
|
|
183
|
+
function formatTomlValue(value) {
|
|
184
|
+
if (typeof value === 'string')
|
|
185
|
+
return JSON.stringify(value);
|
|
186
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
187
|
+
return String(value);
|
|
188
|
+
if (Array.isArray(value))
|
|
189
|
+
return `[${value.map(formatTomlValue).join(', ')}]`;
|
|
190
|
+
if (isTomlTableValue(value))
|
|
191
|
+
return `{ ${formatTomlInlineTable(value)} }`;
|
|
192
|
+
throw new Error(`无法序列化 TOML 值: ${String(value)}`);
|
|
193
|
+
}
|
|
194
|
+
function formatTomlInlineTable(value) {
|
|
195
|
+
return Object.entries(value)
|
|
196
|
+
.filter((entry) => isSerializableTomlValue(entry[1]))
|
|
197
|
+
.map(([key, item]) => `${formatTomlKey(key)} = ${formatTomlValue(item)}`)
|
|
198
|
+
.join(', ');
|
|
199
|
+
}
|
|
200
|
+
function formatTomlKey(key) {
|
|
201
|
+
if (TOML_BARE_KEY_PATTERN.test(key))
|
|
202
|
+
return key;
|
|
203
|
+
return JSON.stringify(key);
|
|
204
|
+
}
|
|
205
|
+
function normalizeTomlLines(lines) {
|
|
206
|
+
return lines.filter((line) => line !== '').join('\n\n') + '\n';
|
|
207
|
+
}
|
|
208
|
+
function escapeRegExp(value) {
|
|
209
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=mcp-toml.js.map
|
package/dist/index.js
CHANGED
|
@@ -271,10 +271,11 @@ async function executeTargetsCommand(ctx) {
|
|
|
271
271
|
}
|
|
272
272
|
// ─── 交互提示 ──────
|
|
273
273
|
async function promptMainAgentSelection(ctx, selectedAgents, currentAgent) {
|
|
274
|
-
const choices = selectedAgents.map((entry) => {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
274
|
+
const choices = selectedAgents.map((entry) => ({
|
|
275
|
+
name: entry.id,
|
|
276
|
+
value: entry.id,
|
|
277
|
+
description: entry.description,
|
|
278
|
+
}));
|
|
278
279
|
const { primaryAgent } = await ctx.prompt.ask({
|
|
279
280
|
type: 'list',
|
|
280
281
|
name: 'primaryAgent',
|
|
@@ -288,11 +289,11 @@ async function promptItemSelection(ctx, entries, currentIds) {
|
|
|
288
289
|
if (entries.length === 0)
|
|
289
290
|
return new Set();
|
|
290
291
|
const choices = entries.map((entry) => {
|
|
291
|
-
const
|
|
292
|
-
const scope = entry.appliesTo?.length ? ` [${entry.appliesTo.join(', ')}]` : '';
|
|
292
|
+
const description = getEntryChoiceDescription(entry);
|
|
293
293
|
return {
|
|
294
|
-
name: `[${entry.type}] ${entry.id}
|
|
294
|
+
name: `[${entry.type}] ${entry.id}`,
|
|
295
295
|
value: entry.id,
|
|
296
|
+
description,
|
|
296
297
|
short: entry.id,
|
|
297
298
|
};
|
|
298
299
|
});
|
|
@@ -339,6 +340,15 @@ function collectSelectedIds(config) {
|
|
|
339
340
|
ids.push(config.agent);
|
|
340
341
|
return new Set(ids);
|
|
341
342
|
}
|
|
343
|
+
function getEntryChoiceDescription(entry) {
|
|
344
|
+
const descriptions = [
|
|
345
|
+
entry.description,
|
|
346
|
+
entry.appliesTo?.length ? `适用: ${entry.appliesTo.join(', ')}` : undefined,
|
|
347
|
+
].filter(Boolean);
|
|
348
|
+
if (descriptions.length === 0)
|
|
349
|
+
return undefined;
|
|
350
|
+
return descriptions.join(' · ');
|
|
351
|
+
}
|
|
342
352
|
function getAiRulesState(ctx) {
|
|
343
353
|
return (ctx.store.getState()['ai-rules'] ?? AI_RULES_STORE_INITIAL);
|
|
344
354
|
}
|
package/dist/pipeline.js
CHANGED
|
@@ -13,10 +13,17 @@ import { getCachePath, hasCachedRepo } from './repo-cache.js';
|
|
|
13
13
|
import { computeHash, spliceMarkerBlock } from './formats/shared.js';
|
|
14
14
|
import { planMarkdownFiles, detectMarkdownConflict, writeMarkdownFile, patchMarkdownInjections, getMarkdownGitignore, stripInjectedSections, removeStaleInjectedFiles, } from './formats/agent-md.js';
|
|
15
15
|
import { planMcpJsonFiles, writeMcpJsonFile, patchMcpJsonInjections, getMcpJsonGitignore } from './formats/mcp-json.js';
|
|
16
|
+
import { planMcpTomlFiles, writeMcpTomlFile, patchMcpTomlInjections, getMcpTomlGitignore } from './formats/mcp-toml.js';
|
|
16
17
|
import { planSkillDirs, writeSkillDir, patchSkillDirInjections } from './formats/skill-dir.js';
|
|
17
18
|
// ─── Constants ──────
|
|
18
19
|
const GITIGNORE_BLOCK_START = '# xfe-ai-rules (auto-generated, do not edit)';
|
|
19
20
|
const GITIGNORE_BLOCK_END = '# end xfe-ai-rules';
|
|
21
|
+
const MCP_CONFIG_FORMATS = new Set(['mcp-json', 'mcp-toml']);
|
|
22
|
+
const FILE_WRITERS = {
|
|
23
|
+
markdown: writeMarkdownFile,
|
|
24
|
+
'mcp-json': writeMcpJsonFile,
|
|
25
|
+
'mcp-toml': writeMcpTomlFile,
|
|
26
|
+
};
|
|
20
27
|
// ─── Pipeline Step 1: Plan ──────
|
|
21
28
|
export function planSync(config, items, injections) {
|
|
22
29
|
const files = [];
|
|
@@ -28,9 +35,11 @@ export function planSync(config, items, injections) {
|
|
|
28
35
|
continue;
|
|
29
36
|
files.push(...planMarkdownFiles(target, items, injections));
|
|
30
37
|
files.push(...planMcpJsonFiles(target, items, injections));
|
|
38
|
+
files.push(...planMcpTomlFiles(target, items, injections));
|
|
31
39
|
skillDirs.push(...planSkillDirs(target, items));
|
|
32
40
|
getMarkdownGitignore(target).forEach((e) => gitignoreEntries.add(e));
|
|
33
41
|
getMcpJsonGitignore(target).forEach((e) => gitignoreEntries.add(e));
|
|
42
|
+
getMcpTomlGitignore(target).forEach((e) => gitignoreEntries.add(e));
|
|
34
43
|
}
|
|
35
44
|
return { files, skillDirs, gitignoreEntries: [...gitignoreEntries] };
|
|
36
45
|
}
|
|
@@ -42,8 +51,8 @@ export async function detectConflicts(plan, projectRoot, lastHashes) {
|
|
|
42
51
|
updatedFiles.push(file);
|
|
43
52
|
continue;
|
|
44
53
|
}
|
|
45
|
-
// MCP
|
|
46
|
-
if (file.format
|
|
54
|
+
// MCP 配置使用前缀/标记块隔离,不做冲突检测
|
|
55
|
+
if (MCP_CONFIG_FORMATS.has(file.format)) {
|
|
47
56
|
updatedFiles.push(file);
|
|
48
57
|
continue;
|
|
49
58
|
}
|
|
@@ -92,7 +101,7 @@ export async function executePlan(plan, projectRoot) {
|
|
|
92
101
|
skipped++;
|
|
93
102
|
continue;
|
|
94
103
|
}
|
|
95
|
-
const hash = file.format
|
|
104
|
+
const hash = await FILE_WRITERS[file.format](file, projectRoot);
|
|
96
105
|
written++;
|
|
97
106
|
if (file.source === 'sync') {
|
|
98
107
|
fileHashes[file.relativePath] = hash;
|
|
@@ -136,10 +145,11 @@ export async function checkSyncStatus(ctx, config) {
|
|
|
136
145
|
export async function applyInjections(ctx, config, injections) {
|
|
137
146
|
const mdResult = await patchMarkdownInjections(config, ctx.projectRoot, injections);
|
|
138
147
|
const mcpResult = await patchMcpJsonInjections(config, ctx.projectRoot, injections);
|
|
148
|
+
const mcpTomlResult = await patchMcpTomlInjections(config, ctx.projectRoot, injections);
|
|
139
149
|
const skillResult = await patchSkillDirInjections(config, ctx.projectRoot, injections);
|
|
140
|
-
const written = mdResult.written + mcpResult.written + skillResult.written;
|
|
141
|
-
const removed = mdResult.removed + mcpResult.removed;
|
|
142
|
-
const allHashes = { ...mdResult.hashes, ...mcpResult.hashes };
|
|
150
|
+
const written = mdResult.written + mcpResult.written + mcpTomlResult.written + skillResult.written;
|
|
151
|
+
const removed = mdResult.removed + mcpResult.removed + mcpTomlResult.removed;
|
|
152
|
+
const allHashes = { ...mdResult.hashes, ...mcpResult.hashes, ...mcpTomlResult.hashes };
|
|
143
153
|
// 删除历史注入独立文件
|
|
144
154
|
const storeState = (ctx.store.getState()['ai-rules'] ?? {});
|
|
145
155
|
const prevPaths = new Set(Object.keys(storeState.lastInjectedHash ?? {}));
|
package/dist/targets.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 内置 IDE target 配置:输出路径、命名规则
|
|
5
5
|
* 路径模板中包含 {id} 的为独立文件,否则为合并文件
|
|
6
6
|
*/
|
|
7
|
-
import { SyncItemType, SyncTarget } from './types.js';
|
|
7
|
+
import { McpConfigFormat, SyncItemType, SyncTarget } from './types.js';
|
|
8
8
|
import type { SyncEntry } from './types.js';
|
|
9
9
|
export interface TargetConfig {
|
|
10
10
|
id: SyncTarget;
|
|
@@ -14,6 +14,7 @@ export interface TargetConfig {
|
|
|
14
14
|
subagents: string;
|
|
15
15
|
skills: string;
|
|
16
16
|
mcp?: string;
|
|
17
|
+
mcpFormat?: McpConfigFormat;
|
|
17
18
|
mcpRootKey?: string;
|
|
18
19
|
gitignoreEntries: string[];
|
|
19
20
|
}
|
package/dist/targets.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 内置 IDE target 配置:输出路径、命名规则
|
|
5
5
|
* 路径模板中包含 {id} 的为独立文件,否则为合并文件
|
|
6
6
|
*/
|
|
7
|
-
import { SyncItemType, SyncTarget } from './types.js';
|
|
7
|
+
import { McpConfigFormat, SyncItemType, SyncTarget } from './types.js';
|
|
8
8
|
// ─── Target 注册表 ──────
|
|
9
9
|
const TARGET_REGISTRY = {
|
|
10
10
|
[SyncTarget.Copilot]: {
|
|
@@ -15,6 +15,7 @@ const TARGET_REGISTRY = {
|
|
|
15
15
|
subagents: '.github/agents/{id}.md',
|
|
16
16
|
skills: '.github/skills/{id}/SKILL.md',
|
|
17
17
|
mcp: '.vscode/mcp.json',
|
|
18
|
+
mcpFormat: McpConfigFormat.Json,
|
|
18
19
|
mcpRootKey: 'servers',
|
|
19
20
|
gitignoreEntries: ['AGENTS.md', '.github/', '.vscode/'],
|
|
20
21
|
},
|
|
@@ -26,6 +27,7 @@ const TARGET_REGISTRY = {
|
|
|
26
27
|
subagents: '.cursor/agents/{id}.md',
|
|
27
28
|
skills: '.cursor/skills/{id}/SKILL.md',
|
|
28
29
|
mcp: '.cursor/mcp.json',
|
|
30
|
+
mcpFormat: McpConfigFormat.Json,
|
|
29
31
|
mcpRootKey: 'mcpServers',
|
|
30
32
|
gitignoreEntries: ['AGENTS.md', '.cursor/'],
|
|
31
33
|
},
|
|
@@ -37,6 +39,7 @@ const TARGET_REGISTRY = {
|
|
|
37
39
|
subagents: '.claude/agents/{id}.md',
|
|
38
40
|
skills: '.claude/skills/{id}/SKILL.md',
|
|
39
41
|
mcp: '.claude/mcp.json',
|
|
42
|
+
mcpFormat: McpConfigFormat.Json,
|
|
40
43
|
mcpRootKey: 'mcpServers',
|
|
41
44
|
gitignoreEntries: ['CLAUDE.md', '.claude/'],
|
|
42
45
|
},
|
|
@@ -47,6 +50,9 @@ const TARGET_REGISTRY = {
|
|
|
47
50
|
rules: 'AGENTS.md',
|
|
48
51
|
subagents: '.codex/agents/{id}.md',
|
|
49
52
|
skills: '.codex/skills/{id}/SKILL.md',
|
|
53
|
+
mcp: '.codex/config.toml',
|
|
54
|
+
mcpFormat: McpConfigFormat.Toml,
|
|
55
|
+
mcpRootKey: 'mcp_servers',
|
|
50
56
|
gitignoreEntries: ['AGENTS.md', '.codex/'],
|
|
51
57
|
},
|
|
52
58
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -22,6 +22,10 @@ export declare enum SyncTarget {
|
|
|
22
22
|
Cursor = "cursor",
|
|
23
23
|
ClaudeCode = "claude-code"
|
|
24
24
|
}
|
|
25
|
+
export declare enum McpConfigFormat {
|
|
26
|
+
Json = "json",
|
|
27
|
+
Toml = "toml"
|
|
28
|
+
}
|
|
25
29
|
export declare const rulesTargetsParameters: z.ZodObject<{
|
|
26
30
|
targets: z.ZodOptional<z.ZodArray<z.ZodEnum<typeof SyncTarget>>>;
|
|
27
31
|
}, z.core.$strip>;
|
|
@@ -60,7 +64,7 @@ export interface SyncEntry {
|
|
|
60
64
|
/** MCP server 原始配置对象(仅 McpServer 类型使用) */
|
|
61
65
|
serverConfig?: Record<string, unknown>;
|
|
62
66
|
}
|
|
63
|
-
export type FileFormat = 'markdown' | 'mcp-json';
|
|
67
|
+
export type FileFormat = 'markdown' | 'mcp-json' | 'mcp-toml';
|
|
64
68
|
/** 计划写入的 skill 目录 */
|
|
65
69
|
export interface SkillDirPlan {
|
|
66
70
|
id: string;
|
package/dist/types.js
CHANGED
|
@@ -23,6 +23,11 @@ export var SyncTarget;
|
|
|
23
23
|
SyncTarget["Cursor"] = "cursor";
|
|
24
24
|
SyncTarget["ClaudeCode"] = "claude-code";
|
|
25
25
|
})(SyncTarget || (SyncTarget = {}));
|
|
26
|
+
export var McpConfigFormat;
|
|
27
|
+
(function (McpConfigFormat) {
|
|
28
|
+
McpConfigFormat["Json"] = "json";
|
|
29
|
+
McpConfigFormat["Toml"] = "toml";
|
|
30
|
+
})(McpConfigFormat || (McpConfigFormat = {}));
|
|
26
31
|
const syncTargetSchema = z.enum(SyncTarget);
|
|
27
32
|
export const rulesTargetsParameters = z.object({
|
|
28
33
|
targets: z.array(syncTargetSchema).optional().describe('需要同步注入的 IDE 目标 ID,可选值: copilot、codex、cursor、claude-code'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xfe-repo/cli-plugin-ai-rules",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.5",
|
|
4
4
|
"description": "XFE AI Rules plugin - sync AI coding rules from remote repo to IDE configs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
"fs-extra": "^11.3.0",
|
|
10
10
|
"gray-matter": "^4.0.3",
|
|
11
11
|
"zod": "^4.3.6",
|
|
12
|
-
"@xfe-repo/cli-core": "2.0.
|
|
13
|
-
"@xfe-repo/cli-plugin-git": "2.0.
|
|
12
|
+
"@xfe-repo/cli-core": "2.0.6",
|
|
13
|
+
"@xfe-repo/cli-plugin-git": "2.0.5"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
16
|
"@types/fs-extra": "^11.0.4",
|