Écrire une particule TypeScript/JavaScript
Une particule est un programme mono-fichier qui expose un ou plusieurs outils (opérations nommées avec une entrée JSON-Schema) à un environnement d'exécution Partite. Cette compétence vous dit tout ce que vous devez savoir pour en produire une qui se compile et s'exécute.
1. Structure du fichier
Un fichier source à la racine du projet, nommé exactement :
Particlefile.ts— préféré (vérification de type complète),Particlefile.js— fonctionne, mais sans types.
Rien d'autre n'est requis. Les dépendances npm sont déclarées en ligne avec
les instructions import standard ; la construction les résout. Pas de
package.json, pas de tsconfig.json, pas de node_modules à gérer.
2. L'exportation par défaut — toute l'API
L'exportation par défaut du fichier est la déclaration de la particule. Forme :
export default {
name: "kebab-case-name", // obligatoire, kebab-case
description: "...", // obligatoire, une ligne
version: "0.1.0", // obligatoire, semver
capabilities: {
// Liste blanche HTTP sortante. Les hôtes non listés ici sont refusés
// à la limite du réseau. Omettez `http` entièrement si la particule
// n'effectue aucune requête sortante.
http: { allowedHosts: ["api.example.com"] },
// Accès au répertoire hôte, désactivé par défaut. Voir "Système de fichiers" ci-dessous.
// filesystem: { mounts: { ... }, temp: { ... } },
},
// Optionnel. Déclaré par nom ; l'utilisateur choisit une méthode lors de la configuration.
credentials: {
example: {
hosts: ["api.example.com"], // lier la substitution à ces hôtes
required: true,
methods: {
// voir "Méthodes d'identifiants" ci-dessous
},
},
},
// Obligatoire. Mappage nom-outil → { description, inputSchema, handler }.
tools: {
do_thing: {
description: "Ce que fait cet outil — une ligne, écrite pour un LLM.",
inputSchema: {
type: "object",
properties: {
name: { type: "string", description: "..." },
},
required: ["name"],
},
handler: async ({ name }: { name: string }) => {
return { result: `hello ${name}` };
},
},
},
// Optionnel. Renvoie la vivacité/la disponibilité. Appelé par `particle ping`.
ping: async () => ({ status: "ok" as const, message: "alive" }),
};
Outils
description: écrite pour un appelant LLM — courte, orientée action.inputSchema: JSON Schema Draft 2020-12, à racine objet. L'hôte valide les arguments par rapport à celui-ci avant d'appeler votre gestionnaire, vous pouvez donc faire confiance à la forme de l'entrée à l'intérieur du gestionnaire. Utilisezrequiredpour marquer les champs obligatoires ; fournissezdefaultpour les optionnels.handler: synchrone ou asynchrone. Renvoie toute valeur sérialisable en JSON. Lancez une erreur pour la signaler à l'appelant — le message lancé est ce qu'il voit.
Ping
Optionnel mais recommandé. Forme de retour :
{ status: "ok" | "degraded" | "unhealthy",
message?: string,
details?: string }
3. API d'exécution disponibles pour les gestionnaires
Ce sont les seules choses non-stdlib que vous pouvez importer. Chacune est un module hôte fourni par l'environnement d'exécution — ils sont livrés en tant que packages npm de types uniquement, l'implémentation est injectée lors de l'exécution.
HTTP sortant
Utilisez la fonction globale fetch(). Il s'agit de l'API Web Fetch standard,
acheminée via l'hôte afin que la liste blanche et la substitution
d'identifiants s'appliquent.
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();
Les hôtes absents de capabilities.http.allowedHosts sont refusés.
fetch simple n'applique pas les identifiants — utilisez
credentials.fetcher pour cela.
Système de fichiers
Désactivé par défaut — un gestionnaire ne voit que son propre bundle.
Déclarez capabilities.filesystem pour accéder au répertoire hôte,
puis lisez et écrivez avec les API standard node:fs (l'environnement
d'exécution les achemine via wasi:filesystem). Aucun module de
capacité à importer.
capabilities: {
filesystem: {
// Répertoires hôtes que *l'utilisateur* mappe vers des chemins réels — de manière
// persistante avec `particle mount <particle> <name> <host-path>`, ou par exécution
// avec `--mount <name>=<host-path>`.
mounts: {
data: {
description: "Où les rapports sont écrits", // affiché dans l'invite d'installation
path: "/mnt/data", // chemin absolu à l'intérieur du bac à sable
access: "readwrite", // "readonly" | "readwrite"
required: true, // refuse de s'exécuter tant que non mappé (false par défaut)
},
},
// Espace temporaire que l'hôte provisionne à chaque exécution et efface à la
// sortie. Toujours en lecture-écriture ; l'utilisateur ne les mappe jamais.
temp: {
work: { description: "Espace temporaire", 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));
- Le gestionnaire ouvre le
pathdéclaré directement ; le nom du mount n'apparaît jamais à l'intérieur du bac à sable. - Écrire sur un mount
readonly, ou lire/écrire tout chemin en dehors d'un mount déclaré, lève une exception. - Un mount
requiredque l'utilisateur ne mappe jamais échoue au moment de l'exécution, pas de la construction/installation. maxSizeest un nombre d'octets avec un suffixeKB/MB/GBoptionnel ("10MB") ; les écritures au-delà échouent.
Identifiants
import { credentials } from "@partite-ai/particle-credentials";
// fetcher(name) renvoie une fonction de type fetch qui applique
// automatiquement l'identifiant à l'emplacement configuré par l'hôte
// (en-tête, paramètre de requête, etc.) avant que la requête ne quitte
// le bac à sable.
const fetcher = await credentials.fetcher("example");
const res = await fetcher("https://api.example.com/me");
// Découvrez quelle méthode l'utilisateur a configurée lors de l'installation ("oauth", "pat", ...).
const method = credentials.getConfiguredMethod("example"); // string | null
// Uniquement pour les identifiants déclarés avec type: "raw".
const value = await credentials.getRaw("some-raw-cred");
credentials.fetcher fonctionne pour les identifiants basic, oauth2,
et apikey. Pour signing-key, utilisez signing.sign. Pour raw,
utilisez getRaw.
Stockage clé/valeur
Limitié à chaque particule. Chaînes uniquement — encodez en base64 le
binaire si nécessaire. Déclarez-le sous capabilities.kv — sans la
déclaration, chaque appel lève une erreur not-declared (aucune
approbation utilisateur n'est nécessaire, juste la déclaration) :
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[]
Rafraîchissement OAuth
Uniquement si vous avez un identifiant oauth2. Le fetcher
rafraîchit automatiquement les jetons expirés ; appelez-le uniquement
lorsqu'un système distant rejette un jeton que vous pensiez encore
valide.
import { oauth } from "@partite-ai/particle-oauth";
await oauth.refresh("example");
Signature
Uniquement pour les identifiants signing-key. Le matériel de clé
n'entre jamais dans le 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éthodes d'identifiants
Chaque identifiant a un dictionnaire methods. L'utilisateur en choisit
une lors de l'installation. Une particule propose souvent plusieurs
méthodes pour le même fournisseur.
oauth2
oauth: {
type: "oauth2",
description: "Se connecter via OAuth",
flows: ["authorization-code", "device-code"], // choisissez l'un de ces trois
scopes: ["read", "write"],
authorizationUrl: "https://provider/oauth/authorize",
tokenUrl: "https://provider/oauth/token",
deviceAuthUrl: "https://provider/oauth/device/code", // obligatoire pour "device-code"
},
Chaînes de flux valides : "authorization-code", "authorization-code-pkce",
"device-code".
apikey
pat: {
type: "apikey",
description: "Utiliser un jeton d'accès personnel",
location: { kind: "auth-scheme", scheme: "Bearer" },
},
location.kind est l'un de :
"header"+name:<name>: <key>"auth-scheme"+scheme:Authorization: <scheme> <key>(par exempleBearer)"query-param"+name: ajouté comme?<name>=<key>
Omettez location entièrement pour laisser l'importateur demander
l'emplacement à l'utilisateur.
basic
basic_auth: { type: "basic", description: "Nom d'utilisateur + mot de passe" },
Se substitue par Authorization: Basic <base64>.
signing-key
webhook: { type: "signing-key", description: "...", algorithm: "hmac-sha256" },
Valide : "hmac-sha256", "hmac-sha512". Utilisez via signing.sign.
raw
api_key_blob: { type: "raw", description: "..." },
Utilisez via credentials.getRaw(name). Choisissez ceci uniquement
lorsque vous avez réellement besoin des octets bruts — les autres
types sont plus sûrs car la valeur n'entre jamais dans votre code.
5. Dépendances npm
Importez tout package ESM pur directement :
import { z } from "zod";
import * as YAML from "yaml";
La construction effectue une analyse des importations et les résout
automatiquement — aucun package.json nécessaire. Contraintes :
- Le package doit être purement JS/TS (pas de liaisons natives, pas d'API Node uniquement).
- Les builds compatibles navigateur fonctionnent le mieux.
- Évitez
fs,child_process, etc. — ils n'existent pas dans l'environnement d'exécution.
6. Règles critiques
-
Aucun appel hôte au niveau du module. Appeler
fetch,kv.get,credentials.fetcher, etc. au niveau supérieur de votre fichier échouera au moment de la construction (la phase d'introspection exécute votre module sous des magasins de pièges). Effectuez chaque appel hôte à l'intérieur d'unhandlerouping.// ✗ Échouera la construction const fetcher = await credentials.fetcher("github"); export default { tools: { ... } }; // ✓ À l'intérieur du gestionnaire export default { tools: { go: { handler: async (args) => { const fetcher = await credentials.fetcher("github"); // ... }}, }, }; -
Déclarez chaque hôte que vous fetch. Une requête vers un hôte absent de
capabilities.http.allowedHostsest refusée. L'erreur se manifeste par une exception levée dans le gestionnaire. -
credentials.<name>.hostsdoit être un sous-ensemble de la liste blanche HTTP. La construction rejette les identifiants liés à des hôtes que la particule ne peut pas atteindre. -
namedoit être en kebab-case (minuscules, traits d'union). La clé du registre est(name, version). -
versiondoit être un semver valide.0.1.0,1.2.3-rc.1, etc. -
Les gestionnaires d'outils doivent renvoyer des valeurs sérialisables en JSON. Renvoyer
undefinedest acceptable (devientnull). Renvoyer une instance de classe,Date,BigIntouMapéchouera à la sérialisation.
7. Flux de travail
Après avoir écrit Particlefile.ts :
particle build # vérifier les types, empaqueter, introspecter, enregistrer
particle ping <name> # vérifier le ping (si défini)
particle run <name> # lister les outils
particle run <name> <tool> --help # afficher les drapeaux de l'outil
particle run <name> <tool> --foo=bar # l'invoquer
particle build guide l'utilisateur à travers la configuration des
identifiants de manière interactive la première fois. Si la construction
échoue, l'erreur nomme la phase (import-scan, typecheck, bundle,
manifest-extract) et le problème spécifique.
Si la particule déclare des montages de système de fichiers, mappez-les
avant l'exécution (ou passez --mount name=path sur particle run
pour un usage unique) :
particle mount <name> <mount-name> <host-path> # enregistrer un mappage persistant
particle mount <name> # lister les montages + les mappages actuels
Pour exposer la particule en tant qu'exécutable autonome (pratique
pour les compétences Claude Code et l'utilisation en shell),
particle link <name> ./<name> écrit un lanceur qui transmet ses
arguments à particle run <name>.
8. Exemple concret
Une particule minimale exposant un outil de recherche de dépôt GitHub :
import { credentials } from "@partite-ai/particle-credentials";
export default {
name: "github-lookup",
description: "Rechercher les métadonnées d'un dépôt GitHub.",
version: "0.1.0",
capabilities: { http: { allowedHosts: ["api.github.com"] } },
credentials: {
github: {
hosts: ["api.github.com"],
required: true,
methods: {
pat: {
type: "apikey",
description: "Jeton d'accès personnel GitHub",
location: { kind: "auth-scheme", scheme: "Bearer" },
},
},
},
},
tools: {
get_repo: {
description: "Récupérer les métadonnées d'un dépôt GitHub.",
inputSchema: {
type: "object",
properties: {
owner: { type: "string", description: "Identifiant du propriétaire." },
repo: { type: "string", description: "Nom du dépôt." },
},
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. Référence rapide
| Besoin | Importation / appel |
|---|---|
| Requête HTTP | await fetch(url, init) |
| HTTP authentifié | const f = await credentials.fetcher(name); await f(url, init) |
| Lire/écrire un répertoire monté | node:fs/promises sur le path déclaré (nécessite capabilities.filesystem) |
| État par particule | kv.get / set / delete / list |
| Forcer le rafraîchissement OAuth | oauth.refresh(name) |
| Signature / vérification HMAC | signing.sign(name, data) / signing.verify(name, data, sig) |
| Découvrir la méthode choisie à la configuration | credentials.getConfiguredMethod(name) |
| Octets bruts de l'identifiant | credentials.getRaw(name) |
Les importations proviennent toujours des packages @partite-ai/particle-<area> —
n'inventez pas d'autres noms.
Source et licence
Adapté de ...
[abrégé pour la traduction]