dotai-cli 1.0.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.
@@ -0,0 +1,261 @@
1
+ import fs from 'fs-extra';
2
+ import { join, basename, dirname } from 'path';
3
+ import matter from 'gray-matter';
4
+ import { getSkillsRepoDir } from './paths.js';
5
+ import { providers, getGlobalSkillPath, getProjectSkillPath } from '../providers/index.js';
6
+
7
+ /**
8
+ * Skill template with frontmatter
9
+ */
10
+ export function createSkillTemplate(name, description, instructions = '') {
11
+ return `---
12
+ name: ${name}
13
+ description: ${description}
14
+ ---
15
+
16
+ # ${name}
17
+
18
+ ${instructions || `Instructions for the ${name} skill.
19
+
20
+ ## When to use this skill
21
+
22
+ - Use this when...
23
+ - This is helpful for...
24
+
25
+ ## How to use it
26
+
27
+ Step-by-step guidance, conventions, and patterns the agent should follow.
28
+
29
+ ## Examples
30
+
31
+ Include examples to help the agent understand expected behavior.
32
+ `}
33
+ `;
34
+ }
35
+
36
+ /**
37
+ * Parse a SKILL.md file
38
+ */
39
+ export async function parseSkillFile(filePath) {
40
+ try {
41
+ const content = await fs.readFile(filePath, 'utf-8');
42
+ const { data, content: body } = matter(content);
43
+ return {
44
+ name: data.name || basename(dirname(filePath)),
45
+ description: data.description || '',
46
+ metadata: data,
47
+ body,
48
+ filePath
49
+ };
50
+ } catch (err) {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * List all skills in the central repository
57
+ */
58
+ export async function listCentralSkills() {
59
+ const skillsDir = getSkillsRepoDir();
60
+
61
+ if (!await fs.pathExists(skillsDir)) {
62
+ return [];
63
+ }
64
+
65
+ const entries = await fs.readdir(skillsDir, { withFileTypes: true });
66
+ const skills = [];
67
+
68
+ for (const entry of entries) {
69
+ if (entry.isDirectory()) {
70
+ const skillMdPath = join(skillsDir, entry.name, 'SKILL.md');
71
+ if (await fs.pathExists(skillMdPath)) {
72
+ const skill = await parseSkillFile(skillMdPath);
73
+ if (skill) {
74
+ skills.push({
75
+ ...skill,
76
+ folder: entry.name,
77
+ path: join(skillsDir, entry.name)
78
+ });
79
+ }
80
+ }
81
+ }
82
+ }
83
+
84
+ return skills;
85
+ }
86
+
87
+ /**
88
+ * Get a skill from central repo by name
89
+ */
90
+ export async function getCentralSkill(name) {
91
+ const skillsDir = getSkillsRepoDir();
92
+ const skillPath = join(skillsDir, name);
93
+ const skillMdPath = join(skillPath, 'SKILL.md');
94
+
95
+ if (!await fs.pathExists(skillMdPath)) {
96
+ return null;
97
+ }
98
+
99
+ const skill = await parseSkillFile(skillMdPath);
100
+ if (skill) {
101
+ skill.folder = name;
102
+ skill.path = skillPath;
103
+ }
104
+
105
+ return skill;
106
+ }
107
+
108
+ /**
109
+ * Create a new skill in the central repository
110
+ */
111
+ export async function createSkill(name, description, instructions = '') {
112
+ const skillsDir = getSkillsRepoDir();
113
+ const skillPath = join(skillsDir, name);
114
+ const skillMdPath = join(skillPath, 'SKILL.md');
115
+
116
+ // Check if skill already exists
117
+ if (await fs.pathExists(skillPath)) {
118
+ throw new Error(`Skill '${name}' already exists`);
119
+ }
120
+
121
+ // Create skill directory
122
+ await fs.ensureDir(skillPath);
123
+
124
+ // Create SKILL.md
125
+ const content = createSkillTemplate(name, description, instructions);
126
+ await fs.writeFile(skillMdPath, content, 'utf-8');
127
+
128
+ return {
129
+ name,
130
+ description,
131
+ path: skillPath,
132
+ skillMdPath
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Install a skill to a specific provider
138
+ */
139
+ export async function installSkillToProvider(skillName, providerId, scope = 'global', projectRoot = process.cwd()) {
140
+ const provider = providers[providerId];
141
+ if (!provider) {
142
+ throw new Error(`Unknown provider: ${providerId}`);
143
+ }
144
+
145
+ // Get source skill
146
+ const skill = await getCentralSkill(skillName);
147
+ if (!skill) {
148
+ throw new Error(`Skill '${skillName}' not found in central repository`);
149
+ }
150
+
151
+ // Determine target path
152
+ const targetPath = scope === 'global'
153
+ ? getGlobalSkillPath(providerId, skillName)
154
+ : getProjectSkillPath(providerId, skillName, projectRoot);
155
+
156
+ // Copy skill folder
157
+ await fs.ensureDir(targetPath);
158
+ await fs.copy(skill.path, targetPath, { overwrite: true });
159
+
160
+ return {
161
+ skillName,
162
+ providerId,
163
+ scope,
164
+ targetPath
165
+ };
166
+ }
167
+
168
+ /**
169
+ * Install a skill to multiple providers
170
+ */
171
+ export async function installSkill(skillName, providerIds, scope = 'global', projectRoot = process.cwd()) {
172
+ const results = [];
173
+
174
+ for (const providerId of providerIds) {
175
+ try {
176
+ const result = await installSkillToProvider(skillName, providerId, scope, projectRoot);
177
+ results.push({ ...result, success: true });
178
+ } catch (err) {
179
+ results.push({
180
+ skillName,
181
+ providerId,
182
+ scope,
183
+ success: false,
184
+ error: err.message
185
+ });
186
+ }
187
+ }
188
+
189
+ return results;
190
+ }
191
+
192
+ /**
193
+ * Uninstall a skill from a provider
194
+ */
195
+ export async function uninstallSkillFromProvider(skillName, providerId, scope = 'global', projectRoot = process.cwd()) {
196
+ const provider = providers[providerId];
197
+ if (!provider) {
198
+ throw new Error(`Unknown provider: ${providerId}`);
199
+ }
200
+
201
+ const targetPath = scope === 'global'
202
+ ? getGlobalSkillPath(providerId, skillName)
203
+ : getProjectSkillPath(providerId, skillName, projectRoot);
204
+
205
+ if (await fs.pathExists(targetPath)) {
206
+ await fs.remove(targetPath);
207
+ return { removed: true, path: targetPath };
208
+ }
209
+
210
+ return { removed: false, path: targetPath };
211
+ }
212
+
213
+ /**
214
+ * Check where a skill is installed
215
+ */
216
+ export async function getSkillInstallStatus(skillName) {
217
+ const status = {};
218
+
219
+ for (const [providerId, provider] of Object.entries(providers)) {
220
+ const globalPath = getGlobalSkillPath(providerId, skillName);
221
+ const projectPath = getProjectSkillPath(providerId, skillName);
222
+
223
+ status[providerId] = {
224
+ name: provider.name,
225
+ global: await fs.pathExists(globalPath),
226
+ globalPath,
227
+ project: await fs.pathExists(projectPath),
228
+ projectPath
229
+ };
230
+ }
231
+
232
+ return status;
233
+ }
234
+
235
+ /**
236
+ * Import a skill from an external path to central repo
237
+ */
238
+ export async function importSkill(sourcePath) {
239
+ const skillMdPath = join(sourcePath, 'SKILL.md');
240
+
241
+ if (!await fs.pathExists(skillMdPath)) {
242
+ throw new Error(`No SKILL.md found in ${sourcePath}`);
243
+ }
244
+
245
+ const skill = await parseSkillFile(skillMdPath);
246
+ if (!skill) {
247
+ throw new Error(`Failed to parse SKILL.md in ${sourcePath}`);
248
+ }
249
+
250
+ const skillName = skill.name || basename(sourcePath);
251
+ const targetPath = join(getSkillsRepoDir(), skillName);
252
+
253
+ // Copy to central repo
254
+ await fs.copy(sourcePath, targetPath, { overwrite: true });
255
+
256
+ return {
257
+ name: skillName,
258
+ path: targetPath,
259
+ imported: true
260
+ };
261
+ }
@@ -0,0 +1,125 @@
1
+ import { join } from 'path';
2
+ import { getHomeDir } from '../lib/paths.js';
3
+
4
+ /**
5
+ * Provider definitions with their skill paths
6
+ * Each provider has:
7
+ * - name: Display name
8
+ * - id: Unique identifier
9
+ * - globalPath: Where global skills are stored
10
+ * - projectPath: Where project skills are stored (relative to project root)
11
+ * - skillFile: The instruction file name (usually SKILL.md)
12
+ * - enabled: Whether this provider is enabled by default
13
+ * - notes: Any special handling notes
14
+ */
15
+ export const providers = {
16
+ 'claude-code': {
17
+ name: 'Claude Code',
18
+ id: 'claude-code',
19
+ globalPath: () => join(getHomeDir(), '.claude', 'skills'),
20
+ projectPath: '.claude/skills',
21
+ skillFile: 'SKILL.md',
22
+ enabled: true,
23
+ description: 'Anthropic Claude Code CLI',
24
+ website: 'https://claude.ai/code'
25
+ },
26
+
27
+ 'cursor': {
28
+ name: 'Cursor',
29
+ id: 'cursor',
30
+ globalPath: () => join(getHomeDir(), '.cursor', 'skills'),
31
+ projectPath: '.cursor/skills',
32
+ skillFile: 'SKILL.md',
33
+ enabled: true,
34
+ description: 'Cursor AI IDE',
35
+ website: 'https://cursor.com'
36
+ },
37
+
38
+ 'gemini-cli': {
39
+ name: 'Gemini CLI',
40
+ id: 'gemini-cli',
41
+ globalPath: () => join(getHomeDir(), '.gemini', 'skills'),
42
+ projectPath: '.gemini/skills',
43
+ skillFile: 'SKILL.md',
44
+ enabled: true,
45
+ description: 'Google Gemini CLI',
46
+ website: 'https://geminicli.com'
47
+ },
48
+
49
+ 'opencode': {
50
+ name: 'OpenCode',
51
+ id: 'opencode',
52
+ globalPath: () => join(getHomeDir(), '.config', 'opencode', 'skill'),
53
+ projectPath: '.opencode/skill',
54
+ skillFile: 'SKILL.md',
55
+ enabled: true,
56
+ description: 'OpenCode AI coding agent',
57
+ website: 'https://opencode.ai'
58
+ },
59
+
60
+ 'codex-cli': {
61
+ name: 'Codex CLI',
62
+ id: 'codex-cli',
63
+ globalPath: () => join(getHomeDir(), '.codex', 'skills'),
64
+ projectPath: 'skills',
65
+ skillFile: 'SKILL.md',
66
+ enabled: true,
67
+ description: 'OpenAI Codex CLI',
68
+ website: 'https://openai.com/codex',
69
+ // Codex also uses AGENTS.md for global instructions
70
+ supportsAgentsMd: true,
71
+ agentsMdGlobalPath: () => join(getHomeDir(), '.codex', 'AGENTS.md'),
72
+ agentsMdProjectPath: 'AGENTS.md'
73
+ },
74
+
75
+ 'antigravity': {
76
+ name: 'Antigravity',
77
+ id: 'antigravity',
78
+ globalPath: () => join(getHomeDir(), '.gemini', 'antigravity', 'skills'),
79
+ projectPath: '.agent/skills',
80
+ skillFile: 'SKILL.md',
81
+ enabled: true,
82
+ description: 'Antigravity agentic system',
83
+ website: 'https://github.com/vuralserhat86/antigravity-agentic-skills'
84
+ }
85
+ };
86
+
87
+ /**
88
+ * Get all provider IDs
89
+ */
90
+ export function getProviderIds() {
91
+ return Object.keys(providers);
92
+ }
93
+
94
+ /**
95
+ * Get provider by ID
96
+ */
97
+ export function getProvider(id) {
98
+ return providers[id];
99
+ }
100
+
101
+ /**
102
+ * Get all enabled providers
103
+ */
104
+ export function getEnabledProviders(config = {}) {
105
+ const enabledProviders = config.enabledProviders || getProviderIds();
106
+ return enabledProviders.map(id => providers[id]).filter(Boolean);
107
+ }
108
+
109
+ /**
110
+ * Get global skill path for a provider and skill name
111
+ */
112
+ export function getGlobalSkillPath(providerId, skillName) {
113
+ const provider = providers[providerId];
114
+ if (!provider) return null;
115
+ return join(provider.globalPath(), skillName);
116
+ }
117
+
118
+ /**
119
+ * Get project skill path for a provider and skill name
120
+ */
121
+ export function getProjectSkillPath(providerId, skillName, projectRoot = process.cwd()) {
122
+ const provider = providers[providerId];
123
+ if (!provider) return null;
124
+ return join(projectRoot, provider.projectPath, skillName);
125
+ }
@@ -0,0 +1,184 @@
1
+ import { join } from 'path';
2
+ import { homedir, platform } from 'os';
3
+
4
+ /**
5
+ * MCP Server provider definitions
6
+ * Each provider has different config file locations and formats
7
+ */
8
+ export const mcpProviders = {
9
+ 'claude-code': {
10
+ name: 'Claude Code',
11
+ id: 'claude-code',
12
+ globalPath: () => join(homedir(), '.claude.json'),
13
+ projectPath: '.mcp.json',
14
+ configKey: 'mcpServers',
15
+ format: 'json',
16
+ enabled: true,
17
+ description: 'Anthropic Claude Code CLI'
18
+ },
19
+
20
+ 'claude-desktop': {
21
+ name: 'Claude Desktop',
22
+ id: 'claude-desktop',
23
+ globalPath: () => {
24
+ if (platform() === 'win32') {
25
+ return join(process.env.APPDATA || '', 'Claude', 'claude_desktop_config.json');
26
+ }
27
+ return join(homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
28
+ },
29
+ projectPath: null, // No project-level config
30
+ configKey: 'mcpServers',
31
+ format: 'json',
32
+ enabled: true,
33
+ description: 'Claude Desktop App'
34
+ },
35
+
36
+ 'cursor': {
37
+ name: 'Cursor',
38
+ id: 'cursor',
39
+ globalPath: () => join(homedir(), '.cursor', 'mcp.json'),
40
+ projectPath: '.cursor/mcp.json',
41
+ configKey: 'mcpServers',
42
+ format: 'json',
43
+ enabled: true,
44
+ description: 'Cursor AI IDE'
45
+ },
46
+
47
+ 'windsurf': {
48
+ name: 'Windsurf',
49
+ id: 'windsurf',
50
+ globalPath: () => join(homedir(), '.codeium', 'windsurf', 'mcp_config.json'),
51
+ projectPath: null,
52
+ configKey: 'mcpServers',
53
+ format: 'json',
54
+ enabled: true,
55
+ description: 'Windsurf IDE by Codeium'
56
+ },
57
+
58
+ 'vscode': {
59
+ name: 'VS Code',
60
+ id: 'vscode',
61
+ globalPath: () => {
62
+ // VS Code stores in different locations per OS
63
+ if (platform() === 'win32') {
64
+ return join(process.env.APPDATA || '', 'Code', 'User', 'settings.json');
65
+ } else if (platform() === 'darwin') {
66
+ return join(homedir(), 'Library', 'Application Support', 'Code', 'User', 'settings.json');
67
+ }
68
+ return join(homedir(), '.config', 'Code', 'User', 'settings.json');
69
+ },
70
+ projectPath: '.vscode/mcp.json',
71
+ configKey: 'mcpServers', // For project config; global uses mcp.servers in settings
72
+ globalConfigKey: 'mcp.servers', // Different key for global settings.json
73
+ format: 'json',
74
+ enabled: true,
75
+ description: 'Visual Studio Code with GitHub Copilot'
76
+ },
77
+
78
+ 'cline': {
79
+ name: 'Cline',
80
+ id: 'cline',
81
+ globalPath: () => {
82
+ // Cline stores in VS Code extension storage
83
+ if (platform() === 'win32') {
84
+ return join(process.env.APPDATA || '', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json');
85
+ } else if (platform() === 'darwin') {
86
+ return join(homedir(), 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json');
87
+ }
88
+ return join(homedir(), '.config', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json');
89
+ },
90
+ projectPath: null,
91
+ configKey: 'mcpServers',
92
+ format: 'json',
93
+ enabled: true,
94
+ description: 'Cline VS Code Extension'
95
+ },
96
+
97
+ 'zed': {
98
+ name: 'Zed',
99
+ id: 'zed',
100
+ globalPath: () => {
101
+ if (platform() === 'darwin') {
102
+ return join(homedir(), '.config', 'zed', 'settings.json');
103
+ }
104
+ return join(homedir(), '.config', 'zed', 'settings.json');
105
+ },
106
+ projectPath: null,
107
+ configKey: 'context_servers', // Zed uses different key!
108
+ format: 'json',
109
+ enabled: true,
110
+ description: 'Zed Editor'
111
+ },
112
+
113
+ 'roo-code': {
114
+ name: 'Roo Code',
115
+ id: 'roo-code',
116
+ globalPath: () => {
117
+ if (platform() === 'win32') {
118
+ return join(process.env.APPDATA || '', 'Code', 'User', 'globalStorage', 'rooveterinaryinc.roo-cline', 'settings', 'mcp_settings.json');
119
+ } else if (platform() === 'darwin') {
120
+ return join(homedir(), 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'rooveterinaryinc.roo-cline', 'settings', 'mcp_settings.json');
121
+ }
122
+ return join(homedir(), '.config', 'Code', 'User', 'globalStorage', 'rooveterinaryinc.roo-cline', 'settings', 'mcp_settings.json');
123
+ },
124
+ projectPath: '.roo/mcp.json',
125
+ configKey: 'mcpServers',
126
+ format: 'json',
127
+ enabled: true,
128
+ description: 'Roo Code VS Code Extension'
129
+ },
130
+
131
+ 'antigravity': {
132
+ name: 'Antigravity',
133
+ id: 'antigravity',
134
+ globalPath: () => {
135
+ // Antigravity stores MCP config in its own location
136
+ // Access via MCP Store > Manage MCP Servers > View raw config
137
+ if (platform() === 'darwin') {
138
+ return join(homedir(), '.antigravity', 'mcp_config.json');
139
+ }
140
+ return join(homedir(), '.antigravity', 'mcp_config.json');
141
+ },
142
+ projectPath: null,
143
+ configKey: 'mcpServers',
144
+ format: 'json',
145
+ enabled: true,
146
+ description: 'Antigravity Editor',
147
+ note: 'Config accessible via MCP Store > Manage MCP Servers > View raw config'
148
+ }
149
+ };
150
+
151
+ /**
152
+ * Get all MCP provider IDs
153
+ */
154
+ export function getMcpProviderIds() {
155
+ return Object.keys(mcpProviders);
156
+ }
157
+
158
+ /**
159
+ * Get MCP provider by ID
160
+ */
161
+ export function getMcpProvider(id) {
162
+ return mcpProviders[id];
163
+ }
164
+
165
+ /**
166
+ * Get all enabled MCP providers
167
+ */
168
+ export function getEnabledMcpProviders(config = {}) {
169
+ const enabledProviders = config.enabledMcpProviders || getMcpProviderIds();
170
+ return enabledProviders.map(id => mcpProviders[id]).filter(Boolean);
171
+ }
172
+
173
+ /**
174
+ * Standard MCP server config format (most providers use this)
175
+ */
176
+ export function createMcpServerConfig(name, command, args = [], env = {}) {
177
+ return {
178
+ [name]: {
179
+ command,
180
+ args,
181
+ ...(Object.keys(env).length > 0 ? { env } : {})
182
+ }
183
+ };
184
+ }