makecc 0.2.10 → 0.2.12

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,270 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import { existsSync } from 'fs';
4
+ import { execSync, spawn } from 'child_process';
5
+
6
+ const MAKECC_SECTION_MARKER = '<!-- makecc-managed-section -->';
7
+ const MAKECC_SECTION_END = '<!-- /makecc-managed-section -->';
8
+
9
+ /**
10
+ * makecc가 로컬 프로젝트에 추가할 CLAUDE.md 기본 템플릿
11
+ */
12
+ const MAKECC_CLAUDE_MD_TEMPLATE = `${MAKECC_SECTION_MARKER}
13
+ ## makecc 스킬/워크플로우 규칙
14
+
15
+ 이 섹션은 makecc에 의해 자동 생성되었습니다.
16
+
17
+ ### Python 가상환경
18
+
19
+ **모든 스킬은 프로젝트 로컬 가상환경을 사용합니다:**
20
+
21
+ \`\`\`
22
+ .venv/
23
+ \`\`\`
24
+
25
+ **사용 패턴:**
26
+
27
+ \`\`\`bash
28
+ # 로컬 venv Python 직접 실행 (권장)
29
+ .venv/bin/python script.py
30
+
31
+ # 또는 활성화 후 실행
32
+ source .venv/bin/activate && python script.py
33
+ \`\`\`
34
+
35
+ ### 패키지 관리
36
+
37
+ **uv를 우선 사용합니다 (pip보다 10-100x 빠름):**
38
+
39
+ \`\`\`bash
40
+ # uv로 패키지 설치 (권장)
41
+ uv pip install --python .venv/bin/python package_name
42
+
43
+ # requirements.txt 설치
44
+ uv pip install --python .venv/bin/python -r requirements.txt
45
+
46
+ # uv가 없으면 pip 폴백
47
+ .venv/bin/pip install package_name
48
+ \`\`\`
49
+
50
+ ### 스킬 저장 경로
51
+
52
+ | 항목 | 경로 |
53
+ |------|------|
54
+ | 스킬 | \`.claude/skills/[skill-name]/\` |
55
+ | 에이전트 | \`.claude/agents/[agent-name].md\` |
56
+ | 워크플로우 | \`.claude/agents/[workflow-name].md\` |
57
+
58
+ ### 스킬 구조
59
+
60
+ \`\`\`
61
+ .claude/skills/[skill-name]/
62
+ ├── SKILL.md # 스킬 정의 (필수)
63
+ ├── scripts/
64
+ │ └── main.py # 메인 스크립트
65
+ └── requirements.txt # 의존성 목록
66
+ \`\`\`
67
+
68
+ ### 스킬 실행 규칙
69
+
70
+ 1. **가상환경 사용**: 항상 \`.venv/bin/python\` 사용
71
+ 2. **의존성 설치**: \`uv pip install\` 우선, 실패 시 \`pip\` 폴백
72
+ 3. **경로 참조**: 상대 경로 대신 절대 경로 사용 권장
73
+ 4. **한글 지원**: 사용자 메시지는 한글로 출력
74
+
75
+ ${MAKECC_SECTION_END}
76
+ `;
77
+
78
+ export class ClaudeMdService {
79
+ private projectRoot: string;
80
+
81
+ constructor(projectRoot?: string) {
82
+ this.projectRoot = projectRoot || process.env.MAKECC_PROJECT_PATH || process.cwd();
83
+ }
84
+
85
+ /**
86
+ * CLAUDE.md 파일 경로
87
+ */
88
+ private getClaudeMdPath(): string {
89
+ return path.join(this.projectRoot, 'CLAUDE.md');
90
+ }
91
+
92
+ /**
93
+ * CLAUDE.md 내용 읽기
94
+ */
95
+ async read(): Promise<string | null> {
96
+ const claudeMdPath = this.getClaudeMdPath();
97
+ if (!existsSync(claudeMdPath)) {
98
+ return null;
99
+ }
100
+ return fs.readFile(claudeMdPath, 'utf-8');
101
+ }
102
+
103
+ /**
104
+ * makecc 섹션이 있는지 확인
105
+ */
106
+ hasMakeccSection(content: string): boolean {
107
+ return content.includes(MAKECC_SECTION_MARKER);
108
+ }
109
+
110
+ /**
111
+ * CLAUDE.md 초기화 - 없으면 생성, 있으면 섹션 추가
112
+ */
113
+ async initialize(): Promise<{ created: boolean; updated: boolean; path: string }> {
114
+ const claudeMdPath = this.getClaudeMdPath();
115
+ const result = { created: false, updated: false, path: claudeMdPath };
116
+
117
+ const existingContent = await this.read();
118
+
119
+ if (existingContent === null) {
120
+ // 파일이 없으면 새로 생성
121
+ const newContent = `# CLAUDE.md
122
+
123
+ 이 파일은 Claude Code가 이 프로젝트의 코드를 다룰 때 참고하는 가이드입니다.
124
+
125
+ ${MAKECC_CLAUDE_MD_TEMPLATE}
126
+ `;
127
+ await fs.writeFile(claudeMdPath, newContent, 'utf-8');
128
+ result.created = true;
129
+ console.log(`Created CLAUDE.md at ${claudeMdPath}`);
130
+ } else if (!this.hasMakeccSection(existingContent)) {
131
+ // 파일은 있지만 makecc 섹션이 없으면 추가
132
+ const updatedContent = existingContent.trimEnd() + '\n\n' + MAKECC_CLAUDE_MD_TEMPLATE;
133
+ await fs.writeFile(claudeMdPath, updatedContent, 'utf-8');
134
+ result.updated = true;
135
+ console.log(`Added makecc section to CLAUDE.md at ${claudeMdPath}`);
136
+ } else {
137
+ console.log(`CLAUDE.md already has makecc section at ${claudeMdPath}`);
138
+ }
139
+
140
+ return result;
141
+ }
142
+
143
+ /**
144
+ * 프로젝트 로컬 .venv 초기화
145
+ */
146
+ async initializeVenv(): Promise<{ created: boolean; path: string }> {
147
+ const venvPath = path.join(this.projectRoot, '.venv');
148
+ const result = { created: false, path: venvPath };
149
+
150
+ if (existsSync(venvPath)) {
151
+ console.log(`Virtual environment already exists at ${venvPath}`);
152
+ return result;
153
+ }
154
+
155
+ console.log(`Creating virtual environment at ${venvPath}...`);
156
+
157
+ try {
158
+ // uv가 있으면 uv venv 사용 (더 빠름)
159
+ if (this.checkCommandExists('uv')) {
160
+ execSync(`uv venv "${venvPath}"`, { cwd: this.projectRoot, stdio: 'inherit' });
161
+ } else {
162
+ // python -m venv 사용
163
+ execSync(`python3 -m venv "${venvPath}"`, { cwd: this.projectRoot, stdio: 'inherit' });
164
+ }
165
+ result.created = true;
166
+ console.log(`Created virtual environment at ${venvPath}`);
167
+ } catch (error) {
168
+ console.error('Failed to create virtual environment:', error);
169
+ throw error;
170
+ }
171
+
172
+ return result;
173
+ }
174
+
175
+ /**
176
+ * uv 설치 확인 및 자동 설치
177
+ */
178
+ async ensureUvInstalled(): Promise<{ installed: boolean; alreadyExists: boolean }> {
179
+ const result = { installed: false, alreadyExists: false };
180
+
181
+ if (this.checkCommandExists('uv')) {
182
+ result.alreadyExists = true;
183
+ console.log('uv is already installed');
184
+ return result;
185
+ }
186
+
187
+ console.log('uv not found, attempting to install...');
188
+
189
+ try {
190
+ // curl 스크립트로 설치
191
+ await this.runCommand('curl', ['-LsSf', 'https://astral.sh/uv/install.sh', '-o', '/tmp/uv-install.sh']);
192
+ await this.runCommand('sh', ['/tmp/uv-install.sh']);
193
+
194
+ // PATH에 추가된 uv 확인
195
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '';
196
+ const uvPath = path.join(homeDir, '.local', 'bin', 'uv');
197
+
198
+ if (existsSync(uvPath)) {
199
+ // PATH에 추가
200
+ process.env.PATH = `${path.dirname(uvPath)}:${process.env.PATH}`;
201
+ result.installed = true;
202
+ console.log('uv installed successfully');
203
+ } else {
204
+ console.warn('uv installation may have failed, falling back to pip');
205
+ }
206
+ } catch (error) {
207
+ console.error('Failed to install uv:', error);
208
+ console.log('Will fall back to pip for package management');
209
+ }
210
+
211
+ return result;
212
+ }
213
+
214
+ /**
215
+ * 전체 초기화 실행
216
+ */
217
+ async setup(): Promise<{
218
+ claudeMd: { created: boolean; updated: boolean; path: string };
219
+ venv: { created: boolean; path: string };
220
+ uv: { installed: boolean; alreadyExists: boolean };
221
+ }> {
222
+ console.log('\n=== makecc Project Setup ===\n');
223
+ console.log(`Project root: ${this.projectRoot}\n`);
224
+
225
+ // 1. uv 설치 확인
226
+ const uvResult = await this.ensureUvInstalled();
227
+
228
+ // 2. CLAUDE.md 초기화
229
+ const claudeMdResult = await this.initialize();
230
+
231
+ // 3. .venv 초기화
232
+ const venvResult = await this.initializeVenv();
233
+
234
+ console.log('\n=== Setup Complete ===\n');
235
+
236
+ return {
237
+ claudeMd: claudeMdResult,
238
+ venv: venvResult,
239
+ uv: uvResult,
240
+ };
241
+ }
242
+
243
+ /**
244
+ * 명령어 존재 확인
245
+ */
246
+ private checkCommandExists(cmd: string): boolean {
247
+ try {
248
+ execSync(`which ${cmd}`, { stdio: 'ignore' });
249
+ return true;
250
+ } catch {
251
+ return false;
252
+ }
253
+ }
254
+
255
+ /**
256
+ * 명령어 실행 (Promise)
257
+ */
258
+ private runCommand(command: string, args: string[]): Promise<void> {
259
+ return new Promise((resolve, reject) => {
260
+ const proc = spawn(command, args, { stdio: 'inherit' });
261
+ proc.on('close', (code) => {
262
+ if (code === 0) resolve();
263
+ else reject(new Error(`Command failed with code ${code}`));
264
+ });
265
+ proc.on('error', reject);
266
+ });
267
+ }
268
+ }
269
+
270
+ export const claudeMdService = new ClaudeMdService();
@@ -2,8 +2,9 @@ import Anthropic from '@anthropic-ai/sdk';
2
2
  import * as fs from 'fs/promises';
3
3
  import * as path from 'path';
4
4
  import { existsSync } from 'fs';
5
- import { spawn } from 'child_process';
5
+ import { spawn, execSync } from 'child_process';
6
6
  import type { ApiSettings } from './workflowAIService';
7
+ import { claudeMdService } from './claudeMdService';
7
8
 
8
9
  export interface GeneratedSkill {
9
10
  skillName: string;
@@ -82,7 +83,7 @@ description: One line description
82
83
 
83
84
  ## Usage
84
85
  \`\`\`bash
85
- ~/.claude/venv/bin/python ~/.claude/skills/skill-id/scripts/main.py [args]
86
+ .venv/bin/python .claude/skills/skill-id/scripts/main.py [args]
86
87
  \`\`\`
87
88
 
88
89
  ## Parameters
@@ -95,7 +96,7 @@ RULES:
95
96
  1. Generate COMPLETE, WORKING code - no placeholders
96
97
  2. Include proper error handling with try-except
97
98
  3. Use Korean for user-facing messages
98
- 4. Scripts run with ~/.claude/venv/bin/python
99
+ 4. Scripts run with .venv/bin/python (project local virtual environment)
99
100
  5. List all required packages in requirements.txt
100
101
  6. RESPOND WITH JSON ONLY - NO OTHER TEXT`;
101
102
 
@@ -107,23 +108,39 @@ export class SkillGeneratorService {
107
108
  }
108
109
 
109
110
  private getClient(settings?: ApiSettings): Anthropic {
110
- if (settings?.apiMode === 'direct' && settings.apiKey) {
111
- return new Anthropic({ apiKey: settings.apiKey });
112
- }
111
+ const DEFAULT_ANTHROPIC_URL = 'https://api.anthropic.com';
113
112
 
114
- if (settings?.apiMode === 'proxy' && settings.proxyUrl) {
113
+ // 1. 프록시 우선: proxyUrl 설정되어 있고, 기본 Anthropic URL이 아닌 경우
114
+ // 프록시 서버가 API 키를 관리하므로 클라이언트는 키 불필요
115
+ if (settings?.proxyUrl && settings.proxyUrl !== DEFAULT_ANTHROPIC_URL) {
116
+ console.log(`Using proxy server: ${settings.proxyUrl}`);
115
117
  return new Anthropic({
116
118
  baseURL: settings.proxyUrl,
117
- apiKey: process.env.ANTHROPIC_API_KEY || 'proxy-mode',
119
+ apiKey: 'proxy-mode', // 프록시 서버가 실제 키를 관리
118
120
  });
119
121
  }
120
122
 
123
+ // 2. 환경변수에 API 키가 있으면 직접 호출
121
124
  const envApiKey = process.env.ANTHROPIC_API_KEY;
122
- if (!envApiKey) {
123
- throw new Error('API 키가 설정되지 않았습니다. 설정에서 API 키를 입력하세요.');
125
+ if (envApiKey) {
126
+ console.log('Using API key from environment variable');
127
+ return new Anthropic({ apiKey: envApiKey });
128
+ }
129
+
130
+ // 3. UI에서 direct 모드로 API 키를 직접 입력한 경우
131
+ if (settings?.apiMode === 'direct' && settings.apiKey) {
132
+ console.log('Using API key from UI settings');
133
+ return new Anthropic({ apiKey: settings.apiKey });
124
134
  }
125
135
 
126
- return new Anthropic({ apiKey: envApiKey });
136
+ // 4. 아무것도 없으면 에러
137
+ throw new Error(
138
+ 'API 키가 설정되지 않았습니다.\n' +
139
+ '해결 방법:\n' +
140
+ '1. 프록시 서버 URL을 설정하거나\n' +
141
+ '2. 프로젝트 .env 파일에 ANTHROPIC_API_KEY를 추가하거나\n' +
142
+ '3. 설정에서 Direct 모드로 API 키를 직접 입력하세요.'
143
+ );
127
144
  }
128
145
 
129
146
  async generate(
@@ -147,9 +164,21 @@ export class SkillGeneratorService {
147
164
  detail: '어떤 스킬이 필요한지 파악 중...',
148
165
  });
149
166
 
167
+ // CLAUDE.md 내용 읽기
168
+ let claudeMdContext = '';
169
+ const claudeMdContent = await claudeMdService.read();
170
+ if (claudeMdContent) {
171
+ claudeMdContext = `\n\n<project_context>
172
+ The following is the project's CLAUDE.md file. Follow these guidelines when generating the skill:
173
+
174
+ ${claudeMdContent}
175
+ </project_context>`;
176
+ console.log('Including CLAUDE.md in API context');
177
+ }
178
+
150
179
  const userPrompt = `Create a skill for: "${prompt}"
151
180
 
152
- Generate complete, working code. Respond with JSON only.`;
181
+ Generate complete, working code. Respond with JSON only.${claudeMdContext}`;
153
182
 
154
183
  try {
155
184
  progress({
@@ -285,41 +314,42 @@ Generate complete, working code. Respond with JSON only.`;
285
314
  progress: SkillProgressCallback
286
315
  ): Promise<void> {
287
316
  return new Promise((resolve, reject) => {
288
- const homeDir = process.env.HOME || process.env.USERPROFILE || '';
289
- const venvPythonPath = path.join(homeDir, '.claude', 'venv', 'bin', 'python');
317
+ // 로컬 프로젝트의 .venv 사용
318
+ const localVenvPythonPath = path.join(this.projectRoot, '.venv', 'bin', 'python');
319
+ const localVenvPipPath = path.join(this.projectRoot, '.venv', 'bin', 'pip');
290
320
 
291
321
  let command: string;
292
322
  let args: string[];
293
323
 
294
324
  // uv를 우선 사용 (10-100x 빠름)
295
- // uv pip install --python <venv-python> -r requirements.txt
296
325
  const useUv = this.checkCommandExists('uv');
297
326
 
298
- if (useUv && existsSync(venvPythonPath)) {
327
+ if (useUv && existsSync(localVenvPythonPath)) {
299
328
  command = 'uv';
300
- args = ['pip', 'install', '--python', venvPythonPath, '-r', requirementsPath];
329
+ args = ['pip', 'install', '--python', localVenvPythonPath, '-r', requirementsPath];
301
330
  progress({
302
331
  step: 'installing',
303
332
  message: '⚡ uv로 패키지 설치 중 (고속)',
304
- detail: 'uv pip install 실행 중...',
333
+ detail: 'uv pip install .venv/',
305
334
  });
306
- } else if (existsSync(path.join(homeDir, '.claude', 'venv', 'bin', 'pip'))) {
307
- // fallback: 전역 venv pip 사용
308
- command = path.join(homeDir, '.claude', 'venv', 'bin', 'pip');
335
+ } else if (existsSync(localVenvPipPath)) {
336
+ // fallback: 로컬 venv pip 사용
337
+ command = localVenvPipPath;
309
338
  args = ['install', '-r', requirementsPath];
310
339
  progress({
311
340
  step: 'installing',
312
341
  message: '📦 pip으로 패키지 설치 중',
313
- detail: 'pip install 실행 중...',
342
+ detail: 'pip install .venv/',
314
343
  });
315
344
  } else {
316
- // fallback: 시스템 pip 사용
345
+ // fallback: 시스템 pip 사용 (경고)
346
+ console.warn('Warning: .venv not found, using system pip');
317
347
  command = 'pip3';
318
348
  args = ['install', '-r', requirementsPath];
319
349
  progress({
320
350
  step: 'installing',
321
- message: '📦 pip으로 패키지 설치 중',
322
- detail: 'pip install 실행 중...',
351
+ message: '⚠️ 시스템 pip 사용 중',
352
+ detail: '.venv가 없어 시스템 pip 사용',
323
353
  });
324
354
  }
325
355
 
@@ -165,31 +165,39 @@ ${AVAILABLE_TOOLS.join(', ')}
165
165
 
166
166
  export class WorkflowAIService {
167
167
  private getClient(settings?: ApiSettings): Anthropic {
168
- // 1. Direct mode with user-provided API key
169
- if (settings?.apiMode === 'direct' && settings.apiKey) {
170
- return new Anthropic({
171
- apiKey: settings.apiKey,
172
- });
173
- }
168
+ const DEFAULT_ANTHROPIC_URL = 'https://api.anthropic.com';
174
169
 
175
- // 2. Proxy mode with custom base URL
176
- if (settings?.apiMode === 'proxy' && settings.proxyUrl) {
170
+ // 1. 프록시 우선: proxyUrl이 설정되어 있고, 기본 Anthropic URL이 아닌 경우
171
+ // 프록시 서버가 API 키를 관리하므로 클라이언트는 키 불필요
172
+ if (settings?.proxyUrl && settings.proxyUrl !== DEFAULT_ANTHROPIC_URL) {
173
+ console.log(`Using proxy server: ${settings.proxyUrl}`);
177
174
  return new Anthropic({
178
175
  baseURL: settings.proxyUrl,
179
- // Proxy server handles the API key
180
- apiKey: process.env.ANTHROPIC_API_KEY || 'proxy-mode',
176
+ apiKey: 'proxy-mode', // 프록시 서버가 실제 키를 관리
181
177
  });
182
178
  }
183
179
 
184
- // 3. Default: use environment variable
180
+ // 2. 환경변수에 API 키가 있으면 직접 호출
185
181
  const envApiKey = process.env.ANTHROPIC_API_KEY;
186
- if (!envApiKey) {
187
- throw new Error('API 키가 설정되지 않았습니다. 설정에서 API 키를 입력하거나 프록시 서버를 설정하세요.');
182
+ if (envApiKey) {
183
+ console.log('Using API key from environment variable');
184
+ return new Anthropic({ apiKey: envApiKey });
188
185
  }
189
186
 
190
- return new Anthropic({
191
- apiKey: envApiKey,
192
- });
187
+ // 3. UI에서 direct 모드로 API 키를 직접 입력한 경우
188
+ if (settings?.apiMode === 'direct' && settings.apiKey) {
189
+ console.log('Using API key from UI settings');
190
+ return new Anthropic({ apiKey: settings.apiKey });
191
+ }
192
+
193
+ // 4. 아무것도 없으면 에러
194
+ throw new Error(
195
+ 'API 키가 설정되지 않았습니다.\n' +
196
+ '해결 방법:\n' +
197
+ '1. 프록시 서버 URL을 설정하거나\n' +
198
+ '2. 프로젝트 .env 파일에 ANTHROPIC_API_KEY를 추가하거나\n' +
199
+ '3. 설정에서 Direct 모드로 API 키를 직접 입력하세요.'
200
+ );
193
201
  }
194
202
 
195
203
  async generate(prompt: string, settings?: ApiSettings): Promise<AIWorkflowResult> {