Files
plimi/README.md
2026-06-02 20:33:09 +02:00

7.3 KiB

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

npm install
npm run dev

Run Unit Tests

npm test

Production Build

npm run build

Docker

Build and run the static production app with Nginx:

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:

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):

import type { PlimiPlugin } from '../../core/plugins/plugin-types';
import { runMyTool } from './run';

export interface MyToolOptions {
  uppercase: boolean;
  prefix: string;
}

export const myToolPlugin: PlimiPlugin<MyToolOptions> = {
  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):

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<ToolResult> {
  // 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:

import { myToolPlugin } from "../../tools/my-tool";

export const pluginRegistry: PlimiPlugin<any>[] = [
  // ... 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:

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:

npm test

See src/tools/regex-tester/run.test.ts for an example of testing both key-based inputs and fallback single-string values.