# Plimi: Privacy-First Browser Toolbox

Plimi is a modern, modular utility suite that executes tool processing directly inside your browser. Tool inputs and outputs are not uploaded by Plimi.

## Features
- **Privacy-First Design**: Tool processing is offline-capable. Audience measurement is self-hosted, narrow in scope, and can be disabled locally.
- **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 -t plimi:local .

docker run --rm -p 8080:80 --name plimi \
  --env-file .env \
  plimi:local
```

Or use Docker Compose:

```bash
cp .env.example .env
# Edit .env, then:
docker compose up --build
```

Environment:
- `VITE_PLIMI_REPO_URL`: build-time public repository URL shown on the Contribute page.
- `VITE_SITE_URL`: canonical public URL used to restrict analytics to the deployed hostname.
- `VITE_LEGAL_NAME`, `VITE_LEGAL_COUNTRY`: controller identity shown in the privacy notice.
- `VITE_PRIVACY_EMAIL`: contact address for privacy and data-subject requests.
- `VITE_PRIVACY_EFFECTIVE_DATE`: effective date displayed on the privacy notice.
- `VITE_ANALYTICS_SCRIPT_URL`, `VITE_ANALYTICS_HOST_URL`, `VITE_ANALYTICS_WEBSITE_ID`: Umami tracker configuration.
- `VITE_ANALYTICS_HOST`, `VITE_ANALYTICS_HOST_COUNTRY`: analytics recipient and hosting location disclosed to visitors.
- `VITE_ANALYTICS_RETENTION_MONTHS`: disclosed analytics retention period. Configure Umami/database deletion to match it.
- `PLIMI_PORT`: host port used by Docker Compose, default `8080`.

Analytics is self-hosted and disabled until the visitor explicitly consents.
The tracker excludes URL search parameters and hashes, honors Do Not Track, and
can be disabled locally through the privacy controls.
The privacy page is available at `/privacy`; deployers must replace all example
controller and hosting values before publishing.

The bundled Nginx configuration disables routine access logging. If a reverse
proxy, CDN, hosting platform, or firewall logs visitor IP addresses or request
metadata, document that processing and enforce an appropriate retention period
in that service.

The Docker image is environment-independent. At container startup, its entrypoint
generates `/config.js` from `.env`, so the same image can be promoted between
environments without rebuilding. The file is ignored by Git and excluded from
the Docker build context.

These values are public browser configuration even though they are supplied at
deploy time. Do not put passwords, private keys, or API secrets in them.

---

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

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

```typescript
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:

```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.
