agentfold 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +197 -0
- package/agentfold.mjs +1115 -0
- package/lib/compose-layers.mjs +23 -0
- package/lib/diff.mjs +47 -0
- package/lib/load-profile.mjs +112 -0
- package/lib/manifest.mjs +66 -0
- package/lib/pipeline-steps.mjs +853 -0
- package/lib/scope.mjs +158 -0
- package/lib/util.mjs +348 -0
- package/lib/validate.mjs +209 -0
- package/package.json +42 -0
|
@@ -0,0 +1,853 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { listFiles, parseFrontMatter, pathExists, toPosixPath, stripScopeFrontMatter } from './util.mjs';
|
|
4
|
+
import { emptyScopeSelector, matchesScope, normalizeScopeMetadata } from './scope.mjs';
|
|
5
|
+
|
|
6
|
+
function shouldInclude({ selector, metadata, fallbackCandidates }) {
|
|
7
|
+
const normalizedSelector = selector || emptyScopeSelector();
|
|
8
|
+
return matchesScope({ selector: normalizedSelector, metadata, fallbackCandidates });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function readScopeMetadata(filePath) {
|
|
12
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
13
|
+
const frontMatter = parseFrontMatter(content) || {};
|
|
14
|
+
const metadata = normalizeScopeMetadata(frontMatter);
|
|
15
|
+
const strippedContent = stripScopeFrontMatter(content);
|
|
16
|
+
return { content, metadata, strippedContent };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function removeSuffix(name, suffix) {
|
|
20
|
+
return name.endsWith(suffix) ? name.slice(0, -suffix.length) : name;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isPlainObject(value) {
|
|
24
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function mergeObjects(base, override) {
|
|
28
|
+
const result = { ...base };
|
|
29
|
+
|
|
30
|
+
for (const [key, overrideValue] of Object.entries(override)) {
|
|
31
|
+
const baseValue = result[key];
|
|
32
|
+
if (isPlainObject(baseValue) && isPlainObject(overrideValue)) {
|
|
33
|
+
result[key] = mergeObjects(baseValue, overrideValue);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
result[key] = overrideValue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function formatTomlPrimitive(value) {
|
|
44
|
+
if (typeof value === 'string') {
|
|
45
|
+
return `"${value
|
|
46
|
+
.replace(/\\/g, '\\\\')
|
|
47
|
+
.replace(/"/g, '\\"')
|
|
48
|
+
.replace(/\n/g, '\\n')}"`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (typeof value === 'number') {
|
|
52
|
+
if (!Number.isFinite(value)) {
|
|
53
|
+
throw new Error('Cannot serialize non-finite number to TOML.');
|
|
54
|
+
}
|
|
55
|
+
return String(value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (typeof value === 'boolean') {
|
|
59
|
+
return value ? 'true' : 'false';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (value instanceof Date) {
|
|
63
|
+
return value.toISOString();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
throw new Error(`Unsupported TOML primitive value type: ${typeof value}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isTomlInlineValue(value) {
|
|
70
|
+
if (value === null || value === undefined) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (Array.isArray(value)) {
|
|
75
|
+
return value.every((item) => isTomlInlineValue(item));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (isPlainObject(value)) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return ['string', 'number', 'boolean'].includes(typeof value) || value instanceof Date;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function toTomlDocument(value) {
|
|
86
|
+
if (!isPlainObject(value)) {
|
|
87
|
+
throw new Error('TOML output requires a JSON object root.');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const lines = [];
|
|
91
|
+
|
|
92
|
+
function formatTomlKeySegment(segment) {
|
|
93
|
+
if (/^[A-Za-z0-9_-]+$/.test(segment)) {
|
|
94
|
+
return segment;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return `"${segment
|
|
98
|
+
.replace(/\\/g, '\\\\')
|
|
99
|
+
.replace(/"/g, '\\"')}"`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function emitTable(tablePath, tableValue) {
|
|
103
|
+
const keys = Object.keys(tableValue).sort((a, b) => a.localeCompare(b));
|
|
104
|
+
const scalarLines = [];
|
|
105
|
+
const nestedTables = [];
|
|
106
|
+
|
|
107
|
+
for (const key of keys) {
|
|
108
|
+
const currentValue = tableValue[key];
|
|
109
|
+
if (currentValue === null || currentValue === undefined) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (isPlainObject(currentValue)) {
|
|
114
|
+
nestedTables.push({ key, value: currentValue });
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (Array.isArray(currentValue)) {
|
|
119
|
+
if (!currentValue.every((item) => isTomlInlineValue(item))) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`Unsupported array value for TOML key "${tablePath ? `${tablePath}.` : ''}${key}".`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
const serializedItems = currentValue.map((item) => formatTomlPrimitive(item));
|
|
125
|
+
scalarLines.push(`${key} = [${serializedItems.join(', ')}]`);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
scalarLines.push(`${key} = ${formatTomlPrimitive(currentValue)}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (tablePath && scalarLines.length > 0) {
|
|
133
|
+
lines.push(`[${tablePath}]`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const scalarLine of scalarLines) {
|
|
137
|
+
lines.push(scalarLine);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (nestedTables.length > 0) {
|
|
141
|
+
if (tablePath || scalarLines.length > 0) {
|
|
142
|
+
lines.push('');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
nestedTables.forEach((entry, index) => {
|
|
146
|
+
const childSegment = formatTomlKeySegment(entry.key);
|
|
147
|
+
const childPath = tablePath ? `${tablePath}.${childSegment}` : childSegment;
|
|
148
|
+
emitTable(childPath, entry.value);
|
|
149
|
+
if (index < nestedTables.length - 1) {
|
|
150
|
+
lines.push('');
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
emitTable('', value);
|
|
157
|
+
return `${lines.join('\n').trimEnd()}\n`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function normalizeCodexMcpConfig(mergedConfig) {
|
|
161
|
+
if (!isPlainObject(mergedConfig)) {
|
|
162
|
+
throw new Error('Codex MCP config must be an object.');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (isPlainObject(mergedConfig.mcp_servers)) {
|
|
166
|
+
return mergedConfig;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!isPlainObject(mergedConfig.servers)) {
|
|
170
|
+
return mergedConfig;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const { servers, ...rest } = mergedConfig;
|
|
174
|
+
return {
|
|
175
|
+
...rest,
|
|
176
|
+
mcp_servers: servers,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function codexProjectTrustComment() {
|
|
181
|
+
return [
|
|
182
|
+
'# NOTE: Project-scoped MCP servers require trusting the project in Codex.',
|
|
183
|
+
'# Codex -> Settings -> Configuration -> Open config.toml',
|
|
184
|
+
'# Add:',
|
|
185
|
+
'# [projects."/path/to/your/project"]',
|
|
186
|
+
'# trust_level = "trusted"',
|
|
187
|
+
'',
|
|
188
|
+
].join('\n');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function listFilesMaybeRecursive(rootDir, recursive) {
|
|
192
|
+
if (!(await pathExists(rootDir))) {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (recursive) {
|
|
197
|
+
return listFiles(rootDir);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const entries = await fs.readdir(rootDir, { withFileTypes: true });
|
|
201
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
202
|
+
return entries
|
|
203
|
+
.filter((entry) => entry.isFile())
|
|
204
|
+
.map((entry) => path.join(rootDir, entry.name));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function collectSkillDirs(skillsRoot) {
|
|
208
|
+
const dirs = [];
|
|
209
|
+
|
|
210
|
+
async function walk(currentDir) {
|
|
211
|
+
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
212
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
213
|
+
|
|
214
|
+
let hasSkillFile = false;
|
|
215
|
+
for (const entry of entries) {
|
|
216
|
+
if (entry.isFile() && entry.name === 'SKILL.md') {
|
|
217
|
+
hasSkillFile = true;
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (hasSkillFile) {
|
|
223
|
+
dirs.push(currentDir);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
for (const entry of entries) {
|
|
227
|
+
if (entry.isDirectory()) {
|
|
228
|
+
await walk(path.join(currentDir, entry.name));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (await pathExists(skillsRoot)) {
|
|
234
|
+
await walk(skillsRoot);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
dirs.sort((a, b) => a.localeCompare(b));
|
|
238
|
+
return dirs;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export async function buildRenderPlan({ preset, layers, logger, projectRoot, configRoot }) {
|
|
242
|
+
const renderMap = new Map();
|
|
243
|
+
const warnings = [];
|
|
244
|
+
const skillDefinitions = [];
|
|
245
|
+
const promptDefinitions = [];
|
|
246
|
+
const subagentDefinitions = [];
|
|
247
|
+
|
|
248
|
+
function addRenderedText(relativePath, content, sourceLabel) {
|
|
249
|
+
const key = toPosixPath(relativePath);
|
|
250
|
+
if (renderMap.has(key)) {
|
|
251
|
+
const existing = renderMap.get(key);
|
|
252
|
+
throw new Error(
|
|
253
|
+
`Destination collision for ${key} between ${existing.source} and ${sourceLabel}`
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
renderMap.set(key, { content, source: sourceLabel });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function addRenderedFile(relativePath, content, sourcePath) {
|
|
260
|
+
addRenderedText(relativePath, content, toPosixPath(sourcePath));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function normalizeSubagentId(baseName) {
|
|
264
|
+
if (baseName.endsWith('.agent.md')) {
|
|
265
|
+
return removeSuffix(baseName, '.agent.md');
|
|
266
|
+
}
|
|
267
|
+
if (baseName.endsWith('.md')) {
|
|
268
|
+
return removeSuffix(baseName, '.md');
|
|
269
|
+
}
|
|
270
|
+
return baseName;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function normalizeRuleId(baseName) {
|
|
274
|
+
if (baseName.endsWith('.instructions.md')) {
|
|
275
|
+
return removeSuffix(baseName, '.instructions.md');
|
|
276
|
+
}
|
|
277
|
+
if (baseName.endsWith('.rule.md')) {
|
|
278
|
+
return removeSuffix(baseName, '.rule.md');
|
|
279
|
+
}
|
|
280
|
+
if (baseName.endsWith('.mdc')) {
|
|
281
|
+
return removeSuffix(baseName, '.mdc');
|
|
282
|
+
}
|
|
283
|
+
if (baseName.endsWith('.md')) {
|
|
284
|
+
return removeSuffix(baseName, '.md');
|
|
285
|
+
}
|
|
286
|
+
return baseName;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function collectCanonicalRules() {
|
|
290
|
+
const rules = new Map();
|
|
291
|
+
|
|
292
|
+
for (const layer of layers) {
|
|
293
|
+
const rulesRoot = path.join(layer.path, 'rules');
|
|
294
|
+
const files = await listFilesMaybeRecursive(rulesRoot, preset.organization.includeRuleSubdirs);
|
|
295
|
+
|
|
296
|
+
for (const filePath of files) {
|
|
297
|
+
const baseName = path.basename(filePath);
|
|
298
|
+
if (!baseName.endsWith('.md') && !baseName.endsWith('.mdc')) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const id = normalizeRuleId(baseName);
|
|
303
|
+
const relativePath = toPosixPath(path.relative(rulesRoot, filePath));
|
|
304
|
+
const { strippedContent, metadata } = await readScopeMetadata(filePath);
|
|
305
|
+
if (!shouldInclude({
|
|
306
|
+
selector: layer.select.rules,
|
|
307
|
+
metadata,
|
|
308
|
+
fallbackCandidates: [id, baseName, relativePath],
|
|
309
|
+
})) {
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (rules.has(id)) {
|
|
314
|
+
const existing = rules.get(id);
|
|
315
|
+
throw new Error(`Rule collision for id "${id}" between ${existing.sourcePath} and ${filePath}`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
rules.set(id, { id, layer: layer.name, sourcePath: filePath, content: strippedContent });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return rules;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function collectLegacyRules(target) {
|
|
326
|
+
const rules = new Map();
|
|
327
|
+
|
|
328
|
+
for (const layer of layers) {
|
|
329
|
+
const root =
|
|
330
|
+
target === 'copilot'
|
|
331
|
+
? path.join(layer.path, 'copilot', 'instructions')
|
|
332
|
+
: path.join(layer.path, 'cursor', 'rules');
|
|
333
|
+
|
|
334
|
+
const files = await listFilesMaybeRecursive(root, preset.organization.includeRuleSubdirs);
|
|
335
|
+
for (const filePath of files) {
|
|
336
|
+
const baseName = path.basename(filePath);
|
|
337
|
+
if (!baseName.endsWith('.instructions.md') && !baseName.endsWith('.md') && !baseName.endsWith('.mdc')) {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const id = normalizeRuleId(baseName);
|
|
342
|
+
const relativePath = toPosixPath(path.relative(root, filePath));
|
|
343
|
+
const { strippedContent, metadata } = await readScopeMetadata(filePath);
|
|
344
|
+
if (!shouldInclude({
|
|
345
|
+
selector: layer.select.rules,
|
|
346
|
+
metadata,
|
|
347
|
+
fallbackCandidates: [id, baseName, relativePath],
|
|
348
|
+
})) {
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (rules.has(id)) {
|
|
353
|
+
const existing = rules.get(id);
|
|
354
|
+
throw new Error(
|
|
355
|
+
`Rule collision for target "${target}" and id "${id}" between ${existing.sourcePath} and ${filePath}`
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
rules.set(id, { id, layer: layer.name, sourcePath: filePath, content: strippedContent });
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return rules;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function collectPortableSubagents() {
|
|
367
|
+
const portable = new Map();
|
|
368
|
+
|
|
369
|
+
for (const layer of layers) {
|
|
370
|
+
const portableRoot = path.join(layer.path, 'subagents');
|
|
371
|
+
const files = await listFilesMaybeRecursive(
|
|
372
|
+
portableRoot,
|
|
373
|
+
preset.organization.includeAgentSubdirs
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
for (const filePath of files) {
|
|
377
|
+
const baseName = path.basename(filePath);
|
|
378
|
+
if (!baseName.endsWith('.md')) {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const agentId = normalizeSubagentId(baseName);
|
|
383
|
+
const relativePath = toPosixPath(path.relative(portableRoot, filePath));
|
|
384
|
+
const { strippedContent, metadata } = await readScopeMetadata(filePath);
|
|
385
|
+
if (!shouldInclude({
|
|
386
|
+
selector: layer.select.subagents,
|
|
387
|
+
metadata,
|
|
388
|
+
fallbackCandidates: [agentId, baseName, relativePath],
|
|
389
|
+
})) {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (portable.has(agentId)) {
|
|
394
|
+
const existing = portable.get(agentId);
|
|
395
|
+
throw new Error(
|
|
396
|
+
`Subagent collision for id "${agentId}" between ${existing.sourcePath} and ${filePath}`
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
portable.set(agentId, {
|
|
401
|
+
id: agentId,
|
|
402
|
+
layer: layer.name,
|
|
403
|
+
sourcePath: filePath,
|
|
404
|
+
content: strippedContent,
|
|
405
|
+
sourceKind: 'portable',
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return portable;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async function collectToolSpecificSubagents(target) {
|
|
414
|
+
const perTarget = new Map();
|
|
415
|
+
|
|
416
|
+
for (const layer of layers) {
|
|
417
|
+
const root =
|
|
418
|
+
target === 'copilot'
|
|
419
|
+
? path.join(layer.path, 'copilot', 'agents')
|
|
420
|
+
: target === 'claude'
|
|
421
|
+
? path.join(layer.path, 'claude', 'agents')
|
|
422
|
+
: path.join(layer.path, 'cursor', 'agents');
|
|
423
|
+
const files = await listFilesMaybeRecursive(root, preset.organization.includeAgentSubdirs);
|
|
424
|
+
|
|
425
|
+
for (const filePath of files) {
|
|
426
|
+
const baseName = path.basename(filePath);
|
|
427
|
+
if (target === 'copilot' && !baseName.endsWith('.agent.md')) {
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (target === 'cursor' && !baseName.endsWith('.md')) {
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (target === 'claude' && !baseName.endsWith('.md')) {
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const agentId = normalizeSubagentId(baseName);
|
|
438
|
+
const relativePath = toPosixPath(path.relative(root, filePath));
|
|
439
|
+
const { strippedContent, metadata } = await readScopeMetadata(filePath);
|
|
440
|
+
if (!shouldInclude({
|
|
441
|
+
selector: layer.select.subagents,
|
|
442
|
+
metadata,
|
|
443
|
+
fallbackCandidates: [agentId, baseName, relativePath],
|
|
444
|
+
})) {
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (perTarget.has(agentId)) {
|
|
449
|
+
const existing = perTarget.get(agentId);
|
|
450
|
+
throw new Error(
|
|
451
|
+
`Subagent collision for target "${target}" and id "${agentId}" between ${existing.sourcePath} and ${filePath}`
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
perTarget.set(agentId, {
|
|
456
|
+
id: agentId,
|
|
457
|
+
layer: layer.name,
|
|
458
|
+
sourcePath: filePath,
|
|
459
|
+
content: strippedContent,
|
|
460
|
+
sourceKind: target,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return perTarget;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const agentSections = [];
|
|
469
|
+
for (const layer of layers) {
|
|
470
|
+
const filePath = path.join(layer.path, 'agents', 'AGENTS.md');
|
|
471
|
+
if (!(await pathExists(filePath))) {
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
476
|
+
agentSections.push(`<!-- Layer: ${layer.name} -->\n${content.trim()}\n`);
|
|
477
|
+
}
|
|
478
|
+
const isConfigRootProject =
|
|
479
|
+
typeof projectRoot === 'string' &&
|
|
480
|
+
typeof configRoot === 'string' &&
|
|
481
|
+
path.resolve(projectRoot) === path.resolve(configRoot);
|
|
482
|
+
|
|
483
|
+
if (agentSections.length > 0 && !isConfigRootProject) {
|
|
484
|
+
addRenderedText('AGENTS.md', `${agentSections.join('\n').trim()}\n`, 'generated:AGENTS');
|
|
485
|
+
if (preset.targets.claude) {
|
|
486
|
+
addRenderedText('CLAUDE.md', `${agentSections.join('\n').trim()}\n`, 'generated:CLAUDE');
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (preset.targets.copilot) {
|
|
491
|
+
const instructionSections = [];
|
|
492
|
+
for (const layer of layers) {
|
|
493
|
+
const filePath = path.join(layer.path, 'copilot', 'copilot-instructions.md');
|
|
494
|
+
if (!(await pathExists(filePath))) {
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
499
|
+
instructionSections.push(`<!-- Layer: ${layer.name} -->\n${content.trim()}\n`);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (instructionSections.length > 0) {
|
|
503
|
+
addRenderedText(
|
|
504
|
+
'.github/copilot-instructions.md',
|
|
505
|
+
`${instructionSections.join('\n').trim()}\n`,
|
|
506
|
+
'generated:copilot-instructions'
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
for (const layer of layers) {
|
|
512
|
+
const skillsRoot = path.join(layer.path, 'skills');
|
|
513
|
+
if (!(await pathExists(skillsRoot))) {
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const skillDirs = await collectSkillDirs(skillsRoot);
|
|
518
|
+
for (const skillDir of skillDirs) {
|
|
519
|
+
const skillName = path.basename(skillDir);
|
|
520
|
+
const relativePath = toPosixPath(path.relative(skillsRoot, skillDir));
|
|
521
|
+
const skillFilePath = path.join(skillDir, 'SKILL.md');
|
|
522
|
+
const skillMeta = await readScopeMetadata(skillFilePath).catch(() => ({
|
|
523
|
+
metadata: { namespace: '', tags: [] },
|
|
524
|
+
}));
|
|
525
|
+
|
|
526
|
+
if (!shouldInclude({
|
|
527
|
+
selector: layer.select.skills,
|
|
528
|
+
metadata: skillMeta.metadata,
|
|
529
|
+
fallbackCandidates: [skillName, relativePath],
|
|
530
|
+
})) {
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const files = await listFiles(skillDir);
|
|
535
|
+
for (const filePath of files) {
|
|
536
|
+
const relativeInSkill = toPosixPath(path.relative(skillDir, filePath));
|
|
537
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
538
|
+
const strippedContent = stripScopeFrontMatter(content);
|
|
539
|
+
addRenderedFile(path.posix.join('.agents/skills', skillName, relativeInSkill), strippedContent, filePath);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
skillDefinitions.push({
|
|
543
|
+
name: skillName,
|
|
544
|
+
layer: layer.name,
|
|
545
|
+
relativePath,
|
|
546
|
+
skillDir,
|
|
547
|
+
skillFilePath,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
async function renderPortableTree({ sourceFolder, destinationFolder, selectorKey }) {
|
|
553
|
+
for (const layer of layers) {
|
|
554
|
+
const sourceRoot = path.join(layer.path, sourceFolder);
|
|
555
|
+
const files = await listFilesMaybeRecursive(sourceRoot, true);
|
|
556
|
+
|
|
557
|
+
for (const filePath of files) {
|
|
558
|
+
const relativePath = toPosixPath(path.relative(sourceRoot, filePath));
|
|
559
|
+
const baseName = path.basename(filePath);
|
|
560
|
+
if (baseName.startsWith('.')) {
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
const id = removeSuffix(baseName, path.extname(baseName));
|
|
564
|
+
const { strippedContent, metadata } = await readScopeMetadata(filePath);
|
|
565
|
+
if (!shouldInclude({
|
|
566
|
+
selector: layer.select[selectorKey],
|
|
567
|
+
metadata,
|
|
568
|
+
fallbackCandidates: [id, baseName, relativePath],
|
|
569
|
+
})) {
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
addRenderedFile(path.posix.join(destinationFolder, relativePath), strippedContent, filePath);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
await renderPortableTree({
|
|
578
|
+
sourceFolder: 'commands',
|
|
579
|
+
destinationFolder: '.agents/commands',
|
|
580
|
+
selectorKey: 'commands',
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
async function collectSelectedMcpJsonFiles() {
|
|
584
|
+
const files = [];
|
|
585
|
+
|
|
586
|
+
for (const layer of layers) {
|
|
587
|
+
const sourceRoot = path.join(layer.path, 'mcp');
|
|
588
|
+
const layerFiles = await listFilesMaybeRecursive(sourceRoot, true);
|
|
589
|
+
|
|
590
|
+
for (const filePath of layerFiles) {
|
|
591
|
+
const relativePath = toPosixPath(path.relative(sourceRoot, filePath));
|
|
592
|
+
const baseName = path.basename(filePath);
|
|
593
|
+
if (baseName.startsWith('.')) {
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const id = removeSuffix(baseName, path.extname(baseName));
|
|
598
|
+
const { strippedContent, metadata } = await readScopeMetadata(filePath);
|
|
599
|
+
if (!shouldInclude({
|
|
600
|
+
selector: layer.select.mcp,
|
|
601
|
+
metadata,
|
|
602
|
+
fallbackCandidates: [id, baseName, relativePath],
|
|
603
|
+
})) {
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const extension = path.extname(baseName).toLowerCase();
|
|
608
|
+
if (extension !== '.json') {
|
|
609
|
+
throw new Error(
|
|
610
|
+
`Unsupported MCP config format at ${filePath}. Only .json files can be merged into .vscode/mcp.json.`
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
files.push({
|
|
615
|
+
layer: layer.name,
|
|
616
|
+
filePath,
|
|
617
|
+
relativePath,
|
|
618
|
+
content: strippedContent,
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return files;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
async function renderMergedMcpConfig({ destinationPath, format }) {
|
|
627
|
+
const selectedFiles = await collectSelectedMcpJsonFiles();
|
|
628
|
+
if (selectedFiles.length === 0) {
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
let merged = {};
|
|
633
|
+
for (const file of selectedFiles) {
|
|
634
|
+
let parsed;
|
|
635
|
+
try {
|
|
636
|
+
parsed = JSON.parse(file.content);
|
|
637
|
+
} catch (error) {
|
|
638
|
+
throw new Error(`Invalid JSON in MCP config ${file.filePath}: ${error.message}`);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (!isPlainObject(parsed)) {
|
|
642
|
+
throw new Error(`MCP config must contain a JSON object at ${file.filePath}`);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
merged = mergeObjects(merged, parsed);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const renderedContent =
|
|
649
|
+
format === 'toml'
|
|
650
|
+
? `${codexProjectTrustComment()}${toTomlDocument(normalizeCodexMcpConfig(merged))}`
|
|
651
|
+
: `${JSON.stringify(merged, null, 2)}\n`;
|
|
652
|
+
|
|
653
|
+
addRenderedText(destinationPath, renderedContent, 'generated:mcp-merged');
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (preset.targets.copilot) {
|
|
657
|
+
await renderMergedMcpConfig({ destinationPath: '.vscode/mcp.json', format: 'json' });
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (preset.targets.cursor) {
|
|
661
|
+
await renderMergedMcpConfig({ destinationPath: '.cursor/mcp.json', format: 'json' });
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (preset.targets.codex) {
|
|
665
|
+
await renderMergedMcpConfig({ destinationPath: '.codex/config.toml', format: 'toml' });
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (preset.targets.copilot) {
|
|
669
|
+
for (const layer of layers) {
|
|
670
|
+
const promptsRoot = path.join(layer.path, 'prompts');
|
|
671
|
+
const promptFiles = await listFilesMaybeRecursive(promptsRoot, true);
|
|
672
|
+
|
|
673
|
+
for (const filePath of promptFiles) {
|
|
674
|
+
const baseName = path.basename(filePath);
|
|
675
|
+
if (!baseName.endsWith('.prompt.md')) {
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const promptName = removeSuffix(baseName, '.prompt.md');
|
|
680
|
+
const relativePath = toPosixPath(path.relative(promptsRoot, filePath));
|
|
681
|
+
const { strippedContent, metadata } = await readScopeMetadata(filePath);
|
|
682
|
+
if (!shouldInclude({
|
|
683
|
+
selector: layer.select.prompts,
|
|
684
|
+
metadata,
|
|
685
|
+
fallbackCandidates: [promptName, baseName, relativePath],
|
|
686
|
+
})) {
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
addRenderedFile(path.posix.join('.github/prompts', baseName), strippedContent, filePath);
|
|
691
|
+
promptDefinitions.push({
|
|
692
|
+
name: baseName,
|
|
693
|
+
layer: layer.name,
|
|
694
|
+
sourcePath: filePath,
|
|
695
|
+
relativePath,
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const canonicalRules = await collectCanonicalRules();
|
|
703
|
+
|
|
704
|
+
async function renderRules(target) {
|
|
705
|
+
const destinationRoot = target === 'copilot' ? '.github/instructions' : '.cursor/rules';
|
|
706
|
+
const extension = target === 'copilot' ? '.instructions.md' : '.mdc';
|
|
707
|
+
|
|
708
|
+
const source =
|
|
709
|
+
canonicalRules.size > 0 ? canonicalRules : await collectLegacyRules(target);
|
|
710
|
+
|
|
711
|
+
for (const rule of source.values()) {
|
|
712
|
+
const outputName = `${rule.id}${extension}`;
|
|
713
|
+
addRenderedFile(path.posix.join(destinationRoot, outputName), rule.content, rule.sourcePath);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (preset.targets.copilot) {
|
|
718
|
+
await renderRules('copilot');
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const sourceMode = preset.subagents.sourceMode;
|
|
722
|
+
const portableSubagents = await collectPortableSubagents();
|
|
723
|
+
|
|
724
|
+
if (preset.targets.cursor) {
|
|
725
|
+
await renderRules('cursor');
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (preset.subagents.portable) {
|
|
729
|
+
for (const agent of portableSubagents.values()) {
|
|
730
|
+
const portableName = `${agent.id}.md`;
|
|
731
|
+
addRenderedFile(path.posix.join('.agents/subagents', portableName), agent.content, agent.sourcePath);
|
|
732
|
+
subagentDefinitions.push({
|
|
733
|
+
target: 'portable',
|
|
734
|
+
id: agent.id,
|
|
735
|
+
name: portableName,
|
|
736
|
+
layer: agent.layer,
|
|
737
|
+
sourcePath: agent.sourcePath,
|
|
738
|
+
sourceKind: agent.sourceKind,
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
async function renderToolSubagents(target) {
|
|
744
|
+
const destinationRoot =
|
|
745
|
+
target === 'copilot'
|
|
746
|
+
? '.github/agents'
|
|
747
|
+
: target === 'claude'
|
|
748
|
+
? '.claude/agents'
|
|
749
|
+
: '.cursor/agents';
|
|
750
|
+
const extension = target === 'copilot' ? '.agent.md' : '.md';
|
|
751
|
+
|
|
752
|
+
if (sourceMode === 'centralized' && portableSubagents.size > 0) {
|
|
753
|
+
for (const agent of portableSubagents.values()) {
|
|
754
|
+
const outputName = `${agent.id}${extension}`;
|
|
755
|
+
addRenderedFile(path.posix.join(destinationRoot, outputName), agent.content, agent.sourcePath);
|
|
756
|
+
subagentDefinitions.push({
|
|
757
|
+
target,
|
|
758
|
+
id: agent.id,
|
|
759
|
+
name: outputName,
|
|
760
|
+
layer: agent.layer,
|
|
761
|
+
sourcePath: agent.sourcePath,
|
|
762
|
+
sourceKind: agent.sourceKind,
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const specific = await collectToolSpecificSubagents(target);
|
|
769
|
+
for (const agent of specific.values()) {
|
|
770
|
+
const outputName = `${agent.id}${extension}`;
|
|
771
|
+
addRenderedFile(path.posix.join(destinationRoot, outputName), agent.content, agent.sourcePath);
|
|
772
|
+
subagentDefinitions.push({
|
|
773
|
+
target,
|
|
774
|
+
id: agent.id,
|
|
775
|
+
name: outputName,
|
|
776
|
+
layer: agent.layer,
|
|
777
|
+
sourcePath: agent.sourcePath,
|
|
778
|
+
sourceKind: agent.sourceKind,
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if (preset.targets.copilot) {
|
|
784
|
+
await renderToolSubagents('copilot');
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (preset.targets.cursor) {
|
|
788
|
+
await renderToolSubagents('cursor');
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (preset.targets.claude) {
|
|
792
|
+
await renderToolSubagents('claude');
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function collectReferencedPortableReferences() {
|
|
796
|
+
const references = new Set();
|
|
797
|
+
const referencePattern = /\.agents\/references\/([A-Za-z0-9._\/-]+)/g;
|
|
798
|
+
|
|
799
|
+
for (const [relativePath, record] of renderMap.entries()) {
|
|
800
|
+
if (relativePath.startsWith('.agents/references/')) {
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const content = record.content;
|
|
805
|
+
let match = referencePattern.exec(content);
|
|
806
|
+
while (match) {
|
|
807
|
+
const normalized = match[1].replace(/^\/+/, '');
|
|
808
|
+
references.add(normalized);
|
|
809
|
+
match = referencePattern.exec(content);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
return [...references].sort((a, b) => a.localeCompare(b));
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
async function renderReferencedPortableReferences() {
|
|
817
|
+
const referencedPaths = collectReferencedPortableReferences();
|
|
818
|
+
const unresolved = new Set(referencedPaths);
|
|
819
|
+
|
|
820
|
+
for (const layer of layers) {
|
|
821
|
+
const sourceRoot = path.join(layer.path, 'references');
|
|
822
|
+
if (!(await pathExists(sourceRoot))) {
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
for (const referencePath of referencedPaths) {
|
|
827
|
+
const fullPath = path.join(sourceRoot, referencePath);
|
|
828
|
+
if (!(await pathExists(fullPath))) {
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const content = await fs.readFile(fullPath, 'utf8');
|
|
833
|
+
const strippedContent = stripScopeFrontMatter(content);
|
|
834
|
+
addRenderedFile(path.posix.join('.agents/references', referencePath), strippedContent, fullPath);
|
|
835
|
+
unresolved.delete(referencePath);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
for (const missing of unresolved) {
|
|
840
|
+
warnings.push(
|
|
841
|
+
`Referenced file .agents/references/${missing} was not found in selected layers references folders.`
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
await renderReferencedPortableReferences();
|
|
847
|
+
|
|
848
|
+
for (const warning of warnings) {
|
|
849
|
+
logger.warn(`Warning: ${warning}`);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
return { renderMap, warnings, skillDefinitions, promptDefinitions, subagentDefinitions };
|
|
853
|
+
}
|