Setup
Install Tools
# Install Noorle CLI
curl -L cli.noorle.dev | sh
# Create a new TypeScript plugin
noorle plugin new my-ts-plugin --lang typescript
cd my-ts-plugin
Project Structure
my-ts-plugin/
├── src/
│ ├── index.ts # Entry point
│ ├── tools.ts # Tool implementations
│ └── utils.ts # Utilities
├── tests/
│ └── tools.test.ts
├── package.json # Dependencies
├── tsconfig.json # TypeScript config
├── noorle.yaml # Plugin config
├── world.wit # Component interface
├── .env
└── README.md
Define Tools
Export functions as plugin tools.Simple Functions
// src/tools.ts
export function greet(name: string): string {
return `Hello, ${name}!`;
}
export function add(a: number, b: number): number {
return a + b;
}
export function multiply(a: number, b: number): number {
return a * b;
}
Complex Types
interface Item {
id: string;
name: string;
count: number;
}
interface Config {
[key: string]: string;
}
export function processItems(
items: Item[],
config: Config
): Item[] {
return items.map(item => ({
...item,
name: `${config.prefix}${item.name}`
}));
}
export function searchItems(
query: string,
limit: number = 10
): Item[] {
return Array.from({ length: Math.min(limit, 5) }, (_, i) => ({
id: `${i}`,
name: `${query}_result_${i}`,
count: i + 1
}));
}
Type Definitions
Define reusable types:// src/types.ts
export interface User {
id: string;
name: string;
email: string;
active: boolean;
}
export interface Result<T> {
success: boolean;
data?: T;
error?: string;
}
export interface ApiResponse {
status: number;
body: string;
headers: Record<string, string>;
}
// Use in tools
export function validateUser(user: User): Result<User> {
if (!user.email.includes("@")) {
return { success: false, error: "Invalid email" };
}
return { success: true, data: user };
}
Working with Dependencies
Add Dependencies
Editpackage.json:
{
"name": "my-ts-plugin",
"version": "0.1.0",
"dependencies": {
"axios": "^1.6.0",
"lodash-es": "^4.17.21",
"uuid": "^9.0.0"
},
"devDependencies": {
"typescript": "^5.0.0",
"@types/node": "^20.0.0"
}
}
Install and Build
# Install dependencies
npm install
# Build the plugin
noorle plugin build
Size Optimization
Choose lightweight dependencies:{
"dependencies": {
// ❌ Heavy
"axios": "^1.6.0", // ~60KB with dependencies
"lodash": "^4.17.0", // ~50KB
// ✅ Lightweight
"uuid": "^9.0.0", // ~10KB
"date-fns": "^2.30.0" // ~20KB
}
}
File I/O
Access filesystem via standard APIs.import * as fs from "fs";
import * as path from "path";
export function readConfig(): string {
const configPath = "/allowed/path/config.json";
return fs.readFileSync(configPath, "utf-8");
}
export function writeCache(key: string, value: string): boolean {
const cacheDir = process.env.CACHE_DIR || "/cache";
const filePath = path.join(cacheDir, `${key}.txt`);
fs.writeFileSync(filePath, value);
return true;
}
export function listFiles(directory: string): string[] {
return fs.readdirSync(directory)
.filter(f => fs.statSync(path.join(directory, f)).isFile())
.map(f => f);
}
Configure Permissions
Innoorle.yaml:
permissions:
filesystem:
allow:
- uri: "fs://cache/**"
access: [read, write]
- uri: "fs://config/**"
access: [read]
environment:
allow:
- key: "CACHE_DIR"
Environment Variables
Access configuration via environment variables.export function getApiCredentials(): Record<string, string> {
return {
apiKey: process.env.API_KEY || "",
apiSecret: process.env.API_SECRET || "",
};
}
export function getDatabaseUrl(): string {
return process.env.DATABASE_URL || "sqlite:///:memory:";
}
export function getConfig(): Record<string, string> {
return {
debug: process.env.DEBUG === "true" ? "true" : "false",
logLevel: process.env.LOG_LEVEL || "info",
timeout: process.env.TIMEOUT || "30000",
};
}
.env:
API_KEY=sk-1234567890
API_SECRET=secret-value
DATABASE_URL=postgres://localhost/mydb
DEBUG=true
LOG_LEVEL=debug
Network Access
Make HTTP requests to allowed hosts.export async function fetchData(url: string): Promise<string> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.text();
}
export async function postJson(
url: string,
data: Record<string, unknown>
): Promise<string> {
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
return response.text();
}
export async function callApi(
endpoint: string,
params: Record<string, string>
): Promise<Record<string, unknown>> {
const url = new URL(`https://api.example.com/${endpoint}`);
Object.entries(params).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
const response = await fetch(url.toString());
return response.json();
}
axios (add to package.json):
import axios from "axios";
export async function callApiAxios(endpoint: string): Promise<string> {
const response = await axios.get(
`https://api.example.com/${endpoint}`,
{ timeout: 5000 }
);
return response.data;
}
permissions:
network:
allow:
- host: "api.example.com"
- host: "*.github.com"
Error Handling
Return errors gracefully with typed results:interface ErrorResult {
success: false;
error: string;
}
interface SuccessResult<T> {
success: true;
data: T;
}
type Result<T> = SuccessResult<T> | ErrorResult;
export function safeOperation(input: string): Result<string> {
try {
if (!input) {
return { success: false, error: "Empty input" };
}
const result = process(input);
return { success: true, data: result };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error"
};
}
}
function process(input: string): string {
if (typeof input !== "string") {
throw new Error("Expected string input");
}
return input.toUpperCase();
}
Testing
Test locally before uploading.Unit Tests
// tests/tools.test.ts
import { greet, add, processItems } from "../src/tools";
describe("Tools", () => {
test("greet returns greeting", () => {
expect(greet("Alice")).toBe("Hello, Alice!");
});
test("add performs addition", () => {
expect(add(2, 3)).toBe(5);
expect(add(1.5, 2.5)).toBe(4.0);
});
test("processItems transforms items", () => {
const items = [{ id: "1", name: "test", count: 5 }];
const config = { prefix: "new_" };
const result = processItems(items, config);
expect(result[0].name).toBe("new_test");
});
});
npm test
Integration Tests
# Build and test the plugin
noorle plugin build
noorle plugin test my_tool --input '{"param": "value"}'
Building and Deploying
Build
noorle plugin build
# Output:
# ✓ Building plugin...
# ✓ Compiling TypeScript to WebAssembly
# ✓ Creating my-ts-plugin.npack (156KB)
Upload
noorle plugin push --api-key YOUR_KEY
# Output:
# ✓ Uploading plugin...
# ✓ Plugin ID: 550e8400-e29b-41d4-a716-446655440000
# ✓ Active version: v1
# ✓ Tools: greet, add, processItems
Best Practices
Input Validation
export function validateEmail(email: string): Record<string, unknown> {
const errors: string[] = [];
if (!email || email.trim().length === 0) {
errors.push("Email cannot be empty");
}
if (!email.includes("@")) {
errors.push("Email must contain @");
}
if (email.length > 255) {
errors.push("Email too long");
}
return errors.length > 0
? { valid: false, errors }
: { valid: true };
}
Logging
function log(message: string, level: "INFO" | "DEBUG" | "ERROR" = "INFO") {
console.error(`[${level}] ${message}`);
}
export function myTool(input: string): string {
log(`Processing: ${input}`);
const result = process(input);
log(`Completed`, "DEBUG");
return result;
}
Documentation
/**
* Transform input text.
*
* @param text - The text to transform
* @param uppercase - Convert to uppercase if true
* @param trim - Remove whitespace if true
* @returns Transformed text
*/
export function transformText(
text: string,
uppercase: boolean = false,
trim: boolean = true
): string {
if (trim) text = text.trim();
if (uppercase) text = text.toUpperCase();
return text;
}
Example: Complete Plugin
// src/weather.ts
import * as fs from "fs";
interface WeatherData {
city: string;
temp: number;
description: string;
}
export async function getWeather(
city: string,
units: "celsius" | "fahrenheit" = "celsius"
): Promise<Record<string, unknown>> {
const apiKey = process.env.OPENWEATHER_API_KEY;
if (!apiKey) {
return { error: "API key not configured" };
}
const url = `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${apiKey}&units=${units}`;
try {
const response = await fetch(url);
const data = await response.json();
return {
city: data.name,
temp: data.main.temp,
description: data.weather[0].description,
};
} catch (error) {
return {
error: `Failed to fetch: ${error instanceof Error ? error.message : "Unknown error"}`,
};
}
}
export function cacheWeather(
city: string,
data: WeatherData
): boolean {
const cacheDir = process.env.CACHE_DIR || "/cache";
const filePath = `${cacheDir}/${city}.json`;
fs.writeFileSync(filePath, JSON.stringify(data));
return true;
}
Troubleshooting
Build fails with type errors:# Check TypeScript compilation
npx tsc --noEmit
- Review
node_modulessize - Remove unused dependencies
- Use tree-shaking compatible libraries
- Verify
.envfile is created - Check
noorle.yamlenvironment permissions - Ensure variable names are exact match