# Plimi: Privacy-First Browser Toolbox Plimi is a modern, modular, zero-backend utility suite that executes all tools directly inside your browser. No data ever leaves your device. ## Features - **Privacy-First Design**: Completely offline-capable. No data is sent to external servers. - **Plugin Architecture**: Modular, strictly-typed plugin ecosystem supporting custom UI or generated UI based on schema. - **Web Workers & WASM**: Supports off-loading expensive operations (e.g. PDF manipulation, media processing) into separate threads. - **High Performance**: Built with React, Vite, and Tailwind CSS v4. --- ## Getting Started ### Prerequisites - Node.js 18+ - npm or pnpm ### Installation & Run Dev Server ```bash npm install npm run dev ``` ### Run Unit Tests ```bash npm test ``` ### Production Build ```bash npm run build ``` ### Docker Build and run the static production app with Nginx: ```bash docker build \ --build-arg VITE_PLIMI_REPO_URL=https://github.com/your-org/plimi \ -t plimi:local . docker run --rm -p 8080:80 --name plimi plimi:local ``` Or use Docker Compose: ```bash VITE_PLIMI_REPO_URL=https://github.com/your-org/plimi PLIMI_PORT=8080 docker compose up --build ``` Environment: - `VITE_PLIMI_REPO_URL`: build-time public repository URL shown on the Contribute page. - `PLIMI_PORT`: host port used by Docker Compose, default `8080`. --- ## How to Create a Tool (Plugin) Adding a new tool to Plimi is seamless thanks to its generic rendering architecture. By default, Plimi generates the tool's interface based on its options schema and input/output definitions. ### Step 1: Create the Plugin Definition Create a new directory in `src/tools/` (e.g., `src/tools/my-tool/index.ts`): ```typescript import type { PlimiPlugin } from '../../core/plugins/plugin-types'; import { runMyTool } from './run'; export interface MyToolOptions { uppercase: boolean; prefix: string; } export const myToolPlugin: PlimiPlugin = { manifest: { id: 'my-tool', name: 'My Custom Tool', description: 'Does something awesome entirely in the browser.', category: 'developer', version: '1.0.0', tags: ['custom', 'developer', 'text'], // 1. Defining Inputs (see Input Options below) input: { type: 'text', multiline: false, // Renders a single-line input field instead of a textarea placeholder: 'Type something...', example: 'Hello Plimi' }, // 2. Defining Outputs (text, json, table, files) output: { type: 'text' }, offlineReady: true, }, // 3. User options generated in the right panel automatically optionsSchema: { fields: [ { type: 'boolean', key: 'uppercase', label: 'Uppercase Output', defaultValue: false }, { type: 'text', key: 'prefix', label: 'Output Prefix', defaultValue: 'Result: ', placeholder: 'e.g. Result: ' } ] }, // Optional: override or add user-facing examples. // If omitted, Plimi automatically builds a "Try example" action from // manifest.example for single-input tools or field.example for grouped inputs. examples: [ { label: 'Try greeting', input: { text: 'Hello Plimi' }, options: { uppercase: true, prefix: 'Result: ' } } ], capabilities: { cancelable: false, worker: false, }, run: runMyTool, }; ``` --- ### Step 2: Implement the Runner Logic Create the runner file (e.g., `src/tools/my-tool/run.ts`): ```typescript import { getTextInput, type ToolInput } from '../../core/io/input-types'; import type { ToolResult } from '../../core/io/output-types'; import type { ToolContext } from '../../core/plugins/plugin-types'; import type { MyToolOptions } from './index'; export async function runMyTool( input: ToolInput, options: MyToolOptions, context: ToolContext ): Promise { // Use getTextInput helper for safe retrieval const text = getTextInput(input); if (!text) { throw new Error("No text provided."); } // Report progress back to the UI progress bar (if needed) context.reportProgress({ percentage: 50, message: "Processing text..." }); let result = text; if (options.uppercase) { result = result.toUpperCase(); } return { type: 'text', value: `${options.prefix}${result}`, }; } ``` --- ### Step 3: Register the Plugin Import your new plugin into `src/core/plugins/plugin-registry.ts` and append it to the `pluginRegistry` array: ```typescript import { myToolPlugin } from "../../tools/my-tool"; export const pluginRegistry: PlimiPlugin[] = [ // ... existing plugins myToolPlugin, ]; ``` The application will automatically register the tool, list it in the directory, enable search, and construct its input and options UI! --- ## UI Abstraction Details ### Input Schema Configuration The input property in the manifest supports three structures: 1. **`type: "none"`**: No input field is rendered (useful for generators, e.g., Lorem Ipsum). 2. **Single Field**: Renders a single input field. - `type: "text"`: Renders a text entry box. - Specify `multiline: false` to make it a compact single-line input field (default is a textarea). - Specify `rows: number` to customize multiline text area height. - `type: "files"`: Renders a drag-and-drop file uploader area. 3. **`type: "group"`**: Renders a group of input fields stacked vertically. Each field requires a unique `key`. - Plimi will automatically expose a shared **"Try example"** action when fields contain example data, allowing the user to populate the form with one click. - Access group values inside the runner using `getTextInput(input, "field_key")`. ### Output Visualizers Plimi automatically formats results based on the returned `ToolResult.type`: - **`type: "text"`**: Renders a code block or plain text area with a global copy button. - **`type: "files"`**: Renders a lists of downloadable files with size comparisons. - **`type: "table"`**: Renders a tabular data format with column headers, row styles, and a **"Copy CSV"** button. - **`type: "json"`**: Renders a flat grid of cards for each key. - Return a key named `"primary"` in your JSON output to showcase it prominently in a large card at the top of the results panel. - Each key in the grid renders with its own individual, hoverable **"Copy"** button for a premium experience. --- ### Writing Custom UIs For complex interfaces (like Canvas-based image manipulation or interactive dropzones), set `customUi: true` under `capabilities` in the manifest and supply a React component to the `customUi` key: ```typescript import { lazy } from "react"; const CustomUiComponent = lazy(() => import("./CustomUiComponent")); export const myPlugin = { // ... capabilities: { customUi: true }, customUi: CustomUiComponent }; ``` Your component will receive the plugin definition as `plugin`, the current `dark` theme boolean, and normalized `examples` as props. Custom UIs can import `ExampleActionButton` from `src/components/tool/ExampleActionButton` to render the same "Try example" action style used by generated tools. --- ### Writing Tests Every tool should be tested. Create a `run.test.ts` alongside your tool and run: ```bash npm test ``` See `src/tools/regex-tester/run.test.ts` for an example of testing both key-based inputs and fallback single-string values.