@lnai/core 0.1.1 → 0.2.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 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"];
5
5
  type ToolId = (typeof TOOL_IDS)[number];
6
6
  declare const CONFIG_FILES: {
7
7
  readonly config: "config.json";
@@ -65,6 +65,7 @@ declare const toolConfigSchema: z.ZodObject<{
65
65
  declare const toolIdSchema: z.ZodEnum<{
66
66
  claudeCode: "claudeCode";
67
67
  opencode: "opencode";
68
+ cursor: "cursor";
68
69
  }>;
69
70
  /** Settings configuration (Claude format as source of truth) */
70
71
  declare const settingsSchema: z.ZodObject<{
@@ -87,6 +88,7 @@ declare const settingsSchema: z.ZodObject<{
87
88
  overrides: z.ZodOptional<z.ZodObject<{
88
89
  claudeCode: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
89
90
  opencode: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
91
+ cursor: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
90
92
  }, z.core.$strip>>;
91
93
  }, z.core.$strip>;
92
94
  /** Main config.json structure. Uses partial object to allow partial tool configs. */
@@ -100,6 +102,10 @@ declare const configSchema: z.ZodObject<{
100
102
  enabled: z.ZodBoolean;
101
103
  versionControl: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
102
104
  }, z.core.$strip>>;
105
+ cursor: 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,18 +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>;
146
+ permissions?: Permissions;
147
+ mcpServers?: Record<string, McpServer>;
149
148
  overrides?: Partial<Record<ToolId, Record<string, unknown>>>;
150
149
  } | null;
151
150
  agents: string | null;
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import * as crypto from 'crypto';
7
7
 
8
8
  // src/constants.ts
9
9
  var UNIFIED_DIR = ".ai";
10
- var TOOL_IDS = ["claudeCode", "opencode"];
10
+ var TOOL_IDS = ["claudeCode", "opencode", "cursor"];
11
11
  var CONFIG_FILES = {
12
12
  config: "config.json",
13
13
  settings: "settings.json",
@@ -20,11 +20,13 @@ var CONFIG_DIRS = {
20
20
  };
21
21
  var TOOL_OUTPUT_DIRS = {
22
22
  claudeCode: ".claude",
23
- opencode: ".opencode"
23
+ opencode: ".opencode",
24
+ cursor: ".cursor"
24
25
  };
25
26
  var OVERRIDE_DIRS = {
26
27
  claudeCode: ".claude",
27
- opencode: ".opencode"
28
+ opencode: ".opencode",
29
+ cursor: ".cursor"
28
30
  };
29
31
 
30
32
  // src/errors.ts
@@ -50,10 +52,10 @@ var ParseError = class extends LnaiError {
50
52
  var ValidationError = class extends LnaiError {
51
53
  path;
52
54
  value;
53
- constructor(message, path7, value) {
55
+ constructor(message, path5, value) {
54
56
  super(message, "VALIDATION_ERROR");
55
57
  this.name = "ValidationError";
56
- this.path = path7;
58
+ this.path = path5;
57
59
  this.value = value;
58
60
  }
59
61
  };
@@ -104,19 +106,21 @@ var toolConfigSchema = z.object({
104
106
  enabled: z.boolean(),
105
107
  versionControl: z.boolean().optional().default(false)
106
108
  });
107
- var toolIdSchema = z.enum(["claudeCode", "opencode"]);
109
+ var toolIdSchema = z.enum(["claudeCode", "opencode", "cursor"]);
108
110
  var settingsSchema = z.object({
109
111
  permissions: permissionsSchema.optional(),
110
112
  mcpServers: z.record(z.string(), mcpServerSchema).optional(),
111
113
  overrides: z.object({
112
114
  claudeCode: z.record(z.string(), z.unknown()).optional(),
113
- opencode: 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()
114
117
  }).optional()
115
118
  });
116
119
  var configSchema = z.object({
117
120
  tools: z.object({
118
121
  claudeCode: toolConfigSchema,
119
- opencode: toolConfigSchema
122
+ opencode: toolConfigSchema,
123
+ cursor: toolConfigSchema
120
124
  }).partial().optional()
121
125
  });
122
126
  var skillFrontmatterSchema = z.object({
@@ -412,6 +416,26 @@ async function fileExists2(filePath) {
412
416
  return false;
413
417
  }
414
418
  }
419
+ async function getOverrideOutputFiles(rootDir, toolId) {
420
+ const outputDir = TOOL_OUTPUT_DIRS[toolId];
421
+ const overrideFiles = await scanOverrideDirectory(rootDir, toolId);
422
+ const result = [];
423
+ for (const overrideFile of overrideFiles) {
424
+ const targetPath = path.join(rootDir, outputDir, overrideFile.relativePath);
425
+ if (await fileExists2(targetPath)) {
426
+ continue;
427
+ }
428
+ const symlinkPath = `${outputDir}/${overrideFile.relativePath}`;
429
+ const symlinkDir = path.dirname(symlinkPath);
430
+ const sourcePath = `${UNIFIED_DIR}/${OVERRIDE_DIRS[toolId]}/${overrideFile.relativePath}`;
431
+ result.push({
432
+ path: symlinkPath,
433
+ type: "symlink",
434
+ target: path.relative(symlinkDir, sourcePath)
435
+ });
436
+ }
437
+ return result;
438
+ }
415
439
 
416
440
  // src/plugins/claude-code/index.ts
417
441
  var claudeCodePlugin = {
@@ -468,22 +492,236 @@ var claudeCodePlugin = {
468
492
  content: finalSettings
469
493
  });
470
494
  }
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)) {
479
- continue;
495
+ const overrideFiles = await getOverrideOutputFiles(rootDir, "claudeCode");
496
+ files.push(...overrideFiles);
497
+ return files;
498
+ },
499
+ validate(state) {
500
+ const warnings = [];
501
+ if (!state.agents) {
502
+ warnings.push({
503
+ path: ["AGENTS.md"],
504
+ message: "No AGENTS.md found - .claude/CLAUDE.md will not be created"
505
+ });
506
+ }
507
+ return { valid: true, errors: [], warnings, skipped: [] };
508
+ }
509
+ };
510
+
511
+ // src/plugins/cursor/transforms.ts
512
+ function transformRuleToCursor(rule) {
513
+ const description = deriveDescription(rule.path, rule.content);
514
+ const globs = rule.frontmatter.paths || [];
515
+ const alwaysApply = globs.length === 0;
516
+ return {
517
+ frontmatter: {
518
+ description,
519
+ globs,
520
+ alwaysApply
521
+ },
522
+ content: rule.content
523
+ };
524
+ }
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
+ function serializeCursorRule(frontmatter, content) {
534
+ const lines = [
535
+ "---",
536
+ `description: ${JSON.stringify(frontmatter.description)}`
537
+ ];
538
+ lines.push("globs:");
539
+ for (const glob of frontmatter.globs) {
540
+ lines.push(` - ${JSON.stringify(glob)}`);
541
+ }
542
+ lines.push(`alwaysApply: ${frontmatter.alwaysApply}`);
543
+ lines.push("---");
544
+ lines.push("");
545
+ lines.push(content);
546
+ return lines.join("\n");
547
+ }
548
+ function transformMcpToCursor(servers) {
549
+ if (!servers || Object.keys(servers).length === 0) {
550
+ return void 0;
551
+ }
552
+ const result = {};
553
+ for (const [name, server] of Object.entries(servers)) {
554
+ if (server.type === "http" || server.type === "sse") {
555
+ const cursorServer = {
556
+ url: server.url
557
+ };
558
+ if (server.headers) {
559
+ cursorServer.headers = transformEnvVarsToCursor(server.headers);
560
+ }
561
+ result[name] = cursorServer;
562
+ } else if (server.command) {
563
+ const cursorServer = {
564
+ command: server.command
565
+ };
566
+ if (server.args && server.args.length > 0) {
567
+ cursorServer.args = server.args;
480
568
  }
569
+ if (server.env) {
570
+ cursorServer.env = transformEnvVarsToCursor(server.env);
571
+ }
572
+ result[name] = cursorServer;
573
+ }
574
+ }
575
+ return Object.keys(result).length > 0 ? result : void 0;
576
+ }
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
+ function transformPermissionsToCursor(permissions) {
588
+ if (!permissions) {
589
+ return { permissions: void 0, hasAskPermissions: false };
590
+ }
591
+ const allow = [];
592
+ const deny = [];
593
+ let hasAskPermissions = false;
594
+ if (permissions.allow) {
595
+ for (const rule of permissions.allow) {
596
+ const transformed = transformPermissionRule(rule);
597
+ if (transformed) {
598
+ allow.push(transformed);
599
+ }
600
+ }
601
+ }
602
+ if (permissions.ask && permissions.ask.length > 0) {
603
+ hasAskPermissions = true;
604
+ for (const rule of permissions.ask) {
605
+ const transformed = transformPermissionRule(rule);
606
+ if (transformed) {
607
+ allow.push(transformed);
608
+ }
609
+ }
610
+ }
611
+ if (permissions.deny) {
612
+ for (const rule of permissions.deny) {
613
+ const transformed = transformPermissionRule(rule);
614
+ if (transformed) {
615
+ deny.push(transformed);
616
+ }
617
+ }
618
+ }
619
+ if (allow.length === 0 && deny.length === 0) {
620
+ return { permissions: void 0, hasAskPermissions };
621
+ }
622
+ return {
623
+ permissions: {
624
+ allow,
625
+ deny
626
+ },
627
+ hasAskPermissions
628
+ };
629
+ }
630
+ function transformPermissionRule(rule) {
631
+ const match = rule.match(/^(\w+)\(([^)]+)\)$/);
632
+ if (!match) {
633
+ return null;
634
+ }
635
+ const tool = match[1];
636
+ let pattern = match[2];
637
+ const cursorTool = tool.toLowerCase() === "bash" ? "Shell" : tool;
638
+ if (pattern.endsWith(":*")) {
639
+ pattern = pattern.slice(0, -2);
640
+ }
641
+ return `${cursorTool}(${pattern})`;
642
+ }
643
+
644
+ // src/plugins/cursor/index.ts
645
+ var cursorPlugin = {
646
+ id: "cursor",
647
+ name: "Cursor",
648
+ async detect(_rootDir) {
649
+ return false;
650
+ },
651
+ async import(_rootDir) {
652
+ return null;
653
+ },
654
+ async export(state, rootDir) {
655
+ const files = [];
656
+ const outputDir = TOOL_OUTPUT_DIRS.cursor;
657
+ if (state.agents) {
481
658
  files.push({
482
- path: `${outputDir}/${overrideFile.relativePath}`,
659
+ path: "AGENTS.md",
483
660
  type: "symlink",
484
- target: `../${UNIFIED_DIR}/${OVERRIDE_DIRS.claudeCode}/${overrideFile.relativePath}`
661
+ target: `${UNIFIED_DIR}/AGENTS.md`
662
+ });
663
+ }
664
+ for (const rule of state.rules) {
665
+ const transformed = transformRuleToCursor(rule);
666
+ const ruleContent = serializeCursorRule(
667
+ transformed.frontmatter,
668
+ transformed.content
669
+ );
670
+ const outputFilename = rule.path.replace(/\.md$/, ".mdc");
671
+ files.push({
672
+ path: `${outputDir}/rules/${outputFilename}`,
673
+ type: "text",
674
+ content: ruleContent
675
+ });
676
+ }
677
+ for (const skill of state.skills) {
678
+ files.push({
679
+ path: `${outputDir}/skills/${skill.path}`,
680
+ type: "symlink",
681
+ target: `../../${UNIFIED_DIR}/skills/${skill.path}`
485
682
  });
486
683
  }
684
+ const mcpServers = transformMcpToCursor(state.settings?.mcpServers);
685
+ const cursorOverrides = state.settings?.overrides?.cursor;
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
+ }
694
+ files.push({
695
+ path: `${outputDir}/mcp.json`,
696
+ type: "json",
697
+ content: mcpContent
698
+ });
699
+ }
700
+ const permissionsResult = transformPermissionsToCursor(
701
+ state.settings?.permissions
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
+ }
717
+ files.push({
718
+ path: `${outputDir}/cli.json`,
719
+ type: "json",
720
+ content: cliContent
721
+ });
722
+ }
723
+ const overrideFiles = await getOverrideOutputFiles(rootDir, "cursor");
724
+ files.push(...overrideFiles);
487
725
  return files;
488
726
  },
489
727
  validate(state) {
@@ -491,9 +729,31 @@ var claudeCodePlugin = {
491
729
  if (!state.agents) {
492
730
  warnings.push({
493
731
  path: ["AGENTS.md"],
494
- message: "No AGENTS.md found - .claude/CLAUDE.md will not be created"
732
+ message: "No AGENTS.md found - root AGENTS.md will not be created"
495
733
  });
496
734
  }
735
+ const permissionsResult = transformPermissionsToCursor(
736
+ state.settings?.permissions
737
+ );
738
+ if (permissionsResult.hasAskPermissions) {
739
+ warnings.push({
740
+ path: ["settings", "permissions", "ask"],
741
+ message: 'Cursor does not support "ask" permission level - these rules will be treated as "allow"'
742
+ });
743
+ }
744
+ const mcpServers = state.settings?.mcpServers;
745
+ if (mcpServers) {
746
+ for (const [name, server] of Object.entries(mcpServers)) {
747
+ const isRemote = server.type === "http" || server.type === "sse";
748
+ const hasCommand = !!server.command;
749
+ if (!isRemote && !hasCommand) {
750
+ warnings.push({
751
+ path: ["settings", "mcpServers", name],
752
+ message: `MCP server "${name}" has no command or type - it will be skipped`
753
+ });
754
+ }
755
+ }
756
+ }
497
757
  return { valid: true, errors: [], warnings, skipped: [] };
498
758
  }
499
759
  };
@@ -504,8 +764,7 @@ function transformMcpToOpenCode(servers) {
504
764
  return void 0;
505
765
  }
506
766
  const result = {};
507
- for (const [name, serverRaw] of Object.entries(servers)) {
508
- const server = serverRaw;
767
+ for (const [name, server] of Object.entries(servers)) {
509
768
  if (server.type === "http" || server.type === "sse") {
510
769
  const openCodeServer = {
511
770
  type: "remote",
@@ -648,22 +907,8 @@ var opencodePlugin = {
648
907
  type: "json",
649
908
  content: finalConfig
650
909
  });
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
- }
910
+ const overrideFiles = await getOverrideOutputFiles(rootDir, "opencode");
911
+ files.push(...overrideFiles);
667
912
  return files;
668
913
  },
669
914
  validate(state) {
@@ -701,6 +946,7 @@ var pluginRegistry = new PluginRegistry();
701
946
 
702
947
  // src/plugins/index.ts
703
948
  pluginRegistry.register(claudeCodePlugin);
949
+ pluginRegistry.register(cursorPlugin);
704
950
  pluginRegistry.register(opencodePlugin);
705
951
  function computeHash(content) {
706
952
  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.2.0",
4
4
  "description": "Core library for LNAI - unified AI config management",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -19,6 +19,7 @@
19
19
  "config",
20
20
  "configuration",
21
21
  "claude",
22
+ "cursor",
22
23
  "opencode",
23
24
  "cli",
24
25
  "ai-tools"