/ skills / bundled / claudeApi.ts
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  }