@lnai/core 0.2.0 → 0.4.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/dist/index.d.ts +13 -8
- package/dist/index.js +376 -145
- package/package.json +4 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
3
|
declare const UNIFIED_DIR = ".ai";
|
|
4
|
-
declare const TOOL_IDS: readonly ["claudeCode", "opencode", "cursor"];
|
|
4
|
+
declare const TOOL_IDS: readonly ["claudeCode", "opencode", "cursor", "copilot", "windsurf"];
|
|
5
5
|
type ToolId = (typeof TOOL_IDS)[number];
|
|
6
6
|
declare const CONFIG_FILES: {
|
|
7
7
|
readonly config: "config.json";
|
|
@@ -66,6 +66,8 @@ declare const toolIdSchema: z.ZodEnum<{
|
|
|
66
66
|
claudeCode: "claudeCode";
|
|
67
67
|
opencode: "opencode";
|
|
68
68
|
cursor: "cursor";
|
|
69
|
+
copilot: "copilot";
|
|
70
|
+
windsurf: "windsurf";
|
|
69
71
|
}>;
|
|
70
72
|
/** Settings configuration (Claude format as source of truth) */
|
|
71
73
|
declare const settingsSchema: z.ZodObject<{
|
|
@@ -85,11 +87,6 @@ declare const settingsSchema: z.ZodObject<{
|
|
|
85
87
|
url: z.ZodOptional<z.ZodString>;
|
|
86
88
|
headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
87
89
|
}, z.core.$strip>>>;
|
|
88
|
-
overrides: z.ZodOptional<z.ZodObject<{
|
|
89
|
-
claudeCode: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
90
|
-
opencode: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
91
|
-
cursor: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
92
|
-
}, z.core.$strip>>;
|
|
93
90
|
}, z.core.$strip>;
|
|
94
91
|
/** Main config.json structure. Uses partial object to allow partial tool configs. */
|
|
95
92
|
declare const configSchema: z.ZodObject<{
|
|
@@ -106,6 +103,14 @@ declare const configSchema: z.ZodObject<{
|
|
|
106
103
|
enabled: z.ZodBoolean;
|
|
107
104
|
versionControl: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
108
105
|
}, z.core.$strip>>;
|
|
106
|
+
copilot: z.ZodOptional<z.ZodObject<{
|
|
107
|
+
enabled: z.ZodBoolean;
|
|
108
|
+
versionControl: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
109
|
+
}, z.core.$strip>>;
|
|
110
|
+
windsurf: z.ZodOptional<z.ZodObject<{
|
|
111
|
+
enabled: z.ZodBoolean;
|
|
112
|
+
versionControl: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
113
|
+
}, z.core.$strip>>;
|
|
109
114
|
}, z.core.$strip>>;
|
|
110
115
|
}, z.core.$strip>;
|
|
111
116
|
/** Skill frontmatter (name and description required) */
|
|
@@ -145,7 +150,6 @@ interface UnifiedState {
|
|
|
145
150
|
settings: {
|
|
146
151
|
permissions?: Permissions;
|
|
147
152
|
mcpServers?: Record<string, McpServer>;
|
|
148
|
-
overrides?: Partial<Record<ToolId, Record<string, unknown>>>;
|
|
149
153
|
} | null;
|
|
150
154
|
agents: string | null;
|
|
151
155
|
rules: MarkdownFile<RuleFrontmatter>[];
|
|
@@ -302,12 +306,13 @@ interface InitOptions {
|
|
|
302
306
|
tools?: ToolId[];
|
|
303
307
|
minimal?: boolean;
|
|
304
308
|
force?: boolean;
|
|
309
|
+
versionControl?: Record<ToolId, boolean>;
|
|
305
310
|
}
|
|
306
311
|
interface InitResult {
|
|
307
312
|
created: string[];
|
|
308
313
|
}
|
|
309
314
|
declare function initUnifiedConfig(options: InitOptions): Promise<InitResult>;
|
|
310
315
|
declare function hasUnifiedConfig(rootDir: string): Promise<boolean>;
|
|
311
|
-
declare function generateDefaultConfig(tools?: ToolId[]): Config;
|
|
316
|
+
declare function generateDefaultConfig(tools?: ToolId[], versionControl?: Record<ToolId, boolean>): Config;
|
|
312
317
|
|
|
313
318
|
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, computeHash, configSchema, generateDefaultConfig, hasUnifiedConfig, initUnifiedConfig, mcpServerSchema, opencodePlugin, parseFrontmatter, parseUnifiedConfig, permissionsSchema, pluginRegistry, ruleFrontmatterSchema, runSyncPipeline, settingsSchema, skillFrontmatterSchema, toolConfigSchema, toolIdSchema, updateGitignore, validateConfig, validateSettings, validateUnifiedState, writeFiles };
|
package/dist/index.js
CHANGED
|
@@ -2,12 +2,17 @@ import { z } from 'zod';
|
|
|
2
2
|
import * as fs3 from 'fs/promises';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import matter from 'gray-matter';
|
|
5
|
-
import deepmerge from 'deepmerge';
|
|
6
5
|
import * as crypto from 'crypto';
|
|
7
6
|
|
|
8
7
|
// src/constants.ts
|
|
9
8
|
var UNIFIED_DIR = ".ai";
|
|
10
|
-
var TOOL_IDS = [
|
|
9
|
+
var TOOL_IDS = [
|
|
10
|
+
"claudeCode",
|
|
11
|
+
"opencode",
|
|
12
|
+
"cursor",
|
|
13
|
+
"copilot",
|
|
14
|
+
"windsurf"
|
|
15
|
+
];
|
|
11
16
|
var CONFIG_FILES = {
|
|
12
17
|
config: "config.json",
|
|
13
18
|
settings: "settings.json",
|
|
@@ -21,12 +26,16 @@ var CONFIG_DIRS = {
|
|
|
21
26
|
var TOOL_OUTPUT_DIRS = {
|
|
22
27
|
claudeCode: ".claude",
|
|
23
28
|
opencode: ".opencode",
|
|
24
|
-
cursor: ".cursor"
|
|
29
|
+
cursor: ".cursor",
|
|
30
|
+
copilot: ".github",
|
|
31
|
+
windsurf: ".windsurf"
|
|
25
32
|
};
|
|
26
33
|
var OVERRIDE_DIRS = {
|
|
27
34
|
claudeCode: ".claude",
|
|
28
35
|
opencode: ".opencode",
|
|
29
|
-
cursor: ".cursor"
|
|
36
|
+
cursor: ".cursor",
|
|
37
|
+
copilot: ".copilot",
|
|
38
|
+
windsurf: ".windsurf"
|
|
30
39
|
};
|
|
31
40
|
|
|
32
41
|
// src/errors.ts
|
|
@@ -106,21 +115,24 @@ var toolConfigSchema = z.object({
|
|
|
106
115
|
enabled: z.boolean(),
|
|
107
116
|
versionControl: z.boolean().optional().default(false)
|
|
108
117
|
});
|
|
109
|
-
var toolIdSchema = z.enum([
|
|
118
|
+
var toolIdSchema = z.enum([
|
|
119
|
+
"claudeCode",
|
|
120
|
+
"opencode",
|
|
121
|
+
"cursor",
|
|
122
|
+
"copilot",
|
|
123
|
+
"windsurf"
|
|
124
|
+
]);
|
|
110
125
|
var settingsSchema = z.object({
|
|
111
126
|
permissions: permissionsSchema.optional(),
|
|
112
|
-
mcpServers: z.record(z.string(), mcpServerSchema).optional()
|
|
113
|
-
overrides: z.object({
|
|
114
|
-
claudeCode: z.record(z.string(), z.unknown()).optional(),
|
|
115
|
-
opencode: z.record(z.string(), z.unknown()).optional(),
|
|
116
|
-
cursor: z.record(z.string(), z.unknown()).optional()
|
|
117
|
-
}).optional()
|
|
127
|
+
mcpServers: z.record(z.string(), mcpServerSchema).optional()
|
|
118
128
|
});
|
|
119
129
|
var configSchema = z.object({
|
|
120
130
|
tools: z.object({
|
|
121
131
|
claudeCode: toolConfigSchema,
|
|
122
132
|
opencode: toolConfigSchema,
|
|
123
|
-
cursor: toolConfigSchema
|
|
133
|
+
cursor: toolConfigSchema,
|
|
134
|
+
copilot: toolConfigSchema,
|
|
135
|
+
windsurf: toolConfigSchema
|
|
124
136
|
}).partial().optional()
|
|
125
137
|
});
|
|
126
138
|
var skillFrontmatterSchema = z.object({
|
|
@@ -403,38 +415,24 @@ async function scanDir(baseDir, currentDir, files) {
|
|
|
403
415
|
}
|
|
404
416
|
}
|
|
405
417
|
}
|
|
406
|
-
function
|
|
407
|
-
return deepmerge(base, override, {
|
|
408
|
-
arrayMerge: (target, source) => [.../* @__PURE__ */ new Set([...target, ...source])]
|
|
409
|
-
});
|
|
410
|
-
}
|
|
411
|
-
async function fileExists2(filePath) {
|
|
412
|
-
try {
|
|
413
|
-
await fs3.access(filePath);
|
|
414
|
-
return true;
|
|
415
|
-
} catch {
|
|
416
|
-
return false;
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
async function getOverrideOutputFiles(rootDir, toolId) {
|
|
418
|
+
async function applyFileOverrides(files, rootDir, toolId) {
|
|
420
419
|
const outputDir = TOOL_OUTPUT_DIRS[toolId];
|
|
421
420
|
const overrideFiles = await scanOverrideDirectory(rootDir, toolId);
|
|
422
|
-
const
|
|
421
|
+
const overridePaths = /* @__PURE__ */ new Set();
|
|
422
|
+
const overrideOutputFiles = [];
|
|
423
423
|
for (const overrideFile of overrideFiles) {
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
}
|
|
428
|
-
const symlinkPath = `${outputDir}/${overrideFile.relativePath}`;
|
|
429
|
-
const symlinkDir = path.dirname(symlinkPath);
|
|
424
|
+
const outputPath = `${outputDir}/${overrideFile.relativePath}`;
|
|
425
|
+
overridePaths.add(outputPath);
|
|
426
|
+
const symlinkDir = path.dirname(outputPath);
|
|
430
427
|
const sourcePath = `${UNIFIED_DIR}/${OVERRIDE_DIRS[toolId]}/${overrideFile.relativePath}`;
|
|
431
|
-
|
|
432
|
-
path:
|
|
428
|
+
overrideOutputFiles.push({
|
|
429
|
+
path: outputPath,
|
|
433
430
|
type: "symlink",
|
|
434
431
|
target: path.relative(symlinkDir, sourcePath)
|
|
435
432
|
});
|
|
436
433
|
}
|
|
437
|
-
|
|
434
|
+
const filteredFiles = files.filter((file) => !overridePaths.has(file.path));
|
|
435
|
+
return [...filteredFiles, ...overrideOutputFiles];
|
|
438
436
|
}
|
|
439
437
|
|
|
440
438
|
// src/plugins/claude-code/index.ts
|
|
@@ -471,30 +469,21 @@ var claudeCodePlugin = {
|
|
|
471
469
|
target: `../../${UNIFIED_DIR}/skills/${skill.path}`
|
|
472
470
|
});
|
|
473
471
|
}
|
|
474
|
-
const
|
|
472
|
+
const settings = {};
|
|
475
473
|
if (state.settings?.permissions) {
|
|
476
|
-
|
|
474
|
+
settings["permissions"] = state.settings.permissions;
|
|
477
475
|
}
|
|
478
476
|
if (state.settings?.mcpServers) {
|
|
479
|
-
|
|
480
|
-
}
|
|
481
|
-
let finalSettings = baseSettings;
|
|
482
|
-
if (state.settings?.overrides?.claudeCode) {
|
|
483
|
-
finalSettings = deepMergeConfigs(
|
|
484
|
-
baseSettings,
|
|
485
|
-
state.settings.overrides.claudeCode
|
|
486
|
-
);
|
|
477
|
+
settings["mcpServers"] = state.settings.mcpServers;
|
|
487
478
|
}
|
|
488
|
-
if (Object.keys(
|
|
479
|
+
if (Object.keys(settings).length > 0) {
|
|
489
480
|
files.push({
|
|
490
481
|
path: `${outputDir}/settings.json`,
|
|
491
482
|
type: "json",
|
|
492
|
-
content:
|
|
483
|
+
content: settings
|
|
493
484
|
});
|
|
494
485
|
}
|
|
495
|
-
|
|
496
|
-
files.push(...overrideFiles);
|
|
497
|
-
return files;
|
|
486
|
+
return applyFileOverrides(files, rootDir, "claudeCode");
|
|
498
487
|
},
|
|
499
488
|
validate(state) {
|
|
500
489
|
const warnings = [];
|
|
@@ -508,6 +497,197 @@ var claudeCodePlugin = {
|
|
|
508
497
|
}
|
|
509
498
|
};
|
|
510
499
|
|
|
500
|
+
// src/utils/transforms.ts
|
|
501
|
+
var ENV_VAR_PATTERN = /\$\{([^}:]+)(:-[^}]*)?\}/g;
|
|
502
|
+
function transformEnvVar(value, format) {
|
|
503
|
+
if (format === "opencode") {
|
|
504
|
+
return value.replace(ENV_VAR_PATTERN, "{env:$1}");
|
|
505
|
+
}
|
|
506
|
+
return value.replace(ENV_VAR_PATTERN, "${env:$1}");
|
|
507
|
+
}
|
|
508
|
+
function transformEnvVars(env, format) {
|
|
509
|
+
const result = {};
|
|
510
|
+
for (const [key, value] of Object.entries(env)) {
|
|
511
|
+
result[key] = transformEnvVar(value, format);
|
|
512
|
+
}
|
|
513
|
+
return result;
|
|
514
|
+
}
|
|
515
|
+
function parsePermissionRule(rule) {
|
|
516
|
+
const match = rule.match(/^(\w+)\(([^)]+)\)$/);
|
|
517
|
+
if (!match) {
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
const tool = match[1];
|
|
521
|
+
const pattern = match[2];
|
|
522
|
+
if (!tool || !pattern) {
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
return { tool, pattern };
|
|
526
|
+
}
|
|
527
|
+
function deriveDescription(filename, content) {
|
|
528
|
+
const headingMatch = content.match(/^#\s+(.+)$/m);
|
|
529
|
+
if (headingMatch && headingMatch[1]) {
|
|
530
|
+
return headingMatch[1];
|
|
531
|
+
}
|
|
532
|
+
const baseName = filename.replace(/\.md$/, "");
|
|
533
|
+
return baseName.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// src/plugins/copilot/transforms.ts
|
|
537
|
+
function transformRuleToCopilot(rule) {
|
|
538
|
+
const description = deriveDescription(rule.path, rule.content);
|
|
539
|
+
const paths = rule.frontmatter.paths || [];
|
|
540
|
+
const frontmatter = {
|
|
541
|
+
description
|
|
542
|
+
};
|
|
543
|
+
if (paths.length > 0) {
|
|
544
|
+
frontmatter.applyTo = paths.join(",");
|
|
545
|
+
}
|
|
546
|
+
return {
|
|
547
|
+
frontmatter,
|
|
548
|
+
content: rule.content
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
function serializeCopilotInstruction(frontmatter, content) {
|
|
552
|
+
const lines = ["---"];
|
|
553
|
+
if (frontmatter.applyTo) {
|
|
554
|
+
lines.push(`applyTo: ${JSON.stringify(frontmatter.applyTo)}`);
|
|
555
|
+
}
|
|
556
|
+
lines.push(`description: ${JSON.stringify(frontmatter.description)}`);
|
|
557
|
+
lines.push("---");
|
|
558
|
+
lines.push("");
|
|
559
|
+
lines.push(content);
|
|
560
|
+
return lines.join("\n");
|
|
561
|
+
}
|
|
562
|
+
function transformMcpToCopilot(servers) {
|
|
563
|
+
if (!servers || Object.keys(servers).length === 0) {
|
|
564
|
+
return void 0;
|
|
565
|
+
}
|
|
566
|
+
const result = {};
|
|
567
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
568
|
+
if (server.type === "http" || server.type === "sse") {
|
|
569
|
+
if (!server.url) {
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
const copilotServer = {
|
|
573
|
+
url: server.url
|
|
574
|
+
};
|
|
575
|
+
if (server.headers && Object.keys(server.headers).length > 0) {
|
|
576
|
+
copilotServer.requestInit = {
|
|
577
|
+
headers: transformEnvVars(server.headers, "copilot")
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
result[name] = copilotServer;
|
|
581
|
+
} else if (server.command) {
|
|
582
|
+
const copilotServer = {
|
|
583
|
+
type: "stdio",
|
|
584
|
+
command: server.command
|
|
585
|
+
};
|
|
586
|
+
if (server.args && server.args.length > 0) {
|
|
587
|
+
copilotServer.args = server.args;
|
|
588
|
+
}
|
|
589
|
+
if (server.env && Object.keys(server.env).length > 0) {
|
|
590
|
+
copilotServer.env = transformEnvVars(server.env, "copilot");
|
|
591
|
+
}
|
|
592
|
+
result[name] = copilotServer;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
if (Object.keys(result).length === 0) {
|
|
596
|
+
return void 0;
|
|
597
|
+
}
|
|
598
|
+
return {
|
|
599
|
+
inputs: [],
|
|
600
|
+
servers: result
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// src/plugins/copilot/index.ts
|
|
605
|
+
var copilotPlugin = {
|
|
606
|
+
id: "copilot",
|
|
607
|
+
name: "GitHub Copilot",
|
|
608
|
+
async detect(_rootDir) {
|
|
609
|
+
return false;
|
|
610
|
+
},
|
|
611
|
+
async import(_rootDir) {
|
|
612
|
+
return null;
|
|
613
|
+
},
|
|
614
|
+
async export(state, rootDir) {
|
|
615
|
+
const files = [];
|
|
616
|
+
if (state.agents) {
|
|
617
|
+
files.push({
|
|
618
|
+
path: ".github/copilot-instructions.md",
|
|
619
|
+
type: "symlink",
|
|
620
|
+
target: `../${UNIFIED_DIR}/AGENTS.md`
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
for (const rule of state.rules) {
|
|
624
|
+
const transformed = transformRuleToCopilot(rule);
|
|
625
|
+
const ruleContent = serializeCopilotInstruction(
|
|
626
|
+
transformed.frontmatter,
|
|
627
|
+
transformed.content
|
|
628
|
+
);
|
|
629
|
+
const outputFilename = rule.path.replace(/\.md$/, ".instructions.md");
|
|
630
|
+
files.push({
|
|
631
|
+
path: `.github/instructions/${outputFilename}`,
|
|
632
|
+
type: "text",
|
|
633
|
+
content: ruleContent
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
for (const skill of state.skills) {
|
|
637
|
+
files.push({
|
|
638
|
+
path: `.github/skills/${skill.path}`,
|
|
639
|
+
type: "symlink",
|
|
640
|
+
target: `../../${UNIFIED_DIR}/skills/${skill.path}`
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
const mcpConfig = transformMcpToCopilot(state.settings?.mcpServers);
|
|
644
|
+
if (mcpConfig) {
|
|
645
|
+
files.push({
|
|
646
|
+
path: ".vscode/mcp.json",
|
|
647
|
+
type: "json",
|
|
648
|
+
content: { inputs: mcpConfig.inputs, servers: mcpConfig.servers }
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
return applyFileOverrides(files, rootDir, "copilot");
|
|
652
|
+
},
|
|
653
|
+
validate(state) {
|
|
654
|
+
const warnings = [];
|
|
655
|
+
if (!state.agents) {
|
|
656
|
+
warnings.push({
|
|
657
|
+
path: ["AGENTS.md"],
|
|
658
|
+
message: "No AGENTS.md found - .github/copilot-instructions.md will not be created"
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
const permissions = state.settings?.permissions;
|
|
662
|
+
const hasPermissions = permissions && (permissions.allow && permissions.allow.length > 0 || permissions.ask && permissions.ask.length > 0 || permissions.deny && permissions.deny.length > 0);
|
|
663
|
+
if (hasPermissions) {
|
|
664
|
+
warnings.push({
|
|
665
|
+
path: ["settings", "permissions"],
|
|
666
|
+
message: "GitHub Copilot does not support permissions - they will be ignored"
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
const mcpServers = state.settings?.mcpServers;
|
|
670
|
+
if (mcpServers) {
|
|
671
|
+
for (const [name, server] of Object.entries(mcpServers)) {
|
|
672
|
+
const isRemote = server.type === "http" || server.type === "sse";
|
|
673
|
+
const hasCommand = !!server.command;
|
|
674
|
+
if (isRemote && !server.url) {
|
|
675
|
+
warnings.push({
|
|
676
|
+
path: ["settings", "mcpServers", name],
|
|
677
|
+
message: `MCP server "${name}" is type "${server.type}" but has no url - it will be skipped`
|
|
678
|
+
});
|
|
679
|
+
} else if (!isRemote && !hasCommand) {
|
|
680
|
+
warnings.push({
|
|
681
|
+
path: ["settings", "mcpServers", name],
|
|
682
|
+
message: `MCP server "${name}" has no command or type - it will be skipped`
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return { valid: true, errors: [], warnings, skipped: [] };
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
|
|
511
691
|
// src/plugins/cursor/transforms.ts
|
|
512
692
|
function transformRuleToCursor(rule) {
|
|
513
693
|
const description = deriveDescription(rule.path, rule.content);
|
|
@@ -522,14 +702,6 @@ function transformRuleToCursor(rule) {
|
|
|
522
702
|
content: rule.content
|
|
523
703
|
};
|
|
524
704
|
}
|
|
525
|
-
function deriveDescription(filename, content) {
|
|
526
|
-
const headingMatch = content.match(/^#\s+(.+)$/m);
|
|
527
|
-
if (headingMatch && headingMatch[1]) {
|
|
528
|
-
return headingMatch[1];
|
|
529
|
-
}
|
|
530
|
-
const baseName = filename.replace(/\.md$/, "");
|
|
531
|
-
return baseName.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
532
|
-
}
|
|
533
705
|
function serializeCursorRule(frontmatter, content) {
|
|
534
706
|
const lines = [
|
|
535
707
|
"---",
|
|
@@ -556,7 +728,7 @@ function transformMcpToCursor(servers) {
|
|
|
556
728
|
url: server.url
|
|
557
729
|
};
|
|
558
730
|
if (server.headers) {
|
|
559
|
-
cursorServer.headers =
|
|
731
|
+
cursorServer.headers = transformEnvVars(server.headers, "cursor");
|
|
560
732
|
}
|
|
561
733
|
result[name] = cursorServer;
|
|
562
734
|
} else if (server.command) {
|
|
@@ -567,23 +739,13 @@ function transformMcpToCursor(servers) {
|
|
|
567
739
|
cursorServer.args = server.args;
|
|
568
740
|
}
|
|
569
741
|
if (server.env) {
|
|
570
|
-
cursorServer.env =
|
|
742
|
+
cursorServer.env = transformEnvVars(server.env, "cursor");
|
|
571
743
|
}
|
|
572
744
|
result[name] = cursorServer;
|
|
573
745
|
}
|
|
574
746
|
}
|
|
575
747
|
return Object.keys(result).length > 0 ? result : void 0;
|
|
576
748
|
}
|
|
577
|
-
function transformEnvVarToCursor(value) {
|
|
578
|
-
return value.replace(/\$\{([^}:]+)(:-[^}]*)?\}/g, "${env:$1}");
|
|
579
|
-
}
|
|
580
|
-
function transformEnvVarsToCursor(env) {
|
|
581
|
-
const result = {};
|
|
582
|
-
for (const [key, value] of Object.entries(env)) {
|
|
583
|
-
result[key] = transformEnvVarToCursor(value);
|
|
584
|
-
}
|
|
585
|
-
return result;
|
|
586
|
-
}
|
|
587
749
|
function transformPermissionsToCursor(permissions) {
|
|
588
750
|
if (!permissions) {
|
|
589
751
|
return { permissions: void 0, hasAskPermissions: false };
|
|
@@ -682,47 +844,22 @@ var cursorPlugin = {
|
|
|
682
844
|
});
|
|
683
845
|
}
|
|
684
846
|
const mcpServers = transformMcpToCursor(state.settings?.mcpServers);
|
|
685
|
-
|
|
686
|
-
const hasMcpOverrides = cursorOverrides?.["mcpServers"] !== void 0;
|
|
687
|
-
if (mcpServers || hasMcpOverrides) {
|
|
688
|
-
let mcpContent = mcpServers ? { mcpServers } : {};
|
|
689
|
-
if (cursorOverrides?.["mcpServers"]) {
|
|
690
|
-
mcpContent = deepMergeConfigs(mcpContent, {
|
|
691
|
-
mcpServers: cursorOverrides["mcpServers"]
|
|
692
|
-
});
|
|
693
|
-
}
|
|
847
|
+
if (mcpServers) {
|
|
694
848
|
files.push({
|
|
695
849
|
path: `${outputDir}/mcp.json`,
|
|
696
850
|
type: "json",
|
|
697
|
-
content:
|
|
851
|
+
content: { mcpServers }
|
|
698
852
|
});
|
|
699
853
|
}
|
|
700
|
-
const
|
|
701
|
-
|
|
702
|
-
);
|
|
703
|
-
const hasCliOverrides = cursorOverrides !== void 0 && Object.keys(cursorOverrides).some((key) => key !== "mcpServers");
|
|
704
|
-
if (permissionsResult.permissions || hasCliOverrides) {
|
|
705
|
-
let cliContent = permissionsResult.permissions ? { permissions: permissionsResult.permissions } : {};
|
|
706
|
-
if (cursorOverrides) {
|
|
707
|
-
const cliOverrides = {};
|
|
708
|
-
for (const [key, value] of Object.entries(cursorOverrides)) {
|
|
709
|
-
if (key !== "mcpServers") {
|
|
710
|
-
cliOverrides[key] = value;
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
if (Object.keys(cliOverrides).length > 0) {
|
|
714
|
-
cliContent = deepMergeConfigs(cliContent, cliOverrides);
|
|
715
|
-
}
|
|
716
|
-
}
|
|
854
|
+
const cliContent = buildCliContent(state.settings?.permissions);
|
|
855
|
+
if (cliContent) {
|
|
717
856
|
files.push({
|
|
718
857
|
path: `${outputDir}/cli.json`,
|
|
719
858
|
type: "json",
|
|
720
859
|
content: cliContent
|
|
721
860
|
});
|
|
722
861
|
}
|
|
723
|
-
|
|
724
|
-
files.push(...overrideFiles);
|
|
725
|
-
return files;
|
|
862
|
+
return applyFileOverrides(files, rootDir, "cursor");
|
|
726
863
|
},
|
|
727
864
|
validate(state) {
|
|
728
865
|
const warnings = [];
|
|
@@ -757,6 +894,13 @@ var cursorPlugin = {
|
|
|
757
894
|
return { valid: true, errors: [], warnings, skipped: [] };
|
|
758
895
|
}
|
|
759
896
|
};
|
|
897
|
+
function buildCliContent(permissions) {
|
|
898
|
+
const permissionsResult = transformPermissionsToCursor(permissions);
|
|
899
|
+
if (!permissionsResult.permissions) {
|
|
900
|
+
return void 0;
|
|
901
|
+
}
|
|
902
|
+
return { permissions: permissionsResult.permissions };
|
|
903
|
+
}
|
|
760
904
|
|
|
761
905
|
// src/plugins/opencode/transforms.ts
|
|
762
906
|
function transformMcpToOpenCode(servers) {
|
|
@@ -781,7 +925,7 @@ function transformMcpToOpenCode(servers) {
|
|
|
781
925
|
command
|
|
782
926
|
};
|
|
783
927
|
if (server.env) {
|
|
784
|
-
openCodeServer.environment = transformEnvVars(server.env);
|
|
928
|
+
openCodeServer.environment = transformEnvVars(server.env, "opencode");
|
|
785
929
|
}
|
|
786
930
|
result[name] = openCodeServer;
|
|
787
931
|
}
|
|
@@ -798,7 +942,7 @@ function transformPermissionsToOpenCode(permissions) {
|
|
|
798
942
|
return;
|
|
799
943
|
}
|
|
800
944
|
for (const rule of rules) {
|
|
801
|
-
const parsed =
|
|
945
|
+
const parsed = parsePermissionRuleForOpenCode(rule);
|
|
802
946
|
if (!parsed) {
|
|
803
947
|
continue;
|
|
804
948
|
}
|
|
@@ -814,28 +958,13 @@ function transformPermissionsToOpenCode(permissions) {
|
|
|
814
958
|
processRules(permissions.deny, "deny");
|
|
815
959
|
return Object.keys(result).length > 0 ? result : void 0;
|
|
816
960
|
}
|
|
817
|
-
function
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
function transformEnvVars(env) {
|
|
821
|
-
const result = {};
|
|
822
|
-
for (const [key, value] of Object.entries(env)) {
|
|
823
|
-
result[key] = transformEnvVar(value);
|
|
824
|
-
}
|
|
825
|
-
return result;
|
|
826
|
-
}
|
|
827
|
-
function parsePermissionRule(rule) {
|
|
828
|
-
const match = rule.match(/^(\w+)\(([^)]+)\)$/);
|
|
829
|
-
if (!match) {
|
|
830
|
-
return null;
|
|
831
|
-
}
|
|
832
|
-
const tool = match[1];
|
|
833
|
-
const pattern = match[2];
|
|
834
|
-
if (!tool || !pattern) {
|
|
961
|
+
function parsePermissionRuleForOpenCode(rule) {
|
|
962
|
+
const parsed = parsePermissionRule(rule);
|
|
963
|
+
if (!parsed) {
|
|
835
964
|
return null;
|
|
836
965
|
}
|
|
837
|
-
const normalizedTool = tool.toLowerCase();
|
|
838
|
-
let normalizedPattern = pattern;
|
|
966
|
+
const normalizedTool = parsed.tool.toLowerCase();
|
|
967
|
+
let normalizedPattern = parsed.pattern;
|
|
839
968
|
if (normalizedPattern.includes(":*")) {
|
|
840
969
|
normalizedPattern = normalizedPattern.replace(/:(\*)/g, " $1");
|
|
841
970
|
}
|
|
@@ -879,37 +1008,28 @@ var opencodePlugin = {
|
|
|
879
1008
|
target: `../../${UNIFIED_DIR}/skills/${skill.path}`
|
|
880
1009
|
});
|
|
881
1010
|
}
|
|
882
|
-
const
|
|
1011
|
+
const config = {
|
|
883
1012
|
$schema: "https://opencode.ai/config.json"
|
|
884
1013
|
};
|
|
885
1014
|
if (state.rules.length > 0) {
|
|
886
|
-
|
|
1015
|
+
config["instructions"] = [`${outputDir}/rules/*.md`];
|
|
887
1016
|
}
|
|
888
1017
|
const mcp = transformMcpToOpenCode(state.settings?.mcpServers);
|
|
889
1018
|
if (mcp) {
|
|
890
|
-
|
|
1019
|
+
config["mcp"] = mcp;
|
|
891
1020
|
}
|
|
892
1021
|
const permission = transformPermissionsToOpenCode(
|
|
893
1022
|
state.settings?.permissions
|
|
894
1023
|
);
|
|
895
1024
|
if (permission) {
|
|
896
|
-
|
|
897
|
-
}
|
|
898
|
-
let finalConfig = baseConfig;
|
|
899
|
-
if (state.settings?.overrides?.opencode) {
|
|
900
|
-
finalConfig = deepMergeConfigs(
|
|
901
|
-
baseConfig,
|
|
902
|
-
state.settings.overrides.opencode
|
|
903
|
-
);
|
|
1025
|
+
config["permission"] = permission;
|
|
904
1026
|
}
|
|
905
1027
|
files.push({
|
|
906
1028
|
path: "opencode.json",
|
|
907
1029
|
type: "json",
|
|
908
|
-
content:
|
|
1030
|
+
content: config
|
|
909
1031
|
});
|
|
910
|
-
|
|
911
|
-
files.push(...overrideFiles);
|
|
912
|
-
return files;
|
|
1032
|
+
return applyFileOverrides(files, rootDir, "opencode");
|
|
913
1033
|
},
|
|
914
1034
|
validate(state) {
|
|
915
1035
|
const warnings = [];
|
|
@@ -944,10 +1064,116 @@ var PluginRegistry = class {
|
|
|
944
1064
|
};
|
|
945
1065
|
var pluginRegistry = new PluginRegistry();
|
|
946
1066
|
|
|
1067
|
+
// src/plugins/windsurf/transforms.ts
|
|
1068
|
+
function transformRuleToWindsurf(rule) {
|
|
1069
|
+
const frontmatter = {
|
|
1070
|
+
trigger: "manual"
|
|
1071
|
+
};
|
|
1072
|
+
return {
|
|
1073
|
+
frontmatter,
|
|
1074
|
+
content: rule.content
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
function serializeWindsurfRule(frontmatter, content) {
|
|
1078
|
+
const lines = ["---"];
|
|
1079
|
+
lines.push(`trigger: ${frontmatter.trigger}`);
|
|
1080
|
+
if (frontmatter.globs && frontmatter.globs.length > 0) {
|
|
1081
|
+
lines.push("globs:");
|
|
1082
|
+
for (const glob of frontmatter.globs) {
|
|
1083
|
+
lines.push(` - ${glob}`);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
if (frontmatter.description) {
|
|
1087
|
+
const escaped = frontmatter.description.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
1088
|
+
lines.push(`description: "${escaped}"`);
|
|
1089
|
+
}
|
|
1090
|
+
lines.push("---");
|
|
1091
|
+
lines.push("");
|
|
1092
|
+
lines.push(content);
|
|
1093
|
+
return lines.join("\n");
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// src/plugins/windsurf/index.ts
|
|
1097
|
+
var windsurfPlugin = {
|
|
1098
|
+
id: "windsurf",
|
|
1099
|
+
name: "Windsurf",
|
|
1100
|
+
async detect(_rootDir) {
|
|
1101
|
+
return false;
|
|
1102
|
+
},
|
|
1103
|
+
async import(_rootDir) {
|
|
1104
|
+
return null;
|
|
1105
|
+
},
|
|
1106
|
+
async export(state, rootDir) {
|
|
1107
|
+
const files = [];
|
|
1108
|
+
const outputDir = TOOL_OUTPUT_DIRS.windsurf;
|
|
1109
|
+
if (state.agents) {
|
|
1110
|
+
files.push({
|
|
1111
|
+
path: "AGENTS.md",
|
|
1112
|
+
type: "symlink",
|
|
1113
|
+
target: `${UNIFIED_DIR}/AGENTS.md`
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
for (const rule of state.rules) {
|
|
1117
|
+
const transformed = transformRuleToWindsurf(rule);
|
|
1118
|
+
const ruleContent = serializeWindsurfRule(
|
|
1119
|
+
transformed.frontmatter,
|
|
1120
|
+
transformed.content
|
|
1121
|
+
);
|
|
1122
|
+
files.push({
|
|
1123
|
+
path: `${outputDir}/rules/${rule.path}`,
|
|
1124
|
+
type: "text",
|
|
1125
|
+
content: ruleContent
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
for (const skill of state.skills) {
|
|
1129
|
+
files.push({
|
|
1130
|
+
path: `${outputDir}/skills/${skill.path}`,
|
|
1131
|
+
type: "symlink",
|
|
1132
|
+
target: `../../${UNIFIED_DIR}/skills/${skill.path}`
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
return applyFileOverrides(files, rootDir, "windsurf");
|
|
1136
|
+
},
|
|
1137
|
+
validate(state) {
|
|
1138
|
+
const warnings = [];
|
|
1139
|
+
const skipped = [];
|
|
1140
|
+
if (!state.agents) {
|
|
1141
|
+
warnings.push({
|
|
1142
|
+
path: ["AGENTS.md"],
|
|
1143
|
+
message: "No AGENTS.md found - root AGENTS.md will not be created"
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
if (state.rules.length > 0) {
|
|
1147
|
+
warnings.push({
|
|
1148
|
+
path: [".windsurf/rules"],
|
|
1149
|
+
message: "Rules are exported with 'trigger: manual' and require explicit @mention to invoke"
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
if (state.settings?.mcpServers && Object.keys(state.settings.mcpServers).length > 0) {
|
|
1153
|
+
skipped.push({
|
|
1154
|
+
feature: "mcpServers",
|
|
1155
|
+
reason: "Windsurf uses global MCP config at ~/.codeium/windsurf/mcp_config.json - project-level MCP servers are not exported"
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
if (state.settings?.permissions) {
|
|
1159
|
+
const hasPermissions = (state.settings.permissions.allow?.length ?? 0) > 0 || (state.settings.permissions.ask?.length ?? 0) > 0 || (state.settings.permissions.deny?.length ?? 0) > 0;
|
|
1160
|
+
if (hasPermissions) {
|
|
1161
|
+
skipped.push({
|
|
1162
|
+
feature: "permissions",
|
|
1163
|
+
reason: "Windsurf does not support declarative permissions"
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
return { valid: true, errors: [], warnings, skipped };
|
|
1168
|
+
}
|
|
1169
|
+
};
|
|
1170
|
+
|
|
947
1171
|
// src/plugins/index.ts
|
|
948
1172
|
pluginRegistry.register(claudeCodePlugin);
|
|
1173
|
+
pluginRegistry.register(copilotPlugin);
|
|
949
1174
|
pluginRegistry.register(cursorPlugin);
|
|
950
1175
|
pluginRegistry.register(opencodePlugin);
|
|
1176
|
+
pluginRegistry.register(windsurfPlugin);
|
|
951
1177
|
function computeHash(content) {
|
|
952
1178
|
return crypto.createHash("sha256").update(content, "utf-8").digest("hex");
|
|
953
1179
|
}
|
|
@@ -1060,9 +1286,8 @@ async function updateGitignore(rootDir, paths) {
|
|
|
1060
1286
|
const markerRegex = new RegExp(`${marker}[\\s\\S]*?${endMarker}\\n?`, "g");
|
|
1061
1287
|
content = content.replace(markerRegex, "");
|
|
1062
1288
|
content = content.trimEnd();
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1065
|
-
);
|
|
1289
|
+
const uniquePaths = [...new Set(paths)];
|
|
1290
|
+
const newSection = ["", marker, ...uniquePaths, endMarker, ""].join("\n");
|
|
1066
1291
|
content = content + newSection;
|
|
1067
1292
|
await fs3.writeFile(gitignorePath, content, "utf-8");
|
|
1068
1293
|
}
|
|
@@ -1138,10 +1363,16 @@ async function runSyncPipeline(options) {
|
|
|
1138
1363
|
return results;
|
|
1139
1364
|
}
|
|
1140
1365
|
async function initUnifiedConfig(options) {
|
|
1141
|
-
const {
|
|
1366
|
+
const {
|
|
1367
|
+
rootDir,
|
|
1368
|
+
tools,
|
|
1369
|
+
minimal = false,
|
|
1370
|
+
force = false,
|
|
1371
|
+
versionControl
|
|
1372
|
+
} = options;
|
|
1142
1373
|
const aiDir = path.join(rootDir, UNIFIED_DIR);
|
|
1143
1374
|
const created = [];
|
|
1144
|
-
const config = generateDefaultConfig(tools);
|
|
1375
|
+
const config = generateDefaultConfig(tools, versionControl);
|
|
1145
1376
|
const exists = await hasUnifiedConfig(rootDir);
|
|
1146
1377
|
if (exists && !force) {
|
|
1147
1378
|
throw new ValidationError(
|
|
@@ -1183,7 +1414,7 @@ async function hasUnifiedConfig(rootDir) {
|
|
|
1183
1414
|
return false;
|
|
1184
1415
|
}
|
|
1185
1416
|
}
|
|
1186
|
-
function generateDefaultConfig(tools) {
|
|
1417
|
+
function generateDefaultConfig(tools, versionControl) {
|
|
1187
1418
|
if (tools) {
|
|
1188
1419
|
const validation = validateToolIds(tools);
|
|
1189
1420
|
if (!validation.valid) {
|
|
@@ -1200,7 +1431,7 @@ function generateDefaultConfig(tools) {
|
|
|
1200
1431
|
for (const toolId of TOOL_IDS) {
|
|
1201
1432
|
toolsConfig[toolId] = {
|
|
1202
1433
|
enabled: enabledTools.includes(toolId),
|
|
1203
|
-
versionControl: false
|
|
1434
|
+
versionControl: versionControl?.[toolId] ?? false
|
|
1204
1435
|
};
|
|
1205
1436
|
}
|
|
1206
1437
|
return { tools: toolsConfig };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lnai/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Core library for LNAI - unified AI config management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
"claude",
|
|
22
22
|
"cursor",
|
|
23
23
|
"opencode",
|
|
24
|
+
"copilot",
|
|
25
|
+
"github-copilot",
|
|
26
|
+
"windsurf",
|
|
24
27
|
"cli",
|
|
25
28
|
"ai-tools"
|
|
26
29
|
],
|
|
@@ -34,7 +37,6 @@
|
|
|
34
37
|
"dist"
|
|
35
38
|
],
|
|
36
39
|
"dependencies": {
|
|
37
|
-
"deepmerge": "^4.3.1",
|
|
38
40
|
"gray-matter": "^4.0.3",
|
|
39
41
|
"zod": "^4.3.6"
|
|
40
42
|
},
|