@anaclumos/taal 1.1.2

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 (42) hide show
  1. package/.claude/settings.json +15 -0
  2. package/.github/workflows/ci.yml +28 -0
  3. package/.github/workflows/publish.yml +42 -0
  4. package/AGENTS.md +123 -0
  5. package/README.md +568 -0
  6. package/biome.jsonc +24 -0
  7. package/lefthook.yml +12 -0
  8. package/package.json +52 -0
  9. package/src/commands/collect.ts +172 -0
  10. package/src/commands/diff.ts +127 -0
  11. package/src/commands/init.ts +66 -0
  12. package/src/commands/list.ts +80 -0
  13. package/src/commands/providers.ts +46 -0
  14. package/src/commands/sync.ts +111 -0
  15. package/src/commands/validate.ts +17 -0
  16. package/src/config/env.ts +49 -0
  17. package/src/config/loader.ts +88 -0
  18. package/src/config/parser.ts +44 -0
  19. package/src/config/schema.ts +67 -0
  20. package/src/errors/index.ts +43 -0
  21. package/src/index.ts +301 -0
  22. package/src/providers/antigravity.ts +24 -0
  23. package/src/providers/base.ts +70 -0
  24. package/src/providers/claude-code.ts +12 -0
  25. package/src/providers/claude-desktop.ts +19 -0
  26. package/src/providers/codex.ts +61 -0
  27. package/src/providers/continue.ts +62 -0
  28. package/src/providers/cursor.ts +25 -0
  29. package/src/providers/index.ts +34 -0
  30. package/src/providers/opencode.ts +42 -0
  31. package/src/providers/registry.ts +74 -0
  32. package/src/providers/types.ts +99 -0
  33. package/src/providers/utils.ts +106 -0
  34. package/src/providers/windsurf.ts +50 -0
  35. package/src/providers/zed.ts +35 -0
  36. package/src/scripts/generate-schema.ts +17 -0
  37. package/src/skills/copy.ts +58 -0
  38. package/src/skills/discovery.ts +87 -0
  39. package/src/skills/validator.ts +95 -0
  40. package/src/utils/atomic-write.ts +35 -0
  41. package/src/utils/backup.ts +27 -0
  42. package/taal.schema.json +91 -0
@@ -0,0 +1,61 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import type { JsonMap } from "@iarna/toml";
4
+ import { stringify as stringifyToml } from "@iarna/toml";
5
+ import type { McpServer } from "../config/schema.js";
6
+ import { atomicWrite } from "../utils/atomic-write.js";
7
+ import { backupConfig } from "../utils/backup.js";
8
+ import { BaseProvider } from "./base.js";
9
+ import { resolveConfigPath } from "./utils.js";
10
+
11
+ export class CodexProvider extends BaseProvider {
12
+ name = "codex";
13
+ configPath = (home: string) => join(home, ".codex", "config.toml");
14
+ format = "toml" as const;
15
+ mcpKey = "mcp_servers";
16
+ skillsPath = (home: string) => join(home, ".codex", "skills");
17
+
18
+ override async writeConfig(config: unknown, home?: string): Promise<void> {
19
+ const homeDir = home || homedir();
20
+ const path = resolveConfigPath(this.configPath, homeDir);
21
+
22
+ backupConfig(path);
23
+
24
+ const tomlContent = stringifyToml(config as JsonMap);
25
+ atomicWrite(path, tomlContent);
26
+ }
27
+
28
+ transformMcpServers(
29
+ servers: Record<string, McpServer>
30
+ ): Record<string, unknown> {
31
+ const transformed: Record<string, unknown> = {};
32
+
33
+ for (const [name, server] of Object.entries(servers)) {
34
+ if (server.url) {
35
+ transformed[name] = {
36
+ url: server.url,
37
+ ...(server.headers && { http_headers: server.headers }),
38
+ enabled: true,
39
+ startup_timeout_sec: 30,
40
+ ...(server.overrides?.codex?.enabled_tools && {
41
+ enabled_tools: server.overrides.codex.enabled_tools,
42
+ }),
43
+ };
44
+ } else if (server.command) {
45
+ transformed[name] = {
46
+ command: server.command,
47
+ ...(server.args && { args: server.args }),
48
+ ...(server.env && { env: server.env }),
49
+ enabled: true,
50
+ ...(server.overrides?.codex?.enabled_tools && {
51
+ enabled_tools: server.overrides.codex.enabled_tools,
52
+ }),
53
+ };
54
+ } else {
55
+ console.warn(`Skipping server "${name}" - missing command or url`);
56
+ }
57
+ }
58
+
59
+ return transformed;
60
+ }
61
+ }
@@ -0,0 +1,62 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { stringify as stringifyYaml } from "yaml";
4
+ import type { McpServer } from "../config/schema.js";
5
+ import { atomicWrite } from "../utils/atomic-write.js";
6
+ import { backupConfig } from "../utils/backup.js";
7
+ import { BaseProvider } from "./base.js";
8
+ import { resolveConfigPath } from "./utils.js";
9
+
10
+ export class ContinueProvider extends BaseProvider {
11
+ name = "continue";
12
+ configPath = (home: string) => join(home, ".continue", "config.yaml");
13
+ format = "yaml" as const;
14
+ mcpKey = "mcpServers";
15
+
16
+ override async writeConfig(config: unknown, home?: string): Promise<void> {
17
+ const homeDir = home || homedir();
18
+ const path = resolveConfigPath(this.configPath, homeDir);
19
+
20
+ backupConfig(path);
21
+
22
+ const yamlContent = stringifyYaml(config);
23
+ atomicWrite(path, yamlContent);
24
+ }
25
+
26
+ transformMcpServers(servers: Record<string, McpServer>): unknown[] {
27
+ const transformed: unknown[] = [];
28
+
29
+ for (const [name, server] of Object.entries(servers)) {
30
+ if (server.url) {
31
+ transformed.push({
32
+ name,
33
+ url: server.url,
34
+ ...(server.headers && {
35
+ headers: this.transformEnvVars(server.headers),
36
+ }),
37
+ });
38
+ } else if (server.command) {
39
+ transformed.push({
40
+ name,
41
+ command: server.command,
42
+ ...(server.args && { args: server.args }),
43
+ ...(server.env && { env: this.transformEnvVars(server.env) }),
44
+ });
45
+ } else {
46
+ console.warn(`Skipping server "${name}" - missing command or url`);
47
+ }
48
+ }
49
+
50
+ return transformed;
51
+ }
52
+
53
+ private transformEnvVars(
54
+ obj: Record<string, string>
55
+ ): Record<string, string> {
56
+ const transformed: Record<string, string> = {};
57
+ for (const [key, value] of Object.entries(obj)) {
58
+ transformed[key] = value.replace(/\$\{([^}]+)\}/g, "${{ secrets.$1 }}");
59
+ }
60
+ return transformed;
61
+ }
62
+ }
@@ -0,0 +1,25 @@
1
+ import { platform } from "node:os";
2
+ import { join } from "node:path";
3
+ import { BaseProvider, createStdioTransformer } from "./base.js";
4
+
5
+ export class CursorProvider extends BaseProvider {
6
+ name = "cursor";
7
+ configPath = (home: string) => {
8
+ const os = platform();
9
+ if (os === "darwin") {
10
+ return join(
11
+ home,
12
+ "Library",
13
+ "Application Support",
14
+ "Cursor",
15
+ "User",
16
+ "settings.json"
17
+ );
18
+ }
19
+ return join(home, ".config", "Cursor", "User", "settings.json");
20
+ };
21
+ format = "json" as const;
22
+ mcpKey = "mcpServers";
23
+
24
+ transformMcpServers = createStdioTransformer("Cursor");
25
+ }
@@ -0,0 +1,34 @@
1
+ import { AntigravityProvider } from "./antigravity.js";
2
+ import { ClaudeCodeProvider } from "./claude-code.js";
3
+ import { ClaudeDesktopProvider } from "./claude-desktop.js";
4
+ import { CodexProvider } from "./codex.js";
5
+ import { ContinueProvider } from "./continue.js";
6
+ import { CursorProvider } from "./cursor.js";
7
+ import { OpenCodeProvider } from "./opencode.js";
8
+ import { registry as providerRegistry } from "./registry.js";
9
+ import { WindsurfProvider } from "./windsurf.js";
10
+ import { ZedProvider } from "./zed.js";
11
+
12
+ let initialized = false;
13
+
14
+ export function initializeProviders(): void {
15
+ if (initialized) {
16
+ return;
17
+ }
18
+
19
+ providerRegistry.register(new ClaudeDesktopProvider());
20
+ providerRegistry.register(new ClaudeCodeProvider());
21
+ providerRegistry.register(new CursorProvider());
22
+ providerRegistry.register(new ContinueProvider());
23
+ providerRegistry.register(new ZedProvider());
24
+ providerRegistry.register(new OpenCodeProvider());
25
+ providerRegistry.register(new CodexProvider());
26
+ providerRegistry.register(new WindsurfProvider());
27
+ providerRegistry.register(new AntigravityProvider());
28
+
29
+ initialized = true;
30
+ }
31
+
32
+ // biome-ignore lint/performance/noBarrelFile: Intentional barrel for provider initialization
33
+ export { registry } from "./registry.js";
34
+ export type { ConfigFormat, Provider, ProviderMetadata } from "./types.js";
@@ -0,0 +1,42 @@
1
+ import { join } from "node:path";
2
+ import type { McpServer } from "../config/schema.js";
3
+ import { BaseProvider } from "./base.js";
4
+
5
+ export class OpenCodeProvider extends BaseProvider {
6
+ name = "opencode";
7
+ configPath = (home: string) =>
8
+ join(home, ".config", "opencode", "opencode.json");
9
+ format = "json" as const;
10
+ mcpKey = "mcp";
11
+ skillsPath = (home: string) => join(home, ".opencode", "skills");
12
+
13
+ transformMcpServers(
14
+ servers: Record<string, McpServer>
15
+ ): Record<string, unknown> {
16
+ const transformed: Record<string, unknown> = {};
17
+
18
+ for (const [name, server] of Object.entries(servers)) {
19
+ if (server.url) {
20
+ transformed[name] = {
21
+ type: "remote",
22
+ url: server.url,
23
+ ...(server.headers && { headers: server.headers }),
24
+ enabled: true,
25
+ };
26
+ } else if (server.command) {
27
+ const command = [server.command, ...(server.args || [])];
28
+
29
+ transformed[name] = {
30
+ type: "local",
31
+ command,
32
+ ...(server.env && { environment: server.env }),
33
+ enabled: true,
34
+ };
35
+ } else {
36
+ console.warn(`Skipping server "${name}" - missing command or url`);
37
+ }
38
+ }
39
+
40
+ return transformed;
41
+ }
42
+ }
@@ -0,0 +1,74 @@
1
+ import type { Provider } from "./types.js";
2
+
3
+ /**
4
+ * Global provider registry
5
+ */
6
+ class ProviderRegistry {
7
+ private readonly providers: Map<string, Provider> = new Map();
8
+
9
+ /**
10
+ * Register a provider
11
+ */
12
+ register(provider: Provider): void {
13
+ this.providers.set(provider.name, provider);
14
+ }
15
+
16
+ /**
17
+ * Get a provider by name
18
+ */
19
+ get(name: string): Provider | undefined {
20
+ return this.providers.get(name);
21
+ }
22
+
23
+ /**
24
+ * Get all registered providers (sorted by name for deterministic ordering)
25
+ */
26
+ getAll(): Provider[] {
27
+ return Array.from(this.providers.values()).sort((a, b) =>
28
+ a.name.localeCompare(b.name)
29
+ );
30
+ }
31
+
32
+ /**
33
+ * Get all provider names (sorted for deterministic ordering)
34
+ */
35
+ getNames(): string[] {
36
+ return Array.from(this.providers.keys()).sort();
37
+ }
38
+
39
+ /**
40
+ * Check if a provider is registered
41
+ */
42
+ has(name: string): boolean {
43
+ return this.providers.has(name);
44
+ }
45
+
46
+ /**
47
+ * Get all installed providers (sorted by name for deterministic ordering)
48
+ */
49
+ async getInstalled(): Promise<Provider[]> {
50
+ const all = this.getAll();
51
+ const installed: Provider[] = [];
52
+
53
+ for (const provider of all) {
54
+ if (await provider.isInstalled()) {
55
+ installed.push(provider);
56
+ }
57
+ }
58
+
59
+ return installed.sort((a, b) => a.name.localeCompare(b.name));
60
+ }
61
+
62
+ /**
63
+ * Clear all registered providers (mainly for testing)
64
+ */
65
+ clear(): void {
66
+ this.providers.clear();
67
+ }
68
+ }
69
+
70
+ // Export singleton instance
71
+ export const registry = new ProviderRegistry();
72
+
73
+ // Export class for testing
74
+ export { ProviderRegistry };
@@ -0,0 +1,99 @@
1
+ import type { McpServer } from "../config/schema.js";
2
+
3
+ export type ConfigFormat = "json" | "yaml" | "toml";
4
+
5
+ export interface StdioServerConfig {
6
+ command: string;
7
+ args?: string[];
8
+ env?: Record<string, string>;
9
+ }
10
+
11
+ export interface HttpServerConfig {
12
+ url: string;
13
+ headers?: Record<string, string>;
14
+ }
15
+
16
+ export interface OpenCodeServerConfig {
17
+ type: "local" | "remote";
18
+ command?: string[];
19
+ url?: string;
20
+ environment?: Record<string, string>;
21
+ headers?: Record<string, string>;
22
+ enabled: boolean;
23
+ }
24
+
25
+ export interface CodexServerConfig {
26
+ command?: string;
27
+ args?: string[];
28
+ env?: Record<string, string>;
29
+ url?: string;
30
+ http_headers?: Record<string, string>;
31
+ enabled: boolean;
32
+ startup_timeout_sec?: number;
33
+ enabled_tools?: string[];
34
+ }
35
+
36
+ export interface WindsurfHttpServerConfig extends HttpServerConfig {
37
+ transport: "streamable-http";
38
+ }
39
+
40
+ export interface ProviderConfig {
41
+ [key: string]: unknown;
42
+ }
43
+
44
+ /**
45
+ * Provider interface - defines how to interact with each AI coding assistant
46
+ */
47
+ export interface Provider {
48
+ /** Provider name (e.g., 'claude-desktop', 'cursor') */
49
+ name: string;
50
+
51
+ /** Path to provider's config file (can be function for dynamic paths) */
52
+ configPath: string | ((home: string) => string);
53
+
54
+ /** Config file format */
55
+ format: ConfigFormat;
56
+
57
+ /** Key in config where MCP servers are stored (e.g., 'mcpServers', 'context_servers') */
58
+ mcpKey: string;
59
+
60
+ /** Optional path to provider's skills directory */
61
+ skillsPath?: string | ((home: string) => string);
62
+
63
+ /**
64
+ * Check if provider is installed on this system
65
+ */
66
+ isInstalled(home?: string): Promise<boolean>;
67
+
68
+ /**
69
+ * Read provider's current config
70
+ */
71
+ readConfig(home?: string): Promise<unknown>;
72
+
73
+ /**
74
+ * Write config to provider's config file
75
+ * Should use atomic write and backup
76
+ */
77
+ writeConfig(config: unknown, home?: string): Promise<void>;
78
+
79
+ /**
80
+ * Transform TAAL MCP servers to provider-specific format
81
+ */
82
+ transformMcpServers(servers: Record<string, McpServer>): unknown;
83
+
84
+ /**
85
+ * Optional: Transform skills to provider-specific format
86
+ */
87
+ transformSkills?(skills: unknown[]): unknown;
88
+ }
89
+
90
+ /**
91
+ * Provider metadata for registration
92
+ */
93
+ export interface ProviderMetadata {
94
+ name: string;
95
+ configPath: string | ((home: string) => string);
96
+ format: ConfigFormat;
97
+ mcpKey: string;
98
+ skillsPath?: string | ((home: string) => string);
99
+ }
@@ -0,0 +1,106 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import type { JsonMap } from "@iarna/toml";
3
+ import { parse as parseToml, stringify as stringifyToml } from "@iarna/toml";
4
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
5
+ import { atomicWrite } from "../utils/atomic-write.js";
6
+ import { backupConfig } from "../utils/backup.js";
7
+ import type { ConfigFormat } from "./types.js";
8
+
9
+ /**
10
+ * Read and parse a JSON config file
11
+ */
12
+ export function readJsonConfig(path: string): unknown {
13
+ if (!existsSync(path)) {
14
+ return {};
15
+ }
16
+
17
+ try {
18
+ const content = readFileSync(path, "utf-8");
19
+ return JSON.parse(content);
20
+ } catch (error) {
21
+ throw new Error(`Failed to read JSON config at ${path}: ${error}`);
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Read and parse a YAML config file
27
+ */
28
+ export function readYamlConfig(path: string): unknown {
29
+ if (!existsSync(path)) {
30
+ return {};
31
+ }
32
+
33
+ try {
34
+ const content = readFileSync(path, "utf-8");
35
+ return parseYaml(content);
36
+ } catch (error) {
37
+ throw new Error(`Failed to read YAML config at ${path}: ${error}`);
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Read and parse a TOML config file
43
+ */
44
+ export function readTomlConfig(path: string): unknown {
45
+ if (!existsSync(path)) {
46
+ return {};
47
+ }
48
+
49
+ try {
50
+ const content = readFileSync(path, "utf-8");
51
+ return parseToml(content);
52
+ } catch (error) {
53
+ throw new Error(`Failed to read TOML config at ${path}: ${error}`);
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Read config file based on format
59
+ */
60
+ export function readConfig(path: string, format: ConfigFormat): unknown {
61
+ switch (format) {
62
+ case "json":
63
+ return readJsonConfig(path);
64
+ case "yaml":
65
+ return readYamlConfig(path);
66
+ case "toml":
67
+ return readTomlConfig(path);
68
+ default:
69
+ throw new Error(`Unsupported config format: ${format}`);
70
+ }
71
+ }
72
+
73
+ export function writeConfig(
74
+ path: string,
75
+ format: ConfigFormat,
76
+ config: unknown
77
+ ): void {
78
+ backupConfig(path);
79
+
80
+ let content: string;
81
+ switch (format) {
82
+ case "json":
83
+ content = JSON.stringify(config, null, 2);
84
+ break;
85
+ case "yaml":
86
+ content = stringifyYaml(config);
87
+ break;
88
+ case "toml":
89
+ content = stringifyToml(config as JsonMap);
90
+ break;
91
+ default:
92
+ throw new Error(`Unsupported config format: ${format}`);
93
+ }
94
+
95
+ atomicWrite(path, content);
96
+ }
97
+
98
+ /**
99
+ * Resolve config path (handle both string and function)
100
+ */
101
+ export function resolveConfigPath(
102
+ configPath: string | ((home: string) => string),
103
+ home: string
104
+ ): string {
105
+ return typeof configPath === "function" ? configPath(home) : configPath;
106
+ }
@@ -0,0 +1,50 @@
1
+ import { platform } from "node:os";
2
+ import { join } from "node:path";
3
+ import type { McpServer } from "../config/schema.js";
4
+ import { BaseProvider } from "./base.js";
5
+
6
+ export class WindsurfProvider extends BaseProvider {
7
+ name = "windsurf";
8
+ configPath = (home: string) => {
9
+ const os = platform();
10
+ if (os === "darwin") {
11
+ return join(
12
+ home,
13
+ "Library",
14
+ "Application Support",
15
+ "Windsurf",
16
+ "User",
17
+ "settings.json"
18
+ );
19
+ }
20
+ return join(home, ".config", "Windsurf", "User", "settings.json");
21
+ };
22
+ format = "json" as const;
23
+ mcpKey = "mcpServers";
24
+
25
+ transformMcpServers(
26
+ servers: Record<string, McpServer>
27
+ ): Record<string, unknown> {
28
+ const transformed: Record<string, unknown> = {};
29
+
30
+ for (const [name, server] of Object.entries(servers)) {
31
+ if (server.url) {
32
+ transformed[name] = {
33
+ url: server.url,
34
+ transport: "streamable-http",
35
+ ...(server.headers && { headers: server.headers }),
36
+ };
37
+ } else if (server.command) {
38
+ transformed[name] = {
39
+ command: server.command,
40
+ ...(server.args && { args: server.args }),
41
+ ...(server.env && { env: server.env }),
42
+ };
43
+ } else {
44
+ console.warn(`Skipping server "${name}" - missing command or url`);
45
+ }
46
+ }
47
+
48
+ return transformed;
49
+ }
50
+ }
@@ -0,0 +1,35 @@
1
+ import { join } from "node:path";
2
+ import type { McpServer } from "../config/schema.js";
3
+ import { BaseProvider } from "./base.js";
4
+
5
+ export class ZedProvider extends BaseProvider {
6
+ name = "zed";
7
+ configPath = (home: string) => join(home, ".config", "zed", "settings.json");
8
+ format = "json" as const;
9
+ mcpKey = "context_servers";
10
+
11
+ transformMcpServers(
12
+ servers: Record<string, McpServer>
13
+ ): Record<string, unknown> {
14
+ const transformed: Record<string, unknown> = {};
15
+
16
+ for (const [name, server] of Object.entries(servers)) {
17
+ if (server.url) {
18
+ transformed[name] = {
19
+ url: server.url,
20
+ ...(server.headers && { headers: server.headers }),
21
+ };
22
+ } else if (server.command) {
23
+ transformed[name] = {
24
+ command: server.command,
25
+ ...(server.args && { args: server.args }),
26
+ ...(server.env && { env: server.env }),
27
+ };
28
+ } else {
29
+ console.warn(`Skipping server "${name}" - missing command or url`);
30
+ }
31
+ }
32
+
33
+ return transformed;
34
+ }
35
+ }
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env bun
2
+ import { writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { zodToJsonSchema } from "zod-to-json-schema";
5
+ import { TaalConfigSchema } from "../config/schema.js";
6
+
7
+ // Generate JSON Schema from Zod schema
8
+ const jsonSchema = zodToJsonSchema(TaalConfigSchema, {
9
+ name: "TaalConfig",
10
+ $refStrategy: "none", // Inline all definitions for simplicity
11
+ });
12
+
13
+ // Write to root directory
14
+ const schemaPath = join(process.cwd(), "taal.schema.json");
15
+ writeFileSync(schemaPath, JSON.stringify(jsonSchema, null, 2));
16
+
17
+ console.log(`✓ Generated JSON Schema at ${schemaPath}`);
@@ -0,0 +1,58 @@
1
+ import { cpSync, existsSync, mkdirSync, rmSync } from "node:fs";
2
+ import { basename, join } from "node:path";
3
+ import type { Skill } from "./discovery.js";
4
+
5
+ /**
6
+ * Copy skills to a provider's skills directory
7
+ *
8
+ * @param skills - Array of skills to copy
9
+ * @param targetPath - Target directory path (provider's skills directory)
10
+ * @param options - Copy options
11
+ */
12
+ export function copySkillsToProvider(
13
+ skills: Skill[],
14
+ targetPath: string,
15
+ options: { overwrite?: boolean } = {}
16
+ ): void {
17
+ const { overwrite = true } = options;
18
+
19
+ // Create target directory if it doesn't exist
20
+ mkdirSync(targetPath, { recursive: true });
21
+
22
+ for (const skill of skills) {
23
+ const skillDirName = basename(skill.path);
24
+ const targetSkillPath = join(targetPath, skillDirName);
25
+
26
+ try {
27
+ // Remove existing skill if overwrite is enabled
28
+ if (overwrite && existsSync(targetSkillPath)) {
29
+ rmSync(targetSkillPath, { recursive: true, force: true });
30
+ }
31
+
32
+ // Copy skill directory recursively
33
+ cpSync(skill.path, targetSkillPath, {
34
+ recursive: true,
35
+ force: overwrite,
36
+ });
37
+
38
+ console.log(`Copied skill "${skill.name}" to ${targetSkillPath}`);
39
+ } catch (error) {
40
+ console.warn(`Failed to copy skill "${skill.name}": ${error}`);
41
+ }
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Copy a single skill to a target directory
47
+ *
48
+ * @param skill - Skill to copy
49
+ * @param targetPath - Target directory path
50
+ * @param options - Copy options
51
+ */
52
+ export function copySkill(
53
+ skill: Skill,
54
+ targetPath: string,
55
+ options: { overwrite?: boolean } = {}
56
+ ): void {
57
+ copySkillsToProvider([skill], targetPath, options);
58
+ }