CommunityProgramación y desarrollogithub.com

partite-ai/write-particle-typescript

Crea, edita o amplía una partícula de Partite en TypeScript o JavaScript usando Particlefile.ts o Particlefile.js. Úsala para manejadores de herramientas, integraciones de API, credenciales y capacidades en tiempo de ejecución. Para partículas en Python, usa la skill de lenguaje correspondiente cuando esté disponible.

¿Qué es write-particle-typescript?

Esta skill proporciona una guía completa para escribir una 'partícula' de Partite en TypeScript o JavaScript. Una partícula es un programa de un solo archivo que expone herramientas con entrada JSON-Schema a un runtime de Partite. Es útil para desarrolladores que desean crear integraciones de API, manejadores de herramientas personalizados o capacidades de autenticación dentro del ecosistema de Partite. La skill detalla la estructura del archivo (Particlefile.ts o Particlefile.js), la exportación predeterminada que define nombre, descripción, versión, capacidades, credenciales y herramientas, así como las APIs de runtime disponibles como HTTP saliente, sistema de archivos, credenciales, almacenamiento clave-valor, actualización OAuth y firma HMAC. También cubre dependencias npm, reglas críticas (como evitar llamadas al host en el ámbito del módulo) y el flujo de trabajo con comandos `particle build`, `particle run` y `particle link`. Esta skill está diseñada para plataformas que ejecutan el runtime de Partite, como Claude Code, Cursor o Codex, y su documentación incluye ejemplos prácticos y una referencia rápida.

Compatible con✓Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/partite-ai/partite-skills/tree/main/skills/write-particle-typescript

Installed? Explore more Programación y desarrollo skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

Escribir una partícula TypeScript/JavaScript

Una partícula es un programa de un solo archivo que expone una o más herramientas (operaciones con nombre y entrada JSON-Schema) a un runtime de Partite. Esta skill te dice todo lo que necesitas para producir una que compile y se ejecute.

1. Estructura del archivo

Un archivo fuente en la raíz del proyecto, con el nombre exacto:

  • Particlefile.ts — preferido (verificación de tipos completa),
  • Particlefile.js — funciona, pero sin tipos.

No se requiere nada más. Las dependencias npm se declaran en línea con sentencias import estándar; la compilación las resuelve. No hay package.json, tsconfig.json ni node_modules que gestionar.

2. La exportación predeterminada — toda la API

La exportación predeterminada del archivo es la declaración de la partícula. Forma:

export default {
  name: "kebab-case-name",        // requerido, kebab-case
  description: "...",              // requerido, una línea
  version: "0.1.0",                // requerido, semver

  capabilities: {
    // Lista blanca de HTTP saliente. Los hosts no incluidos aquí son denegados
    // en el límite de la red. Omite `http` por completo si la partícula
    // no realiza solicitudes salientes.
    http: { allowedHosts: ["api.example.com"] },

    // Acceso a directorios del host, desactivado por defecto. Ver "Sistema de archivos" más abajo.
    // filesystem: { mounts: { ... }, temp: { ... } },
  },

  // Opcional. Declarado por nombre; el usuario elige un método en la configuración.
  credentials: {
    example: {
      hosts: ["api.example.com"],   // vincular sustitución a estos
      required: true,
      methods: {
        // ver "Métodos de credencial" más abajo
      },
    },
  },

  // Requerido. Mapa de nombre-de-herramienta → { description, inputSchema, handler }.
  tools: {
    do_thing: {
      description: "Qué hace esta herramienta — una línea, escrita para un LLM.",
      inputSchema: {
        type: "object",
        properties: {
          name: { type: "string", description: "..." },
        },
        required: ["name"],
      },
      handler: async ({ name }: { name: string }) => {
        return { result: `hello ${name}` };
      },
    },
  },

  // Opcional. Devuelve vitalidad/disponibilidad. Se llama con `particle ping`.
  ping: async () => ({ status: "ok" as const, message: "alive" }),
};

Herramientas

  • description: escrita para un invocador LLM — corta, orientada a la acción.
  • inputSchema: JSON Schema Draft 2020-12, con raíz en objeto. El host valida los argumentos contra él antes de llamar a tu handler, por lo que puedes confiar en la forma de la entrada dentro del handler. Usa required para marcar campos obligatorios; proporciona default para los opcionales.
  • handler: síncrono o asíncrono. Devuelve cualquier valor serializable a JSON. Lanza una excepción para mostrar un error al invocador: el mensaje lanzado es lo que ven.

Ping

Opcional pero recomendado. Forma de retorno:

{ status: "ok" | "degraded" | "unhealthy",
  message?: string,
  details?: string }

3. APIs de runtime disponibles para los handlers

Estas son las únicas cosas no estándar que puedes importar. Cada una es un módulo host proporcionado por el runtime; se distribuyen como paquetes npm solo de tipos, la implementación se inyecta en la ejecución.

HTTP saliente

Usa la fetch() global. Es la API Web Fetch estándar, enrutada a través del host para que se apliquen la lista blanca y la sustitución de credenciales.

const res = await fetch("https://api.example.com/things", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ ... }),
});
if (!res.ok) throw new Error(`upstream ${res.status}`);
const data = await res.json();

Los hosts que no estén en capabilities.http.allowedHosts son denegados. La fetch simple no aplica credenciales; usa credentials.fetcher para eso.

Sistema de archivos

Desactivado por defecto: un handler solo ve su propio paquete. Declara capabilities.filesystem para obtener acceso al directorio host, luego lee y escribe con las APIs normales de node:fs (el runtime las enruta a través de wasi:filesystem). No hay módulo de capacidad que importar.

capabilities: {
  filesystem: {
    // Directorios host que el *usuario* mapea a rutas reales — de forma persistente
    // con `particle mount <particle> <name> <host-path>`, o por ejecución
    // con `--mount <name>=<host-path>`.
    mounts: {
      data: {
        description: "Donde se escriben los informes",  // se muestra en el prompt de instalación
        path: "/mnt/data",                          // ruta absoluta dentro del sandbox
        access: "readwrite",                        // "readonly" | "readwrite"
        required: true,                             // rechazar ejecución hasta que se mapee (por defecto false)
      },
    },
    // Espacio temporal que el host provisiona fresco cada ejecución y limpia al
    // salir. Siempre lectura-escritura; el usuario nunca lo mapea.
    temp: {
      work: { description: "Espacio temporal", path: "/tmp/work", maxSize: "10MB" },
    },
  },
},
import { readFile, writeFile } from "node:fs/promises";

const input = await readFile("/mnt/data/in.json", "utf8");
await writeFile("/mnt/data/out.json", JSON.stringify(result));
  • El handler abre la path declarada directamente; el nombre del montaje nunca aparece dentro del sandbox.
  • Escribir en un montaje readonly, o leer/escribir cualquier ruta fuera de un montaje declarado, lanza una excepción.
  • Un montaje required que el usuario nunca mapea falla en tiempo de ejecución, no en compilación/instalación.
  • maxSize es un recuento de bytes con un sufijo opcional KB/MB/GB ("10MB"); las escrituras que lo superen fallan.

Credenciales

import { credentials } from "@partite-ai/particle-credentials";

// fetcher(nombre) devuelve una función con forma de fetch que aplica automáticamente
// la credencial en la ubicación configurada por el host (cabecera, parámetro de consulta,
// etc.) antes de que la solicitud salga del sandbox.
const fetcher = await credentials.fetcher("example");
const res = await fetcher("https://api.example.com/me");

// Descubre qué método configuró el usuario en la instalación ("oauth", "pat", ...).
const method = credentials.getConfiguredMethod("example"); // string | null

// Solo para credenciales declaradas con tipo: "raw".
const value = await credentials.getRaw("some-raw-cred");

credentials.fetcher funciona para credenciales basic, oauth2 y apikey. Para signing-key, usa signing.sign. Para raw, usa getRaw.

Almacenamiento clave/valor

Con ámbito por partícula. Solo cadenas: codifica en base64 los binarios si es necesario. Decláralo bajo capabilities.kv — sin la declaración cada llamada lanza un error not-declared (no se necesita aprobación del usuario, solo la declaración):

capabilities: { kv: { enabled: true } },
import { kv } from "@partite-ai/particle-kv";

await kv.set("cursor", "abc123");
const v = await kv.get("cursor");        // string | null
await kv.delete("cursor");
const keys = await kv.list("prefix:");    // string[]

Actualización OAuth

Solo si tienes una credencial oauth2. El fetcher actualiza automáticamente los tokens caducados; llama a esto solo cuando un servicio upstream rechaza un token que creías que aún era válido.

import { oauth } from "@partite-ai/particle-oauth";
await oauth.refresh("example");

Firma

Solo para credenciales signing-key. El material clave nunca entra en JS.

import { signing } from "@partite-ai/particle-signing";

const sig = await signing.sign("webhook-key", new TextEncoder().encode(payload));
const ok  = await signing.verify("webhook-key", data, sig);

4. Métodos de credencial

Cada credencial tiene un mapa methods. El usuario elige uno en la configuración. Una partícula comúnmente ofrece múltiples métodos para el mismo proveedor.

oauth2

oauth: {
  type: "oauth2",
  description: "Iniciar sesión mediante OAuth",
  flows: ["authorization-code", "device-code"],   // elige cualquiera de estos tres
  scopes: ["read", "write"],
  authorizationUrl: "https://provider/oauth/authorize",
  tokenUrl:         "https://provider/oauth/token",
  deviceAuthUrl:    "https://provider/oauth/device/code",  // requerido para "device-code"
},

Cadenas de flujo válidas: "authorization-code", "authorization-code-pkce", "device-code".

apikey

pat: {
  type: "apikey",
  description: "Usar un token de acceso personal",
  location: { kind: "auth-scheme", scheme: "Bearer" },
},

location.kind es uno de:

  • "header" + name: <name>: <key>
  • "auth-scheme" + scheme: Authorization: <scheme> <key> (ej. Bearer)
  • "query-param" + name: añadido como ?<name>=<key>

Omite location por completo para que el importador solicite al usuario.

basic

basic_auth: { type: "basic", description: "Usuario + contraseña" },

Sustituye como Authorization: Basic <base64>.

signing-key

webhook: { type: "signing-key", description: "...", algorithm: "hmac-sha256" },

Válidos: "hmac-sha256", "hmac-sha512". Úsalo mediante signing.sign.

raw

api_key_blob: { type: "raw", description: "..." },

Úsalo mediante credentials.getRaw(nombre). Elígelo solo cuando realmente necesites los bytes sin procesar; los otros tipos son más seguros porque el valor nunca entra en tu código.

5. Dependencias npm

Importa cualquier paquete npm puro ESM directamente:

import { z } from "zod";
import * as YAML from "yaml";

La compilación realiza un escaneo de importaciones y las resuelve automáticamente — no se necesita package.json. Restricciones:

  • El paquete debe ser puro JS/TS (sin enlaces nativos, sin APIs exclusivas de Node).
  • Las compilaciones compatibles con navegador funcionan mejor.
  • Evita fs, child_process, etc. — no existen en el runtime.

6. Reglas críticas

  1. No hacer llamadas al host en el ámbito del módulo. Llamar a fetch, kv.get, credentials.fetcher, etc. en el nivel superior de tu archivo fallará en tiempo de compilación (la fase de introspección ejecuta tu módulo bajo tiendas trampa). Haz todas las llamadas al host dentro de un handler o ping.

    // ✗ Fallará la compilación
    const fetcher = await credentials.fetcher("github");
    export default { tools: { ... } };
    
    // ✓ Dentro del handler
    export default {
      tools: {
        go: { handler: async (args) => {
          const fetcher = await credentials.fetcher("github");
          // ...
        }},
      },
    };
    
  2. Declara cada host al que hagas fetch. Una solicitud a un host que falte en capabilities.http.allowedHosts es denegada. El error se manifiesta como una excepción lanzada en el handler.

  3. credentials.<nombre>.hosts debe ser un subconjunto de la lista blanca HTTP. La compilación rechaza credenciales vinculadas a hosts que la partícula no puede alcanzar.

  4. name debe ser kebab-case (minúsculas, guiones). La clave del registro es (name, version).

  5. version debe ser semver válido. 0.1.0, 1.2.3-rc.1, etc.

  6. Los handlers de herramientas deben devolver valores serializables a JSON. Devolver undefined está bien (se convierte en null). Devolver una instancia de clase, Date, BigInt o Map fallará al serializar.

7. Flujo de trabajo

Después de escribir Particlefile.ts:

particle build                          # verificación de tipos, empaquetado, introspección, registro
particle ping <nombre>                    # verificar ping (si está definido)
particle run  <nombre>                    # listar herramientas
particle run  <nombre> <herramienta> --help      # mostrar los flags de la herramienta
particle run  <nombre> <herramienta> --foo=bar   # invocarla

particle build guía al usuario a través de la configuración de credenciales de forma interactiva la primera vez. Si la compilación falla, el error nombra la fase (import-scan, typecheck, bundle, manifest-extract) y el problema específico.

Si la partícula declara montajes del sistema de archivos, mapealos antes de ejecutar (o pasa --mount nombre=ruta en particle run para una ejecución única):

particle mount <nombre> <nombre-del-montaje> <ruta-host>   # guarda un mapeo persistente
particle mount <nombre>                            # lista montajes + mapeos actuales

Para exponer la partícula como un ejecutable independiente (útil para skills de Claude Code y uso en shell), particle link <nombre> ./<nombre> escribe un lanzador que reenvía sus argumentos a particle run <nombre>.

8. Ejemplo práctico

Una partícula mínima que expone una herramienta de búsqueda de repositorios de GitHub:

import { credentials } from "@partite-ai/particle-credentials";

export default {
  name: "github-lookup",
  description: "Buscar metadatos de repositorios de GitHub.",
  version: "0.1.0",

  capabilities: { http: { allowedHosts: ["api.github.com"] } },

  credentials: {
    github: {
      hosts: ["api.github.com"],
      required: true,
      methods: {
        pat: {
          type: "apikey",
          description: "Token de acceso personal de GitHub",
          location: { kind: "auth-scheme", scheme: "Bearer" },
        },
      },
    },
  },

  tools: {
    get_repo: {
      description: "Obtener metadatos de un repositorio de GitHub.",
      inputSchema: {
        type: "object",
        properties: {
          owner: { type: "string", description: "Login del propietario." },
          repo:  { type: "string", description: "Nombre del repositorio." },
        },
        required: ["owner", "repo"],
      },
      handler: async ({ owner, repo }: { owner: string; repo: string }) => {
        const fetcher = await credentials.fetcher("github");
        const res = await fetcher(`https://api.github.com/repos/${owner}/${repo}`, {
          headers: { Accept: "application/vnd.github+json" },
        });
        if (!res.ok) throw new Error(`GitHub ${res.status}: ${await res.text()}`);
        const r = (await res.json()) as Record<string, unknown>;
        return {
          full_name: r.full_name,
          stars:     r.stargazers_count,
          url:       r.html_url,
        };
      },
    },
  },
};

9. Referencia rápida

NecesidadImport / llamada
Solicitud HTTPawait fetch(url, init)
HTTP autenticadoconst f = await credentials.fetcher(nombre); await f(url, init)
Leer/escribir un directorio montadonode:fs/promises en la path declarada (necesita capabilities.filesystem)
Estado por partículakv.get / set / delete / list
Forzar actualización OAuthoauth.refresh(nombre)
Firmar / verificar HMACsigning.sign(nombre, datos) / signing.verify(nombre, datos, sig)
Descubrir método elegido en la instalacióncredentials.getConfiguredMethod(nombre)
Bytes de credencial sin procesarcredentials.getRaw(nombre)

Las importaciones siempre provienen de los paquetes @partite-ai/particle-<área> — no inventes otros nombres.

Fuente y licencia

Adaptado de...

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

Skills relacionados