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. Usarequiredpara marcar campos obligatorios; proporcionadefaultpara 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
pathdeclarada 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
requiredque el usuario nunca mapea falla en tiempo de ejecución, no en compilación/instalación. maxSizees un recuento de bytes con un sufijo opcionalKB/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
-
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 unhandleroping.// ✗ 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"); // ... }}, }, }; -
Declara cada host al que hagas fetch. Una solicitud a un host que falte en
capabilities.http.allowedHostses denegada. El error se manifiesta como una excepción lanzada en el handler. -
credentials.<nombre>.hostsdebe ser un subconjunto de la lista blanca HTTP. La compilación rechaza credenciales vinculadas a hosts que la partícula no puede alcanzar. -
namedebe ser kebab-case (minúsculas, guiones). La clave del registro es(name, version). -
versiondebe ser semver válido.0.1.0,1.2.3-rc.1, etc. -
Los handlers de herramientas deben devolver valores serializables a JSON. Devolver
undefinedestá bien (se convierte ennull). Devolver una instancia de clase,Date,BigIntoMapfallará 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
| Necesidad | Import / llamada |
|---|---|
| Solicitud HTTP | await fetch(url, init) |
| HTTP autenticado | const f = await credentials.fetcher(nombre); await f(url, init) |
| Leer/escribir un directorio montado | node:fs/promises en la path declarada (necesita capabilities.filesystem) |
| Estado por partícula | kv.get / set / delete / list |
| Forzar actualización OAuth | oauth.refresh(nombre) |
| Firmar / verificar HMAC | signing.sign(nombre, datos) / signing.verify(nombre, datos, sig) |
| Descubrir método elegido en la instalación | credentials.getConfiguredMethod(nombre) |
| Bytes de credencial sin procesar | credentials.getRaw(nombre) |
Las importaciones siempre provienen de los paquetes @partite-ai/particle-<área> — no inventes otros nombres.
Fuente y licencia
Adaptado de...