claudeApi.ts
1 import { readdir } from 'fs/promises' 2 import { getCwd } from '../../utils/cwd.js' 3 import { registerBundledSkill } from '../bundledSkills.js' 4 5 // claudeApiContent.js bundles 247KB of .md strings. Lazy-load inside 6 // getPromptForCommand so they only enter memory when /claude-api is invoked. 7 type SkillContent = typeof import('./claudeApiContent.js') 8 9 type DetectedLanguage = 10 | 'python' 11 | 'typescript' 12 | 'java' 13 | 'go' 14 | 'ruby' 15 | 'csharp' 16 | 'php' 17 | 'curl' 18 19 const LANGUAGE_INDICATORS: Record<DetectedLanguage, string[]> = { 20 python: ['.py', 'requirements.txt', 'pyproject.toml', 'setup.py', 'Pipfile'], 21 typescript: ['.ts', '.tsx', 'tsconfig.json', 'package.json'], 22 java: ['.java', 'pom.xml', 'build.gradle'], 23 go: ['.go', 'go.mod'], 24 ruby: ['.rb', 'Gemfile'], 25 csharp: ['.cs', '.csproj'], 26 php: ['.php', 'composer.json'], 27 curl: [], 28 } 29 30 async function detectLanguage(): Promise<DetectedLanguage | null> { 31 const cwd = getCwd() 32 let entries: string[] 33 try { 34 entries = await readdir(cwd) 35 } catch { 36 return null 37 } 38 39 for (const [lang, indicators] of Object.entries(LANGUAGE_INDICATORS) as [ 40 DetectedLanguage, 41 string[], 42 ][]) { 43 if (indicators.length === 0) continue 44 for (const indicator of indicators) { 45 if (indicator.startsWith('.')) { 46 if (entries.some(e => e.endsWith(indicator))) return lang 47 } else { 48 if (entries.includes(indicator)) return lang 49 } 50 } 51 } 52 return null 53 } 54 55 function getFilesForLanguage( 56 lang: DetectedLanguage, 57 content: SkillContent, 58 ): string[] { 59 return Object.keys(content.SKILL_FILES).filter( 60 path => path.startsWith(`${lang}/`) || path.startsWith('shared/'), 61 ) 62 } 63 64 function processContent(md: string, content: SkillContent): string { 65 // Strip HTML comments. Loop to handle nested comments. 66 let out = md 67 let prev 68 do { 69 prev = out 70 out = out.replace(/<!--[\s\S]*?-->\n?/g, '') 71 } while (out !== prev) 72 73 out = out.replace( 74 /\{\{(\w+)\}\}/g, 75 (match, key: string) => 76 (content.SKILL_MODEL_VARS as Record<string, string>)[key] ?? match, 77 ) 78 return out 79 } 80 81 function buildInlineReference( 82 filePaths: string[], 83 content: SkillContent, 84 ): string { 85 const sections: string[] = [] 86 for (const filePath of filePaths.sort()) { 87 const md = content.SKILL_FILES[filePath] 88 if (!md) continue 89 sections.push( 90 `<doc path="${filePath}">\n${processContent(md, content).trim()}\n</doc>`, 91 ) 92 } 93 return sections.join('\n\n') 94 } 95 96 const INLINE_READING_GUIDE = `## Reference Documentation 97 98 The relevant documentation for your detected language is included below in \`<doc>\` tags. Each tag has a \`path\` attribute showing its original file path. Use this to find the right section: 99 100 ### Quick Task Reference 101 102 **Single text classification/summarization/extraction/Q&A:** 103 → Refer to \`{lang}/claude-api/README.md\` 104 105 **Chat UI or real-time response display:** 106 → Refer to \`{lang}/claude-api/README.md\` + \`{lang}/claude-api/streaming.md\` 107 108 **Long-running conversations (may exceed context window):** 109 → Refer to \`{lang}/claude-api/README.md\` — see Compaction section 110 111 **Prompt caching / optimize caching / "why is my cache hit rate low":** 112 → Refer to \`shared/prompt-caching.md\` + \`{lang}/claude-api/README.md\` (Prompt Caching section) 113 114 **Function calling / tool use / agents:** 115 → Refer to \`{lang}/claude-api/README.md\` + \`shared/tool-use-concepts.md\` + \`{lang}/claude-api/tool-use.md\` 116 117 **Batch processing (non-latency-sensitive):** 118 → Refer to \`{lang}/claude-api/README.md\` + \`{lang}/claude-api/batches.md\` 119 120 **File uploads across multiple requests:** 121 → Refer to \`{lang}/claude-api/README.md\` + \`{lang}/claude-api/files-api.md\` 122 123 **Agent with built-in tools (file/web/terminal) (Python & TypeScript only):** 124 → Refer to \`{lang}/agent-sdk/README.md\` + \`{lang}/agent-sdk/patterns.md\` 125 126 **Error handling:** 127 → Refer to \`shared/error-codes.md\` 128 129 **Latest docs via WebFetch:** 130 → Refer to \`shared/live-sources.md\` for URLs` 131 132 function buildPrompt( 133 lang: DetectedLanguage | null, 134 args: string, 135 content: SkillContent, 136 ): string { 137 // Take the SKILL.md content up to the "Reading Guide" section 138 const cleanPrompt = processContent(content.SKILL_PROMPT, content) 139 const readingGuideIdx = cleanPrompt.indexOf('## Reading Guide') 140 const basePrompt = 141 readingGuideIdx !== -1 142 ? cleanPrompt.slice(0, readingGuideIdx).trimEnd() 143 : cleanPrompt 144 145 const parts: string[] = [basePrompt] 146 147 if (lang) { 148 const filePaths = getFilesForLanguage(lang, content) 149 const readingGuide = INLINE_READING_GUIDE.replace(/\{lang\}/g, lang) 150 parts.push(readingGuide) 151 parts.push( 152 '---\n\n## Included Documentation\n\n' + 153 buildInlineReference(filePaths, content), 154 ) 155 } else { 156 // No language detected — include all docs and let the model ask 157 parts.push(INLINE_READING_GUIDE.replace(/\{lang\}/g, 'unknown')) 158 parts.push( 159 'No project language was auto-detected. Ask the user which language they are using, then refer to the matching docs below.', 160 ) 161 parts.push( 162 '---\n\n## Included Documentation\n\n' + 163 buildInlineReference(Object.keys(content.SKILL_FILES), content), 164 ) 165 } 166 167 // Preserve the "When to Use WebFetch" and "Common Pitfalls" sections 168 const webFetchIdx = cleanPrompt.indexOf('## When to Use WebFetch') 169 if (webFetchIdx !== -1) { 170 parts.push(cleanPrompt.slice(webFetchIdx).trimEnd()) 171 } 172 173 if (args) { 174 parts.push(`## User Request\n\n${args}`) 175 } 176 177 return parts.join('\n\n') 178 } 179 180 export function registerClaudeApiSkill(): void { 181 registerBundledSkill({ 182 name: 'claude-api', 183 description: 184 'Build apps with the Claude API or Anthropic SDK.\n' + 185 'TRIGGER when: code imports `anthropic`/`@anthropic-ai/sdk`/`claude_agent_sdk`, or user asks to use Claude API, Anthropic SDKs, or Agent SDK.\n' + 186 'DO NOT TRIGGER when: code imports `openai`/other AI SDK, general programming, or ML/data-science tasks.', 187 allowedTools: ['Read', 'Grep', 'Glob', 'WebFetch'], 188 userInvocable: true, 189 async getPromptForCommand(args) { 190 const content = await import('./claudeApiContent.js') 191 const lang = await detectLanguage() 192 const prompt = buildPrompt(lang, args, content) 193 return [{ type: 'text', text: prompt }] 194 }, 195 }) 196 }