@lnai/core 0.2.0 → 0.3.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 +6 -7
- package/dist/index.js +244 -138
- package/package.json +3 -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"];
|
|
5
5
|
type ToolId = (typeof TOOL_IDS)[number];
|
|
6
6
|
declare const CONFIG_FILES: {
|
|
7
7
|
readonly config: "config.json";
|
|
@@ -66,6 +66,7 @@ declare const toolIdSchema: z.ZodEnum<{
|
|
|
66
66
|
claudeCode: "claudeCode";
|
|
67
67
|
opencode: "opencode";
|
|
68
68
|
cursor: "cursor";
|
|
69
|
+
copilot: "copilot";
|
|
69
70
|
}>;
|
|
70
71
|
/** Settings configuration (Claude format as source of truth) */
|
|
71
72
|
declare const settingsSchema: z.ZodObject<{
|
|
@@ -85,11 +86,6 @@ declare const settingsSchema: z.ZodObject<{
|
|
|
85
86
|
url: z.ZodOptional<z.ZodString>;
|
|
86
87
|
headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
87
88
|
}, 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
89
|
}, z.core.$strip>;
|
|
94
90
|
/** Main config.json structure. Uses partial object to allow partial tool configs. */
|
|
95
91
|
declare const configSchema: z.ZodObject<{
|
|
@@ -106,6 +102,10 @@ declare const configSchema: z.ZodObject<{
|
|
|
106
102
|
enabled: z.ZodBoolean;
|
|
107
103
|
versionControl: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
108
104
|
}, z.core.$strip>>;
|
|
105
|
+
copilot: z.ZodOptional<z.ZodObject<{
|
|
106
|
+
enabled: z.ZodBoolean;
|
|
107
|
+
versionControl: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
108
|
+
}, z.core.$strip>>;
|
|
109
109
|
}, z.core.$strip>>;
|
|
110
110
|
}, z.core.$strip>;
|
|
111
111
|
/** Skill frontmatter (name and description required) */
|
|
@@ -145,7 +145,6 @@ interface UnifiedState {
|
|
|
145
145
|
settings: {
|
|
146
146
|
permissions?: Permissions;
|
|
147
147
|
mcpServers?: Record<string, McpServer>;
|
|
148
|
-
overrides?: Partial<Record<ToolId, Record<string, unknown>>>;
|
|
149
148
|
} | null;
|
|
150
149
|
agents: string | null;
|
|
151
150
|
rules: MarkdownFile<RuleFrontmatter>[];
|
package/dist/index.js
CHANGED
|
@@ -2,12 +2,11 @@ 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 = ["claudeCode", "opencode", "cursor"];
|
|
9
|
+
var TOOL_IDS = ["claudeCode", "opencode", "cursor", "copilot"];
|
|
11
10
|
var CONFIG_FILES = {
|
|
12
11
|
config: "config.json",
|
|
13
12
|
settings: "settings.json",
|
|
@@ -21,12 +20,14 @@ var CONFIG_DIRS = {
|
|
|
21
20
|
var TOOL_OUTPUT_DIRS = {
|
|
22
21
|
claudeCode: ".claude",
|
|
23
22
|
opencode: ".opencode",
|
|
24
|
-
cursor: ".cursor"
|
|
23
|
+
cursor: ".cursor",
|
|
24
|
+
copilot: ".github"
|
|
25
25
|
};
|
|
26
26
|
var OVERRIDE_DIRS = {
|
|
27
27
|
claudeCode: ".claude",
|
|
28
28
|
opencode: ".opencode",
|
|
29
|
-
cursor: ".cursor"
|
|
29
|
+
cursor: ".cursor",
|
|
30
|
+
copilot: ".copilot"
|
|
30
31
|
};
|
|
31
32
|
|
|
32
33
|
// src/errors.ts
|
|
@@ -106,21 +107,17 @@ var toolConfigSchema = z.object({
|
|
|
106
107
|
enabled: z.boolean(),
|
|
107
108
|
versionControl: z.boolean().optional().default(false)
|
|
108
109
|
});
|
|
109
|
-
var toolIdSchema = z.enum(["claudeCode", "opencode", "cursor"]);
|
|
110
|
+
var toolIdSchema = z.enum(["claudeCode", "opencode", "cursor", "copilot"]);
|
|
110
111
|
var settingsSchema = z.object({
|
|
111
112
|
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()
|
|
113
|
+
mcpServers: z.record(z.string(), mcpServerSchema).optional()
|
|
118
114
|
});
|
|
119
115
|
var configSchema = z.object({
|
|
120
116
|
tools: z.object({
|
|
121
117
|
claudeCode: toolConfigSchema,
|
|
122
118
|
opencode: toolConfigSchema,
|
|
123
|
-
cursor: toolConfigSchema
|
|
119
|
+
cursor: toolConfigSchema,
|
|
120
|
+
copilot: toolConfigSchema
|
|
124
121
|
}).partial().optional()
|
|
125
122
|
});
|
|
126
123
|
var skillFrontmatterSchema = z.object({
|
|
@@ -403,38 +400,24 @@ async function scanDir(baseDir, currentDir, files) {
|
|
|
403
400
|
}
|
|
404
401
|
}
|
|
405
402
|
}
|
|
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) {
|
|
403
|
+
async function applyFileOverrides(files, rootDir, toolId) {
|
|
420
404
|
const outputDir = TOOL_OUTPUT_DIRS[toolId];
|
|
421
405
|
const overrideFiles = await scanOverrideDirectory(rootDir, toolId);
|
|
422
|
-
const
|
|
406
|
+
const overridePaths = /* @__PURE__ */ new Set();
|
|
407
|
+
const overrideOutputFiles = [];
|
|
423
408
|
for (const overrideFile of overrideFiles) {
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
}
|
|
428
|
-
const symlinkPath = `${outputDir}/${overrideFile.relativePath}`;
|
|
429
|
-
const symlinkDir = path.dirname(symlinkPath);
|
|
409
|
+
const outputPath = `${outputDir}/${overrideFile.relativePath}`;
|
|
410
|
+
overridePaths.add(outputPath);
|
|
411
|
+
const symlinkDir = path.dirname(outputPath);
|
|
430
412
|
const sourcePath = `${UNIFIED_DIR}/${OVERRIDE_DIRS[toolId]}/${overrideFile.relativePath}`;
|
|
431
|
-
|
|
432
|
-
path:
|
|
413
|
+
overrideOutputFiles.push({
|
|
414
|
+
path: outputPath,
|
|
433
415
|
type: "symlink",
|
|
434
416
|
target: path.relative(symlinkDir, sourcePath)
|
|
435
417
|
});
|
|
436
418
|
}
|
|
437
|
-
|
|
419
|
+
const filteredFiles = files.filter((file) => !overridePaths.has(file.path));
|
|
420
|
+
return [...filteredFiles, ...overrideOutputFiles];
|
|
438
421
|
}
|
|
439
422
|
|
|
440
423
|
// src/plugins/claude-code/index.ts
|
|
@@ -471,30 +454,21 @@ var claudeCodePlugin = {
|
|
|
471
454
|
target: `../../${UNIFIED_DIR}/skills/${skill.path}`
|
|
472
455
|
});
|
|
473
456
|
}
|
|
474
|
-
const
|
|
457
|
+
const settings = {};
|
|
475
458
|
if (state.settings?.permissions) {
|
|
476
|
-
|
|
459
|
+
settings["permissions"] = state.settings.permissions;
|
|
477
460
|
}
|
|
478
461
|
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
|
-
);
|
|
462
|
+
settings["mcpServers"] = state.settings.mcpServers;
|
|
487
463
|
}
|
|
488
|
-
if (Object.keys(
|
|
464
|
+
if (Object.keys(settings).length > 0) {
|
|
489
465
|
files.push({
|
|
490
466
|
path: `${outputDir}/settings.json`,
|
|
491
467
|
type: "json",
|
|
492
|
-
content:
|
|
468
|
+
content: settings
|
|
493
469
|
});
|
|
494
470
|
}
|
|
495
|
-
|
|
496
|
-
files.push(...overrideFiles);
|
|
497
|
-
return files;
|
|
471
|
+
return applyFileOverrides(files, rootDir, "claudeCode");
|
|
498
472
|
},
|
|
499
473
|
validate(state) {
|
|
500
474
|
const warnings = [];
|
|
@@ -508,6 +482,197 @@ var claudeCodePlugin = {
|
|
|
508
482
|
}
|
|
509
483
|
};
|
|
510
484
|
|
|
485
|
+
// src/utils/transforms.ts
|
|
486
|
+
var ENV_VAR_PATTERN = /\$\{([^}:]+)(:-[^}]*)?\}/g;
|
|
487
|
+
function transformEnvVar(value, format) {
|
|
488
|
+
if (format === "opencode") {
|
|
489
|
+
return value.replace(ENV_VAR_PATTERN, "{env:$1}");
|
|
490
|
+
}
|
|
491
|
+
return value.replace(ENV_VAR_PATTERN, "${env:$1}");
|
|
492
|
+
}
|
|
493
|
+
function transformEnvVars(env, format) {
|
|
494
|
+
const result = {};
|
|
495
|
+
for (const [key, value] of Object.entries(env)) {
|
|
496
|
+
result[key] = transformEnvVar(value, format);
|
|
497
|
+
}
|
|
498
|
+
return result;
|
|
499
|
+
}
|
|
500
|
+
function parsePermissionRule(rule) {
|
|
501
|
+
const match = rule.match(/^(\w+)\(([^)]+)\)$/);
|
|
502
|
+
if (!match) {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
const tool = match[1];
|
|
506
|
+
const pattern = match[2];
|
|
507
|
+
if (!tool || !pattern) {
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
return { tool, pattern };
|
|
511
|
+
}
|
|
512
|
+
function deriveDescription(filename, content) {
|
|
513
|
+
const headingMatch = content.match(/^#\s+(.+)$/m);
|
|
514
|
+
if (headingMatch && headingMatch[1]) {
|
|
515
|
+
return headingMatch[1];
|
|
516
|
+
}
|
|
517
|
+
const baseName = filename.replace(/\.md$/, "");
|
|
518
|
+
return baseName.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// src/plugins/copilot/transforms.ts
|
|
522
|
+
function transformRuleToCopilot(rule) {
|
|
523
|
+
const description = deriveDescription(rule.path, rule.content);
|
|
524
|
+
const paths = rule.frontmatter.paths || [];
|
|
525
|
+
const frontmatter = {
|
|
526
|
+
description
|
|
527
|
+
};
|
|
528
|
+
if (paths.length > 0) {
|
|
529
|
+
frontmatter.applyTo = paths.join(",");
|
|
530
|
+
}
|
|
531
|
+
return {
|
|
532
|
+
frontmatter,
|
|
533
|
+
content: rule.content
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
function serializeCopilotInstruction(frontmatter, content) {
|
|
537
|
+
const lines = ["---"];
|
|
538
|
+
if (frontmatter.applyTo) {
|
|
539
|
+
lines.push(`applyTo: ${JSON.stringify(frontmatter.applyTo)}`);
|
|
540
|
+
}
|
|
541
|
+
lines.push(`description: ${JSON.stringify(frontmatter.description)}`);
|
|
542
|
+
lines.push("---");
|
|
543
|
+
lines.push("");
|
|
544
|
+
lines.push(content);
|
|
545
|
+
return lines.join("\n");
|
|
546
|
+
}
|
|
547
|
+
function transformMcpToCopilot(servers) {
|
|
548
|
+
if (!servers || Object.keys(servers).length === 0) {
|
|
549
|
+
return void 0;
|
|
550
|
+
}
|
|
551
|
+
const result = {};
|
|
552
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
553
|
+
if (server.type === "http" || server.type === "sse") {
|
|
554
|
+
if (!server.url) {
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
const copilotServer = {
|
|
558
|
+
url: server.url
|
|
559
|
+
};
|
|
560
|
+
if (server.headers && Object.keys(server.headers).length > 0) {
|
|
561
|
+
copilotServer.requestInit = {
|
|
562
|
+
headers: transformEnvVars(server.headers, "copilot")
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
result[name] = copilotServer;
|
|
566
|
+
} else if (server.command) {
|
|
567
|
+
const copilotServer = {
|
|
568
|
+
type: "stdio",
|
|
569
|
+
command: server.command
|
|
570
|
+
};
|
|
571
|
+
if (server.args && server.args.length > 0) {
|
|
572
|
+
copilotServer.args = server.args;
|
|
573
|
+
}
|
|
574
|
+
if (server.env && Object.keys(server.env).length > 0) {
|
|
575
|
+
copilotServer.env = transformEnvVars(server.env, "copilot");
|
|
576
|
+
}
|
|
577
|
+
result[name] = copilotServer;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if (Object.keys(result).length === 0) {
|
|
581
|
+
return void 0;
|
|
582
|
+
}
|
|
583
|
+
return {
|
|
584
|
+
inputs: [],
|
|
585
|
+
servers: result
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// src/plugins/copilot/index.ts
|
|
590
|
+
var copilotPlugin = {
|
|
591
|
+
id: "copilot",
|
|
592
|
+
name: "GitHub Copilot",
|
|
593
|
+
async detect(_rootDir) {
|
|
594
|
+
return false;
|
|
595
|
+
},
|
|
596
|
+
async import(_rootDir) {
|
|
597
|
+
return null;
|
|
598
|
+
},
|
|
599
|
+
async export(state, rootDir) {
|
|
600
|
+
const files = [];
|
|
601
|
+
if (state.agents) {
|
|
602
|
+
files.push({
|
|
603
|
+
path: ".github/copilot-instructions.md",
|
|
604
|
+
type: "symlink",
|
|
605
|
+
target: `../${UNIFIED_DIR}/AGENTS.md`
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
for (const rule of state.rules) {
|
|
609
|
+
const transformed = transformRuleToCopilot(rule);
|
|
610
|
+
const ruleContent = serializeCopilotInstruction(
|
|
611
|
+
transformed.frontmatter,
|
|
612
|
+
transformed.content
|
|
613
|
+
);
|
|
614
|
+
const outputFilename = rule.path.replace(/\.md$/, ".instructions.md");
|
|
615
|
+
files.push({
|
|
616
|
+
path: `.github/instructions/${outputFilename}`,
|
|
617
|
+
type: "text",
|
|
618
|
+
content: ruleContent
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
for (const skill of state.skills) {
|
|
622
|
+
files.push({
|
|
623
|
+
path: `.github/skills/${skill.path}`,
|
|
624
|
+
type: "symlink",
|
|
625
|
+
target: `../../${UNIFIED_DIR}/skills/${skill.path}`
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
const mcpConfig = transformMcpToCopilot(state.settings?.mcpServers);
|
|
629
|
+
if (mcpConfig) {
|
|
630
|
+
files.push({
|
|
631
|
+
path: ".vscode/mcp.json",
|
|
632
|
+
type: "json",
|
|
633
|
+
content: { inputs: mcpConfig.inputs, servers: mcpConfig.servers }
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
return applyFileOverrides(files, rootDir, "copilot");
|
|
637
|
+
},
|
|
638
|
+
validate(state) {
|
|
639
|
+
const warnings = [];
|
|
640
|
+
if (!state.agents) {
|
|
641
|
+
warnings.push({
|
|
642
|
+
path: ["AGENTS.md"],
|
|
643
|
+
message: "No AGENTS.md found - .github/copilot-instructions.md will not be created"
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
const permissions = state.settings?.permissions;
|
|
647
|
+
const hasPermissions = permissions && (permissions.allow && permissions.allow.length > 0 || permissions.ask && permissions.ask.length > 0 || permissions.deny && permissions.deny.length > 0);
|
|
648
|
+
if (hasPermissions) {
|
|
649
|
+
warnings.push({
|
|
650
|
+
path: ["settings", "permissions"],
|
|
651
|
+
message: "GitHub Copilot does not support permissions - they will be ignored"
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
const mcpServers = state.settings?.mcpServers;
|
|
655
|
+
if (mcpServers) {
|
|
656
|
+
for (const [name, server] of Object.entries(mcpServers)) {
|
|
657
|
+
const isRemote = server.type === "http" || server.type === "sse";
|
|
658
|
+
const hasCommand = !!server.command;
|
|
659
|
+
if (isRemote && !server.url) {
|
|
660
|
+
warnings.push({
|
|
661
|
+
path: ["settings", "mcpServers", name],
|
|
662
|
+
message: `MCP server "${name}" is type "${server.type}" but has no url - it will be skipped`
|
|
663
|
+
});
|
|
664
|
+
} else if (!isRemote && !hasCommand) {
|
|
665
|
+
warnings.push({
|
|
666
|
+
path: ["settings", "mcpServers", name],
|
|
667
|
+
message: `MCP server "${name}" has no command or type - it will be skipped`
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return { valid: true, errors: [], warnings, skipped: [] };
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
|
|
511
676
|
// src/plugins/cursor/transforms.ts
|
|
512
677
|
function transformRuleToCursor(rule) {
|
|
513
678
|
const description = deriveDescription(rule.path, rule.content);
|
|
@@ -522,14 +687,6 @@ function transformRuleToCursor(rule) {
|
|
|
522
687
|
content: rule.content
|
|
523
688
|
};
|
|
524
689
|
}
|
|
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
690
|
function serializeCursorRule(frontmatter, content) {
|
|
534
691
|
const lines = [
|
|
535
692
|
"---",
|
|
@@ -556,7 +713,7 @@ function transformMcpToCursor(servers) {
|
|
|
556
713
|
url: server.url
|
|
557
714
|
};
|
|
558
715
|
if (server.headers) {
|
|
559
|
-
cursorServer.headers =
|
|
716
|
+
cursorServer.headers = transformEnvVars(server.headers, "cursor");
|
|
560
717
|
}
|
|
561
718
|
result[name] = cursorServer;
|
|
562
719
|
} else if (server.command) {
|
|
@@ -567,23 +724,13 @@ function transformMcpToCursor(servers) {
|
|
|
567
724
|
cursorServer.args = server.args;
|
|
568
725
|
}
|
|
569
726
|
if (server.env) {
|
|
570
|
-
cursorServer.env =
|
|
727
|
+
cursorServer.env = transformEnvVars(server.env, "cursor");
|
|
571
728
|
}
|
|
572
729
|
result[name] = cursorServer;
|
|
573
730
|
}
|
|
574
731
|
}
|
|
575
732
|
return Object.keys(result).length > 0 ? result : void 0;
|
|
576
733
|
}
|
|
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
734
|
function transformPermissionsToCursor(permissions) {
|
|
588
735
|
if (!permissions) {
|
|
589
736
|
return { permissions: void 0, hasAskPermissions: false };
|
|
@@ -682,47 +829,22 @@ var cursorPlugin = {
|
|
|
682
829
|
});
|
|
683
830
|
}
|
|
684
831
|
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
|
-
}
|
|
832
|
+
if (mcpServers) {
|
|
694
833
|
files.push({
|
|
695
834
|
path: `${outputDir}/mcp.json`,
|
|
696
835
|
type: "json",
|
|
697
|
-
content:
|
|
836
|
+
content: { mcpServers }
|
|
698
837
|
});
|
|
699
838
|
}
|
|
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
|
-
}
|
|
839
|
+
const cliContent = buildCliContent(state.settings?.permissions);
|
|
840
|
+
if (cliContent) {
|
|
717
841
|
files.push({
|
|
718
842
|
path: `${outputDir}/cli.json`,
|
|
719
843
|
type: "json",
|
|
720
844
|
content: cliContent
|
|
721
845
|
});
|
|
722
846
|
}
|
|
723
|
-
|
|
724
|
-
files.push(...overrideFiles);
|
|
725
|
-
return files;
|
|
847
|
+
return applyFileOverrides(files, rootDir, "cursor");
|
|
726
848
|
},
|
|
727
849
|
validate(state) {
|
|
728
850
|
const warnings = [];
|
|
@@ -757,6 +879,13 @@ var cursorPlugin = {
|
|
|
757
879
|
return { valid: true, errors: [], warnings, skipped: [] };
|
|
758
880
|
}
|
|
759
881
|
};
|
|
882
|
+
function buildCliContent(permissions) {
|
|
883
|
+
const permissionsResult = transformPermissionsToCursor(permissions);
|
|
884
|
+
if (!permissionsResult.permissions) {
|
|
885
|
+
return void 0;
|
|
886
|
+
}
|
|
887
|
+
return { permissions: permissionsResult.permissions };
|
|
888
|
+
}
|
|
760
889
|
|
|
761
890
|
// src/plugins/opencode/transforms.ts
|
|
762
891
|
function transformMcpToOpenCode(servers) {
|
|
@@ -781,7 +910,7 @@ function transformMcpToOpenCode(servers) {
|
|
|
781
910
|
command
|
|
782
911
|
};
|
|
783
912
|
if (server.env) {
|
|
784
|
-
openCodeServer.environment = transformEnvVars(server.env);
|
|
913
|
+
openCodeServer.environment = transformEnvVars(server.env, "opencode");
|
|
785
914
|
}
|
|
786
915
|
result[name] = openCodeServer;
|
|
787
916
|
}
|
|
@@ -798,7 +927,7 @@ function transformPermissionsToOpenCode(permissions) {
|
|
|
798
927
|
return;
|
|
799
928
|
}
|
|
800
929
|
for (const rule of rules) {
|
|
801
|
-
const parsed =
|
|
930
|
+
const parsed = parsePermissionRuleForOpenCode(rule);
|
|
802
931
|
if (!parsed) {
|
|
803
932
|
continue;
|
|
804
933
|
}
|
|
@@ -814,28 +943,13 @@ function transformPermissionsToOpenCode(permissions) {
|
|
|
814
943
|
processRules(permissions.deny, "deny");
|
|
815
944
|
return Object.keys(result).length > 0 ? result : void 0;
|
|
816
945
|
}
|
|
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) {
|
|
946
|
+
function parsePermissionRuleForOpenCode(rule) {
|
|
947
|
+
const parsed = parsePermissionRule(rule);
|
|
948
|
+
if (!parsed) {
|
|
830
949
|
return null;
|
|
831
950
|
}
|
|
832
|
-
const
|
|
833
|
-
|
|
834
|
-
if (!tool || !pattern) {
|
|
835
|
-
return null;
|
|
836
|
-
}
|
|
837
|
-
const normalizedTool = tool.toLowerCase();
|
|
838
|
-
let normalizedPattern = pattern;
|
|
951
|
+
const normalizedTool = parsed.tool.toLowerCase();
|
|
952
|
+
let normalizedPattern = parsed.pattern;
|
|
839
953
|
if (normalizedPattern.includes(":*")) {
|
|
840
954
|
normalizedPattern = normalizedPattern.replace(/:(\*)/g, " $1");
|
|
841
955
|
}
|
|
@@ -879,37 +993,28 @@ var opencodePlugin = {
|
|
|
879
993
|
target: `../../${UNIFIED_DIR}/skills/${skill.path}`
|
|
880
994
|
});
|
|
881
995
|
}
|
|
882
|
-
const
|
|
996
|
+
const config = {
|
|
883
997
|
$schema: "https://opencode.ai/config.json"
|
|
884
998
|
};
|
|
885
999
|
if (state.rules.length > 0) {
|
|
886
|
-
|
|
1000
|
+
config["instructions"] = [`${outputDir}/rules/*.md`];
|
|
887
1001
|
}
|
|
888
1002
|
const mcp = transformMcpToOpenCode(state.settings?.mcpServers);
|
|
889
1003
|
if (mcp) {
|
|
890
|
-
|
|
1004
|
+
config["mcp"] = mcp;
|
|
891
1005
|
}
|
|
892
1006
|
const permission = transformPermissionsToOpenCode(
|
|
893
1007
|
state.settings?.permissions
|
|
894
1008
|
);
|
|
895
1009
|
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
|
-
);
|
|
1010
|
+
config["permission"] = permission;
|
|
904
1011
|
}
|
|
905
1012
|
files.push({
|
|
906
1013
|
path: "opencode.json",
|
|
907
1014
|
type: "json",
|
|
908
|
-
content:
|
|
1015
|
+
content: config
|
|
909
1016
|
});
|
|
910
|
-
|
|
911
|
-
files.push(...overrideFiles);
|
|
912
|
-
return files;
|
|
1017
|
+
return applyFileOverrides(files, rootDir, "opencode");
|
|
913
1018
|
},
|
|
914
1019
|
validate(state) {
|
|
915
1020
|
const warnings = [];
|
|
@@ -946,6 +1051,7 @@ var pluginRegistry = new PluginRegistry();
|
|
|
946
1051
|
|
|
947
1052
|
// src/plugins/index.ts
|
|
948
1053
|
pluginRegistry.register(claudeCodePlugin);
|
|
1054
|
+
pluginRegistry.register(copilotPlugin);
|
|
949
1055
|
pluginRegistry.register(cursorPlugin);
|
|
950
1056
|
pluginRegistry.register(opencodePlugin);
|
|
951
1057
|
function computeHash(content) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lnai/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Core library for LNAI - unified AI config management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
"claude",
|
|
22
22
|
"cursor",
|
|
23
23
|
"opencode",
|
|
24
|
+
"copilot",
|
|
25
|
+
"github-copilot",
|
|
24
26
|
"cli",
|
|
25
27
|
"ai-tools"
|
|
26
28
|
],
|
|
@@ -34,7 +36,6 @@
|
|
|
34
36
|
"dist"
|
|
35
37
|
],
|
|
36
38
|
"dependencies": {
|
|
37
|
-
"deepmerge": "^4.3.1",
|
|
38
39
|
"gray-matter": "^4.0.3",
|
|
39
40
|
"zod": "^4.3.6"
|
|
40
41
|
},
|