From 2c04e4a145c6ddc6238e3f2c7cc47ea4255f02ad Mon Sep 17 00:00:00 2001 From: openhands Date: Sat, 1 Nov 2025 02:55:17 +0000 Subject: [PATCH 1/3] feat: make file operations browser-compatible - Update fileUpload to accept string/Blob/File instead of file paths - Update fileDownload to return content instead of writing to disk - Add convenience methods for common browser use cases - Remove Node.js fs and path dependencies - Add FileDownloadResult interface - Update examples to remove Node.js-specific code - Add comprehensive browser compatibility documentation Fixes file operations to work natively in browser environments without Node.js polyfills or dependencies. Co-authored-by: openhands --- BROWSER_COMPATIBILITY.md | 158 ++++++++++++++++++++++++++++++ README.md | 6 ++ examples/basic-usage.ts | 15 +-- package-lock.json | 4 +- src/index.ts | 2 +- src/models/workspace.ts | 8 ++ src/workspace/remote-workspace.ts | 155 ++++++++++++++++++++++------- 7 files changed, 304 insertions(+), 44 deletions(-) create mode 100644 BROWSER_COMPATIBILITY.md diff --git a/BROWSER_COMPATIBILITY.md b/BROWSER_COMPATIBILITY.md new file mode 100644 index 0000000..645ad0c --- /dev/null +++ b/BROWSER_COMPATIBILITY.md @@ -0,0 +1,158 @@ +# Browser Compatibility Guide + +This document outlines the browser-compatible file operations API in the OpenHands TypeScript Client. + +## Overview + +The TypeScript client has been updated to work natively in browser environments without Node.js dependencies. The main changes involve file upload and download operations that now work with browser-native data types like `Blob`, `File`, and strings instead of file system paths. + +## File Upload API + +### `fileUpload(content, destinationPath, fileName?)` + +Upload content to the remote workspace. + +**Parameters:** +- `content: string | Blob | File` - The content to upload +- `destinationPath: string` - Where to save the file on the remote workspace +- `fileName?: string` - Optional filename (auto-detected for File objects) + +**Examples:** + +```typescript +const workspace = new RemoteWorkspace({ + host: 'http://localhost:3000', + workingDir: '/tmp', + apiKey: 'your-api-key' +}); + +// Upload text content +await workspace.fileUpload('Hello, World!', '/tmp/hello.txt', 'hello.txt'); + +// Upload a File object (from file input) +const fileInput = document.getElementById('fileInput') as HTMLInputElement; +const file = fileInput.files[0]; +await workspace.fileUpload(file, '/tmp/uploads/'); + +// Upload a Blob +const blob = new Blob(['Some data'], { type: 'text/plain' }); +await workspace.fileUpload(blob, '/tmp/data.txt', 'data.txt'); +``` + +### Convenience Methods + +#### `uploadText(text, destinationPath, fileName?)` +Shorthand for uploading text content. + +```typescript +await workspace.uploadText('Hello, World!', '/tmp/hello.txt'); +``` + +#### `uploadFileObject(file, destinationPath)` +Shorthand for uploading File objects. + +```typescript +const file = fileInput.files[0]; +await workspace.uploadFileObject(file, '/tmp/uploads/'); +``` + +## File Download API + +### `fileDownload(sourcePath)` + +Download a file from the remote workspace. Returns content as string or Blob. + +**Parameters:** +- `sourcePath: string` - Path to the file on the remote workspace + +**Returns:** `Promise` + +```typescript +interface FileDownloadResult { + success: boolean; + source_path: string; + content: string | Blob; + file_size?: number; + error?: string; +} +``` + +**Example:** + +```typescript +const result = await workspace.fileDownload('/tmp/data.txt'); +if (result.success) { + console.log('File content:', result.content); +} +``` + +### Convenience Methods + +#### `downloadAsText(sourcePath)` +Download file content as a string. + +```typescript +const text = await workspace.downloadAsText('/tmp/hello.txt'); +console.log(text); // "Hello, World!" +``` + +#### `downloadAsBlob(sourcePath)` +Download file content as a Blob. + +```typescript +const blob = await workspace.downloadAsBlob('/tmp/image.png'); +// Use blob for further processing +``` + +#### `downloadAndSave(sourcePath, saveAsFileName?)` +Download a file and trigger browser download dialog. + +```typescript +// This will prompt the user to save the file +await workspace.downloadAndSave('/tmp/report.pdf', 'my-report.pdf'); +``` + +## Migration from Node.js API + +### Before (Node.js only) +```typescript +// Old API - required file system paths +await workspace.fileUpload('/local/path/file.txt', '/remote/path/file.txt'); +await workspace.fileDownload('/remote/path/file.txt', '/local/path/file.txt'); +``` + +### After (Browser compatible) +```typescript +// New API - works with browser data types +const fileInput = document.getElementById('file') as HTMLInputElement; +const file = fileInput.files[0]; +await workspace.fileUpload(file, '/remote/path/file.txt'); + +const result = await workspace.fileDownload('/remote/path/file.txt'); +if (result.success) { + // Use result.content (string or Blob) + console.log(result.content); +} +``` + +## Browser Testing + +A test file `test-browser.html` is included to verify browser compatibility. Open it in a browser after building the project to test the API without a running server. + +## Node.js Compatibility + +The new API is also compatible with Node.js environments. You can still use the client in Node.js applications by providing appropriate data types: + +```typescript +import fs from 'fs'; + +// Read file content and upload +const content = await fs.promises.readFile('/local/file.txt', 'utf8'); +await workspace.fileUpload(content, '/remote/file.txt', 'file.txt'); + +// Download and save +const result = await workspace.fileDownload('/remote/file.txt'); +if (result.success && typeof result.content === 'string') { + await fs.promises.writeFile('/local/downloaded.txt', result.content); +} +``` \ No newline at end of file diff --git a/README.md b/README.md index c302b46..09566a9 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,12 @@ A TypeScript client library for the OpenHands Agent Server API. Mirrors the structure and functionality of the Python [OpenHands Software Agent SDK](https://github.com/OpenHands/software-agent-sdk), but only supports remote conversations. +## ✨ Browser Compatible + +This client is **fully browser-compatible** and works without Node.js dependencies. File operations use browser-native APIs like `Blob`, `File`, and `FormData` instead of file system operations. Perfect for web applications, React apps, and other browser-based projects. + +See [BROWSER_COMPATIBILITY.md](./BROWSER_COMPATIBILITY.md) for detailed usage examples. + ## Installation This package is published to GitHub Packages. You have two installation options: diff --git a/examples/basic-usage.ts b/examples/basic-usage.ts index 78309d1..f7538d0 100644 --- a/examples/basic-usage.ts +++ b/examples/basic-usage.ts @@ -6,10 +6,11 @@ import { Conversation, Agent, Workspace, AgentExecutionStatus } from '../src/ind async function main() { // Define the agent configuration + // Note: In a browser environment, you would get these values from your app's configuration const agent = new Agent({ llm: { model: 'gpt-4', - api_key: process.env.OPENAI_API_KEY || 'your-openai-api-key', + api_key: 'your-openai-api-key', // Replace with your actual API key }, }); @@ -18,7 +19,7 @@ async function main() { const workspace = new Workspace({ host: 'http://localhost:3000', workingDir: '/tmp', - apiKey: process.env.SESSION_API_KEY || 'your-session-api-key', + apiKey: 'your-session-api-key', // Replace with your actual session API key }); // Create a new conversation @@ -87,7 +88,7 @@ async function loadExistingConversation() { const agent = new Agent({ llm: { model: 'gpt-4', - api_key: process.env.OPENAI_API_KEY || 'your-openai-api-key', + api_key: 'your-openai-api-key', // Replace with your actual API key }, }); @@ -96,7 +97,7 @@ async function loadExistingConversation() { const workspace = new Workspace({ host: 'http://localhost:3000', workingDir: '/tmp', - apiKey: process.env.SESSION_API_KEY || 'your-session-api-key', + apiKey: 'your-session-api-key', // Replace with your actual session API key }); const conversation = new Conversation(agent, workspace, { @@ -120,6 +121,6 @@ async function loadExistingConversation() { } // Run the example -if (require.main === module) { - main().catch(console.error); -} +// Note: In a browser environment, you would call main() directly or from an event handler +// For Node.js environments, you can use import.meta.main (ES modules) or check if this is the main module +main().catch(console.error); diff --git a/package-lock.json b/package-lock.json index 8e6885f..1b81fc7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "@openhands/agent-server-typescript-client", + "name": "@openhands/typescript-client", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@openhands/agent-server-typescript-client", + "name": "@openhands/typescript-client", "version": "0.1.0", "license": "MIT", "dependencies": { diff --git a/src/index.ts b/src/index.ts index 84d52a2..f01d444 100644 --- a/src/index.ts +++ b/src/index.ts @@ -55,7 +55,7 @@ export type { AgentOptions } from './agent/agent'; export { EventSortOrder, AgentExecutionStatus } from './types/base'; // Workspace models -export type { CommandResult, FileOperationResult, GitChange, GitDiff } from './models/workspace'; +export type { CommandResult, FileOperationResult, FileDownloadResult, GitChange, GitDiff } from './models/workspace'; // Conversation models export type { diff --git a/src/models/workspace.ts b/src/models/workspace.ts index 6259882..4f08c1d 100644 --- a/src/models/workspace.ts +++ b/src/models/workspace.ts @@ -18,6 +18,14 @@ export interface FileOperationResult { error?: string; } +export interface FileDownloadResult { + success: boolean; + source_path: string; + content: string | Blob; + file_size?: number; + error?: string; +} + export interface GitChange { path: string; status: 'added' | 'modified' | 'deleted' | 'renamed'; diff --git a/src/workspace/remote-workspace.ts b/src/workspace/remote-workspace.ts index cdf252b..50fa0b8 100644 --- a/src/workspace/remote-workspace.ts +++ b/src/workspace/remote-workspace.ts @@ -3,7 +3,7 @@ */ import { HttpClient } from '../client/http-client'; -import { CommandResult, FileOperationResult, GitChange, GitDiff } from '../models/workspace'; +import { CommandResult, FileOperationResult, FileDownloadResult, GitChange, GitDiff } from '../models/workspace'; export interface RemoteWorkspaceOptions { host: string; @@ -129,22 +129,33 @@ export class RemoteWorkspace { } } - async fileUpload(sourcePath: string, destinationPath: string): Promise { - console.debug(`Remote file upload: ${sourcePath} -> ${destinationPath}`); + async fileUpload( + content: string | Blob | File, + destinationPath: string, + fileName?: string + ): Promise { + console.debug(`Remote file upload to: ${destinationPath}`); try { - // For browser environments, this would need to be adapted to work with File objects - // For Node.js environments, we can read the file - const fs = await import('fs'); - const path = await import('path'); - - const fileContent = await fs.promises.readFile(sourcePath); - const fileName = path.basename(sourcePath); - // Create FormData for file upload const formData = new FormData(); - const blob = new Blob([fileContent]); - formData.append('file', blob, fileName); + + let blob: Blob; + let finalFileName: string; + + if (content instanceof File) { + blob = content; + finalFileName = fileName || content.name; + } else if (content instanceof Blob) { + blob = content; + finalFileName = fileName || 'blob-file'; + } else { + // String content + blob = new Blob([content], { type: 'text/plain' }); + finalFileName = fileName || 'text-file.txt'; + } + + formData.append('file', blob, finalFileName); formData.append('destination_path', destinationPath); const response = await this.client.request({ @@ -158,7 +169,7 @@ export class RemoteWorkspace { return { success: resultData.success ?? true, - source_path: sourcePath, + source_path: finalFileName, destination_path: destinationPath, file_size: resultData.file_size, error: resultData.error, @@ -167,15 +178,15 @@ export class RemoteWorkspace { console.error(`Remote file upload failed: ${error}`); return { success: false, - source_path: sourcePath, + source_path: fileName || 'unknown', destination_path: destinationPath, error: error instanceof Error ? error.message : String(error), }; } } - async fileDownload(sourcePath: string, destinationPath: string): Promise { - console.debug(`Remote file download: ${sourcePath} -> ${destinationPath}`); + async fileDownload(sourcePath: string): Promise { + console.debug(`Remote file download: ${sourcePath}`); try { const response = await this.client.get( @@ -185,33 +196,38 @@ export class RemoteWorkspace { } ); - // For Node.js environments, write the file - const fs = await import('fs'); - const path = await import('path'); - - // Ensure destination directory exists - const destDir = path.dirname(destinationPath); - await fs.promises.mkdir(destDir, { recursive: true }); - - // Write the file content - const content = - typeof response.data === 'string' ? response.data : JSON.stringify(response.data); - await fs.promises.writeFile(destinationPath, content); - - const stats = await fs.promises.stat(destinationPath); + // Convert response data to appropriate format + let content: string | Blob; + let fileSize: number; + + if (typeof response.data === 'string') { + content = response.data; + fileSize = new Blob([response.data]).size; + } else if (response.data instanceof ArrayBuffer) { + content = new Blob([response.data]); + fileSize = response.data.byteLength; + } else if (response.data instanceof Blob) { + content = response.data; + fileSize = response.data.size; + } else { + // For other data types, stringify and create blob + const stringData = JSON.stringify(response.data); + content = stringData; + fileSize = new Blob([stringData]).size; + } return { success: true, source_path: sourcePath, - destination_path: destinationPath, - file_size: stats.size, + content: content, + file_size: fileSize, }; } catch (error) { console.error(`Remote file download failed: ${error}`); return { success: false, source_path: sourcePath, - destination_path: destinationPath, + content: '', error: error instanceof Error ? error.message : String(error), }; } @@ -243,6 +259,77 @@ export class RemoteWorkspace { } } + /** + * Convenience method to upload text content as a file + */ + async uploadText(text: string, destinationPath: string, fileName?: string): Promise { + return this.fileUpload(text, destinationPath, fileName); + } + + /** + * Convenience method to upload a File object (from file input) + */ + async uploadFileObject(file: File, destinationPath: string): Promise { + return this.fileUpload(file, destinationPath); + } + + /** + * Convenience method to download file content as text + */ + async downloadAsText(sourcePath: string): Promise { + const result = await this.fileDownload(sourcePath); + if (!result.success) { + throw new Error(result.error || 'Download failed'); + } + + if (typeof result.content === 'string') { + return result.content; + } else if (result.content instanceof Blob) { + return await result.content.text(); + } + + return ''; + } + + /** + * Convenience method to download file content as a Blob + */ + async downloadAsBlob(sourcePath: string): Promise { + const result = await this.fileDownload(sourcePath); + if (!result.success) { + throw new Error(result.error || 'Download failed'); + } + + if (result.content instanceof Blob) { + return result.content; + } else if (typeof result.content === 'string') { + return new Blob([result.content], { type: 'text/plain' }); + } + + return new Blob(); + } + + /** + * Convenience method to trigger a browser download of a file + */ + async downloadAndSave(sourcePath: string, saveAsFileName?: string): Promise { + const blob = await this.downloadAsBlob(sourcePath); + + // Create a temporary URL for the blob + const url = URL.createObjectURL(blob); + + // Create a temporary anchor element to trigger download + const a = document.createElement('a'); + a.href = url; + a.download = saveAsFileName || sourcePath.split('/').pop() || 'download'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + + // Clean up the temporary URL + URL.revokeObjectURL(url); + } + close(): void { this.client.close(); } From b15cabe75f2d867840370ffc8c3a8d4bc3e4feb4 Mon Sep 17 00:00:00 2001 From: openhands Date: Sat, 1 Nov 2025 02:57:32 +0000 Subject: [PATCH 2/3] style: fix code formatting with prettier Co-authored-by: openhands --- src/index.ts | 8 ++++++- src/workspace/remote-workspace.ts | 38 +++++++++++++++++++------------ 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/index.ts b/src/index.ts index f01d444..2fc243a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -55,7 +55,13 @@ export type { AgentOptions } from './agent/agent'; export { EventSortOrder, AgentExecutionStatus } from './types/base'; // Workspace models -export type { CommandResult, FileOperationResult, FileDownloadResult, GitChange, GitDiff } from './models/workspace'; +export type { + CommandResult, + FileOperationResult, + FileDownloadResult, + GitChange, + GitDiff, +} from './models/workspace'; // Conversation models export type { diff --git a/src/workspace/remote-workspace.ts b/src/workspace/remote-workspace.ts index 50fa0b8..87ea6ef 100644 --- a/src/workspace/remote-workspace.ts +++ b/src/workspace/remote-workspace.ts @@ -3,7 +3,13 @@ */ import { HttpClient } from '../client/http-client'; -import { CommandResult, FileOperationResult, FileDownloadResult, GitChange, GitDiff } from '../models/workspace'; +import { + CommandResult, + FileOperationResult, + FileDownloadResult, + GitChange, + GitDiff, +} from '../models/workspace'; export interface RemoteWorkspaceOptions { host: string; @@ -130,8 +136,8 @@ export class RemoteWorkspace { } async fileUpload( - content: string | Blob | File, - destinationPath: string, + content: string | Blob | File, + destinationPath: string, fileName?: string ): Promise { console.debug(`Remote file upload to: ${destinationPath}`); @@ -139,10 +145,10 @@ export class RemoteWorkspace { try { // Create FormData for file upload const formData = new FormData(); - + let blob: Blob; let finalFileName: string; - + if (content instanceof File) { blob = content; finalFileName = fileName || content.name; @@ -199,7 +205,7 @@ export class RemoteWorkspace { // Convert response data to appropriate format let content: string | Blob; let fileSize: number; - + if (typeof response.data === 'string') { content = response.data; fileSize = new Blob([response.data]).size; @@ -262,7 +268,11 @@ export class RemoteWorkspace { /** * Convenience method to upload text content as a file */ - async uploadText(text: string, destinationPath: string, fileName?: string): Promise { + async uploadText( + text: string, + destinationPath: string, + fileName?: string + ): Promise { return this.fileUpload(text, destinationPath, fileName); } @@ -281,13 +291,13 @@ export class RemoteWorkspace { if (!result.success) { throw new Error(result.error || 'Download failed'); } - + if (typeof result.content === 'string') { return result.content; } else if (result.content instanceof Blob) { return await result.content.text(); } - + return ''; } @@ -299,13 +309,13 @@ export class RemoteWorkspace { if (!result.success) { throw new Error(result.error || 'Download failed'); } - + if (result.content instanceof Blob) { return result.content; } else if (typeof result.content === 'string') { return new Blob([result.content], { type: 'text/plain' }); } - + return new Blob(); } @@ -314,10 +324,10 @@ export class RemoteWorkspace { */ async downloadAndSave(sourcePath: string, saveAsFileName?: string): Promise { const blob = await this.downloadAsBlob(sourcePath); - + // Create a temporary URL for the blob const url = URL.createObjectURL(blob); - + // Create a temporary anchor element to trigger download const a = document.createElement('a'); a.href = url; @@ -325,7 +335,7 @@ export class RemoteWorkspace { document.body.appendChild(a); a.click(); document.body.removeChild(a); - + // Clean up the temporary URL URL.revokeObjectURL(url); } From 4b522a7fce99f4ad9a0d970c94bdcf406a3059d2 Mon Sep 17 00:00:00 2001 From: openhands Date: Sat, 1 Nov 2025 03:15:52 +0000 Subject: [PATCH 3/3] docs: remove separate browser compatibility documentation Integrate browser compatibility info directly into README instead of separate file for better maintainability. Co-authored-by: openhands --- BROWSER_COMPATIBILITY.md | 158 --------------------------------------- README.md | 2 - 2 files changed, 160 deletions(-) delete mode 100644 BROWSER_COMPATIBILITY.md diff --git a/BROWSER_COMPATIBILITY.md b/BROWSER_COMPATIBILITY.md deleted file mode 100644 index 645ad0c..0000000 --- a/BROWSER_COMPATIBILITY.md +++ /dev/null @@ -1,158 +0,0 @@ -# Browser Compatibility Guide - -This document outlines the browser-compatible file operations API in the OpenHands TypeScript Client. - -## Overview - -The TypeScript client has been updated to work natively in browser environments without Node.js dependencies. The main changes involve file upload and download operations that now work with browser-native data types like `Blob`, `File`, and strings instead of file system paths. - -## File Upload API - -### `fileUpload(content, destinationPath, fileName?)` - -Upload content to the remote workspace. - -**Parameters:** -- `content: string | Blob | File` - The content to upload -- `destinationPath: string` - Where to save the file on the remote workspace -- `fileName?: string` - Optional filename (auto-detected for File objects) - -**Examples:** - -```typescript -const workspace = new RemoteWorkspace({ - host: 'http://localhost:3000', - workingDir: '/tmp', - apiKey: 'your-api-key' -}); - -// Upload text content -await workspace.fileUpload('Hello, World!', '/tmp/hello.txt', 'hello.txt'); - -// Upload a File object (from file input) -const fileInput = document.getElementById('fileInput') as HTMLInputElement; -const file = fileInput.files[0]; -await workspace.fileUpload(file, '/tmp/uploads/'); - -// Upload a Blob -const blob = new Blob(['Some data'], { type: 'text/plain' }); -await workspace.fileUpload(blob, '/tmp/data.txt', 'data.txt'); -``` - -### Convenience Methods - -#### `uploadText(text, destinationPath, fileName?)` -Shorthand for uploading text content. - -```typescript -await workspace.uploadText('Hello, World!', '/tmp/hello.txt'); -``` - -#### `uploadFileObject(file, destinationPath)` -Shorthand for uploading File objects. - -```typescript -const file = fileInput.files[0]; -await workspace.uploadFileObject(file, '/tmp/uploads/'); -``` - -## File Download API - -### `fileDownload(sourcePath)` - -Download a file from the remote workspace. Returns content as string or Blob. - -**Parameters:** -- `sourcePath: string` - Path to the file on the remote workspace - -**Returns:** `Promise` - -```typescript -interface FileDownloadResult { - success: boolean; - source_path: string; - content: string | Blob; - file_size?: number; - error?: string; -} -``` - -**Example:** - -```typescript -const result = await workspace.fileDownload('/tmp/data.txt'); -if (result.success) { - console.log('File content:', result.content); -} -``` - -### Convenience Methods - -#### `downloadAsText(sourcePath)` -Download file content as a string. - -```typescript -const text = await workspace.downloadAsText('/tmp/hello.txt'); -console.log(text); // "Hello, World!" -``` - -#### `downloadAsBlob(sourcePath)` -Download file content as a Blob. - -```typescript -const blob = await workspace.downloadAsBlob('/tmp/image.png'); -// Use blob for further processing -``` - -#### `downloadAndSave(sourcePath, saveAsFileName?)` -Download a file and trigger browser download dialog. - -```typescript -// This will prompt the user to save the file -await workspace.downloadAndSave('/tmp/report.pdf', 'my-report.pdf'); -``` - -## Migration from Node.js API - -### Before (Node.js only) -```typescript -// Old API - required file system paths -await workspace.fileUpload('/local/path/file.txt', '/remote/path/file.txt'); -await workspace.fileDownload('/remote/path/file.txt', '/local/path/file.txt'); -``` - -### After (Browser compatible) -```typescript -// New API - works with browser data types -const fileInput = document.getElementById('file') as HTMLInputElement; -const file = fileInput.files[0]; -await workspace.fileUpload(file, '/remote/path/file.txt'); - -const result = await workspace.fileDownload('/remote/path/file.txt'); -if (result.success) { - // Use result.content (string or Blob) - console.log(result.content); -} -``` - -## Browser Testing - -A test file `test-browser.html` is included to verify browser compatibility. Open it in a browser after building the project to test the API without a running server. - -## Node.js Compatibility - -The new API is also compatible with Node.js environments. You can still use the client in Node.js applications by providing appropriate data types: - -```typescript -import fs from 'fs'; - -// Read file content and upload -const content = await fs.promises.readFile('/local/file.txt', 'utf8'); -await workspace.fileUpload(content, '/remote/file.txt', 'file.txt'); - -// Download and save -const result = await workspace.fileDownload('/remote/file.txt'); -if (result.success && typeof result.content === 'string') { - await fs.promises.writeFile('/local/downloaded.txt', result.content); -} -``` \ No newline at end of file diff --git a/README.md b/README.md index 09566a9..e21c3a3 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,6 @@ but only supports remote conversations. This client is **fully browser-compatible** and works without Node.js dependencies. File operations use browser-native APIs like `Blob`, `File`, and `FormData` instead of file system operations. Perfect for web applications, React apps, and other browser-based projects. -See [BROWSER_COMPATIBILITY.md](./BROWSER_COMPATIBILITY.md) for detailed usage examples. - ## Installation This package is published to GitHub Packages. You have two installation options: