@lnai/core 0.1.1 → 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.
Files changed (3) hide show
  1. package/dist/index.d.ts +14 -16
  2. package/dist/index.js +453 -101
  3. 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"];
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";
@@ -65,6 +65,8 @@ declare const toolConfigSchema: z.ZodObject<{
65
65
  declare const toolIdSchema: z.ZodEnum<{
66
66
  claudeCode: "claudeCode";
67
67
  opencode: "opencode";
68
+ cursor: "cursor";
69
+ copilot: "copilot";
68
70
  }>;
69
71
  /** Settings configuration (Claude format as source of truth) */
70
72
  declare const settingsSchema: z.ZodObject<{
@@ -84,10 +86,6 @@ declare const settingsSchema: z.ZodObject<{
84
86
  url: z.ZodOptional<z.ZodString>;
85
87
  headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
86
88
  }, z.core.$strip>>>;
87
- overrides: z.ZodOptional<z.ZodObject<{
88
- claudeCode: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
89
- opencode: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
90
- }, z.core.$strip>>;
91
89
  }, z.core.$strip>;
92
90
  /** Main config.json structure. Uses partial object to allow partial tool configs. */
93
91
  declare const configSchema: z.ZodObject<{
@@ -100,6 +98,14 @@ declare const configSchema: z.ZodObject<{
100
98
  enabled: z.ZodBoolean;
101
99
  versionControl: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
102
100
  }, z.core.$strip>>;
101
+ cursor: z.ZodOptional<z.ZodObject<{
102
+ enabled: z.ZodBoolean;
103
+ versionControl: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
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>>;
103
109
  }, z.core.$strip>>;
104
110
  }, z.core.$strip>;
105
111
  /** Skill frontmatter (name and description required) */
@@ -134,19 +140,11 @@ interface MarkdownFrontmatter {
134
140
  /** All parsed configuration from the .ai directory */
135
141
  interface UnifiedState {
136
142
  config: {
137
- tools?: Partial<Record<ToolId, {
138
- enabled: boolean;
139
- versionControl?: boolean;
140
- }>>;
143
+ tools?: Partial<Record<ToolId, ToolConfig>>;
141
144
  };
142
145
  settings: {
143
- permissions?: {
144
- allow?: string[];
145
- ask?: string[];
146
- deny?: string[];
147
- };
148
- mcpServers?: Record<string, unknown>;
149
- overrides?: Partial<Record<ToolId, Record<string, unknown>>>;
146
+ permissions?: Permissions;
147
+ mcpServers?: Record<string, McpServer>;
150
148
  } | null;
151
149
  agents: string | null;
152
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"];
9
+ var TOOL_IDS = ["claudeCode", "opencode", "cursor", "copilot"];
11
10
  var CONFIG_FILES = {
12
11
  config: "config.json",
13
12
  settings: "settings.json",
@@ -20,11 +19,15 @@ var CONFIG_DIRS = {
20
19
  };
21
20
  var TOOL_OUTPUT_DIRS = {
22
21
  claudeCode: ".claude",
23
- opencode: ".opencode"
22
+ opencode: ".opencode",
23
+ cursor: ".cursor",
24
+ copilot: ".github"
24
25
  };
25
26
  var OVERRIDE_DIRS = {
26
27
  claudeCode: ".claude",
27
- opencode: ".opencode"
28
+ opencode: ".opencode",
29
+ cursor: ".cursor",
30
+ copilot: ".copilot"
28
31
  };
29
32
 
30
33
  // src/errors.ts
@@ -50,10 +53,10 @@ var ParseError = class extends LnaiError {
50
53
  var ValidationError = class extends LnaiError {
51
54
  path;
52
55
  value;
53
- constructor(message, path7, value) {
56
+ constructor(message, path5, value) {
54
57
  super(message, "VALIDATION_ERROR");
55
58
  this.name = "ValidationError";
56
- this.path = path7;
59
+ this.path = path5;
57
60
  this.value = value;
58
61
  }
59
62
  };
@@ -104,19 +107,17 @@ var toolConfigSchema = z.object({
104
107
  enabled: z.boolean(),
105
108
  versionControl: z.boolean().optional().default(false)
106
109
  });
107
- var toolIdSchema = z.enum(["claudeCode", "opencode"]);
110
+ var toolIdSchema = z.enum(["claudeCode", "opencode", "cursor", "copilot"]);
108
111
  var settingsSchema = z.object({
109
112
  permissions: permissionsSchema.optional(),
110
- mcpServers: z.record(z.string(), mcpServerSchema).optional(),
111
- overrides: z.object({
112
- claudeCode: z.record(z.string(), z.unknown()).optional(),
113
- opencode: z.record(z.string(), z.unknown()).optional()
114
- }).optional()
113
+ mcpServers: z.record(z.string(), mcpServerSchema).optional()
115
114
  });
116
115
  var configSchema = z.object({
117
116
  tools: z.object({
118
117
  claudeCode: toolConfigSchema,
119
- opencode: toolConfigSchema
118
+ opencode: toolConfigSchema,
119
+ cursor: toolConfigSchema,
120
+ copilot: toolConfigSchema
120
121
  }).partial().optional()
121
122
  });
122
123
  var skillFrontmatterSchema = z.object({
@@ -399,18 +400,24 @@ async function scanDir(baseDir, currentDir, files) {
399
400
  }
400
401
  }
401
402
  }
402
- function deepMergeConfigs(base, override) {
403
- return deepmerge(base, override, {
404
- arrayMerge: (target, source) => [.../* @__PURE__ */ new Set([...target, ...source])]
405
- });
406
- }
407
- async function fileExists2(filePath) {
408
- try {
409
- await fs3.access(filePath);
410
- return true;
411
- } catch {
412
- return false;
403
+ async function applyFileOverrides(files, rootDir, toolId) {
404
+ const outputDir = TOOL_OUTPUT_DIRS[toolId];
405
+ const overrideFiles = await scanOverrideDirectory(rootDir, toolId);
406
+ const overridePaths = /* @__PURE__ */ new Set();
407
+ const overrideOutputFiles = [];
408
+ for (const overrideFile of overrideFiles) {
409
+ const outputPath = `${outputDir}/${overrideFile.relativePath}`;
410
+ overridePaths.add(outputPath);
411
+ const symlinkDir = path.dirname(outputPath);
412
+ const sourcePath = `${UNIFIED_DIR}/${OVERRIDE_DIRS[toolId]}/${overrideFile.relativePath}`;
413
+ overrideOutputFiles.push({
414
+ path: outputPath,
415
+ type: "symlink",
416
+ target: path.relative(symlinkDir, sourcePath)
417
+ });
413
418
  }
419
+ const filteredFiles = files.filter((file) => !overridePaths.has(file.path));
420
+ return [...filteredFiles, ...overrideOutputFiles];
414
421
  }
415
422
 
416
423
  // src/plugins/claude-code/index.ts
@@ -447,65 +454,446 @@ var claudeCodePlugin = {
447
454
  target: `../../${UNIFIED_DIR}/skills/${skill.path}`
448
455
  });
449
456
  }
450
- const baseSettings = {};
457
+ const settings = {};
451
458
  if (state.settings?.permissions) {
452
- baseSettings["permissions"] = state.settings.permissions;
459
+ settings["permissions"] = state.settings.permissions;
453
460
  }
454
461
  if (state.settings?.mcpServers) {
455
- baseSettings["mcpServers"] = state.settings.mcpServers;
462
+ settings["mcpServers"] = state.settings.mcpServers;
456
463
  }
457
- let finalSettings = baseSettings;
458
- if (state.settings?.overrides?.claudeCode) {
459
- finalSettings = deepMergeConfigs(
460
- baseSettings,
461
- state.settings.overrides.claudeCode
462
- );
463
- }
464
- if (Object.keys(finalSettings).length > 0) {
464
+ if (Object.keys(settings).length > 0) {
465
465
  files.push({
466
466
  path: `${outputDir}/settings.json`,
467
467
  type: "json",
468
- content: finalSettings
468
+ content: settings
469
469
  });
470
470
  }
471
- const overrideFiles = await scanOverrideDirectory(rootDir, "claudeCode");
472
- for (const overrideFile of overrideFiles) {
473
- const targetPath = path.join(
474
- rootDir,
475
- outputDir,
476
- overrideFile.relativePath
477
- );
478
- if (await fileExists2(targetPath)) {
471
+ return applyFileOverrides(files, rootDir, "claudeCode");
472
+ },
473
+ validate(state) {
474
+ const warnings = [];
475
+ if (!state.agents) {
476
+ warnings.push({
477
+ path: ["AGENTS.md"],
478
+ message: "No AGENTS.md found - .claude/CLAUDE.md will not be created"
479
+ });
480
+ }
481
+ return { valid: true, errors: [], warnings, skipped: [] };
482
+ }
483
+ };
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) {
479
555
  continue;
480
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) {
481
602
  files.push({
482
- path: `${outputDir}/${overrideFile.relativePath}`,
603
+ path: ".github/copilot-instructions.md",
483
604
  type: "symlink",
484
- target: `../${UNIFIED_DIR}/${OVERRIDE_DIRS.claudeCode}/${overrideFile.relativePath}`
605
+ target: `../${UNIFIED_DIR}/AGENTS.md`
485
606
  });
486
607
  }
487
- return files;
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");
488
637
  },
489
638
  validate(state) {
490
639
  const warnings = [];
491
640
  if (!state.agents) {
492
641
  warnings.push({
493
642
  path: ["AGENTS.md"],
494
- message: "No AGENTS.md found - .claude/CLAUDE.md will not be created"
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"
495
652
  });
496
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
+ }
497
672
  return { valid: true, errors: [], warnings, skipped: [] };
498
673
  }
499
674
  };
500
675
 
676
+ // src/plugins/cursor/transforms.ts
677
+ function transformRuleToCursor(rule) {
678
+ const description = deriveDescription(rule.path, rule.content);
679
+ const globs = rule.frontmatter.paths || [];
680
+ const alwaysApply = globs.length === 0;
681
+ return {
682
+ frontmatter: {
683
+ description,
684
+ globs,
685
+ alwaysApply
686
+ },
687
+ content: rule.content
688
+ };
689
+ }
690
+ function serializeCursorRule(frontmatter, content) {
691
+ const lines = [
692
+ "---",
693
+ `description: ${JSON.stringify(frontmatter.description)}`
694
+ ];
695
+ lines.push("globs:");
696
+ for (const glob of frontmatter.globs) {
697
+ lines.push(` - ${JSON.stringify(glob)}`);
698
+ }
699
+ lines.push(`alwaysApply: ${frontmatter.alwaysApply}`);
700
+ lines.push("---");
701
+ lines.push("");
702
+ lines.push(content);
703
+ return lines.join("\n");
704
+ }
705
+ function transformMcpToCursor(servers) {
706
+ if (!servers || Object.keys(servers).length === 0) {
707
+ return void 0;
708
+ }
709
+ const result = {};
710
+ for (const [name, server] of Object.entries(servers)) {
711
+ if (server.type === "http" || server.type === "sse") {
712
+ const cursorServer = {
713
+ url: server.url
714
+ };
715
+ if (server.headers) {
716
+ cursorServer.headers = transformEnvVars(server.headers, "cursor");
717
+ }
718
+ result[name] = cursorServer;
719
+ } else if (server.command) {
720
+ const cursorServer = {
721
+ command: server.command
722
+ };
723
+ if (server.args && server.args.length > 0) {
724
+ cursorServer.args = server.args;
725
+ }
726
+ if (server.env) {
727
+ cursorServer.env = transformEnvVars(server.env, "cursor");
728
+ }
729
+ result[name] = cursorServer;
730
+ }
731
+ }
732
+ return Object.keys(result).length > 0 ? result : void 0;
733
+ }
734
+ function transformPermissionsToCursor(permissions) {
735
+ if (!permissions) {
736
+ return { permissions: void 0, hasAskPermissions: false };
737
+ }
738
+ const allow = [];
739
+ const deny = [];
740
+ let hasAskPermissions = false;
741
+ if (permissions.allow) {
742
+ for (const rule of permissions.allow) {
743
+ const transformed = transformPermissionRule(rule);
744
+ if (transformed) {
745
+ allow.push(transformed);
746
+ }
747
+ }
748
+ }
749
+ if (permissions.ask && permissions.ask.length > 0) {
750
+ hasAskPermissions = true;
751
+ for (const rule of permissions.ask) {
752
+ const transformed = transformPermissionRule(rule);
753
+ if (transformed) {
754
+ allow.push(transformed);
755
+ }
756
+ }
757
+ }
758
+ if (permissions.deny) {
759
+ for (const rule of permissions.deny) {
760
+ const transformed = transformPermissionRule(rule);
761
+ if (transformed) {
762
+ deny.push(transformed);
763
+ }
764
+ }
765
+ }
766
+ if (allow.length === 0 && deny.length === 0) {
767
+ return { permissions: void 0, hasAskPermissions };
768
+ }
769
+ return {
770
+ permissions: {
771
+ allow,
772
+ deny
773
+ },
774
+ hasAskPermissions
775
+ };
776
+ }
777
+ function transformPermissionRule(rule) {
778
+ const match = rule.match(/^(\w+)\(([^)]+)\)$/);
779
+ if (!match) {
780
+ return null;
781
+ }
782
+ const tool = match[1];
783
+ let pattern = match[2];
784
+ const cursorTool = tool.toLowerCase() === "bash" ? "Shell" : tool;
785
+ if (pattern.endsWith(":*")) {
786
+ pattern = pattern.slice(0, -2);
787
+ }
788
+ return `${cursorTool}(${pattern})`;
789
+ }
790
+
791
+ // src/plugins/cursor/index.ts
792
+ var cursorPlugin = {
793
+ id: "cursor",
794
+ name: "Cursor",
795
+ async detect(_rootDir) {
796
+ return false;
797
+ },
798
+ async import(_rootDir) {
799
+ return null;
800
+ },
801
+ async export(state, rootDir) {
802
+ const files = [];
803
+ const outputDir = TOOL_OUTPUT_DIRS.cursor;
804
+ if (state.agents) {
805
+ files.push({
806
+ path: "AGENTS.md",
807
+ type: "symlink",
808
+ target: `${UNIFIED_DIR}/AGENTS.md`
809
+ });
810
+ }
811
+ for (const rule of state.rules) {
812
+ const transformed = transformRuleToCursor(rule);
813
+ const ruleContent = serializeCursorRule(
814
+ transformed.frontmatter,
815
+ transformed.content
816
+ );
817
+ const outputFilename = rule.path.replace(/\.md$/, ".mdc");
818
+ files.push({
819
+ path: `${outputDir}/rules/${outputFilename}`,
820
+ type: "text",
821
+ content: ruleContent
822
+ });
823
+ }
824
+ for (const skill of state.skills) {
825
+ files.push({
826
+ path: `${outputDir}/skills/${skill.path}`,
827
+ type: "symlink",
828
+ target: `../../${UNIFIED_DIR}/skills/${skill.path}`
829
+ });
830
+ }
831
+ const mcpServers = transformMcpToCursor(state.settings?.mcpServers);
832
+ if (mcpServers) {
833
+ files.push({
834
+ path: `${outputDir}/mcp.json`,
835
+ type: "json",
836
+ content: { mcpServers }
837
+ });
838
+ }
839
+ const cliContent = buildCliContent(state.settings?.permissions);
840
+ if (cliContent) {
841
+ files.push({
842
+ path: `${outputDir}/cli.json`,
843
+ type: "json",
844
+ content: cliContent
845
+ });
846
+ }
847
+ return applyFileOverrides(files, rootDir, "cursor");
848
+ },
849
+ validate(state) {
850
+ const warnings = [];
851
+ if (!state.agents) {
852
+ warnings.push({
853
+ path: ["AGENTS.md"],
854
+ message: "No AGENTS.md found - root AGENTS.md will not be created"
855
+ });
856
+ }
857
+ const permissionsResult = transformPermissionsToCursor(
858
+ state.settings?.permissions
859
+ );
860
+ if (permissionsResult.hasAskPermissions) {
861
+ warnings.push({
862
+ path: ["settings", "permissions", "ask"],
863
+ message: 'Cursor does not support "ask" permission level - these rules will be treated as "allow"'
864
+ });
865
+ }
866
+ const mcpServers = state.settings?.mcpServers;
867
+ if (mcpServers) {
868
+ for (const [name, server] of Object.entries(mcpServers)) {
869
+ const isRemote = server.type === "http" || server.type === "sse";
870
+ const hasCommand = !!server.command;
871
+ if (!isRemote && !hasCommand) {
872
+ warnings.push({
873
+ path: ["settings", "mcpServers", name],
874
+ message: `MCP server "${name}" has no command or type - it will be skipped`
875
+ });
876
+ }
877
+ }
878
+ }
879
+ return { valid: true, errors: [], warnings, skipped: [] };
880
+ }
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
+ }
889
+
501
890
  // src/plugins/opencode/transforms.ts
502
891
  function transformMcpToOpenCode(servers) {
503
892
  if (!servers || Object.keys(servers).length === 0) {
504
893
  return void 0;
505
894
  }
506
895
  const result = {};
507
- for (const [name, serverRaw] of Object.entries(servers)) {
508
- const server = serverRaw;
896
+ for (const [name, server] of Object.entries(servers)) {
509
897
  if (server.type === "http" || server.type === "sse") {
510
898
  const openCodeServer = {
511
899
  type: "remote",
@@ -522,7 +910,7 @@ function transformMcpToOpenCode(servers) {
522
910
  command
523
911
  };
524
912
  if (server.env) {
525
- openCodeServer.environment = transformEnvVars(server.env);
913
+ openCodeServer.environment = transformEnvVars(server.env, "opencode");
526
914
  }
527
915
  result[name] = openCodeServer;
528
916
  }
@@ -539,7 +927,7 @@ function transformPermissionsToOpenCode(permissions) {
539
927
  return;
540
928
  }
541
929
  for (const rule of rules) {
542
- const parsed = parsePermissionRule(rule);
930
+ const parsed = parsePermissionRuleForOpenCode(rule);
543
931
  if (!parsed) {
544
932
  continue;
545
933
  }
@@ -555,28 +943,13 @@ function transformPermissionsToOpenCode(permissions) {
555
943
  processRules(permissions.deny, "deny");
556
944
  return Object.keys(result).length > 0 ? result : void 0;
557
945
  }
558
- function transformEnvVar(value) {
559
- return value.replace(/\$\{([^}:]+)(:-[^}]*)?\}/g, "{env:$1}");
560
- }
561
- function transformEnvVars(env) {
562
- const result = {};
563
- for (const [key, value] of Object.entries(env)) {
564
- result[key] = transformEnvVar(value);
565
- }
566
- return result;
567
- }
568
- function parsePermissionRule(rule) {
569
- const match = rule.match(/^(\w+)\(([^)]+)\)$/);
570
- if (!match) {
571
- return null;
572
- }
573
- const tool = match[1];
574
- const pattern = match[2];
575
- if (!tool || !pattern) {
946
+ function parsePermissionRuleForOpenCode(rule) {
947
+ const parsed = parsePermissionRule(rule);
948
+ if (!parsed) {
576
949
  return null;
577
950
  }
578
- const normalizedTool = tool.toLowerCase();
579
- let normalizedPattern = pattern;
951
+ const normalizedTool = parsed.tool.toLowerCase();
952
+ let normalizedPattern = parsed.pattern;
580
953
  if (normalizedPattern.includes(":*")) {
581
954
  normalizedPattern = normalizedPattern.replace(/:(\*)/g, " $1");
582
955
  }
@@ -620,51 +993,28 @@ var opencodePlugin = {
620
993
  target: `../../${UNIFIED_DIR}/skills/${skill.path}`
621
994
  });
622
995
  }
623
- const baseConfig = {
996
+ const config = {
624
997
  $schema: "https://opencode.ai/config.json"
625
998
  };
626
999
  if (state.rules.length > 0) {
627
- baseConfig["instructions"] = [`${outputDir}/rules/*.md`];
1000
+ config["instructions"] = [`${outputDir}/rules/*.md`];
628
1001
  }
629
1002
  const mcp = transformMcpToOpenCode(state.settings?.mcpServers);
630
1003
  if (mcp) {
631
- baseConfig["mcp"] = mcp;
1004
+ config["mcp"] = mcp;
632
1005
  }
633
1006
  const permission = transformPermissionsToOpenCode(
634
1007
  state.settings?.permissions
635
1008
  );
636
1009
  if (permission) {
637
- baseConfig["permission"] = permission;
638
- }
639
- let finalConfig = baseConfig;
640
- if (state.settings?.overrides?.opencode) {
641
- finalConfig = deepMergeConfigs(
642
- baseConfig,
643
- state.settings.overrides.opencode
644
- );
1010
+ config["permission"] = permission;
645
1011
  }
646
1012
  files.push({
647
1013
  path: "opencode.json",
648
1014
  type: "json",
649
- content: finalConfig
1015
+ content: config
650
1016
  });
651
- const overrideFiles = await scanOverrideDirectory(rootDir, "opencode");
652
- for (const overrideFile of overrideFiles) {
653
- const targetPath = path.join(
654
- rootDir,
655
- outputDir,
656
- overrideFile.relativePath
657
- );
658
- if (await fileExists2(targetPath)) {
659
- continue;
660
- }
661
- files.push({
662
- path: `${outputDir}/${overrideFile.relativePath}`,
663
- type: "symlink",
664
- target: `../${UNIFIED_DIR}/${OVERRIDE_DIRS.opencode}/${overrideFile.relativePath}`
665
- });
666
- }
667
- return files;
1017
+ return applyFileOverrides(files, rootDir, "opencode");
668
1018
  },
669
1019
  validate(state) {
670
1020
  const warnings = [];
@@ -701,6 +1051,8 @@ var pluginRegistry = new PluginRegistry();
701
1051
 
702
1052
  // src/plugins/index.ts
703
1053
  pluginRegistry.register(claudeCodePlugin);
1054
+ pluginRegistry.register(copilotPlugin);
1055
+ pluginRegistry.register(cursorPlugin);
704
1056
  pluginRegistry.register(opencodePlugin);
705
1057
  function computeHash(content) {
706
1058
  return crypto.createHash("sha256").update(content, "utf-8").digest("hex");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lnai/core",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Core library for LNAI - unified AI config management",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -19,7 +19,10 @@
19
19
  "config",
20
20
  "configuration",
21
21
  "claude",
22
+ "cursor",
22
23
  "opencode",
24
+ "copilot",
25
+ "github-copilot",
23
26
  "cli",
24
27
  "ai-tools"
25
28
  ],
@@ -33,7 +36,6 @@
33
36
  "dist"
34
37
  ],
35
38
  "dependencies": {
36
- "deepmerge": "^4.3.1",
37
39
  "gray-matter": "^4.0.3",
38
40
  "zod": "^4.3.6"
39
41
  },