Escrever uma partícula TypeScript/JavaScript
Uma partícula é um programa de arquivo único que expõe uma ou mais ferramentas (operações nomeadas com entrada JSON-Schema) para um runtime de partículas. Esta habilidade fornece tudo que você precisa para produzir uma que compile e execute.
1. Estrutura de arquivos
Um arquivo fonte na raiz do projeto, nomeado exatamente:
Particlefile.ts— preferido (com verificação de tipos completa),Particlefile.js— funciona, mas sem tipos.
Nada mais é necessário. Dependências npm são declaradas inline com instruções import padrão; a compilação as resolve. Sem package.json, sem tsconfig.json, sem node_modules para gerenciar.
2. A exportação padrão — a API completa
A exportação padrão do arquivo é a declaração da partícula. Forma:
export default {
name: "nome-em-kebab-case", // obrigatório, kebab-case
description: "...", // obrigatório, uma linha
version: "0.1.0", // obrigatório, semver
capabilities: {
// Lista de permissões HTTP de saída. Hosts não listados aqui são negados
// no limite da rede. Omita `http` inteiramente se a partícula não fizer
// requisições de saída.
http: { allowedHosts: ["api.exemplo.com"] },
// Acesso a diretórios do host, desligado por padrão. Veja "Sistema de arquivos" abaixo.
// filesystem: { mounts: { ... }, temp: { ... } },
},
// Opcional. Declarado por nome; o usuário escolhe um método na configuração.
credentials: {
exemplo: {
hosts: ["api.exemplo.com"], // vincula a substituição a estes
required: true,
methods: {
// veja "Métodos de credenciais" abaixo
},
},
},
// Obrigatório. Mapa de nome-da-ferramenta → { description, inputSchema, handler }.
tools: {
fazer_coisa: {
description: "O que esta ferramenta faz — uma linha, escrita para um LLM.",
inputSchema: {
type: "object",
properties: {
nome: { type: "string", description: "..." },
},
required: ["nome"],
},
handler: async ({ nome }: { nome: string }) => {
return { resultado: `olá ${nome}` };
},
},
},
// Opcional. Retorna vivacidade/prontidão. Chamado por `particle ping`.
ping: async () => ({ status: "ok" as const, message: "vivo" }),
};
Ferramentas
description: escrita para um chamador LLM — curta, orientada à ação.inputSchema: JSON Schema Draft 2020-12, com raiz em objeto. O host valida argumentos contra ele antes de chamar seu handler, então você pode confiar na forma da entrada dentro do handler. Userequiredpara marcar campos obrigatórios; forneçadefaultpara opcionais.handler: síncrono ou assíncrono. Retorne qualquer valor serializável em JSON. Lance um erro para sinalizar um erro ao chamador — a mensagem lançada é o que eles veem.
Ping
Opcional, mas recomendado. Forma de retorno:
{ status: "ok" | "degraded" | "unhealthy",
message?: string,
details?: string }
3. APIs de runtime disponíveis para handlers
Estas são as únicas coisas não padrão da biblioteca que você pode importar. Cada uma é um módulo do host fornecido pelo runtime — elas são fornecidas como pacotes npm apenas de tipo, a implementação é injetada na execução.
HTTP de saída
Use o fetch() global. É a API Fetch Web padrão, roteada através do host para que a lista de permissões e a substituição de credenciais se apliquem.
const res = await fetch("https://api.exemplo.com/coisas", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ... }),
});
if (!res.ok) throw new Error(`upstream ${res.status}`);
const data = await res.json();
Hosts não em capabilities.http.allowedHosts são negados. fetch simples não aplica credenciais — use credentials.fetcher para isso.
Sistema de arquivos
Desligado por padrão — um handler vê apenas seu próprio pacote. Declare capabilities.filesystem para obter acesso a diretórios do host, então leia e escreva com APIs node:fs comuns (o runtime as roteia através de wasi:filesystem). Nenhum módulo de capacidade para importar.
capabilities: {
filesystem: {
// Diretórios do host que o *usuário* mapeia para caminhos reais — persistentemente
// com `particle mount <particle> <nome> <caminho-host>`, ou por execução
// com `--mount <nome>=<caminho-host>`.
mounts: {
dados: {
description: "Onde os relatórios são escritos", // mostrado no prompt de instalação
path: "/mnt/dados", // caminho absoluto dentro do sandbox
access: "readwrite", // "readonly" | "readwrite"
required: true, // recusa executar até mapeado (padrão false)
},
},
// Espaço temporário que o host provisiona fresco a cada execução e limpa na
// saída. Sempre leitura-escrita; o usuário nunca mapeia estes.
temp: {
trabalho: { description: "Espaço temporário", path: "/tmp/trabalho", maxSize: "10MB" },
},
},
},
import { readFile, writeFile } from "node:fs/promises";
const input = await readFile("/mnt/dados/entrada.json", "utf8");
await writeFile("/mnt/dados/saida.json", JSON.stringify(resultado));
- O handler abre o
pathdeclarado diretamente; o nome do mount nunca aparece dentro do sandbox. - Escrever em um mount
readonly, ou ler/escrever qualquer caminho fora de um mount declarado, lança um erro. - Um mount
requiredque o usuário nunca mapeia falha em tempo de execução, não na compilação/instalação. maxSizeé uma contagem de bytes com um sufixo opcionalKB/MB/GB("10MB"); escritas além disso falham.
Credenciais
import { credentials } from "@partite-ai/particle-credentials";
// fetcher(nome) retorna uma função em forma de fetch que aplica automaticamente
// a credencial no local configurado pelo host (cabeçalho, parâmetro de consulta,
// etc.) antes da requisição sair do sandbox.
const fetcher = await credentials.fetcher("exemplo");
const res = await fetcher("https://api.exemplo.com/me");
// Descubra qual método o usuário configurou na instalação ("oauth", "pat", ...).
const metodo = credentials.getConfiguredMethod("exemplo"); // string | null
// Apenas para credenciais declaradas com type: "raw".
const valor = await credentials.getRaw("alguma-cred-raw");
credentials.fetcher funciona para credenciais basic, oauth2 e apikey. Para signing-key, use signing.sign. Para raw, use getRaw.
Armazenamento chave/valor
Escopo por partícula. Apenas strings — codifique binário em base64 se necessário. Declare-o sob capabilities.kv — sem a declaração, toda chamada lança um erro de não-declarado (nenhuma aprovação do usuário é necessária, apenas a declaração):
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 chaves = await kv.list("prefixo:"); // string[]
Atualização OAuth
Apenas se você tiver uma credencial oauth2. O fetcher atualiza automaticamente tokens expirados; chame isso apenas quando um upstream rejeitar um token que você pensava ainda ser válido.
import { oauth } from "@partite-ai/particle-oauth";
await oauth.refresh("exemplo");
Assinatura
Apenas para credenciais signing-key. O material da chave nunca entra no JS.
import { signing } from "@partite-ai/particle-signing";
const sig = await signing.sign("chave-webhook", new TextEncoder().encode(payload));
const ok = await signing.verify("chave-webhook", dados, sig);
4. Métodos de credenciais
Cada credencial tem um mapa methods. O usuário escolhe um na configuração. Uma partícula comumente oferece múltiplos métodos para o mesmo provedor.
oauth2
oauth: {
type: "oauth2",
description: "Entrar via OAuth",
flows: ["authorization-code", "device-code"], // escolha qualquer um destes três
scopes: ["read", "write"],
authorizationUrl: "https://provedor/oauth/autorizar",
tokenUrl: "https://provedor/oauth/token",
deviceAuthUrl: "https://provedor/oauth/dispositivo/codigo", // obrigatório para "device-code"
},
Strings de fluxo válidas: "authorization-code", "authorization-code-pkce", "device-code".
apikey
pat: {
type: "apikey",
description: "Usar um token de acesso pessoal",
location: { kind: "auth-scheme", scheme: "Bearer" },
},
location.kind é um de:
"header"+name:<name>: <key>"auth-scheme"+scheme:Authorization: <scheme> <key>(ex.:Bearer)"query-param"+name: anexado como?<name>=<key>
Omita location inteiramente para deixar o importador solicitar ao usuário.
basic
basic_auth: { type: "basic", description: "Nome de usuário + senha" },
Substitui como Authorization: Basic <base64>.
signing-key
webhook: { type: "signing-key", description: "...", algorithm: "hmac-sha256" },
Válidos: "hmac-sha256", "hmac-sha512". Use via signing.sign.
raw
api_key_blob: { type: "raw", description: "..." },
Use via credentials.getRaw(nome). Escolha isso apenas quando você genuinamente precisar dos bytes brutos — os outros tipos são mais seguros porque o valor nunca entra no seu código.
5. Dependências npm
Importe qualquer pacote npm puro-ESM diretamente:
import { z } from "zod";
import * as YAML from "yaml";
A compilação faz uma varredura de importações e as resolve automaticamente — sem package.json necessário. Restrições:
- O pacote deve ser puro JS/TS (sem ligações nativas, sem APIs apenas Node).
- Compilações compatíveis com navegador funcionam melhor.
- Evite
fs,child_process, etc. — eles não existem no runtime.
6. Regras críticas
-
Nenhuma chamada de host no escopo do módulo. Chamar
fetch,kv.get,credentials.fetcher, etc. no nível superior do seu arquivo falhará no tempo de compilação (a fase de introspecção executa seu módulo sob armadilhas de armazenamento). Faça toda chamada de host dentro de umhandlerouping.// ✗ Falhará a compilação const fetcher = await credentials.fetcher("github"); export default { tools: { ... } }; // ✓ Dentro do handler export default { tools: { ir: { handler: async (args) => { const fetcher = await credentials.fetcher("github"); // ... }}, }, }; -
Declare todo host que você acessar. Uma requisição a um host ausente de
capabilities.http.allowedHostsé negada. O erro aparece como uma exceção lançada no handler. -
credentials.<nome>.hostsdeve ser um subconjunto da lista de permissões HTTP. A compilação rejeita credenciais vinculadas a hosts que a partícula não pode alcançar. -
namedeve estar em kebab-case (minúsculas, hífens). A chave do registro é(name, version). -
versiondeve ser semver válido.0.1.0,1.2.3-rc.1, etc. -
Handlers de ferramenta devem retornar valores serializáveis em JSON. Retornar
undefinedé aceitável (torna-senull). Retornar uma instância de classe,Date,BigIntouMapfalhará ao serializar.
7. Fluxo de trabalho
Após escrever Particlefile.ts:
particle build # verificação de tipos, empacotamento, introspecção, registro
particle ping <nome> # verifique ping (se definido)
particle run <nome> # liste ferramentas
particle run <nome> <ferramenta> --help # mostre flags da ferramenta
particle run <nome> <ferramenta> --foo=bar # invoque-a
particle build guia o usuário pela configuração de credenciais interativamente na primeira vez. Se a compilação falhar, o erro nomeia a fase (import-scan, typecheck, bundle, manifest-extract) e o problema específico.
Se a partícula declarar montagens de sistema de arquivos, mapeie-as antes de executar (ou passe --mount nome=caminho em particle run para uma execução única):
particle mount <nome> <nome-da-montagem> <caminho-host> # salve um mapeamento persistente
particle mount <nome> # liste montagens + mapeamentos atuais
Para expor a partícula como um executável independente (útil para habilidades Claude Code e uso em shell), particle link <nome> ./<nome> escreve um lançador que encaminha seus argumentos para particle run <nome>.
8. Exemplo prático
Uma partícula mínima expondo uma ferramenta de consulta de repositório GitHub:
import { credentials } from "@partite-ai/particle-credentials";
export default {
name: "github-lookup",
description: "Consultar metadados de repositório 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 acesso pessoal GitHub",
location: { kind: "auth-scheme", scheme: "Bearer" },
},
},
},
},
tools: {
get_repo: {
description: "Buscar metadados para um repositório GitHub.",
inputSchema: {
type: "object",
properties: {
owner: { type: "string", description: "Login do proprietário." },
repo: { type: "string", description: "Nome do repositório." },
},
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. Referência rápida
| Necessidade | Import / chamada |
|---|---|
| Requisição HTTP | await fetch(url, init) |
| HTTP autenticado | const f = await credentials.fetcher(nome); await f(url, init) |
| Ler/escrever um dir montado | node:fs/promises no path declarado (precisa de capabilities.filesystem) |
| Estado por partícula | kv.get / set / delete / list |
| Forçar atualização OAuth | oauth.refresh(nome) |
| Assinar / verificar HMAC | signing.sign(nome, dados) / signing.verify(nome, dados, sig) |
| Descobrir método escolhido na instalação | credentials.getConfiguredMethod(nome) |
| Bytes de credencial raw | credentials.getRaw(nome) |
Importações sempre vêm dos pacotes @partite-ai/particle-<area> — não invente outros nomes.
Fonte e licença
Adaptado de ...