Módulo 6: Hooks Avanzados y SDK Headless

5. SDK Headless — TypeScript: Claude Code en el Ecosistema Node.js

5. SDK Headless — TypeScript: Claude Code en el Ecosistema Node.js

Descripción

Si tu stack principal es JavaScript o TypeScript, no necesitas cambiar a Python para automatizar Claude Code. Todo lo que aprendiste en la cápsula anterior — modo headless con -p, output JSON, --allowedTools — funciona igual desde Node.js. Pero el ecosistema TypeScript agrega ventajas: tipado estático para parsear respuestas de Claude, integración directa con build tools como esbuild y Vite, y el paquete @anthropic-ai/claude-code que ofrece una API nativa sin pasar por subprocess.

Esta cápsula cubre dos enfoques: subprocess (usando child_process de Node.js, similar a Python) y el paquete SDK (@anthropic-ai/claude-code). El primer enfoque funciona en cualquier entorno donde Claude CLI esté instalado. El segundo ofrece una API más limpia y tipada, pero requiere instalar el paquete.

Al terminar sabrás ejecutar Claude Code desde scripts TypeScript, integrar la automatización con tu tooling de Node.js (build scripts, deploy scripts, monitoring), y elegir entre Python y TypeScript según el caso de uso.


Enfoque 1: subprocess con child_process

El patrón básico

Node.js tiene child_process como equivalente de subprocess de Python:

import { execSync } from "child_process";

const result = execSync(
  'claude -p "¿Cuántos archivos TypeScript hay en src/?" --output-format json --allowedTools "Read,Glob,Grep"',
  { encoding: "utf-8", timeout: 60000 }
);

const output = JSON.parse(result);
console.log(output.result);
console.log(`Cost: $${output.cost_usd?.toFixed(4)}`);

execSync vs exec vs spawn

MétodoTipoCuándo usarlo
execSyncSíncrono, bloqueaScripts simples, tareas cortas
execAsync con callbackTareas de duración media
spawnAsync con streamsEjecuciones largas, streaming

Función reutilizable con tipado

import { execSync } from "child_process";

interface ClaudeResult {
  type: string;
  subtype: string;
  is_error: boolean;
  result: string;
  session_id: string;
  cost_usd: number;
  duration_ms: number;
  num_turns: number;
}

interface ClaudeError {
  is_error: true;
  error: string;
  error_type: "timeout" | "not_found" | "parse_error" | "exit_code";
}

type ClaudeOutput = ClaudeResult | ClaudeError;

function runClaude(
  prompt: string,
  tools: string[] = [],
  timeoutMs: number = 300000
): ClaudeOutput {
  const toolsArg = tools.length > 0
    ? `--allowedTools "${tools.join(",")}"`
    : "";

  const cmd = `claude -p "${prompt.replace(/"/g, '\\"')}" --output-format json ${toolsArg}`;

  try {
    const result = execSync(cmd, {
      encoding: "utf-8",
      timeout: timeoutMs,
      maxBuffer: 10 * 1024 * 1024,
    });

    return JSON.parse(result) as ClaudeResult;
  } catch (error: any) {
    if (error.killed) {
      return { is_error: true, error: "Timeout", error_type: "timeout" };
    }
    if (error.code === "ENOENT") {
      return { is_error: true, error: "claude not found", error_type: "not_found" };
    }
    return {
      is_error: true,
      error: error.message || "Unknown error",
      error_type: "exit_code",
    };
  }
}

const output = runClaude(
  "Lista las funciones exportadas en src/api/",
  ["Read", "Grep", "Glob"]
);

if (output.is_error) {
  console.error(`Error: ${(output as ClaudeError).error}`);
} else {
  const result = output as ClaudeResult;
  console.log(result.result);
  console.log(`Cost: $${result.cost_usd.toFixed(4)}`);
}

Versión async con exec

Para scripts que necesitan ejecutar múltiples invocaciones sin bloquear:

import { exec } from "child_process";
import { promisify } from "util";

const execAsync = promisify(exec);

async function runClaudeAsync(
  prompt: string,
  tools: string[] = [],
  timeoutMs: number = 300000
): Promise<ClaudeResult | ClaudeError> {
  const toolsArg = tools.length > 0
    ? `--allowedTools "${tools.join(",")}"`
    : "";

  const cmd = `claude -p "${prompt.replace(/"/g, '\\"')}" --output-format json ${toolsArg}`;

  try {
    const { stdout } = await execAsync(cmd, {
      encoding: "utf-8",
      timeout: timeoutMs,
      maxBuffer: 10 * 1024 * 1024,
    });

    return JSON.parse(stdout) as ClaudeResult;
  } catch (error: any) {
    return {
      is_error: true,
      error: error.message || "Unknown error",
      error_type: "exit_code",
    };
  }
}

async function main() {
  const result = await runClaudeAsync(
    "Analiza src/components/ y lista todos los componentes React",
    ["Read", "Glob", "Grep"]
  );

  if (!result.is_error) {
    console.log((result as ClaudeResult).result);
  }
}

main();

Enfoque 2: Paquete @anthropic-ai/claude-code

Instalación

npm install @anthropic-ai/claude-code

API básica

El paquete ofrece una API más limpia que subprocess:

import { claude } from "@anthropic-ai/claude-code";

async function main() {
  const result = await claude({
    prompt: "Analiza src/ y reporta la estructura del proyecto",
    allowedTools: ["Read", "Glob", "Grep"],
    options: {
      maxTurns: 10,
    },
  });

  console.log(result.text);
  console.log(`Cost: $${result.costUsd.toFixed(4)}`);
}

main();

Tipado nativo

La ventaja principal del paquete es el tipado:

import { claude, ClaudeResult } from "@anthropic-ai/claude-code";

async function analyzeModule(path: string): Promise<ClaudeResult> {
  return claude({
    prompt: `Analiza ${path} y reporta: estructura, dependencias, issues`,
    allowedTools: ["Read", "Glob", "Grep"],
    options: {
      maxTurns: 15,
    },
  });
}

async function main() {
  const result = await analyzeModule("src/api/");

  if (result.isError) {
    console.error("Analysis failed:", result.text);
    return;
  }

  console.log("Analysis:", result.text);
  console.log("Turns used:", result.numTurns);
  console.log("Duration:", result.durationMs, "ms");
}

main();

Streaming con el paquete

import { claude } from "@anthropic-ai/claude-code";

async function main() {
  const stream = claude.stream({
    prompt: "Genera un reporte de calidad de código para src/",
    allowedTools: ["Read", "Glob", "Grep"],
  });

  for await (const event of stream) {
    if (event.type === "text") {
      process.stdout.write(event.content);
    }
  }

  const result = await stream.finalResult();
  console.log(`\n\nCost: $${result.costUsd.toFixed(4)}`);
}

main();

Scripts de Automatización en TypeScript

Script 1: Generador de Documentación de Componentes

#!/usr/bin/env npx tsx
/**
 * Genera documentación automática para componentes React.
 * Uso: npx tsx scripts/gen-component-docs.ts
 */

import { execSync } from "child_process";
import * as fs from "fs";
import * as path from "path";

interface ClaudeResult {
  result: string;
  is_error: boolean;
  cost_usd: number;
}

function runClaude(prompt: string, tools: string[]): ClaudeResult | null {
  try {
    const toolsStr = tools.join(",");
    const result = execSync(
      `claude -p "${prompt.replace(/"/g, '\\"')}" --output-format json --allowedTools "${toolsStr}"`,
      { encoding: "utf-8", timeout: 120000 }
    );
    return JSON.parse(result);
  } catch {
    return null;
  }
}

function getComponentFiles(): string[] {
  const result = execSync(
    'find src/components -name "*.tsx" -not -name "*.test.*" -not -name "*.spec.*"',
    { encoding: "utf-8" }
  );
  return result.trim().split("\n").filter(Boolean);
}

function main() {
  const components = getComponentFiles();
  console.log(`Found ${components.length} components\n`);

  const docs: string[] = ["# Component Documentation\n"];
  let totalCost = 0;

  for (const file of components) {
    console.log(`Documenting: ${file}`);

    const output = runClaude(
      `Lee ${file} y genera documentación para el componente React que contiene.
Incluye: nombre, props (con tipos), descripción, ejemplo de uso.
Formato Markdown. Sé conciso.`,
      ["Read"]
    );

    if (output && !output.is_error) {
      docs.push(output.result);
      docs.push("\n---\n");
      totalCost += output.cost_usd || 0;
    }
  }

  const outputPath = "docs/components.md";
  fs.mkdirSync(path.dirname(outputPath), { recursive: true });
  fs.writeFileSync(outputPath, docs.join("\n"));

  console.log(`\nDocs written to ${outputPath}`);
  console.log(`Total cost: $${totalCost.toFixed(4)}`);
}

main();

Script 2: Build Validator

#!/usr/bin/env npx tsx
/**
 * Valida el build y pide a Claude que corrija errores.
 * Uso: npx tsx scripts/build-validator.ts
 */

import { execSync } from "child_process";

interface ClaudeResult {
  result: string;
  is_error: boolean;
  cost_usd: number;
}

function runBuild(): { success: boolean; output: string } {
  try {
    const output = execSync("npm run build 2>&1", { encoding: "utf-8" });
    return { success: true, output };
  } catch (error: any) {
    return { success: false, output: error.stdout || error.message };
  }
}

function fixWithClaude(buildErrors: string): ClaudeResult | null {
  const prompt = `El build de TypeScript está fallando con estos errores:

${buildErrors}

Lee los archivos mencionados en los errores y corrígelos.
Solo corrige errores de TypeScript (types, imports, syntax).
No cambies la lógica de negocio.`;

  try {
    const result = execSync(
      `claude -p "${prompt.replace(/"/g, '\\"').replace(/\n/g, "\\n")}" --output-format json --allowedTools "Read,Write,Edit,Grep,Glob"`,
      { encoding: "utf-8", timeout: 300000 }
    );
    return JSON.parse(result);
  } catch {
    return null;
  }
}

function main() {
  const maxAttempts = 3;
  let totalCost = 0;

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    console.log(`\n--- Build attempt ${attempt}/${maxAttempts} ---`);

    const { success, output } = runBuild();

    if (success) {
      console.log("Build successful!");
      console.log(`Total fix cost: $${totalCost.toFixed(4)}`);
      return;
    }

    console.log("Build failed. Asking Claude to fix...");

    const fix = fixWithClaude(output.slice(0, 3000));

    if (fix && !fix.is_error) {
      totalCost += fix.cost_usd || 0;
      console.log(`Fix applied ($${(fix.cost_usd || 0).toFixed(4)})`);
    } else {
      console.log("Claude fix failed");
    }
  }

  console.log(`Build still failing after ${maxAttempts} attempts`);
  console.log(`Total cost: $${totalCost.toFixed(4)}`);
  process.exit(1);
}

main();

Script 3: Monitoring y Reporte de Proyecto

#!/usr/bin/env npx tsx
/**
 * Genera un reporte de salud del proyecto.
 * Uso: npx tsx scripts/project-health.ts
 */

import { execSync } from "child_process";
import * as fs from "fs";

interface ClaudeResult {
  result: string;
  is_error: boolean;
  cost_usd: number;
  duration_ms: number;
}

function runClaude(prompt: string, tools: string[]): ClaudeResult | null {
  try {
    const result = execSync(
      `claude -p "${prompt.replace(/"/g, '\\"')}" --output-format json --allowedTools "${tools.join(",")}"`,
      { encoding: "utf-8", timeout: 300000, maxBuffer: 10 * 1024 * 1024 }
    );
    return JSON.parse(result);
  } catch {
    return null;
  }
}

interface HealthCheck {
  name: string;
  prompt: string;
  tools: string[];
}

const checks: HealthCheck[] = [
  {
    name: "Code Quality",
    prompt: "Analiza src/ para code quality: complejidad, duplicación, naming. Score 1-10 con justificación breve.",
    tools: ["Read", "Grep", "Glob"],
  },
  {
    name: "Security",
    prompt: "Busca en src/ problemas de seguridad: secrets, injection, eval, unsafe patterns. Lista findings con severidad.",
    tools: ["Read", "Grep", "Glob"],
  },
  {
    name: "Dependencies",
    prompt: "Analiza package.json y busca: deps desactualizadas, deps sin usar, vulnerabilidades conocidas. Reporta findings.",
    tools: ["Read", "Grep", "Glob"],
  },
];

async function main() {
  console.log("Project Health Report\n");

  const results: { name: string; result: string; cost: number }[] = [];
  let totalCost = 0;

  for (const check of checks) {
    console.log(`Running: ${check.name}...`);
    const output = runClaude(check.prompt, check.tools);

    if (output && !output.is_error) {
      results.push({
        name: check.name,
        result: output.result,
        cost: output.cost_usd || 0,
      });
      totalCost += output.cost_usd || 0;
    } else {
      results.push({
        name: check.name,
        result: "Check failed",
        cost: 0,
      });
    }
  }

  const report = [
    `# Project Health Report — ${new Date().toISOString().split("T")[0]}`,
    "",
    ...results.map((r) => [
      `## ${r.name}`,
      "",
      r.result,
      "",
      `*Cost: $${r.cost.toFixed(4)}*`,
      "",
      "---",
      "",
    ]).flat(),
    `**Total cost: $${totalCost.toFixed(4)}**`,
  ].join("\n");

  fs.mkdirSync("reports", { recursive: true });
  const reportPath = `reports/health-${new Date().toISOString().split("T")[0]}.md`;
  fs.writeFileSync(reportPath, report);

  console.log(`\nReport saved: ${reportPath}`);
  console.log(`Total cost: $${totalCost.toFixed(4)}`);
}

main();

Ejecución Paralela en TypeScript

Promise.all para análisis concurrente

import { execSync } from "child_process";

interface ClaudeResult {
  result: string;
  is_error: boolean;
  cost_usd: number;
}

async function analyzeModuleAsync(modulePath: string): Promise<{
  path: string;
  analysis: ClaudeResult | null;
}> {
  return new Promise((resolve) => {
    try {
      const output = execSync(
        `claude -p "Analiza ${modulePath}: estructura, exports, issues. Conciso." --output-format json --allowedTools "Read,Grep,Glob"`,
        { encoding: "utf-8", timeout: 120000 }
      );
      resolve({ path: modulePath, analysis: JSON.parse(output) });
    } catch {
      resolve({ path: modulePath, analysis: null });
    }
  });
}

async function main() {
  const modules = ["src/api/", "src/components/", "src/services/"];

  console.log(`Analyzing ${modules.length} modules in parallel...\n`);

  const results = await Promise.all(
    modules.map((m) => analyzeModuleAsync(m))
  );

  let totalCost = 0;

  for (const { path, analysis } of results) {
    console.log(`\n## ${path}`);
    if (analysis && !analysis.is_error) {
      console.log(analysis.result.slice(0, 300));
      totalCost += analysis.cost_usd || 0;
    } else {
      console.log("Analysis failed");
    }
  }

  console.log(`\nTotal cost: $${totalCost.toFixed(4)}`);
}

main();

Comparación: Python SDK vs TypeScript SDK

Cuándo usar Python

EscenarioPor qué Python
Scripts de CI/CDPython es el estándar en DevOps y CI pipelines
Data processingPandas, numpy para analizar resultados
ML/AI workflowsIntegración con ecosistema de ML
Backend automationScripts de migración, seeding, mantenimiento
Quick scriptsMenor boilerplate para scripts one-off

Cuándo usar TypeScript

EscenarioPor qué TypeScript
Build toolsIntegración con esbuild, Vite, webpack
Frontend toolingScripts que interactúan con el codebase frontend
Type safetyTipado estático para parsear respuestas de Claude
Node.js ecosystemnpm scripts, herramientas de desarrollo JS
Full-stack JS projectsConsistencia de lenguaje en todo el stack

Comparación directa

AspectoPythonTypeScript
Invocaciónsubprocess.run()execSync() / exec()
Asyncasyncio / ThreadPoolExecutorPromise.all() / async/await
JSON parsingjson.loads()JSON.parse()
TipadoOpcional (type hints)Nativo y estricto
Paquete SDKsubprocess approach@anthropic-ai/claude-code
BoilerplateMenorMayor (tipos, interfaces)
Error handlingtry/excepttry/catch
StreamingPopen + line iterationspawn + event listeners
Ecosistema CIMás comúnMenos común
Script runnerpython script.pynpx tsx script.ts

La regla de decisión

¿Tu proyecto es JS/TS?  ──→ TypeScript
¿Es un script de CI/CD? ──→ Python
¿Necesitas tipado fuerte para los resultados? ──→ TypeScript
¿Es un script one-off rápido? ──→ Python
¿Integra con build tools JS? ──→ TypeScript
¿Integra con data pipelines? ──→ Python

Si no estás seguro: usa el lenguaje que más usas en tu día a día. Ambos funcionan igual de bien para invocar Claude Code en modo headless.


Integración con npm scripts

package.json

{
  "scripts": {
    "review": "npx tsx scripts/code-review.ts",
    "docs:generate": "npx tsx scripts/gen-component-docs.ts",
    "health": "npx tsx scripts/project-health.ts",
    "build:fix": "npx tsx scripts/build-validator.ts",
    "changelog": "npx tsx scripts/changelog.ts"
  },
  "devDependencies": {
    "tsx": "^4.0.0",
    "typescript": "^5.0.0"
  }
}
npm run review
npm run docs:generate
npm run health
npm run build:fix

Esto integra los scripts de automatización directamente en tu flujo de trabajo habitual de npm.


Troubleshooting

"Cannot find module '@anthropic-ai/claude-code'"

Causa: El paquete no está instalado.

Solución:

npm install @anthropic-ai/claude-code

Si el paquete no está disponible en tu versión de npm o no existe aún como paquete público, usa el enfoque de subprocess que funciona con cualquier instalación de Claude CLI.

"execSync: command not found: claude"

Causa: Claude CLI no está en el PATH cuando Node.js ejecuta el subprocess.

Solución:

import { execSync } from "child_process";

const claudePath = execSync("which claude", { encoding: "utf-8" }).trim();

const result = execSync(
  `${claudePath} -p "prompt" --output-format json`,
  { encoding: "utf-8" }
);

"MaxBuffer exceeded"

Causa: El output de Claude excede el buffer por defecto de execSync (1MB).

Solución:

const result = execSync(cmd, {
  encoding: "utf-8",
  maxBuffer: 10 * 1024 * 1024, // 10MB
});

"SyntaxError: Unexpected token in JSON"

Causa: El output incluye texto no-JSON (warnings, stderr mezclado).

Solución:

const raw = execSync(cmd, { encoding: "utf-8" });
const jsonStart = raw.indexOf("{");
const jsonEnd = raw.lastIndexOf("}") + 1;
if (jsonStart >= 0 && jsonEnd > jsonStart) {
  const output = JSON.parse(raw.slice(jsonStart, jsonEnd));
}

"Timeout con npx tsx"

Causa: npx tsx agrega overhead de startup. El timeout de execSync puede no ser suficiente.

Solución: Incrementa el timeout o compila el script a JavaScript primero:

npx tsc scripts/review.ts --outDir dist/
node dist/review.js

Ejercicios

Ejercicio 1: "Hello World" headless en TypeScript (Fácil)

Escribe un script TypeScript que ejecute Claude en modo headless para contar archivos .ts en src/, parsee el JSON, e imprima resultado y costo.

Ver solución
#!/usr/bin/env npx tsx
import { execSync } from "child_process";

const result = execSync(
  'claude -p "¿Cuántos archivos .ts hay en src/?" --output-format json --allowedTools "Glob"',
  { encoding: "utf-8", timeout: 60000 }
);

const output = JSON.parse(result);
console.log(`Resultado: ${output.result}`);
console.log(`Costo: $${(output.cost_usd || 0).toFixed(4)}`);

Ejercicio 2: Función tipada reutilizable (Fácil)

Crea una función askClaude con tipos TypeScript para input y output que encapsule la invocación headless con error handling completo.

Ver solución
import { execSync } from "child_process";

interface ClaudeInput {
  prompt: string;
  tools?: string[];
  timeoutMs?: number;
}

interface ClaudeSuccess {
  is_error: false;
  result: string;
  cost_usd: number;
  duration_ms: number;
}

interface ClaudeFailure {
  is_error: true;
  error: string;
}

type ClaudeOutput = ClaudeSuccess | ClaudeFailure;

function askClaude(input: ClaudeInput): ClaudeOutput {
  const { prompt, tools = [], timeoutMs = 300000 } = input;
  const toolsArg = tools.length ? `--allowedTools "${tools.join(",")}"` : "";
  const cmd = `claude -p "${prompt.replace(/"/g, '\\"')}" --output-format json ${toolsArg}`;

  try {
    const raw = execSync(cmd, {
      encoding: "utf-8",
      timeout: timeoutMs,
      maxBuffer: 10 * 1024 * 1024,
    });
    const parsed = JSON.parse(raw);
    return { is_error: false, result: parsed.result, cost_usd: parsed.cost_usd || 0, duration_ms: parsed.duration_ms || 0 };
  } catch (error: any) {
    return { is_error: true, error: error.message || "Unknown error" };
  }
}

const output = askClaude({ prompt: "Lista exports de src/api/", tools: ["Read", "Grep"] });
if (!output.is_error) {
  console.log(output.result);
} else {
  console.error(output.error);
}

Ejercicio 3: Generador de docs para componentes (Medio)

Escribe un script que lea todos los archivos .tsx en src/components/, pida a Claude que documente cada uno, y genere un archivo docs/components.md.

Ver solución
#!/usr/bin/env npx tsx
import { execSync } from "child_process";
import * as fs from "fs";

function runClaude(prompt: string, tools: string[]): string | null {
  try {
    const result = execSync(
      `claude -p "${prompt.replace(/"/g, '\\"')}" --output-format json --allowedTools "${tools.join(",")}"`,
      { encoding: "utf-8", timeout: 120000, maxBuffer: 5 * 1024 * 1024 }
    );
    const parsed = JSON.parse(result);
    return parsed.is_error ? null : parsed.result;
  } catch {
    return null;
  }
}

const files = execSync('find src/components -name "*.tsx" -not -name "*.test.*"', { encoding: "utf-8" })
  .trim()
  .split("\n")
  .filter(Boolean);

const docs: string[] = ["# Components\n"];

for (const file of files) {
  console.log(`Documenting: ${file}`);
  const doc = runClaude(`Lee ${file}. Documenta: nombre, props, descripción, ejemplo. Markdown conciso.`, ["Read"]);
  if (doc) {
    docs.push(doc, "\n---\n");
  }
}

fs.mkdirSync("docs", { recursive: true });
fs.writeFileSync("docs/components.md", docs.join("\n"));
console.log("Done: docs/components.md");

Ejercicio 4: Build fixer con retry (Medio)

Escribe un script TypeScript que: (1) corra npm run build, (2) si falla, pase los errores a Claude para que corrija, (3) repita hasta 3 veces, (4) reporte si tuvo éxito y el costo total.

Ver solución
#!/usr/bin/env npx tsx
import { execSync } from "child_process";

function build(): { ok: boolean; output: string } {
  try {
    return { ok: true, output: execSync("npm run build 2>&1", { encoding: "utf-8" }) };
  } catch (e: any) {
    return { ok: false, output: e.stdout || e.message };
  }
}

function fixWithClaude(errors: string): number {
  try {
    const prompt = errors.slice(0, 3000).replace(/"/g, '\\"').replace(/\n/g, "\\n");
    const result = execSync(
      `claude -p "Fix these TypeScript build errors:\\n${prompt}" --output-format json --allowedTools "Read,Write,Edit,Grep,Glob"`,
      { encoding: "utf-8", timeout: 300000 }
    );
    return JSON.parse(result).cost_usd || 0;
  } catch {
    return 0;
  }
}

let totalCost = 0;
for (let i = 1; i <= 3; i++) {
  console.log(`\nAttempt ${i}/3`);
  const { ok, output } = build();
  if (ok) {
    console.log(`Build passed! Cost: $${totalCost.toFixed(4)}`);
    process.exit(0);
  }
  totalCost += fixWithClaude(output);
}

console.log(`Build still failing. Cost: $${totalCost.toFixed(4)}`);
process.exit(1);

Ejercicio 5: Análisis paralelo con Promise.all (Difícil)

Escribe un script que analice 4 directorios en paralelo, cada uno con su propia invocación headless, usando Promise.all. Consolida los resultados en un reporte Markdown.

Ver solución
#!/usr/bin/env npx tsx
import { exec } from "child_process";
import { promisify } from "util";
import * as fs from "fs";

const execAsync = promisify(exec);

async function analyze(dir: string): Promise<{ dir: string; result: string; cost: number }> {
  try {
    const { stdout } = await execAsync(
      `claude -p "Analiza ${dir}: archivos, exports, issues. Conciso." --output-format json --allowedTools "Read,Grep,Glob"`,
      { encoding: "utf-8", timeout: 120000, maxBuffer: 5 * 1024 * 1024 }
    );
    const parsed = JSON.parse(stdout);
    return { dir, result: parsed.result || "No result", cost: parsed.cost_usd || 0 };
  } catch (e: any) {
    return { dir, result: `Error: ${e.message}`, cost: 0 };
  }
}

async function main() {
  const dirs = ["src/api/", "src/components/", "src/services/", "src/utils/"];
  console.log(`Analyzing ${dirs.length} modules in parallel...\n`);

  const results = await Promise.all(dirs.map(analyze));

  const totalCost = results.reduce((sum, r) => sum + r.cost, 0);

  const report = [
    `# Module Analysis — ${new Date().toISOString().split("T")[0]}`,
    "",
    ...results.flatMap((r) => [`## ${r.dir}`, "", r.result, "", "---", ""]),
    `**Total cost: $${totalCost.toFixed(4)}**`,
  ].join("\n");

  fs.mkdirSync("reports", { recursive: true });
  fs.writeFileSync("reports/analysis.md", report);
  console.log(`Report: reports/analysis.md ($${totalCost.toFixed(4)})`);
}

main();

Resumen

  • TypeScript ofrece dos enfoques para SDK headless: child_process (subprocess) y @anthropic-ai/claude-code (paquete nativo)
  • execSync para scripts simples, exec (promisified) para async, spawn para streaming
  • El tipado estático de TypeScript da seguridad al parsear respuestas JSON de Claude
  • Scripts se ejecutan con npx tsx para soporte directo de TypeScript sin compilación
  • Promise.all habilita análisis paralelo de múltiples módulos
  • Integración directa con npm scripts (npm run review, npm run docs:generate)
  • Python vs TypeScript: Python para CI/CD y data, TypeScript para build tools y proyectos JS
  • El paquete SDK (@anthropic-ai/claude-code) ofrece API más limpia pero requiere instalación — subprocess funciona siempre
  • maxBuffer debe incrementarse (10MB+) para outputs grandes de Claude

Recursos Adicionales

  1. Claude Code CLI Reference — Flags -p, --output-format, --allowedTools
  2. Node.js child_process — Documentación oficial de exec, execSync, spawn
  3. tsx (TypeScript Execute) — Runner de TypeScript para scripts
  4. TypeScript Handbook — Referencia de tipos e interfaces
  5. Claude Code Best Practices — Buenas prácticas de automatización
  6. npm Scripts — Integración con npm
  7. Claude Code Hooks — Hooks que complementan el SDK
  8. Claude Code Overview — Contexto general de Claude Code

Siguiente cápsula: En la cápsula 06 (proyecto) integras todo: hooks de todos los eventos + un script SDK que orquesta el pipeline completo. SessionStart configura, PreToolUse valida, PostToolUse lintea, SubagentStop reporta, y un script Python dispara y procesa todo el flujo. El proyecto cierra el módulo demostrando que hooks + SDK convierten Claude Code en un sistema automatizado de desarrollo.