@lnai/core 0.6.0 → 0.6.6
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/dist/index.d.ts +69 -5
- package/dist/index.js +361 -232
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -40,6 +40,10 @@ declare class PluginError extends LnaiError {
|
|
|
40
40
|
readonly pluginId: string;
|
|
41
41
|
constructor(message: string, pluginId: string, cause?: Error);
|
|
42
42
|
}
|
|
43
|
+
declare class InvalidToolError extends LnaiError {
|
|
44
|
+
readonly invalidTools: string[];
|
|
45
|
+
constructor(invalidTools: string[], validTools: string[]);
|
|
46
|
+
}
|
|
43
47
|
|
|
44
48
|
/** MCP Server configuration (Claude format as source of truth) */
|
|
45
49
|
declare const mcpServerSchema: z.ZodObject<{
|
|
@@ -202,6 +206,25 @@ interface SyncResult {
|
|
|
202
206
|
changes: ChangeResult[];
|
|
203
207
|
validation: ValidationResult;
|
|
204
208
|
}
|
|
209
|
+
/** Entry for a single file in the manifest */
|
|
210
|
+
interface ManifestEntry {
|
|
211
|
+
path: string;
|
|
212
|
+
type: "json" | "text" | "symlink";
|
|
213
|
+
hash?: string;
|
|
214
|
+
target?: string;
|
|
215
|
+
}
|
|
216
|
+
/** Manifest for a single tool's generated files */
|
|
217
|
+
interface ToolManifest {
|
|
218
|
+
version: 1;
|
|
219
|
+
tool: ToolId;
|
|
220
|
+
generatedAt: string;
|
|
221
|
+
files: ManifestEntry[];
|
|
222
|
+
}
|
|
223
|
+
/** Root manifest tracking all LNAI-generated files */
|
|
224
|
+
interface LnaiManifest {
|
|
225
|
+
version: 1;
|
|
226
|
+
tools: Partial<Record<ToolId, ToolManifest>>;
|
|
227
|
+
}
|
|
205
228
|
|
|
206
229
|
declare function parseFrontmatter(content: string): {
|
|
207
230
|
frontmatter: Record<string, unknown>;
|
|
@@ -260,7 +283,7 @@ declare const claudeCodePlugin: Plugin;
|
|
|
260
283
|
* Output structure:
|
|
261
284
|
* - AGENTS.md (symlink -> .ai/AGENTS.md) [at project root]
|
|
262
285
|
* - <dir>/AGENTS.md (generated from .ai/rules/*.md, per glob directory)
|
|
263
|
-
* - .
|
|
286
|
+
* - .agents/skills/<name>/ (symlink -> ../../.ai/skills/<name>)
|
|
264
287
|
* - .codex/config.toml (generated from settings.mcpServers)
|
|
265
288
|
* - .codex/<path> (symlink -> ../.ai/.codex/<path>) for override files
|
|
266
289
|
*/
|
|
@@ -272,7 +295,7 @@ declare const codexPlugin: Plugin;
|
|
|
272
295
|
* Output structure:
|
|
273
296
|
* - AGENTS.md (symlink -> .ai/AGENTS.md) [at project root]
|
|
274
297
|
* - .opencode/rules/ (symlink -> ../.ai/rules)
|
|
275
|
-
* - .
|
|
298
|
+
* - .agents/skills/<name>/ (symlink -> ../../.ai/skills/<name>)
|
|
276
299
|
* - opencode.json (generated config merged with .ai/.opencode/opencode.json)
|
|
277
300
|
* - .opencode/<path> (symlink -> ../.ai/.opencode/<path>) for other override files
|
|
278
301
|
*/
|
|
@@ -298,8 +321,8 @@ interface SyncOptions {
|
|
|
298
321
|
tools?: ToolId[];
|
|
299
322
|
/** Preview changes without writing files */
|
|
300
323
|
dryRun?: boolean;
|
|
301
|
-
/**
|
|
302
|
-
|
|
324
|
+
/** Skip cleanup of orphaned files */
|
|
325
|
+
skipCleanup?: boolean;
|
|
303
326
|
}
|
|
304
327
|
/**
|
|
305
328
|
* Run the sync pipeline to export .ai/ config to native tool formats.
|
|
@@ -320,9 +343,50 @@ declare function writeFiles(files: OutputFile[], options: WriterOptions): Promis
|
|
|
320
343
|
/**
|
|
321
344
|
* Update .gitignore with paths that should not be version controlled.
|
|
322
345
|
* Manages a dedicated "lnai-generated" section to avoid conflicts with user entries.
|
|
346
|
+
* Replaces the managed section on each run so stale paths are removed.
|
|
323
347
|
*/
|
|
324
348
|
declare function updateGitignore(rootDir: string, paths: string[]): Promise<void>;
|
|
325
349
|
|
|
350
|
+
declare const MANIFEST_FILENAME = ".lnai-manifest.json";
|
|
351
|
+
/**
|
|
352
|
+
* Read the LNAI manifest from the .ai directory.
|
|
353
|
+
* Returns null if the manifest doesn't exist.
|
|
354
|
+
*/
|
|
355
|
+
declare function readManifest(rootDir: string): Promise<LnaiManifest | null>;
|
|
356
|
+
/**
|
|
357
|
+
* Write the LNAI manifest to the .ai directory.
|
|
358
|
+
*/
|
|
359
|
+
declare function writeManifest(rootDir: string, manifest: LnaiManifest): Promise<void>;
|
|
360
|
+
/**
|
|
361
|
+
* Build a tool manifest from output files.
|
|
362
|
+
*/
|
|
363
|
+
declare function buildToolManifest(toolId: ToolId, files: OutputFile[]): ToolManifest;
|
|
364
|
+
/**
|
|
365
|
+
* Update a single tool's manifest entry.
|
|
366
|
+
*/
|
|
367
|
+
declare function updateToolManifest(manifest: LnaiManifest, toolId: ToolId, files: OutputFile[]): LnaiManifest;
|
|
368
|
+
/**
|
|
369
|
+
* Create an empty manifest.
|
|
370
|
+
*/
|
|
371
|
+
declare function createEmptyManifest(): LnaiManifest;
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Compute which files should be deleted based on previous and current manifest entries.
|
|
375
|
+
* Returns paths that exist in previous but not in current.
|
|
376
|
+
*/
|
|
377
|
+
declare function computeFilesToDelete(previousFiles: ManifestEntry[], currentFiles: OutputFile[]): string[];
|
|
378
|
+
/**
|
|
379
|
+
* Delete files that are no longer in the manifest.
|
|
380
|
+
* Returns ChangeResult[] with action: "delete" for each deleted file.
|
|
381
|
+
* When dryRun is true, no files are actually deleted.
|
|
382
|
+
*/
|
|
383
|
+
declare function deleteFiles(paths: string[], rootDir: string, dryRun: boolean): Promise<ChangeResult[]>;
|
|
384
|
+
/**
|
|
385
|
+
* Clean up empty parent directories after file deletion.
|
|
386
|
+
* Stops at the project root directory.
|
|
387
|
+
*/
|
|
388
|
+
declare function cleanupEmptyParentDirs(filePath: string, rootDir: string): Promise<void>;
|
|
389
|
+
|
|
326
390
|
interface InitOptions {
|
|
327
391
|
rootDir: string;
|
|
328
392
|
tools?: ToolId[];
|
|
@@ -337,4 +401,4 @@ declare function initUnifiedConfig(options: InitOptions): Promise<InitResult>;
|
|
|
337
401
|
declare function hasUnifiedConfig(rootDir: string): Promise<boolean>;
|
|
338
402
|
declare function generateDefaultConfig(tools?: ToolId[], versionControl?: Record<ToolId, boolean>): Config;
|
|
339
403
|
|
|
340
|
-
export { CONFIG_DIRS, CONFIG_FILES, type ChangeResult, type Config, FileNotFoundError, type InitOptions, type InitResult, LnaiError, type MarkdownFile, type MarkdownFrontmatter, type McpServer, type OutputFile, ParseError, type PermissionLevel, type Permissions, type Plugin, PluginError, type RuleFrontmatter, type Settings, type SkillFrontmatter, type SkippedFeatureDetail, type SyncOptions, type SyncResult, TOOL_IDS, TOOL_OUTPUT_DIRS, type ToolConfig, type ToolId, UNIFIED_DIR, type UnifiedState, ValidationError, type ValidationErrorDetail, type ValidationResult, type ValidationWarningDetail, WriteError, type WriterOptions, claudeCodePlugin, codexPlugin, computeHash, configSchema, generateDefaultConfig, hasUnifiedConfig, initUnifiedConfig, mcpServerSchema, opencodePlugin, parseFrontmatter, parseUnifiedConfig, permissionsSchema, pluginRegistry, ruleFrontmatterSchema, runSyncPipeline, settingsSchema, skillFrontmatterSchema, toolConfigSchema, toolIdSchema, updateGitignore, validateConfig, validateSettings, validateUnifiedState, writeFiles };
|
|
404
|
+
export { CONFIG_DIRS, CONFIG_FILES, type ChangeResult, type Config, FileNotFoundError, type InitOptions, type InitResult, InvalidToolError, LnaiError, type LnaiManifest, MANIFEST_FILENAME, type ManifestEntry, type MarkdownFile, type MarkdownFrontmatter, type McpServer, type OutputFile, ParseError, type PermissionLevel, type Permissions, type Plugin, PluginError, type RuleFrontmatter, type Settings, type SkillFrontmatter, type SkippedFeatureDetail, type SyncOptions, type SyncResult, TOOL_IDS, TOOL_OUTPUT_DIRS, type ToolConfig, type ToolId, type ToolManifest, UNIFIED_DIR, type UnifiedState, ValidationError, type ValidationErrorDetail, type ValidationResult, type ValidationWarningDetail, WriteError, type WriterOptions, buildToolManifest, claudeCodePlugin, cleanupEmptyParentDirs, codexPlugin, computeFilesToDelete, computeHash, configSchema, createEmptyManifest, deleteFiles, generateDefaultConfig, hasUnifiedConfig, initUnifiedConfig, mcpServerSchema, opencodePlugin, parseFrontmatter, parseUnifiedConfig, permissionsSchema, pluginRegistry, readManifest, ruleFrontmatterSchema, runSyncPipeline, settingsSchema, skillFrontmatterSchema, toolConfigSchema, toolIdSchema, updateGitignore, updateToolManifest, validateConfig, validateSettings, validateUnifiedState, writeFiles, writeManifest };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import * as
|
|
2
|
+
import * as fs4 from 'fs/promises';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import matter from 'gray-matter';
|
|
5
5
|
import * as crypto from 'crypto';
|
|
@@ -67,10 +67,10 @@ var ParseError = class extends LnaiError {
|
|
|
67
67
|
var ValidationError = class extends LnaiError {
|
|
68
68
|
path;
|
|
69
69
|
value;
|
|
70
|
-
constructor(message,
|
|
70
|
+
constructor(message, path8, value) {
|
|
71
71
|
super(message, "VALIDATION_ERROR");
|
|
72
72
|
this.name = "ValidationError";
|
|
73
|
-
this.path =
|
|
73
|
+
this.path = path8;
|
|
74
74
|
this.value = value;
|
|
75
75
|
}
|
|
76
76
|
};
|
|
@@ -104,6 +104,17 @@ var PluginError = class extends LnaiError {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
};
|
|
107
|
+
var InvalidToolError = class extends LnaiError {
|
|
108
|
+
invalidTools;
|
|
109
|
+
constructor(invalidTools, validTools) {
|
|
110
|
+
super(
|
|
111
|
+
`Invalid tool(s): ${invalidTools.join(", ")}. Valid tools: ${validTools.join(", ")}`,
|
|
112
|
+
"INVALID_TOOL"
|
|
113
|
+
);
|
|
114
|
+
this.name = "InvalidToolError";
|
|
115
|
+
this.invalidTools = invalidTools;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
107
118
|
var mcpServerSchema = z.object({
|
|
108
119
|
command: z.string().optional(),
|
|
109
120
|
args: z.array(z.string()).optional(),
|
|
@@ -204,7 +215,7 @@ async function parseUnifiedConfig(rootDir) {
|
|
|
204
215
|
}
|
|
205
216
|
async function fileExists(filePath) {
|
|
206
217
|
try {
|
|
207
|
-
await
|
|
218
|
+
await fs4.access(filePath);
|
|
208
219
|
return true;
|
|
209
220
|
} catch {
|
|
210
221
|
return false;
|
|
@@ -212,7 +223,7 @@ async function fileExists(filePath) {
|
|
|
212
223
|
}
|
|
213
224
|
async function readJsonFile(filePath) {
|
|
214
225
|
try {
|
|
215
|
-
const content = await
|
|
226
|
+
const content = await fs4.readFile(filePath, "utf-8");
|
|
216
227
|
return JSON.parse(content);
|
|
217
228
|
} catch (error) {
|
|
218
229
|
if (error.code === "ENOENT") {
|
|
@@ -227,7 +238,7 @@ async function readJsonFile(filePath) {
|
|
|
227
238
|
}
|
|
228
239
|
async function readMarkdownFile(filePath) {
|
|
229
240
|
try {
|
|
230
|
-
return await
|
|
241
|
+
return await fs4.readFile(filePath, "utf-8");
|
|
231
242
|
} catch (error) {
|
|
232
243
|
if (error.code === "ENOENT") {
|
|
233
244
|
throw new FileNotFoundError(`File not found: ${filePath}`, filePath);
|
|
@@ -244,7 +255,7 @@ async function readMarkdownDirectory(dirPath) {
|
|
|
244
255
|
if (!await fileExists(dirPath)) {
|
|
245
256
|
return files;
|
|
246
257
|
}
|
|
247
|
-
const entries = await
|
|
258
|
+
const entries = await fs4.readdir(dirPath, { withFileTypes: true });
|
|
248
259
|
for (const entry of entries) {
|
|
249
260
|
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
250
261
|
const filePath = path.join(dirPath, entry.name);
|
|
@@ -264,7 +275,7 @@ async function readSkillsDirectory(dirPath) {
|
|
|
264
275
|
if (!await fileExists(dirPath)) {
|
|
265
276
|
return skills;
|
|
266
277
|
}
|
|
267
|
-
const entries = await
|
|
278
|
+
const entries = await fs4.readdir(dirPath, { withFileTypes: true });
|
|
268
279
|
for (const entry of entries) {
|
|
269
280
|
if (entry.isDirectory()) {
|
|
270
281
|
const skillFile = path.join(dirPath, entry.name, "SKILL.md");
|
|
@@ -402,10 +413,43 @@ function validateToolIds(tools) {
|
|
|
402
413
|
}
|
|
403
414
|
return { valid: true, errors: [], warnings: [], skipped: [] };
|
|
404
415
|
}
|
|
416
|
+
|
|
417
|
+
// src/utils/agents.ts
|
|
418
|
+
function createRootAgentsMdSymlink(state) {
|
|
419
|
+
if (!state.agents) {
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
return {
|
|
423
|
+
path: "AGENTS.md",
|
|
424
|
+
type: "symlink",
|
|
425
|
+
target: `${UNIFIED_DIR}/AGENTS.md`
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
function createSkillSymlinks(state, outputDir) {
|
|
429
|
+
const depth = outputDir.split("/").length + 1;
|
|
430
|
+
const prefix = "../".repeat(depth);
|
|
431
|
+
return state.skills.map((skill) => ({
|
|
432
|
+
path: `${outputDir}/skills/${skill.path}`,
|
|
433
|
+
type: "symlink",
|
|
434
|
+
target: `${prefix}${UNIFIED_DIR}/skills/${skill.path}`
|
|
435
|
+
}));
|
|
436
|
+
}
|
|
437
|
+
function createNoAgentsMdWarning(outputDescription) {
|
|
438
|
+
return {
|
|
439
|
+
path: ["AGENTS.md"],
|
|
440
|
+
message: `No AGENTS.md found - ${outputDescription} will not be created`
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
function hasPermissionsConfigured(permissions) {
|
|
444
|
+
if (!permissions) {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
return (permissions.allow?.length ?? 0) > 0 || (permissions.ask?.length ?? 0) > 0 || (permissions.deny?.length ?? 0) > 0;
|
|
448
|
+
}
|
|
405
449
|
async function scanOverrideDirectory(rootDir, toolId) {
|
|
406
450
|
const overrideDir = path.join(rootDir, UNIFIED_DIR, OVERRIDE_DIRS[toolId]);
|
|
407
451
|
try {
|
|
408
|
-
await
|
|
452
|
+
await fs4.access(overrideDir);
|
|
409
453
|
} catch {
|
|
410
454
|
return [];
|
|
411
455
|
}
|
|
@@ -414,7 +458,7 @@ async function scanOverrideDirectory(rootDir, toolId) {
|
|
|
414
458
|
return files;
|
|
415
459
|
}
|
|
416
460
|
async function scanDir(baseDir, currentDir, files) {
|
|
417
|
-
const entries = await
|
|
461
|
+
const entries = await fs4.readdir(currentDir, { withFileTypes: true });
|
|
418
462
|
for (const entry of entries) {
|
|
419
463
|
const absolutePath = path.join(currentDir, entry.name);
|
|
420
464
|
if (entry.isDirectory()) {
|
|
@@ -446,6 +490,7 @@ async function applyFileOverrides(files, rootDir, toolId) {
|
|
|
446
490
|
}
|
|
447
491
|
|
|
448
492
|
// src/plugins/claude-code/index.ts
|
|
493
|
+
var OUTPUT_DIR = TOOL_OUTPUT_DIRS.claudeCode;
|
|
449
494
|
var claudeCodePlugin = {
|
|
450
495
|
id: "claudeCode",
|
|
451
496
|
name: "Claude Code",
|
|
@@ -457,28 +502,21 @@ var claudeCodePlugin = {
|
|
|
457
502
|
},
|
|
458
503
|
async export(state, rootDir) {
|
|
459
504
|
const files = [];
|
|
460
|
-
const outputDir = TOOL_OUTPUT_DIRS.claudeCode;
|
|
461
505
|
if (state.agents) {
|
|
462
506
|
files.push({
|
|
463
|
-
path: `${
|
|
507
|
+
path: `${OUTPUT_DIR}/CLAUDE.md`,
|
|
464
508
|
type: "symlink",
|
|
465
509
|
target: `../${UNIFIED_DIR}/AGENTS.md`
|
|
466
510
|
});
|
|
467
511
|
}
|
|
468
512
|
if (state.rules.length > 0) {
|
|
469
513
|
files.push({
|
|
470
|
-
path: `${
|
|
514
|
+
path: `${OUTPUT_DIR}/rules`,
|
|
471
515
|
type: "symlink",
|
|
472
516
|
target: `../${UNIFIED_DIR}/rules`
|
|
473
517
|
});
|
|
474
518
|
}
|
|
475
|
-
|
|
476
|
-
files.push({
|
|
477
|
-
path: `${outputDir}/skills/${skill.path}`,
|
|
478
|
-
type: "symlink",
|
|
479
|
-
target: `../../${UNIFIED_DIR}/skills/${skill.path}`
|
|
480
|
-
});
|
|
481
|
-
}
|
|
519
|
+
files.push(...createSkillSymlinks(state, OUTPUT_DIR));
|
|
482
520
|
const settings = {};
|
|
483
521
|
if (state.settings?.permissions) {
|
|
484
522
|
settings["permissions"] = state.settings.permissions;
|
|
@@ -488,7 +526,7 @@ var claudeCodePlugin = {
|
|
|
488
526
|
}
|
|
489
527
|
if (Object.keys(settings).length > 0) {
|
|
490
528
|
files.push({
|
|
491
|
-
path: `${
|
|
529
|
+
path: `${OUTPUT_DIR}/settings.json`,
|
|
492
530
|
type: "json",
|
|
493
531
|
content: settings
|
|
494
532
|
});
|
|
@@ -498,10 +536,7 @@ var claudeCodePlugin = {
|
|
|
498
536
|
validate(state) {
|
|
499
537
|
const warnings = [];
|
|
500
538
|
if (!state.agents) {
|
|
501
|
-
warnings.push(
|
|
502
|
-
path: ["AGENTS.md"],
|
|
503
|
-
message: "No AGENTS.md found - .claude/CLAUDE.md will not be created"
|
|
504
|
-
});
|
|
539
|
+
warnings.push(createNoAgentsMdWarning(".claude/CLAUDE.md"));
|
|
505
540
|
}
|
|
506
541
|
return { valid: true, errors: [], warnings, skipped: [] };
|
|
507
542
|
}
|
|
@@ -510,8 +545,8 @@ function getDirFromGlob(glob) {
|
|
|
510
545
|
const cleanPath = glob.replace(/(\*\*|\*|\{.*,.*\}).*$/, "");
|
|
511
546
|
const dir = cleanPath.replace(/\/$/, "");
|
|
512
547
|
if (dir === glob) {
|
|
513
|
-
const
|
|
514
|
-
return
|
|
548
|
+
const dirname5 = path.dirname(dir);
|
|
549
|
+
return dirname5 === "." && !dir.includes("/") ? "." : dirname5;
|
|
515
550
|
}
|
|
516
551
|
if (!dir) {
|
|
517
552
|
return ".";
|
|
@@ -543,6 +578,8 @@ ${rule.content}
|
|
|
543
578
|
}
|
|
544
579
|
|
|
545
580
|
// src/plugins/codex/index.ts
|
|
581
|
+
var OUTPUT_DIR2 = TOOL_OUTPUT_DIRS.codex;
|
|
582
|
+
var SKILLS_DIR = ".agents";
|
|
546
583
|
var codexPlugin = {
|
|
547
584
|
id: "codex",
|
|
548
585
|
name: "Codex",
|
|
@@ -554,13 +591,9 @@ var codexPlugin = {
|
|
|
554
591
|
},
|
|
555
592
|
async export(state, rootDir) {
|
|
556
593
|
const files = [];
|
|
557
|
-
const
|
|
558
|
-
if (
|
|
559
|
-
files.push(
|
|
560
|
-
path: "AGENTS.md",
|
|
561
|
-
type: "symlink",
|
|
562
|
-
target: `${UNIFIED_DIR}/AGENTS.md`
|
|
563
|
-
});
|
|
594
|
+
const agentsSymlink = createRootAgentsMdSymlink(state);
|
|
595
|
+
if (agentsSymlink) {
|
|
596
|
+
files.push(agentsSymlink);
|
|
564
597
|
}
|
|
565
598
|
const rulesMap = groupRulesByDirectory(state.rules);
|
|
566
599
|
for (const [dir, contents] of rulesMap.entries()) {
|
|
@@ -574,17 +607,11 @@ var codexPlugin = {
|
|
|
574
607
|
content: combinedContent
|
|
575
608
|
});
|
|
576
609
|
}
|
|
577
|
-
|
|
578
|
-
files.push({
|
|
579
|
-
path: `${outputDir}/skills/${skill.path}`,
|
|
580
|
-
type: "symlink",
|
|
581
|
-
target: `../../${UNIFIED_DIR}/skills/${skill.path}`
|
|
582
|
-
});
|
|
583
|
-
}
|
|
610
|
+
files.push(...createSkillSymlinks(state, SKILLS_DIR));
|
|
584
611
|
const configToml = buildCodexConfigToml(state.settings?.mcpServers);
|
|
585
612
|
if (configToml) {
|
|
586
613
|
files.push({
|
|
587
|
-
path: `${
|
|
614
|
+
path: `${OUTPUT_DIR2}/config.toml`,
|
|
588
615
|
type: "text",
|
|
589
616
|
content: configToml
|
|
590
617
|
});
|
|
@@ -595,10 +622,7 @@ var codexPlugin = {
|
|
|
595
622
|
const warnings = [];
|
|
596
623
|
const skipped = [];
|
|
597
624
|
if (!state.agents) {
|
|
598
|
-
warnings.push(
|
|
599
|
-
path: ["AGENTS.md"],
|
|
600
|
-
message: "No AGENTS.md found - root AGENTS.md will not be created"
|
|
601
|
-
});
|
|
625
|
+
warnings.push(createNoAgentsMdWarning("root AGENTS.md"));
|
|
602
626
|
}
|
|
603
627
|
const rulesMap = groupRulesByDirectory(state.rules);
|
|
604
628
|
if (rulesMap.has(".")) {
|
|
@@ -618,14 +642,11 @@ var codexPlugin = {
|
|
|
618
642
|
}
|
|
619
643
|
}
|
|
620
644
|
}
|
|
621
|
-
if (state.settings?.permissions) {
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
reason: "Codex rules are not generated from LNAI permissions"
|
|
627
|
-
});
|
|
628
|
-
}
|
|
645
|
+
if (hasPermissionsConfigured(state.settings?.permissions)) {
|
|
646
|
+
skipped.push({
|
|
647
|
+
feature: "permissions",
|
|
648
|
+
reason: "Codex rules are not generated from LNAI permissions"
|
|
649
|
+
});
|
|
629
650
|
}
|
|
630
651
|
return { valid: true, errors: [], warnings, skipped };
|
|
631
652
|
}
|
|
@@ -684,6 +705,32 @@ function formatTomlInlineTable(values) {
|
|
|
684
705
|
return `{ ${entries.join(", ")} }`;
|
|
685
706
|
}
|
|
686
707
|
|
|
708
|
+
// src/utils/mcp.ts
|
|
709
|
+
function validateMcpServers(mcpServers, pathPrefix) {
|
|
710
|
+
const warnings = [];
|
|
711
|
+
if (!mcpServers) {
|
|
712
|
+
return warnings;
|
|
713
|
+
}
|
|
714
|
+
for (const [name, server] of Object.entries(mcpServers)) {
|
|
715
|
+
const isRemote = server.type === "http" || server.type === "sse";
|
|
716
|
+
const hasCommand = !!server.command;
|
|
717
|
+
const hasUrl = !!server.url;
|
|
718
|
+
if (!isRemote && !hasCommand) {
|
|
719
|
+
warnings.push({
|
|
720
|
+
path: [...pathPrefix, name],
|
|
721
|
+
message: `MCP server "${name}" has no command or type - it will be skipped`
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
if (isRemote && !hasUrl) {
|
|
725
|
+
warnings.push({
|
|
726
|
+
path: [...pathPrefix, name],
|
|
727
|
+
message: `MCP server "${name}" is remote but has no URL - it will be skipped`
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return warnings;
|
|
732
|
+
}
|
|
733
|
+
|
|
687
734
|
// src/utils/transforms.ts
|
|
688
735
|
var ENV_VAR_PATTERN = /\$\{([^}:]+)(:-[^}]*)?\}/g;
|
|
689
736
|
function transformEnvVar(value, format) {
|
|
@@ -789,6 +836,7 @@ function transformMcpToCopilot(servers) {
|
|
|
789
836
|
}
|
|
790
837
|
|
|
791
838
|
// src/plugins/copilot/index.ts
|
|
839
|
+
var OUTPUT_DIR3 = TOOL_OUTPUT_DIRS.copilot;
|
|
792
840
|
var copilotPlugin = {
|
|
793
841
|
id: "copilot",
|
|
794
842
|
name: "GitHub Copilot",
|
|
@@ -800,12 +848,9 @@ var copilotPlugin = {
|
|
|
800
848
|
},
|
|
801
849
|
async export(state, rootDir) {
|
|
802
850
|
const files = [];
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
type: "symlink",
|
|
807
|
-
target: `../${UNIFIED_DIR}/AGENTS.md`
|
|
808
|
-
});
|
|
851
|
+
const agentsSymlink = createRootAgentsMdSymlink(state);
|
|
852
|
+
if (agentsSymlink) {
|
|
853
|
+
files.push(agentsSymlink);
|
|
809
854
|
}
|
|
810
855
|
for (const rule of state.rules) {
|
|
811
856
|
const transformed = transformRuleToCopilot(rule);
|
|
@@ -815,18 +860,12 @@ var copilotPlugin = {
|
|
|
815
860
|
);
|
|
816
861
|
const outputFilename = rule.path.replace(/\.md$/, ".instructions.md");
|
|
817
862
|
files.push({
|
|
818
|
-
path:
|
|
863
|
+
path: `${OUTPUT_DIR3}/instructions/${outputFilename}`,
|
|
819
864
|
type: "text",
|
|
820
865
|
content: ruleContent
|
|
821
866
|
});
|
|
822
867
|
}
|
|
823
|
-
|
|
824
|
-
files.push({
|
|
825
|
-
path: `.github/skills/${skill.path}`,
|
|
826
|
-
type: "symlink",
|
|
827
|
-
target: `../../${UNIFIED_DIR}/skills/${skill.path}`
|
|
828
|
-
});
|
|
829
|
-
}
|
|
868
|
+
files.push(...createSkillSymlinks(state, OUTPUT_DIR3));
|
|
830
869
|
const mcpConfig = transformMcpToCopilot(state.settings?.mcpServers);
|
|
831
870
|
if (mcpConfig) {
|
|
832
871
|
files.push({
|
|
@@ -841,37 +880,20 @@ var copilotPlugin = {
|
|
|
841
880
|
const warnings = [];
|
|
842
881
|
const skipped = [];
|
|
843
882
|
if (!state.agents) {
|
|
844
|
-
warnings.push(
|
|
845
|
-
path: ["AGENTS.md"],
|
|
846
|
-
message: "No AGENTS.md found - .github/copilot-instructions.md will not be created"
|
|
847
|
-
});
|
|
883
|
+
warnings.push(createNoAgentsMdWarning("root AGENTS.md"));
|
|
848
884
|
}
|
|
849
|
-
|
|
850
|
-
const hasPermissions = permissions && (permissions.allow && permissions.allow.length > 0 || permissions.ask && permissions.ask.length > 0 || permissions.deny && permissions.deny.length > 0);
|
|
851
|
-
if (hasPermissions) {
|
|
885
|
+
if (hasPermissionsConfigured(state.settings?.permissions)) {
|
|
852
886
|
skipped.push({
|
|
853
887
|
feature: "permissions",
|
|
854
888
|
reason: "GitHub Copilot does not support declarative permissions"
|
|
855
889
|
});
|
|
856
890
|
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
warnings.push({
|
|
864
|
-
path: ["settings", "mcpServers", name],
|
|
865
|
-
message: `MCP server "${name}" is type "${server.type}" but has no url - it will be skipped`
|
|
866
|
-
});
|
|
867
|
-
} else if (!isRemote && !hasCommand) {
|
|
868
|
-
warnings.push({
|
|
869
|
-
path: ["settings", "mcpServers", name],
|
|
870
|
-
message: `MCP server "${name}" has no command or type - it will be skipped`
|
|
871
|
-
});
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
}
|
|
891
|
+
warnings.push(
|
|
892
|
+
...validateMcpServers(state.settings?.mcpServers, [
|
|
893
|
+
"settings",
|
|
894
|
+
"mcpServers"
|
|
895
|
+
])
|
|
896
|
+
);
|
|
875
897
|
return { valid: true, errors: [], warnings, skipped };
|
|
876
898
|
}
|
|
877
899
|
};
|
|
@@ -992,6 +1014,7 @@ function transformPermissionRule(rule) {
|
|
|
992
1014
|
}
|
|
993
1015
|
|
|
994
1016
|
// src/plugins/cursor/index.ts
|
|
1017
|
+
var OUTPUT_DIR4 = TOOL_OUTPUT_DIRS.cursor;
|
|
995
1018
|
var cursorPlugin = {
|
|
996
1019
|
id: "cursor",
|
|
997
1020
|
name: "Cursor",
|
|
@@ -1003,13 +1026,9 @@ var cursorPlugin = {
|
|
|
1003
1026
|
},
|
|
1004
1027
|
async export(state, rootDir) {
|
|
1005
1028
|
const files = [];
|
|
1006
|
-
const
|
|
1007
|
-
if (
|
|
1008
|
-
files.push(
|
|
1009
|
-
path: "AGENTS.md",
|
|
1010
|
-
type: "symlink",
|
|
1011
|
-
target: `${UNIFIED_DIR}/AGENTS.md`
|
|
1012
|
-
});
|
|
1029
|
+
const agentsSymlink = createRootAgentsMdSymlink(state);
|
|
1030
|
+
if (agentsSymlink) {
|
|
1031
|
+
files.push(agentsSymlink);
|
|
1013
1032
|
}
|
|
1014
1033
|
for (const rule of state.rules) {
|
|
1015
1034
|
const transformed = transformRuleToCursor(rule);
|
|
@@ -1019,22 +1038,16 @@ var cursorPlugin = {
|
|
|
1019
1038
|
);
|
|
1020
1039
|
const outputFilename = rule.path.replace(/\.md$/, ".mdc");
|
|
1021
1040
|
files.push({
|
|
1022
|
-
path: `${
|
|
1041
|
+
path: `${OUTPUT_DIR4}/rules/${outputFilename}`,
|
|
1023
1042
|
type: "text",
|
|
1024
1043
|
content: ruleContent
|
|
1025
1044
|
});
|
|
1026
1045
|
}
|
|
1027
|
-
|
|
1028
|
-
files.push({
|
|
1029
|
-
path: `${outputDir}/skills/${skill.path}`,
|
|
1030
|
-
type: "symlink",
|
|
1031
|
-
target: `../../${UNIFIED_DIR}/skills/${skill.path}`
|
|
1032
|
-
});
|
|
1033
|
-
}
|
|
1046
|
+
files.push(...createSkillSymlinks(state, OUTPUT_DIR4));
|
|
1034
1047
|
const mcpServers = transformMcpToCursor(state.settings?.mcpServers);
|
|
1035
1048
|
if (mcpServers) {
|
|
1036
1049
|
files.push({
|
|
1037
|
-
path: `${
|
|
1050
|
+
path: `${OUTPUT_DIR4}/mcp.json`,
|
|
1038
1051
|
type: "json",
|
|
1039
1052
|
content: { mcpServers }
|
|
1040
1053
|
});
|
|
@@ -1042,7 +1055,7 @@ var cursorPlugin = {
|
|
|
1042
1055
|
const cliContent = buildCliContent(state.settings?.permissions);
|
|
1043
1056
|
if (cliContent) {
|
|
1044
1057
|
files.push({
|
|
1045
|
-
path: `${
|
|
1058
|
+
path: `${OUTPUT_DIR4}/cli.json`,
|
|
1046
1059
|
type: "json",
|
|
1047
1060
|
content: cliContent
|
|
1048
1061
|
});
|
|
@@ -1052,10 +1065,7 @@ var cursorPlugin = {
|
|
|
1052
1065
|
validate(state) {
|
|
1053
1066
|
const warnings = [];
|
|
1054
1067
|
if (!state.agents) {
|
|
1055
|
-
warnings.push(
|
|
1056
|
-
path: ["AGENTS.md"],
|
|
1057
|
-
message: "No AGENTS.md found - root AGENTS.md will not be created"
|
|
1058
|
-
});
|
|
1068
|
+
warnings.push(createNoAgentsMdWarning("root AGENTS.md"));
|
|
1059
1069
|
}
|
|
1060
1070
|
const permissionsResult = transformPermissionsToCursor(
|
|
1061
1071
|
state.settings?.permissions
|
|
@@ -1066,19 +1076,12 @@ var cursorPlugin = {
|
|
|
1066
1076
|
message: 'Cursor does not support "ask" permission level - these rules will be treated as "allow"'
|
|
1067
1077
|
});
|
|
1068
1078
|
}
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
warnings.push({
|
|
1076
|
-
path: ["settings", "mcpServers", name],
|
|
1077
|
-
message: `MCP server "${name}" has no command or type - it will be skipped`
|
|
1078
|
-
});
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1079
|
+
warnings.push(
|
|
1080
|
+
...validateMcpServers(state.settings?.mcpServers, [
|
|
1081
|
+
"settings",
|
|
1082
|
+
"mcpServers"
|
|
1083
|
+
])
|
|
1084
|
+
);
|
|
1082
1085
|
return { valid: true, errors: [], warnings, skipped: [] };
|
|
1083
1086
|
}
|
|
1084
1087
|
};
|
|
@@ -1113,6 +1116,7 @@ function transformMcpToGemini(mcpServers) {
|
|
|
1113
1116
|
}
|
|
1114
1117
|
|
|
1115
1118
|
// src/plugins/gemini/index.ts
|
|
1119
|
+
var OUTPUT_DIR5 = TOOL_OUTPUT_DIRS.gemini;
|
|
1116
1120
|
var geminiPlugin = {
|
|
1117
1121
|
id: "gemini",
|
|
1118
1122
|
name: "Gemini CLI",
|
|
@@ -1124,13 +1128,9 @@ var geminiPlugin = {
|
|
|
1124
1128
|
},
|
|
1125
1129
|
async export(state, rootDir) {
|
|
1126
1130
|
const files = [];
|
|
1127
|
-
const
|
|
1128
|
-
if (
|
|
1129
|
-
files.push(
|
|
1130
|
-
path: `${outputDir}/GEMINI.md`,
|
|
1131
|
-
type: "symlink",
|
|
1132
|
-
target: `../${UNIFIED_DIR}/AGENTS.md`
|
|
1133
|
-
});
|
|
1131
|
+
const agentsSymlink = createRootAgentsMdSymlink(state);
|
|
1132
|
+
if (agentsSymlink) {
|
|
1133
|
+
files.push(agentsSymlink);
|
|
1134
1134
|
}
|
|
1135
1135
|
const rulesMap = groupRulesByDirectory(state.rules);
|
|
1136
1136
|
for (const [dir, contents] of rulesMap.entries()) {
|
|
@@ -1142,19 +1142,21 @@ var geminiPlugin = {
|
|
|
1142
1142
|
content: combinedContent
|
|
1143
1143
|
});
|
|
1144
1144
|
}
|
|
1145
|
-
|
|
1146
|
-
files.push({
|
|
1147
|
-
path: `${outputDir}/skills/${skill.path}`,
|
|
1148
|
-
type: "symlink",
|
|
1149
|
-
target: `../../${UNIFIED_DIR}/skills/${skill.path}`
|
|
1150
|
-
});
|
|
1151
|
-
}
|
|
1145
|
+
files.push(...createSkillSymlinks(state, OUTPUT_DIR5));
|
|
1152
1146
|
const mcpServers = transformMcpToGemini(state.settings?.mcpServers);
|
|
1153
|
-
|
|
1147
|
+
const hasAgents = !!state.agents;
|
|
1148
|
+
if (mcpServers || hasAgents) {
|
|
1149
|
+
const settingsContent = {};
|
|
1150
|
+
if (hasAgents) {
|
|
1151
|
+
settingsContent["context"] = { fileName: ["AGENTS.md"] };
|
|
1152
|
+
}
|
|
1153
|
+
if (mcpServers) {
|
|
1154
|
+
settingsContent["mcpServers"] = mcpServers;
|
|
1155
|
+
}
|
|
1154
1156
|
files.push({
|
|
1155
|
-
path: `${
|
|
1157
|
+
path: `${OUTPUT_DIR5}/settings.json`,
|
|
1156
1158
|
type: "json",
|
|
1157
|
-
content:
|
|
1159
|
+
content: settingsContent
|
|
1158
1160
|
});
|
|
1159
1161
|
}
|
|
1160
1162
|
return applyFileOverrides(files, rootDir, "gemini");
|
|
@@ -1163,19 +1165,13 @@ var geminiPlugin = {
|
|
|
1163
1165
|
const warnings = [];
|
|
1164
1166
|
const skipped = [];
|
|
1165
1167
|
if (!state.agents) {
|
|
1166
|
-
warnings.push(
|
|
1167
|
-
path: ["AGENTS.md"],
|
|
1168
|
-
message: "No AGENTS.md found - GEMINI.md will not be created"
|
|
1169
|
-
});
|
|
1168
|
+
warnings.push(createNoAgentsMdWarning("root AGENTS.md"));
|
|
1170
1169
|
}
|
|
1171
|
-
if (state.settings?.permissions) {
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
reason: "Gemini CLI does not support declarative permissions - permissions must be granted interactively"
|
|
1177
|
-
});
|
|
1178
|
-
}
|
|
1170
|
+
if (hasPermissionsConfigured(state.settings?.permissions)) {
|
|
1171
|
+
skipped.push({
|
|
1172
|
+
feature: "permissions",
|
|
1173
|
+
reason: "Gemini CLI does not support declarative permissions - permissions must be granted interactively"
|
|
1174
|
+
});
|
|
1179
1175
|
}
|
|
1180
1176
|
if (state.rules.length > 0) {
|
|
1181
1177
|
warnings.push({
|
|
@@ -1260,6 +1256,8 @@ function parsePermissionRuleForOpenCode(rule) {
|
|
|
1260
1256
|
}
|
|
1261
1257
|
|
|
1262
1258
|
// src/plugins/opencode/index.ts
|
|
1259
|
+
var OUTPUT_DIR6 = TOOL_OUTPUT_DIRS.opencode;
|
|
1260
|
+
var SKILLS_DIR2 = ".agents";
|
|
1263
1261
|
var opencodePlugin = {
|
|
1264
1262
|
id: "opencode",
|
|
1265
1263
|
name: "OpenCode",
|
|
@@ -1271,33 +1269,23 @@ var opencodePlugin = {
|
|
|
1271
1269
|
},
|
|
1272
1270
|
async export(state, rootDir) {
|
|
1273
1271
|
const files = [];
|
|
1274
|
-
const
|
|
1275
|
-
if (
|
|
1276
|
-
files.push(
|
|
1277
|
-
path: "AGENTS.md",
|
|
1278
|
-
type: "symlink",
|
|
1279
|
-
target: `${UNIFIED_DIR}/AGENTS.md`
|
|
1280
|
-
});
|
|
1272
|
+
const agentsSymlink = createRootAgentsMdSymlink(state);
|
|
1273
|
+
if (agentsSymlink) {
|
|
1274
|
+
files.push(agentsSymlink);
|
|
1281
1275
|
}
|
|
1282
1276
|
if (state.rules.length > 0) {
|
|
1283
1277
|
files.push({
|
|
1284
|
-
path: `${
|
|
1278
|
+
path: `${OUTPUT_DIR6}/rules`,
|
|
1285
1279
|
type: "symlink",
|
|
1286
1280
|
target: `../${UNIFIED_DIR}/rules`
|
|
1287
1281
|
});
|
|
1288
1282
|
}
|
|
1289
|
-
|
|
1290
|
-
files.push({
|
|
1291
|
-
path: `${outputDir}/skills/${skill.path}`,
|
|
1292
|
-
type: "symlink",
|
|
1293
|
-
target: `../../${UNIFIED_DIR}/skills/${skill.path}`
|
|
1294
|
-
});
|
|
1295
|
-
}
|
|
1283
|
+
files.push(...createSkillSymlinks(state, SKILLS_DIR2));
|
|
1296
1284
|
const config = {
|
|
1297
1285
|
$schema: "https://opencode.ai/config.json"
|
|
1298
1286
|
};
|
|
1299
1287
|
if (state.rules.length > 0) {
|
|
1300
|
-
config["instructions"] = [`${
|
|
1288
|
+
config["instructions"] = [`${OUTPUT_DIR6}/rules/*.md`];
|
|
1301
1289
|
}
|
|
1302
1290
|
const mcp = transformMcpToOpenCode(state.settings?.mcpServers);
|
|
1303
1291
|
if (mcp) {
|
|
@@ -1319,11 +1307,14 @@ var opencodePlugin = {
|
|
|
1319
1307
|
validate(state) {
|
|
1320
1308
|
const warnings = [];
|
|
1321
1309
|
if (!state.agents) {
|
|
1322
|
-
warnings.push(
|
|
1323
|
-
path: ["AGENTS.md"],
|
|
1324
|
-
message: "No AGENTS.md found - root AGENTS.md will not be created"
|
|
1325
|
-
});
|
|
1310
|
+
warnings.push(createNoAgentsMdWarning("root AGENTS.md"));
|
|
1326
1311
|
}
|
|
1312
|
+
warnings.push(
|
|
1313
|
+
...validateMcpServers(state.settings?.mcpServers, [
|
|
1314
|
+
"settings",
|
|
1315
|
+
"mcpServers"
|
|
1316
|
+
])
|
|
1317
|
+
);
|
|
1327
1318
|
return { valid: true, errors: [], warnings, skipped: [] };
|
|
1328
1319
|
}
|
|
1329
1320
|
};
|
|
@@ -1379,6 +1370,7 @@ function serializeWindsurfRule(frontmatter, content) {
|
|
|
1379
1370
|
}
|
|
1380
1371
|
|
|
1381
1372
|
// src/plugins/windsurf/index.ts
|
|
1373
|
+
var OUTPUT_DIR7 = TOOL_OUTPUT_DIRS.windsurf;
|
|
1382
1374
|
var windsurfPlugin = {
|
|
1383
1375
|
id: "windsurf",
|
|
1384
1376
|
name: "Windsurf",
|
|
@@ -1390,13 +1382,9 @@ var windsurfPlugin = {
|
|
|
1390
1382
|
},
|
|
1391
1383
|
async export(state, rootDir) {
|
|
1392
1384
|
const files = [];
|
|
1393
|
-
const
|
|
1394
|
-
if (
|
|
1395
|
-
files.push(
|
|
1396
|
-
path: "AGENTS.md",
|
|
1397
|
-
type: "symlink",
|
|
1398
|
-
target: `${UNIFIED_DIR}/AGENTS.md`
|
|
1399
|
-
});
|
|
1385
|
+
const agentsSymlink = createRootAgentsMdSymlink(state);
|
|
1386
|
+
if (agentsSymlink) {
|
|
1387
|
+
files.push(agentsSymlink);
|
|
1400
1388
|
}
|
|
1401
1389
|
for (const rule of state.rules) {
|
|
1402
1390
|
const transformed = transformRuleToWindsurf(rule);
|
|
@@ -1405,28 +1393,19 @@ var windsurfPlugin = {
|
|
|
1405
1393
|
transformed.content
|
|
1406
1394
|
);
|
|
1407
1395
|
files.push({
|
|
1408
|
-
path: `${
|
|
1396
|
+
path: `${OUTPUT_DIR7}/rules/${rule.path}`,
|
|
1409
1397
|
type: "text",
|
|
1410
1398
|
content: ruleContent
|
|
1411
1399
|
});
|
|
1412
1400
|
}
|
|
1413
|
-
|
|
1414
|
-
files.push({
|
|
1415
|
-
path: `${outputDir}/skills/${skill.path}`,
|
|
1416
|
-
type: "symlink",
|
|
1417
|
-
target: `../../${UNIFIED_DIR}/skills/${skill.path}`
|
|
1418
|
-
});
|
|
1419
|
-
}
|
|
1401
|
+
files.push(...createSkillSymlinks(state, OUTPUT_DIR7));
|
|
1420
1402
|
return applyFileOverrides(files, rootDir, "windsurf");
|
|
1421
1403
|
},
|
|
1422
1404
|
validate(state) {
|
|
1423
1405
|
const warnings = [];
|
|
1424
1406
|
const skipped = [];
|
|
1425
1407
|
if (!state.agents) {
|
|
1426
|
-
warnings.push(
|
|
1427
|
-
path: ["AGENTS.md"],
|
|
1428
|
-
message: "No AGENTS.md found - root AGENTS.md will not be created"
|
|
1429
|
-
});
|
|
1408
|
+
warnings.push(createNoAgentsMdWarning("root AGENTS.md"));
|
|
1430
1409
|
}
|
|
1431
1410
|
if (state.rules.length > 0) {
|
|
1432
1411
|
warnings.push({
|
|
@@ -1440,14 +1419,11 @@ var windsurfPlugin = {
|
|
|
1440
1419
|
reason: "Windsurf uses global MCP config at ~/.codeium/windsurf/mcp_config.json - project-level MCP servers are not exported"
|
|
1441
1420
|
});
|
|
1442
1421
|
}
|
|
1443
|
-
if (state.settings?.permissions) {
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
reason: "Windsurf does not support declarative permissions"
|
|
1449
|
-
});
|
|
1450
|
-
}
|
|
1422
|
+
if (hasPermissionsConfigured(state.settings?.permissions)) {
|
|
1423
|
+
skipped.push({
|
|
1424
|
+
feature: "permissions",
|
|
1425
|
+
reason: "Windsurf does not support declarative permissions"
|
|
1426
|
+
});
|
|
1451
1427
|
}
|
|
1452
1428
|
return { valid: true, errors: [], warnings, skipped };
|
|
1453
1429
|
}
|
|
@@ -1461,21 +1437,65 @@ pluginRegistry.register(codexPlugin);
|
|
|
1461
1437
|
pluginRegistry.register(opencodePlugin);
|
|
1462
1438
|
pluginRegistry.register(windsurfPlugin);
|
|
1463
1439
|
pluginRegistry.register(geminiPlugin);
|
|
1440
|
+
function computeFilesToDelete(previousFiles, currentFiles) {
|
|
1441
|
+
const currentPaths = new Set(currentFiles.map((f) => f.path));
|
|
1442
|
+
return previousFiles.map((f) => f.path).filter((p) => !currentPaths.has(p));
|
|
1443
|
+
}
|
|
1444
|
+
async function deleteFiles(paths, rootDir, dryRun) {
|
|
1445
|
+
const results = [];
|
|
1446
|
+
for (const relativePath of paths) {
|
|
1447
|
+
const fullPath = path.join(rootDir, relativePath);
|
|
1448
|
+
try {
|
|
1449
|
+
await fs4.lstat(fullPath);
|
|
1450
|
+
} catch (error) {
|
|
1451
|
+
if (error.code === "ENOENT") {
|
|
1452
|
+
continue;
|
|
1453
|
+
}
|
|
1454
|
+
throw error;
|
|
1455
|
+
}
|
|
1456
|
+
if (!dryRun) {
|
|
1457
|
+
await fs4.unlink(fullPath);
|
|
1458
|
+
await cleanupEmptyParentDirs(fullPath, rootDir);
|
|
1459
|
+
}
|
|
1460
|
+
results.push({
|
|
1461
|
+
path: relativePath,
|
|
1462
|
+
action: "delete"
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
return results;
|
|
1466
|
+
}
|
|
1467
|
+
async function cleanupEmptyParentDirs(filePath, rootDir) {
|
|
1468
|
+
let dir = path.dirname(filePath);
|
|
1469
|
+
const normalizedRoot = path.normalize(rootDir);
|
|
1470
|
+
while (dir !== normalizedRoot && dir.startsWith(normalizedRoot + path.sep)) {
|
|
1471
|
+
try {
|
|
1472
|
+
const entries = await fs4.readdir(dir);
|
|
1473
|
+
if (entries.length === 0) {
|
|
1474
|
+
await fs4.rmdir(dir);
|
|
1475
|
+
dir = path.dirname(dir);
|
|
1476
|
+
} else {
|
|
1477
|
+
break;
|
|
1478
|
+
}
|
|
1479
|
+
} catch {
|
|
1480
|
+
break;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1464
1484
|
function computeHash(content) {
|
|
1465
1485
|
return crypto.createHash("sha256").update(content, "utf-8").digest("hex");
|
|
1466
1486
|
}
|
|
1467
1487
|
async function readExistingFile(filePath) {
|
|
1468
1488
|
try {
|
|
1469
|
-
return await
|
|
1489
|
+
return await fs4.readFile(filePath, "utf-8");
|
|
1470
1490
|
} catch {
|
|
1471
1491
|
return null;
|
|
1472
1492
|
}
|
|
1473
1493
|
}
|
|
1474
1494
|
async function getSymlinkTarget(filePath) {
|
|
1475
1495
|
try {
|
|
1476
|
-
const stats = await
|
|
1496
|
+
const stats = await fs4.lstat(filePath);
|
|
1477
1497
|
if (stats.isSymbolicLink()) {
|
|
1478
|
-
return await
|
|
1498
|
+
return await fs4.readlink(filePath);
|
|
1479
1499
|
}
|
|
1480
1500
|
return null;
|
|
1481
1501
|
} catch {
|
|
@@ -1483,15 +1503,15 @@ async function getSymlinkTarget(filePath) {
|
|
|
1483
1503
|
}
|
|
1484
1504
|
}
|
|
1485
1505
|
async function ensureDir(dirPath) {
|
|
1486
|
-
await
|
|
1506
|
+
await fs4.mkdir(dirPath, { recursive: true });
|
|
1487
1507
|
}
|
|
1488
1508
|
async function removeIfExists(filePath) {
|
|
1489
1509
|
try {
|
|
1490
|
-
const stats = await
|
|
1510
|
+
const stats = await fs4.lstat(filePath);
|
|
1491
1511
|
if (stats.isDirectory() && !stats.isSymbolicLink()) {
|
|
1492
|
-
await
|
|
1512
|
+
await fs4.rm(filePath, { recursive: true, force: true });
|
|
1493
1513
|
} else {
|
|
1494
|
-
await
|
|
1514
|
+
await fs4.unlink(filePath);
|
|
1495
1515
|
}
|
|
1496
1516
|
} catch (error) {
|
|
1497
1517
|
if (error.code !== "ENOENT") {
|
|
@@ -1503,6 +1523,12 @@ async function writeSingleFile(file, rootDir, dryRun) {
|
|
|
1503
1523
|
const fullPath = path.join(rootDir, file.path);
|
|
1504
1524
|
const dirPath = path.dirname(fullPath);
|
|
1505
1525
|
if (file.type === "symlink") {
|
|
1526
|
+
if (!file.target) {
|
|
1527
|
+
throw new WriteError(
|
|
1528
|
+
`Symlink file missing target: ${file.path}`,
|
|
1529
|
+
file.path
|
|
1530
|
+
);
|
|
1531
|
+
}
|
|
1506
1532
|
const target = file.target;
|
|
1507
1533
|
const existingTarget = await getSymlinkTarget(fullPath);
|
|
1508
1534
|
if (existingTarget === target) {
|
|
@@ -1514,7 +1540,7 @@ async function writeSingleFile(file, rootDir, dryRun) {
|
|
|
1514
1540
|
if (!dryRun) {
|
|
1515
1541
|
await ensureDir(dirPath);
|
|
1516
1542
|
await removeIfExists(fullPath);
|
|
1517
|
-
await
|
|
1543
|
+
await fs4.symlink(target, fullPath);
|
|
1518
1544
|
}
|
|
1519
1545
|
return {
|
|
1520
1546
|
path: file.path,
|
|
@@ -1535,7 +1561,11 @@ async function writeSingleFile(file, rootDir, dryRun) {
|
|
|
1535
1561
|
}
|
|
1536
1562
|
if (!dryRun) {
|
|
1537
1563
|
await ensureDir(dirPath);
|
|
1538
|
-
await
|
|
1564
|
+
const existingSymlink = await getSymlinkTarget(fullPath);
|
|
1565
|
+
if (existingSymlink !== null) {
|
|
1566
|
+
await removeIfExists(fullPath);
|
|
1567
|
+
}
|
|
1568
|
+
await fs4.writeFile(fullPath, content, "utf-8");
|
|
1539
1569
|
}
|
|
1540
1570
|
return {
|
|
1541
1571
|
path: file.path,
|
|
@@ -1564,19 +1594,101 @@ async function writeFiles(files, options) {
|
|
|
1564
1594
|
async function updateGitignore(rootDir, paths) {
|
|
1565
1595
|
const gitignorePath = path.join(rootDir, ".gitignore");
|
|
1566
1596
|
let content = "";
|
|
1597
|
+
let hasExistingFile = true;
|
|
1567
1598
|
try {
|
|
1568
|
-
content = await
|
|
1599
|
+
content = await fs4.readFile(gitignorePath, "utf-8");
|
|
1569
1600
|
} catch {
|
|
1601
|
+
hasExistingFile = false;
|
|
1570
1602
|
}
|
|
1571
1603
|
const marker = "# lnai-generated";
|
|
1572
1604
|
const endMarker = "# end lnai-generated";
|
|
1573
1605
|
const markerRegex = new RegExp(`${marker}[\\s\\S]*?${endMarker}\\n?`, "g");
|
|
1606
|
+
const hasManagedSection = new RegExp(`${marker}[\\s\\S]*?${endMarker}`).test(
|
|
1607
|
+
content
|
|
1608
|
+
);
|
|
1574
1609
|
content = content.replace(markerRegex, "");
|
|
1575
|
-
|
|
1576
|
-
const uniquePaths = [...new Set(paths)];
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1610
|
+
const baseContent = content.trimEnd();
|
|
1611
|
+
const uniquePaths = [...new Set(paths)].sort();
|
|
1612
|
+
if (uniquePaths.length === 0) {
|
|
1613
|
+
if (!hasManagedSection && !hasExistingFile) {
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
const cleanedContent = baseContent.length > 0 ? `${baseContent}
|
|
1617
|
+
` : baseContent;
|
|
1618
|
+
await fs4.writeFile(gitignorePath, cleanedContent, "utf-8");
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
const managedSection = [marker, ...uniquePaths, endMarker].join("\n");
|
|
1622
|
+
const nextContent = baseContent.length > 0 ? `${baseContent}
|
|
1623
|
+
|
|
1624
|
+
${managedSection}
|
|
1625
|
+
` : `${managedSection}
|
|
1626
|
+
`;
|
|
1627
|
+
await fs4.writeFile(gitignorePath, nextContent, "utf-8");
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// src/manifest/index.ts
|
|
1631
|
+
var MANIFEST_FILENAME = ".lnai-manifest.json";
|
|
1632
|
+
async function readManifest(rootDir) {
|
|
1633
|
+
const manifestPath = path.join(rootDir, UNIFIED_DIR, MANIFEST_FILENAME);
|
|
1634
|
+
try {
|
|
1635
|
+
const content = await fs4.readFile(manifestPath, "utf-8");
|
|
1636
|
+
const manifest = JSON.parse(content);
|
|
1637
|
+
if (manifest.version !== 1) {
|
|
1638
|
+
console.warn(
|
|
1639
|
+
`[lnai] Unknown manifest version ${manifest.version}, skipping cleanup`
|
|
1640
|
+
);
|
|
1641
|
+
return null;
|
|
1642
|
+
}
|
|
1643
|
+
return manifest;
|
|
1644
|
+
} catch (error) {
|
|
1645
|
+
if (error.code === "ENOENT") {
|
|
1646
|
+
return null;
|
|
1647
|
+
}
|
|
1648
|
+
console.warn(`[lnai] Failed to read manifest: ${error.message}`);
|
|
1649
|
+
return null;
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
async function writeManifest(rootDir, manifest) {
|
|
1653
|
+
const manifestPath = path.join(rootDir, UNIFIED_DIR, MANIFEST_FILENAME);
|
|
1654
|
+
const content = JSON.stringify(manifest, null, 2) + "\n";
|
|
1655
|
+
await fs4.writeFile(manifestPath, content, "utf-8");
|
|
1656
|
+
}
|
|
1657
|
+
function buildToolManifest(toolId, files) {
|
|
1658
|
+
const entries = files.map((file) => {
|
|
1659
|
+
const entry = {
|
|
1660
|
+
path: file.path,
|
|
1661
|
+
type: file.type
|
|
1662
|
+
};
|
|
1663
|
+
if (file.type === "symlink") {
|
|
1664
|
+
entry.target = file.target;
|
|
1665
|
+
} else {
|
|
1666
|
+
const content = file.type === "json" ? JSON.stringify(file.content, null, 2) + "\n" : String(file.content);
|
|
1667
|
+
entry.hash = computeHash(content);
|
|
1668
|
+
}
|
|
1669
|
+
return entry;
|
|
1670
|
+
});
|
|
1671
|
+
return {
|
|
1672
|
+
version: 1,
|
|
1673
|
+
tool: toolId,
|
|
1674
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1675
|
+
files: entries
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
function updateToolManifest(manifest, toolId, files) {
|
|
1679
|
+
return {
|
|
1680
|
+
...manifest,
|
|
1681
|
+
tools: {
|
|
1682
|
+
...manifest.tools,
|
|
1683
|
+
[toolId]: buildToolManifest(toolId, files)
|
|
1684
|
+
}
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1687
|
+
function createEmptyManifest() {
|
|
1688
|
+
return {
|
|
1689
|
+
version: 1,
|
|
1690
|
+
tools: {}
|
|
1691
|
+
};
|
|
1580
1692
|
}
|
|
1581
1693
|
|
|
1582
1694
|
// src/pipeline/index.ts
|
|
@@ -1598,7 +1710,12 @@ function getToolsToSync(config, requestedTools) {
|
|
|
1598
1710
|
return enabledTools;
|
|
1599
1711
|
}
|
|
1600
1712
|
async function runSyncPipeline(options) {
|
|
1601
|
-
const {
|
|
1713
|
+
const {
|
|
1714
|
+
rootDir,
|
|
1715
|
+
dryRun = false,
|
|
1716
|
+
skipCleanup = false,
|
|
1717
|
+
tools: requestedTools
|
|
1718
|
+
} = options;
|
|
1602
1719
|
if (requestedTools && requestedTools.length > 0) {
|
|
1603
1720
|
const toolValidation = validateToolIds(requestedTools);
|
|
1604
1721
|
if (!toolValidation.valid) {
|
|
@@ -1624,8 +1741,8 @@ async function runSyncPipeline(options) {
|
|
|
1624
1741
|
if (toolsToSync.length === 0) {
|
|
1625
1742
|
return [];
|
|
1626
1743
|
}
|
|
1744
|
+
let manifest = await readManifest(rootDir) ?? createEmptyManifest();
|
|
1627
1745
|
const results = [];
|
|
1628
|
-
const pathsToIgnore = [];
|
|
1629
1746
|
for (const toolId of toolsToSync) {
|
|
1630
1747
|
const plugin = pluginRegistry.get(toolId);
|
|
1631
1748
|
if (!plugin) {
|
|
@@ -1633,18 +1750,30 @@ async function runSyncPipeline(options) {
|
|
|
1633
1750
|
}
|
|
1634
1751
|
const validation = plugin.validate(state);
|
|
1635
1752
|
const outputFiles = await plugin.export(state, rootDir);
|
|
1636
|
-
|
|
1753
|
+
let deleteChanges = [];
|
|
1754
|
+
if (!skipCleanup && manifest.tools[toolId]) {
|
|
1755
|
+
const toDelete = computeFilesToDelete(
|
|
1756
|
+
manifest.tools[toolId].files,
|
|
1757
|
+
outputFiles
|
|
1758
|
+
);
|
|
1759
|
+
deleteChanges = await deleteFiles(toDelete, rootDir, dryRun);
|
|
1760
|
+
}
|
|
1761
|
+
const writeChanges = await writeFiles(outputFiles, { rootDir, dryRun });
|
|
1762
|
+
const changes = [...deleteChanges, ...writeChanges];
|
|
1637
1763
|
results.push({
|
|
1638
1764
|
tool: toolId,
|
|
1639
1765
|
changes,
|
|
1640
1766
|
validation
|
|
1641
1767
|
});
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
pathsToIgnore.push(...outputFiles.map((f) => f.path));
|
|
1768
|
+
if (!dryRun) {
|
|
1769
|
+
manifest = updateToolManifest(manifest, toolId, outputFiles);
|
|
1645
1770
|
}
|
|
1646
1771
|
}
|
|
1647
|
-
if (
|
|
1772
|
+
if (!dryRun) {
|
|
1773
|
+
await writeManifest(rootDir, manifest);
|
|
1774
|
+
const pathsToIgnore = Object.entries(manifest.tools).flatMap(
|
|
1775
|
+
([toolId, toolManifest]) => !state.config.tools?.[toolId]?.versionControl && toolManifest?.files ? toolManifest.files.map((file) => file.path) : []
|
|
1776
|
+
);
|
|
1648
1777
|
await updateGitignore(rootDir, pathsToIgnore);
|
|
1649
1778
|
}
|
|
1650
1779
|
return results;
|
|
@@ -1669,12 +1798,12 @@ async function initUnifiedConfig(options) {
|
|
|
1669
1798
|
);
|
|
1670
1799
|
}
|
|
1671
1800
|
if (exists) {
|
|
1672
|
-
await
|
|
1801
|
+
await fs4.rm(aiDir, { recursive: true, force: true });
|
|
1673
1802
|
}
|
|
1674
|
-
await
|
|
1803
|
+
await fs4.mkdir(aiDir, { recursive: true });
|
|
1675
1804
|
created.push(UNIFIED_DIR);
|
|
1676
1805
|
const configPath = path.join(aiDir, CONFIG_FILES.config);
|
|
1677
|
-
await
|
|
1806
|
+
await fs4.writeFile(
|
|
1678
1807
|
configPath,
|
|
1679
1808
|
JSON.stringify(config, null, 2) + "\n",
|
|
1680
1809
|
"utf-8"
|
|
@@ -1683,10 +1812,10 @@ async function initUnifiedConfig(options) {
|
|
|
1683
1812
|
if (!minimal) {
|
|
1684
1813
|
for (const dir of [CONFIG_DIRS.rules, CONFIG_DIRS.skills]) {
|
|
1685
1814
|
const dirPath = path.join(aiDir, dir);
|
|
1686
|
-
await
|
|
1815
|
+
await fs4.mkdir(dirPath, { recursive: true });
|
|
1687
1816
|
created.push(path.join(UNIFIED_DIR, dir));
|
|
1688
1817
|
const gitkeepPath = path.join(dirPath, ".gitkeep");
|
|
1689
|
-
await
|
|
1818
|
+
await fs4.writeFile(gitkeepPath, "", "utf-8");
|
|
1690
1819
|
created.push(path.join(UNIFIED_DIR, dir, ".gitkeep"));
|
|
1691
1820
|
}
|
|
1692
1821
|
}
|
|
@@ -1695,7 +1824,7 @@ async function initUnifiedConfig(options) {
|
|
|
1695
1824
|
async function hasUnifiedConfig(rootDir) {
|
|
1696
1825
|
const aiDir = path.join(rootDir, UNIFIED_DIR);
|
|
1697
1826
|
try {
|
|
1698
|
-
const stats = await
|
|
1827
|
+
const stats = await fs4.stat(aiDir);
|
|
1699
1828
|
return stats.isDirectory();
|
|
1700
1829
|
} catch {
|
|
1701
1830
|
return false;
|
|
@@ -1724,4 +1853,4 @@ function generateDefaultConfig(tools, versionControl) {
|
|
|
1724
1853
|
return { tools: toolsConfig };
|
|
1725
1854
|
}
|
|
1726
1855
|
|
|
1727
|
-
export { CONFIG_DIRS, CONFIG_FILES, FileNotFoundError, LnaiError, ParseError, PluginError, TOOL_IDS, TOOL_OUTPUT_DIRS, UNIFIED_DIR, ValidationError, WriteError, claudeCodePlugin, codexPlugin, computeHash, configSchema, generateDefaultConfig, hasUnifiedConfig, initUnifiedConfig, mcpServerSchema, opencodePlugin, parseFrontmatter, parseUnifiedConfig, permissionsSchema, pluginRegistry, ruleFrontmatterSchema, runSyncPipeline, settingsSchema, skillFrontmatterSchema, toolConfigSchema, toolIdSchema, updateGitignore, validateConfig, validateSettings, validateUnifiedState, writeFiles };
|
|
1856
|
+
export { CONFIG_DIRS, CONFIG_FILES, FileNotFoundError, InvalidToolError, LnaiError, MANIFEST_FILENAME, ParseError, PluginError, TOOL_IDS, TOOL_OUTPUT_DIRS, UNIFIED_DIR, ValidationError, WriteError, buildToolManifest, claudeCodePlugin, cleanupEmptyParentDirs, codexPlugin, computeFilesToDelete, computeHash, configSchema, createEmptyManifest, deleteFiles, generateDefaultConfig, hasUnifiedConfig, initUnifiedConfig, mcpServerSchema, opencodePlugin, parseFrontmatter, parseUnifiedConfig, permissionsSchema, pluginRegistry, readManifest, ruleFrontmatterSchema, runSyncPipeline, settingsSchema, skillFrontmatterSchema, toolConfigSchema, toolIdSchema, updateGitignore, updateToolManifest, validateConfig, validateSettings, validateUnifiedState, writeFiles, writeManifest };
|