makecc 0.2.17 → 0.2.19

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,332 @@
1
+ import { promises as fs, existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+ import { nanoid } from 'nanoid';
5
+ // Default makecc home directory
6
+ const MAKECC_HOME = process.env.MAKECC_HOME || join(homedir(), 'makecc');
7
+ // Gallery items (static for now, could be fetched from a registry later)
8
+ const GALLERY_ITEMS = [
9
+ {
10
+ id: 'gmail-assistant',
11
+ name: 'Gmail Assistant',
12
+ description: 'Read, write and organize emails using Claude',
13
+ thumbnail: '/gallery/gmail.png',
14
+ author: 'makecc',
15
+ downloads: 1234,
16
+ tags: ['email', 'productivity'],
17
+ },
18
+ {
19
+ id: 'web-scraper',
20
+ name: 'Web Scraper',
21
+ description: 'Extract data from websites automatically',
22
+ thumbnail: '/gallery/scraper.png',
23
+ author: 'makecc',
24
+ downloads: 892,
25
+ tags: ['data', 'automation'],
26
+ },
27
+ {
28
+ id: 'document-analyzer',
29
+ name: 'Document Analyzer',
30
+ description: 'Analyze and summarize PDF documents',
31
+ thumbnail: '/gallery/docs.png',
32
+ author: 'makecc',
33
+ downloads: 567,
34
+ tags: ['pdf', 'analysis'],
35
+ },
36
+ ];
37
+ class ProjectService {
38
+ makeccHome;
39
+ constructor() {
40
+ this.makeccHome = MAKECC_HOME;
41
+ }
42
+ getMakeccHome() {
43
+ return this.makeccHome;
44
+ }
45
+ async ensureMakeccHome() {
46
+ if (!existsSync(this.makeccHome)) {
47
+ await fs.mkdir(this.makeccHome, { recursive: true });
48
+ console.log(`Created makecc home directory: ${this.makeccHome}`);
49
+ }
50
+ }
51
+ async listProjects() {
52
+ await this.ensureMakeccHome();
53
+ const entries = await fs.readdir(this.makeccHome, { withFileTypes: true });
54
+ const projects = [];
55
+ for (const entry of entries) {
56
+ if (!entry.isDirectory())
57
+ continue;
58
+ if (entry.name.startsWith('.'))
59
+ continue;
60
+ const projectPath = join(this.makeccHome, entry.name);
61
+ const project = await this.loadProjectInfo(projectPath, entry.name);
62
+ if (project) {
63
+ projects.push(project);
64
+ }
65
+ }
66
+ // Sort by lastModified (most recent first)
67
+ projects.sort((a, b) => new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime());
68
+ return projects;
69
+ }
70
+ async loadProjectInfo(projectPath, name) {
71
+ try {
72
+ const stat = await fs.stat(projectPath);
73
+ // Read project.json if exists
74
+ const configPath = join(projectPath, 'project.json');
75
+ let config = {};
76
+ if (existsSync(configPath)) {
77
+ const content = await fs.readFile(configPath, 'utf-8');
78
+ config = JSON.parse(content);
79
+ }
80
+ // Count skills and agents
81
+ const claudePath = join(projectPath, '.claude');
82
+ let skillCount = 0;
83
+ let agentCount = 0;
84
+ if (existsSync(claudePath)) {
85
+ const skillsPath = join(claudePath, 'skills');
86
+ const agentsPath = join(claudePath, 'agents');
87
+ if (existsSync(skillsPath)) {
88
+ const skills = await fs.readdir(skillsPath, { withFileTypes: true });
89
+ skillCount = skills.filter(s => s.isDirectory()).length;
90
+ }
91
+ if (existsSync(agentsPath)) {
92
+ const agents = await fs.readdir(agentsPath);
93
+ agentCount = agents.filter(a => a.endsWith('.md')).length;
94
+ }
95
+ }
96
+ return {
97
+ id: config.id || name,
98
+ name,
99
+ description: config.description || '',
100
+ skillCount,
101
+ agentCount,
102
+ path: projectPath,
103
+ lastModified: stat.mtime.toISOString(),
104
+ createdAt: config.createdAt || stat.birthtime.toISOString(),
105
+ };
106
+ }
107
+ catch (error) {
108
+ console.error(`Failed to load project info for ${name}:`, error);
109
+ return null;
110
+ }
111
+ }
112
+ async createProject(name, description) {
113
+ await this.ensureMakeccHome();
114
+ // Sanitize project name
115
+ const sanitizedName = name
116
+ .toLowerCase()
117
+ .replace(/[^a-z0-9-]/g, '-')
118
+ .replace(/-+/g, '-')
119
+ .replace(/^-|-$/g, '');
120
+ const projectPath = join(this.makeccHome, sanitizedName);
121
+ if (existsSync(projectPath)) {
122
+ throw new Error(`Project "${sanitizedName}" already exists`);
123
+ }
124
+ // Create project directory structure
125
+ await fs.mkdir(projectPath, { recursive: true });
126
+ await fs.mkdir(join(projectPath, '.claude', 'skills'), { recursive: true });
127
+ await fs.mkdir(join(projectPath, '.claude', 'agents'), { recursive: true });
128
+ // Create project.json
129
+ const projectId = nanoid(10);
130
+ const now = new Date().toISOString();
131
+ const config = {
132
+ id: projectId,
133
+ name: sanitizedName,
134
+ description,
135
+ createdAt: now,
136
+ };
137
+ await fs.writeFile(join(projectPath, 'project.json'), JSON.stringify(config, null, 2));
138
+ // Create CLAUDE.md
139
+ const claudeMd = `# ${name}
140
+
141
+ ${description}
142
+
143
+ ## Project Structure
144
+
145
+ \`\`\`
146
+ ${sanitizedName}/
147
+ ├── .claude/
148
+ │ ├── skills/ # Claude Code skills
149
+ │ └── agents/ # Claude Code agents
150
+ └── project.json # Project configuration
151
+ \`\`\`
152
+
153
+ ## Getting Started
154
+
155
+ 1. Open this project in makecc
156
+ 2. Create skills using natural language
157
+ 3. Build workflows to automate tasks
158
+ `;
159
+ await fs.writeFile(join(projectPath, 'CLAUDE.md'), claudeMd);
160
+ return {
161
+ id: projectId,
162
+ name: sanitizedName,
163
+ description,
164
+ skillCount: 0,
165
+ agentCount: 0,
166
+ path: projectPath,
167
+ lastModified: now,
168
+ createdAt: now,
169
+ };
170
+ }
171
+ async deleteProject(projectId) {
172
+ const projects = await this.listProjects();
173
+ const project = projects.find(p => p.id === projectId || p.name === projectId);
174
+ if (!project) {
175
+ throw new Error(`Project "${projectId}" not found`);
176
+ }
177
+ // Move to trash instead of permanent delete
178
+ const trashPath = join(this.makeccHome, '.trash');
179
+ await fs.mkdir(trashPath, { recursive: true });
180
+ const trashName = `${project.name}_${Date.now()}`;
181
+ await fs.rename(project.path, join(trashPath, trashName));
182
+ }
183
+ async getProject(projectId) {
184
+ const projects = await this.listProjects();
185
+ return projects.find(p => p.id === projectId || p.name === projectId) || null;
186
+ }
187
+ getGalleryItems() {
188
+ return GALLERY_ITEMS;
189
+ }
190
+ async createSampleProjects() {
191
+ await this.ensureMakeccHome();
192
+ // Check if samples already exist
193
+ const projects = await this.listProjects();
194
+ const hasGmail = projects.some(p => p.name === 'gmail-assistant');
195
+ const hasScraper = projects.some(p => p.name === 'web-scraper');
196
+ if (!hasGmail) {
197
+ await this.createGmailSampleProject();
198
+ }
199
+ if (!hasScraper) {
200
+ await this.createScraperSampleProject();
201
+ }
202
+ }
203
+ async createGmailSampleProject() {
204
+ const projectPath = join(this.makeccHome, 'gmail-assistant');
205
+ await fs.mkdir(projectPath, { recursive: true });
206
+ await fs.mkdir(join(projectPath, '.claude', 'skills', 'gmail'), { recursive: true });
207
+ await fs.mkdir(join(projectPath, '.claude', 'agents'), { recursive: true });
208
+ // project.json
209
+ await fs.writeFile(join(projectPath, 'project.json'), JSON.stringify({
210
+ id: 'gmail-sample',
211
+ name: 'gmail-assistant',
212
+ description: 'Read, write and organize emails using Claude',
213
+ createdAt: new Date().toISOString(),
214
+ }, null, 2));
215
+ // Sample skill: SKILL.md
216
+ const skillMd = `---
217
+ name: gmail
218
+ description: Gmail 읽기/쓰기 스킬
219
+ ---
220
+
221
+ # Gmail Skill
222
+
223
+ 이 스킬은 Gmail API를 사용하여 이메일을 읽고 쓸 수 있게 합니다.
224
+
225
+ ## 사용법
226
+
227
+ \`\`\`bash
228
+ python scripts/main.py read --count 10
229
+ python scripts/main.py send --to "user@example.com" --subject "Hello" --body "Hi there!"
230
+ \`\`\`
231
+
232
+ ## 기능
233
+
234
+ - 받은편지함 읽기
235
+ - 이메일 검색
236
+ - 이메일 작성 및 전송
237
+ - 라벨 관리
238
+ `;
239
+ await fs.writeFile(join(projectPath, '.claude', 'skills', 'gmail', 'SKILL.md'), skillMd);
240
+ // Sample Python script
241
+ const mainPy = `#!/usr/bin/env python3
242
+ """Gmail skill for reading and writing emails."""
243
+
244
+ import argparse
245
+ import json
246
+
247
+ def read_emails(count: int = 10):
248
+ """Read recent emails from inbox."""
249
+ print(json.dumps({
250
+ "status": "success",
251
+ "message": f"Would read {count} emails from Gmail",
252
+ "note": "This is a sample - configure Gmail API credentials to use"
253
+ }))
254
+
255
+ def send_email(to: str, subject: str, body: str):
256
+ """Send an email."""
257
+ print(json.dumps({
258
+ "status": "success",
259
+ "message": f"Would send email to {to}",
260
+ "subject": subject,
261
+ "note": "This is a sample - configure Gmail API credentials to use"
262
+ }))
263
+
264
+ def main():
265
+ parser = argparse.ArgumentParser(description='Gmail Skill')
266
+ subparsers = parser.add_subparsers(dest='command', help='Commands')
267
+
268
+ # Read command
269
+ read_parser = subparsers.add_parser('read', help='Read emails')
270
+ read_parser.add_argument('--count', type=int, default=10, help='Number of emails')
271
+
272
+ # Send command
273
+ send_parser = subparsers.add_parser('send', help='Send email')
274
+ send_parser.add_argument('--to', required=True, help='Recipient')
275
+ send_parser.add_argument('--subject', required=True, help='Subject')
276
+ send_parser.add_argument('--body', required=True, help='Body')
277
+
278
+ args = parser.parse_args()
279
+
280
+ if args.command == 'read':
281
+ read_emails(args.count)
282
+ elif args.command == 'send':
283
+ send_email(args.to, args.subject, args.body)
284
+ else:
285
+ parser.print_help()
286
+
287
+ if __name__ == '__main__':
288
+ main()
289
+ `;
290
+ await fs.mkdir(join(projectPath, '.claude', 'skills', 'gmail', 'scripts'), { recursive: true });
291
+ await fs.writeFile(join(projectPath, '.claude', 'skills', 'gmail', 'scripts', 'main.py'), mainPy);
292
+ console.log('Created sample project: gmail-assistant');
293
+ }
294
+ async createScraperSampleProject() {
295
+ const projectPath = join(this.makeccHome, 'web-scraper');
296
+ await fs.mkdir(projectPath, { recursive: true });
297
+ await fs.mkdir(join(projectPath, '.claude', 'agents'), { recursive: true });
298
+ // project.json
299
+ await fs.writeFile(join(projectPath, 'project.json'), JSON.stringify({
300
+ id: 'scraper-sample',
301
+ name: 'web-scraper',
302
+ description: 'Extract data from websites automatically',
303
+ createdAt: new Date().toISOString(),
304
+ }, null, 2));
305
+ // Sample agent
306
+ const agentMd = `---
307
+ name: web-scraper
308
+ description: 웹 페이지에서 데이터를 추출하는 에이전트
309
+ tools: WebFetch, Read, Write
310
+ model: sonnet
311
+ ---
312
+
313
+ # Web Scraper Agent
314
+
315
+ 이 에이전트는 웹 페이지에서 데이터를 추출하고 구조화합니다.
316
+
317
+ ## 워크플로우
318
+
319
+ 1. URL을 입력받습니다
320
+ 2. WebFetch로 페이지 내용을 가져옵니다
321
+ 3. 원하는 데이터를 추출합니다
322
+ 4. JSON 또는 CSV로 결과를 저장합니다
323
+
324
+ ## 사용 예시
325
+
326
+ "https://example.com에서 모든 제품 이름과 가격을 추출해줘"
327
+ `;
328
+ await fs.writeFile(join(projectPath, '.claude', 'agents', 'web-scraper.md'), agentMd);
329
+ console.log('Created sample project: web-scraper');
330
+ }
331
+ }
332
+ export const projectService = new ProjectService();
@@ -4,23 +4,25 @@ import * as path from 'path';
4
4
  import { existsSync } from 'fs';
5
5
  import { spawn } from 'child_process';
6
6
  import { claudeMdService } from './claudeMdService';
7
- const SYSTEM_PROMPT = `You are a Claude Code skill generator. You MUST respond with ONLY a valid JSON object. No markdown, no code blocks, no explanations - just pure JSON.
7
+ const SYSTEM_PROMPT = `You are an expert Claude Code skill generator. Generate production-quality skills with complete, working code.
8
8
 
9
- Your response must follow this exact JSON schema:
9
+ RESPOND WITH ONLY A VALID JSON OBJECT - NO MARKDOWN, NO CODE BLOCKS, NO EXPLANATIONS.
10
+
11
+ ## JSON Schema
10
12
 
11
13
  {
12
- "skillName": "Human readable skill name",
14
+ "skillName": "Human Readable Skill Name",
13
15
  "skillId": "skill-id-in-kebab-case",
14
- "description": "Brief description of what this skill does",
16
+ "description": "Comprehensive description including what it does AND when to use it. Include trigger phrases in Korean. Example: 'PDF 문서에서 텍스트 추출 및 분석. \"PDF 읽어줘\", \"PDF 분석해줘\", \"PDF에서 텍스트 추출\" 등의 요청 시 사용.'",
15
17
  "files": [
16
18
  {
17
19
  "path": "SKILL.md",
18
- "content": "Full SKILL.md content here",
20
+ "content": "Full SKILL.md content (see format below)",
19
21
  "language": "markdown"
20
22
  },
21
23
  {
22
24
  "path": "scripts/main.py",
23
- "content": "Full Python script content here",
25
+ "content": "Full Python script (150-300 lines, production quality)",
24
26
  "language": "python"
25
27
  },
26
28
  {
@@ -31,36 +33,155 @@ Your response must follow this exact JSON schema:
31
33
  ]
32
34
  }
33
35
 
34
- SKILL.md must follow this format:
36
+ ## SKILL.md Format (MUST follow exactly)
37
+
35
38
  ---
36
39
  name: skill-id
37
- description: One line description
40
+ description: Detailed description with trigger phrases. Include what it does AND specific Korean trigger phrases like "~해줘", "~만들어줘". This is the PRIMARY mechanism for skill activation.
38
41
  ---
39
42
 
40
43
  # Skill Name
41
44
 
42
- ## When to use
43
- - Use case 1
44
- - Use case 2
45
+ Brief description of what this skill does.
46
+
47
+ ## 실행 요구사항 (필수)
48
+
49
+ List any prerequisites:
50
+ - API keys needed (with instructions to check/request)
51
+ - Environment setup
52
+ - Dependencies
53
+
54
+ ## 빠른 시작
55
+
56
+ \\\`\\\`\\\`bash
57
+ .venv/bin/python .claude/skills/skill-id/scripts/main.py \\\\
58
+ --required-arg "value" \\\\
59
+ --output output.ext
60
+ \\\`\\\`\\\`
61
+
62
+ ## 스크립트 옵션
63
+
64
+ | 옵션 | 설명 | 기본값 |
65
+ |------|------|--------|
66
+ | \\\`--arg1\\\`, \\\`-a\\\` | Description | default |
67
+ | \\\`--output\\\`, \\\`-o\\\` | 출력 경로 (필수) | - |
68
+
69
+ ## 사용 예시
70
+
71
+ ### 예시 1: Basic Usage
72
+ \\\`\\\`\\\`bash
73
+ .venv/bin/python .claude/skills/skill-id/scripts/main.py \\\\
74
+ --arg "value" --output result.ext
75
+ \\\`\\\`\\\`
76
+
77
+ ### 예시 2: Advanced Usage
78
+ \\\`\\\`\\\`bash
79
+ .venv/bin/python .claude/skills/skill-id/scripts/main.py \\\\
80
+ --arg "value" --advanced-option
81
+ \\\`\\\`\\\`
82
+
83
+ ## 제한사항
84
+
85
+ - List any limitations or constraints
86
+
87
+ ## Python Script Requirements (scripts/main.py)
88
+
89
+ The script MUST:
90
+ 1. Be 150-300 lines of COMPLETE, WORKING code
91
+ 2. Use argparse with --help support
92
+ 3. Include comprehensive error handling (try-except)
93
+ 4. Print Korean status messages with emoji (✅ 완료, ❌ 오류, ⏳ 처리 중)
94
+ 5. Check dependencies at startup with helpful install instructions
95
+ 6. Support common use cases with sensible defaults
96
+ 7. Include docstring with usage examples
97
+
98
+ ## Script Template Structure
99
+
100
+ \\\`\\\`\\\`python
101
+ #!/usr/bin/env python3
102
+ """
103
+ Skill Name - Brief description
104
+
105
+ Usage:
106
+ python main.py --arg "value" --output output.ext
107
+
108
+ Examples:
109
+ python main.py --input data.txt --output result.txt
110
+ """
111
+
112
+ import argparse
113
+ import sys
114
+ from pathlib import Path
115
+
116
+ def check_dependencies():
117
+ """Check required packages"""
118
+ try:
119
+ import required_package
120
+ return True
121
+ except ImportError:
122
+ print("❌ required_package가 설치되어 있지 않습니다.")
123
+ print(" 설치: uv pip install --python .venv/bin/python required_package")
124
+ return False
125
+
126
+ def main_function(arg1, arg2, output_path):
127
+ """Main processing logic with error handling"""
128
+ print(f"⏳ 처리 중...")
129
+
130
+ try:
131
+ # Processing logic here
132
+ result = process(arg1, arg2)
133
+
134
+ # Save output
135
+ output_file = Path(output_path)
136
+ output_file.parent.mkdir(parents=True, exist_ok=True)
137
+
138
+ with open(output_file, 'w') as f:
139
+ f.write(result)
140
+
141
+ print(f"✅ 완료!")
142
+ print(f" 파일: {output_file}")
143
+ return str(output_file)
144
+
145
+ except Exception as e:
146
+ print(f"❌ 오류 발생: {e}")
147
+ sys.exit(1)
148
+
149
+ def main():
150
+ parser = argparse.ArgumentParser(
151
+ description="Skill description",
152
+ formatter_class=argparse.RawDescriptionHelpFormatter,
153
+ epilog=\"\"\"
154
+ 예시:
155
+ python main.py --input data.txt --output result.txt
156
+ python main.py --input data.txt --output result.txt --option
157
+ \"\"\"
158
+ )
159
+
160
+ parser.add_argument("--input", "-i", required=True, help="입력 파일")
161
+ parser.add_argument("--output", "-o", required=True, help="출력 경로")
162
+ parser.add_argument("--option", action="store_true", help="옵션 설명")
163
+
164
+ args = parser.parse_args()
165
+
166
+ if not check_dependencies():
167
+ sys.exit(1)
45
168
 
46
- ## Usage
47
- \`\`\`bash
48
- .venv/bin/python .claude/skills/skill-id/scripts/main.py [args]
49
- \`\`\`
169
+ main_function(args.input, args.option, args.output)
50
170
 
51
- ## Parameters
52
- - \`--param1\`: Description
171
+ if __name__ == "__main__":
172
+ main()
173
+ \\\`\\\`\\\`
53
174
 
54
- ## Example
55
- [Usage example]
175
+ ## Critical Rules
56
176
 
57
- RULES:
58
- 1. Generate COMPLETE, WORKING code - no placeholders
59
- 2. Include proper error handling with try-except
60
- 3. Use Korean for user-facing messages
61
- 4. Scripts run with .venv/bin/python (project local virtual environment)
62
- 5. List all required packages in requirements.txt
63
- 6. RESPOND WITH JSON ONLY - NO OTHER TEXT`;
177
+ 1. GENERATE COMPLETE, WORKING CODE - NO PLACEHOLDERS, NO "# TODO", NO "pass"
178
+ 2. Scripts must be 150-300 lines with real implementation
179
+ 3. Include ALL necessary imports and helper functions
180
+ 4. Use Korean for user-facing messages, English for code/logs
181
+ 5. description field MUST include Korean trigger phrases
182
+ 6. SKILL.md MUST have complete usage examples with actual commands
183
+ 7. Always include requirements.txt with specific packages needed
184
+ 8. RESPOND WITH JSON ONLY - NO OTHER TEXT`;
64
185
  export class SkillGeneratorService {
65
186
  projectRoot;
66
187
  constructor(projectRoot) {