TypeScript/JavaScript particle を書く
Particle は、Particle ランタイムに対してツール(JSON-Schema 入力を持つ名前付き操作)を公開する単一ファイルのプログラムです。このスキルは、ビルドと実行が可能な Particle を生成するために必要なすべてを説明します。
1. ファイルレイアウト
プロジェクトルートに、以下の正確な名前でソースファイルを1つ配置します:
Particlefile.ts— 推奨(完全な型チェック)Particlefile.js— 動作しますが、型はありません。
他に必要なものはありません。npm 依存関係は、標準の import 文でインライン宣言されます。ビルドがそれらを解決します。package.json、tsconfig.json、node_modules を管理する必要はありません。
2. デフォルトエクスポート — 全体のAPI
ファイルのデフォルトエクスポートが particle 宣言です。形式:
export default {
name: "kebab-case-name", // 必須、kebab-case
description: "...", // 必須、1行
version: "0.1.0", // 必須、semver
capabilities: {
// アウトバウンドHTTP許可リスト。ここにリストされていないホストへのリクエストはネットワーク境界で拒否されます。
// particle がアウトバウンドリクエストを行わない場合は、`http` を省略してください。
http: { allowedHosts: ["api.example.com"] },
// ホストディレクトリへのアクセス。デフォルトではオフです。以下の「ファイルシステム」を参照してください。
// filesystem: { mounts: { ... }, temp: { ... } },
},
// オプション。名前ごとに宣言します。ユーザーはセットアップ時にメソッドを選択します。
credentials: {
example: {
hosts: ["api.example.com"], // これらのホストにサブスティテューションをバインド
required: true,
methods: {
// 以下の「資格情報メソッド」を参照
},
},
},
// 必須。ツール名 → { description, inputSchema, handler } のマップ。
tools: {
do_thing: {
description: "このツールが行うこと — LLM向けに書かれた1行の説明。",
inputSchema: {
type: "object",
properties: {
name: { type: "string", description: "..." },
},
required: ["name"],
},
handler: async ({ name }: { name: string }) => {
return { result: `hello ${name}` };
},
},
},
// オプション。liveness/readiness を返します。`particle ping` によって呼び出されます。
ping: async () => ({ status: "ok" as const, message: "alive" }),
};
Tools
description: LLMの呼び出し元向けに書かれます — 短く、アクション指向。inputSchema: JSON Schema Draft 2020-12、オブジェクトをルートとします。ホストは、ハンドラを呼び出す前に引数を検証するため、ハンドラ内で入力の形状を信頼できます。requiredを使用して必須フィールドをマークし、オプションのフィールドにはdefaultを指定します。handler: 同期または非同期。JSON シリアライズ可能な任意の値を返します。呼び出し元にエラーを通知するには throw します — スローされたメッセージが表示されます。
Ping
オプションですが推奨されます。戻り値の形式:
{ status: "ok" | "degraded" | "unhealthy",
message?: string,
details?: string }
3. ハンドラで利用可能なランタイムAPI
インポートできるのは、これらのみが非標準ライブラリのものです。それぞれはランタイムによって提供されるホストモジュールです — 型のみのnpmパッケージとして提供され、実装は実行時に注入されます。
アウトバウンドHTTP
グローバル fetch() を使用します。これは標準の Web Fetch API で、ホストを経由してルーティングされるため、許可リストと資格情報のサブスティテューションが適用されます。
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();
capabilities.http.allowedHosts にないホストは拒否されます。通常の fetch は資格情報を適用しません — そのためには credentials.fetcher を使用してください。
ファイルシステム
デフォルトではオフ — ハンドラは自身のバンドルのみを参照します。capabilities.filesystem を宣言してホストディレクトリへのアクセスを取得し、通常の node:fs API を使用して読み書きします(ランタイムはこれらを wasi:filesystem 経由でルーティングします)。インポートする機能モジュールはありません。
capabilities: {
filesystem: {
// *ユーザー*が実際のパスにマッピングするホストディレクトリ — `particle mount <particle> <name> <host-path>` で永続的に、
// または `--mount <name>=<host-path>` で実行ごとにマッピングします。
mounts: {
data: {
description: "レポートの書き込み先", // インストールプロンプトに表示されます
path: "/mnt/data", // サンドボックス内の絶対パス
access: "readwrite", // "readonly" | "readwrite"
required: true, // マッピングされるまで実行を拒否(デフォルト false)
},
},
// ホストが実行ごとに新しくプロビジョニングし、終了時にクリアするスクラッチスペース。
// 常に読み書き可能。ユーザーがマッピングすることはありません。
temp: {
work: { description: "スクラッチスペース", 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));
- ハンドラは宣言された
pathを直接開きます。マウント名がサンドボックス内に現れることはありません。 readonlyマウントへの書き込み、または宣言されたマウント外のパスの読み取り/書き込みは、エラーをスローします。- ユーザーがマッピングしなかった
requiredマウントは、ビルド/インストール時ではなく実行時に失敗します。 maxSizeは、オプションのKB/MB/GBサフィックス("10MB")を持つバイト数です。これを超える書き込みは失敗します。
資格情報
import { credentials } from "@partite-ai/particle-credentials";
// fetcher(name) は、リクエストがサンドボックスから送出される前に、ホストが設定した場所(ヘッダー、クエリパラメータなど)に資格情報を自動適用する fetch 形式の関数を返します。
const fetcher = await credentials.fetcher("example");
const res = await fetcher("https://api.example.com/me");
// ユーザーがセットアップ時に設定したメソッドを検出します("oauth", "pat", ...)。
const method = credentials.getConfiguredMethod("example"); // string | null
// `type: "raw"` で宣言された資格情報のみ。
const value = await credentials.getRaw("some-raw-cred");
credentials.fetcher は、basic、oauth2、apikey 資格情報で動作します。signing-key の場合は signing.sign を、raw の場合は getRaw を使用します。
キーバリューストア
Particleごとにスコープされます。文字列のみ — バイナリの場合は必要に応じて base64 エンコードしてください。capabilities.kv の下に宣言します — 宣言がない場合、すべての呼び出しは not-declared エラーをスローします(ユーザーの承認は不要で、宣言のみが必要です):
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[]
OAuth リフレッシュ
oauth2 資格情報がある場合のみ。フェッチャーは期限切れのトークンを自動的にリフレッシュしますが、まだ有効だと思っていたトークンが上流で拒否された場合にのみこれを呼び出してください。
import { oauth } from "@partite-ai/particle-oauth";
await oauth.refresh("example");
署名
signing-key 資格情報のみ。鍵の素材は 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. 資格情報メソッド
各資格情報には methods マップがあります。ユーザーはセットアップ時に1つを選択します。Particle は通常、同じプロバイダに対して複数のメソッドを提供します。
oauth2
oauth: {
type: "oauth2",
description: "OAuth でサインイン",
flows: ["authorization-code", "device-code"], // この3つから任意のものを選択
scopes: ["read", "write"],
authorizationUrl: "https://provider/oauth/authorize",
tokenUrl: "https://provider/oauth/token",
deviceAuthUrl: "https://provider/oauth/device/code", // "device-code" の場合必須
},
有効なフロー文字列: "authorization-code"、"authorization-code-pkce"、"device-code"。
apikey
pat: {
type: "apikey",
description: "パーソナルアクセストークンを使用",
location: { kind: "auth-scheme", scheme: "Bearer" },
},
location.kind は以下のいずれか:
"header"+name:<name>: <key>"auth-scheme"+scheme:Authorization: <scheme> <key>(例:Bearer)"query-param"+name:?<name>=<key>として追加
location を完全に省略すると、インポーターがユーザーに入力を促します。
basic
basic_auth: { type: "basic", description: "ユーザー名 + パスワード" },
Authorization: Basic <base64> としてサブスティテュートされます。
signing-key
webhook: { type: "signing-key", description: "...", algorithm: "hmac-sha256" },
有効: "hmac-sha256"、"hmac-sha512"。signing.sign を使用します。
raw
api_key_blob: { type: "raw", description: "..." },
credentials.getRaw(name) を使用します。本当に生のバイトが必要な場合にのみこれを選択してください — 他の型は値がコードに入らないため、より安全です。
5. npm 依存関係
任意の pure-ESM npm パッケージを直接インポートします:
import { z } from "zod";
import * as YAML from "yaml";
ビルドはインポートスキャンを行い、自動的に解決します — package.json は不要です。制約:
- パッケージは pure-JS/TS である必要があります(ネイティブバインディングや Node 専用APIは不可)。
- ブラウザ互換のビルドが最適です。
fs、child_processなどは避けてください — これらはランタイムに存在しません。
6. 重要なルール
-
モジュールスコープでのホスト呼び出し禁止。 ファイルのトップレベルで
fetch、kv.get、credentials.fetcherなどを呼び出すと、ビルド時に失敗します(イントロスペクトフェーズでは、トラップストアの下でモジュールが実行されます)。すべてのホスト呼び出しはhandlerまたはping内で行ってください。// ✗ ビルドに失敗します const fetcher = await credentials.fetcher("github"); export default { tools: { ... } }; // ✓ ハンドラ内で export default { tools: { go: { handler: async (args) => { const fetcher = await credentials.fetcher("github"); // ... }}, }, }; -
フェッチするすべてのホストを宣言してください。
capabilities.http.allowedHostsにないホストへのリクエストは拒否されます。エラーはハンドラ内でスローされた例外として発生します。 -
credentials.<name>.hostsは HTTP 許可リストのサブセットである必要があります。 ビルドは、particle が到達できないホストにバインドされた資格情報を拒否します。 -
nameは kebab-case(小文字、ハイフン)でなければなりません。 レジストリキーは(name, version)です。 -
versionは有効な semver でなければなりません。0.1.0、1.2.3-rc.1など。 -
ツールハンドラは JSON シリアライズ可能な値を返す必要があります。
undefinedを返すのは問題ありません(nullになります)。クラスインスタンス、Date、BigInt、Mapを返すとシリアライズに失敗します。
7. ワークフロー
Particlefile.ts を作成した後:
particle build # 型チェック、バンドル、イントロスペクト、登録
particle ping <name> # ping を確認(定義されている場合)
particle run <name> # ツール一覧
particle run <name> <tool> --help # ツールのフラグを表示
particle run <name> <tool> --foo=bar # 実行
particle build は、初回に対話的に資格情報設定をユーザーに案内します。ビルドが失敗した場合、エラーはフェーズ(import-scan、typecheck、bundle、manifest-extract)と具体的な問題を示します。
particle がファイルシステムマウントを宣言している場合、実行前にマッピングするか、particle run で --mount name=path を渡します(一時的な場合):
particle mount <name> <mount-name> <host-path> # 永続的なマッピングを保存
particle mount <name> # マウントと現在のマッピングを一覧表示
particle をスタンドアロン実行可能ファイルとして公開するには(Claude Code スキルやシェルでの使用に便利)、particle link <name> ./<name> が引数を particle run <name> に転送するランチャーを書き込みます。
8. 実例
GitHub リポジトリ検索ツールを公開する最小限の particle:
import { credentials } from "@partite-ai/particle-credentials";
export default {
name: "github-lookup",
description: "GitHub リポジトリのメタデータを検索します。",
version: "0.1.0",
capabilities: { http: { allowedHosts: ["api.github.com"] } },
credentials: {
github: {
hosts: ["api.github.com"],
required: true,
methods: {
pat: {
type: "apikey",
description: "GitHub パーソナルアクセストークン",
location: { kind: "auth-scheme", scheme: "Bearer" },
},
},
},
},
tools: {
get_repo: {
description: "GitHub リポジトリのメタデータを取得します。",
inputSchema: {
type: "object",
properties: {
owner: { type: "string", description: "オーナーのログイン名。" },
repo: { type: "string", description: "リポジトリ名。" },
},
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. クイックリファレンス
| 必要なもの | インポート / 呼び出し |
|---|---|
| HTTP リクエスト | await fetch(url, init) |
| 認証付きHTTP | const f = await credentials.fetcher(name); await f(url, init) |
| マウントされたディレクトリの読み書き | 宣言された path 上の node:fs/promises(capabilities.filesystem が必要) |
| Particleごとの状態 | kv.get / set / delete / list |
| OAuth 強制リフレッシュ | oauth.refresh(name) |
| HMAC 署名 / 検証 | signing.sign(name, data) / signing.verify(name, data, sig) |
| セットアップ時に選択されたメソッドを検出 | credentials.getConfiguredMethod(name) |
| 生の資格情報バイト | credentials.getRaw(name) |
インポートは常に @partite-ai/particle-<area> パッケージから行います — 他の名前は発明しないでください。
出典とライセンス
(ライセンス情報は原文に記載されていますが、翻訳対象外として省略します)