@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.
- package/dist/index.d.ts +14 -16
- package/dist/index.js +453 -101
- 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
|
-
|
|
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,
|
|
56
|
+
constructor(message, path5, value) {
|
|
54
57
|
super(message, "VALIDATION_ERROR");
|
|
55
58
|
this.name = "ValidationError";
|
|
56
|
-
this.path =
|
|
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
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
|
457
|
+
const settings = {};
|
|
451
458
|
if (state.settings?.permissions) {
|
|
452
|
-
|
|
459
|
+
settings["permissions"] = state.settings.permissions;
|
|
453
460
|
}
|
|
454
461
|
if (state.settings?.mcpServers) {
|
|
455
|
-
|
|
462
|
+
settings["mcpServers"] = state.settings.mcpServers;
|
|
456
463
|
}
|
|
457
|
-
|
|
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:
|
|
468
|
+
content: settings
|
|
469
469
|
});
|
|
470
470
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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:
|
|
603
|
+
path: ".github/copilot-instructions.md",
|
|
483
604
|
type: "symlink",
|
|
484
|
-
target: `../${UNIFIED_DIR}
|
|
605
|
+
target: `../${UNIFIED_DIR}/AGENTS.md`
|
|
485
606
|
});
|
|
486
607
|
}
|
|
487
|
-
|
|
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 - .
|
|
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,
|
|
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 =
|
|
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
|
|
559
|
-
|
|
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
|
|
996
|
+
const config = {
|
|
624
997
|
$schema: "https://opencode.ai/config.json"
|
|
625
998
|
};
|
|
626
999
|
if (state.rules.length > 0) {
|
|
627
|
-
|
|
1000
|
+
config["instructions"] = [`${outputDir}/rules/*.md`];
|
|
628
1001
|
}
|
|
629
1002
|
const mcp = transformMcpToOpenCode(state.settings?.mcpServers);
|
|
630
1003
|
if (mcp) {
|
|
631
|
-
|
|
1004
|
+
config["mcp"] = mcp;
|
|
632
1005
|
}
|
|
633
1006
|
const permission = transformPermissionsToOpenCode(
|
|
634
1007
|
state.settings?.permissions
|
|
635
1008
|
);
|
|
636
1009
|
if (permission) {
|
|
637
|
-
|
|
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:
|
|
1015
|
+
content: config
|
|
650
1016
|
});
|
|
651
|
-
|
|
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.
|
|
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
|
},
|