TypeScript/JavaScript Particle 작성
Particle은 도구(JSON-Schema 입력을 사용하는 명명된 작업)를 Particle 런타임에 노출하는 단일 파일 프로그램입니다. 이 스킬은 빌드 및 실행 가능한 Particle을 만드는 데 필요한 모든 것을 알려줍니다.
1. 파일 레이아웃
프로젝트 루트에 하나의 소스 파일을 다음 이름 중 하나로 배치합니다:
Particlefile.ts— 권장됨 (완전한 타입 검사)Particlefile.js— 작동하지만 타입 없음
다른 것은 필요하지 않습니다. npm 종속성은 표준 import 문으로 인라인 선언되며, 빌드 시 해결됩니다.
package.json, tsconfig.json, node_modules를 관리할 필요가 없습니다.
2. 기본 내보내기 — 전체 API
파일의 기본 내보내기는 particle 선언입니다. 형태:
export default {
name: "kebab-case-name", // 필수, 케밥-케이스
description: "...", // 필수, 한 줄
version: "0.1.0", // 필수, semver
capabilities: {
// 아웃바운드 HTTP 허용 목록. 여기에 없는 호스트는
// 네트워크 경계에서 차단됩니다. 아웃바운드 요청이 없으면
// `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용으로 작성.",
inputSchema: {
type: "object",
properties: {
name: { type: "string", description: "..." },
},
required: ["name"],
},
handler: async ({ name }: { name: string }) => {
return { result: `hello ${name}` };
},
},
},
// 선택 사항. 활성/준비 상태를 반환합니다. `particle ping`에 의해 호출됩니다.
ping: async () => ({ status: "ok" as const, message: "살아 있음" }),
};
도구
description: LLM 호출자를 위해 작성 — 간결하고 행동 지향적입니다.inputSchema: JSON Schema Draft 2020-12, 객체 루트. 호스트는 핸들러를 호출하기 전에 인수의 유효성을 검사하므로 핸들러 내에서 입력 형태를 신뢰할 수 있습니다.required를 사용하여 필수 필드를 표시하고, 선택 사항에는default를 제공하세요.handler: 동기 또는 비동기. JSON 직렬화 가능한 값을 반환하세요. 오류를 호출자에게 표시하려면 throw하세요 — 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(`업스트림 ${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 자격 증명이 있는 경우에만 해당합니다. fetcher가 만료된 토큰을 자동으로 갱신합니다; 여전히 유효하다고 생각하는 토큰을 업스트림이 거부하는 경우에만 호출하세요.
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 맵이 있습니다. 사용자가 설정 시 하나를 선택합니다. Particle은 일반적으로 동일한 공급자에 대해 여러 메서드를 제공합니다.
oauth2
oauth: {
type: "oauth2",
description: "OAuth로 로그인",
flows: ["authorization-code", "device-code"], // 이 세 가지 중 하나를 선택
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 종속성
순수 ESM npm 패키지를 직접 가져올 수 있습니다:
import { z } from "zod";
import * as YAML from "yaml";
빌드가 가져오기를 스캔하고 자동으로 해결합니다 — package.json이 필요 없습니다. 제약 사항:
- 패키지는 순수 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에 없는 호스트에 대한 요청은 거부됩니다. 오류는 핸들러에서 throw된 예외로 나타납니다. -
credentials.<name>.hosts는 HTTP 허용 목록의 하위 집합이어야 합니다. 빌드는 Particle이 도달할 수 없는 호스트에 바인딩된 자격 증명을 거부합니다. -
name은 케밥-케이스여야 합니다. (소문자, 하이픈) 레지스트리 키는(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) |
| 마운트된 디렉토리 읽기/쓰기 | node:fs/promises — 선언된 path 사용 (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> 패키지에서 가져옵니다 — 다른 이름을 만들지 마세요.
출처 및 라이선스
[적응됨: ...]