# How to Develop and Install Custom Plugins for OfficeCLI

> Learn to develop and install custom plugins for OfficeCLI. Extend OfficeCLI's capabilities with standalone executables and leverage the built-in registry installer for seamless integration.

- Repository: [OfficeAI/OfficeCLI](https://github.com/iofficeai/OfficeCLI)
- Tags: how-to-guide
- Published: 2026-07-08

---

**OfficeCLI extends its core document capabilities through standalone plugin executables that communicate via JSON-L over stdout, discovered through specific directory hierarchies or PATH entries, and installed manually or via the built-in registry installer.**

OfficeCLI, maintained by the iOfficeAI/OfficeCLI repository, implements a well-defined **plugin protocol** that allows developers to add support for legacy formats, custom exporters, and specialized document handlers. The architecture requires plugins to act as standalone executables that the main binary discovers and invokes based on file extensions and declared capabilities. This guide covers the complete workflow for developing and installing these plugins according to the official specification.

## Understanding the OfficeCLI Plugin Architecture

The plugin system relies on a **main binary** that manages external executables through structured inter-process communication defined in [`plugins/plugin-protocol.md`](https://github.com/iOfficeAI/OfficeCLI/blob/main/plugins/plugin-protocol.md).

### Plugin Discovery Mechanism

The main binary searches for plugins in a hierarchical order: environment variables first, then the user-local `~/.officecli/plugins` directory, followed by bundled `plugins/` directories, and finally the system PATH using the naming pattern `officecli-<kind>-<ext>`. This discovery sequence allows both system-wide and user-specific plugin installations without conflicts.

### Plugin Kinds and Lifecycle

OfficeCLI defines three **plugin kinds** that determine the interaction pattern. The `dump-reader` kind imports legacy formats into the internal document representation, the `exporter` kind renders documents to external targets like PDF or EPUB, and the `format-handler` kind maintains long-lived sessions for interactive editing. Each kind enforces a specific contract regarding input arguments, output streams, and session duration according to the protocol specification.

### The Plugin Manifest

Every plugin must emit a JSON manifest when invoked with the `--info` argument. This manifest declares the plugin `name`, `version`, `protocol` version (currently 1), supported `kinds`, file `extensions`, `runtime` environment, and `idle_timeout_seconds` budget. The main binary validates this manifest during discovery to ensure compatibility and uses it to populate help output and command availability.

## Developing a Custom Plugin

Creating a plugin requires implementing the protocol contracts regardless of programming language, focusing on three core responsibilities: manifest emission, command handling, and IPC compliance.

### Choosing a Plugin Kind

Most custom development starts with either a **dump-reader** to convert legacy formats into OfficeCLI's internal representation, or an **exporter** to render documents to external formats. The `format-handler` kind requires implementing a persistent session loop processing `open`, `save`, and `close` messages, making it suitable for complex editing scenarios.

### Implementing the --info Flag

Your executable must handle the `--info` argument by outputting a single-line JSON object containing required fields. The main binary calls this during discovery to build the plugin registry. Required fields include `name`, `version`, `protocol` set to 1, `kinds` as an array, `extensions` as an array, and `idle_timeout_seconds` as an object with default values.

### Handling IPC and JSON-L Protocol

Communication occurs via **JSON-L lines** over stdout, with each line containing exactly one JSON object. The protocol enforces UTF-8 encoding without BOM and requires specific envelope fields: `protocol` set to 1 and `msg_type` indicating the message type. Dump-readers stream batch items using commands like `add` and `set`, while exporters write target file paths upon completion.

### Managing Idle Timeouts

The main binary implements an **idle-watchdog** that terminates unresponsive plugins exceeding their declared timeout. To prevent termination during long operations, emit heartbeat JSON objects on stderr at regular intervals shorter than the `idle_timeout_seconds` declared in your manifest. The watchdog monitors both stdout activity and stderr heartbeats.

## Code Examples for Custom Plugins

The following examples demonstrate minimal implementations for different plugin kinds using the protocol defined in [`plugins/plugin-protocol.md`](https://github.com/iOfficeAI/OfficeCLI/blob/main/plugins/plugin-protocol.md).

### Minimal Dump-Reader in C#

This C# example reads legacy `.doc` files and streams document construction commands as JSON-L:

```csharp
using System.Text.Json;

if (args.Length > 0 && args[0] == "--info")
{
    Console.WriteLine(JsonSerializer.Serialize(new {
        name = "officecli-doc-minimal",
        version = "0.0.1",
        protocol = 1,
        kinds = new[] { "dump-reader" },
        extensions = new[] { ".doc" },
        target = "docx",
        runtime = "dotnet",
        idle_timeout_seconds = new { @default = 30 }
    }));
    return 0;
}

// args: dump <source-file>
string sourcePath = args[1];

// Emit document construction command
var stdout = Console.Out;
stdout.WriteLine(JsonSerializer.Serialize(new {
    command = "add",
    parent = "/body",
    type = "paragraph",
    props = new { text = "Hello from .doc" }
}));
stdout.Flush();
return 0;

```

### Minimal Exporter in Go

This Go plugin converts documents to PDF using LibreOffice while maintaining heartbeat activity on stderr:

```go
package main

import (
    "encoding/json"
    "fmt"
    "os"
    "os/exec"
    "time"
)

func main() {
    if len(os.Args) > 1 && os.Args[1] == "--info" {
        json.NewEncoder(os.Stdout).Encode(map[string]any{
            "name":       "officecli-pdf-min",
            "version":    "0.0.1",
            "protocol":   1,
            "kinds":      []string{"exporter"},
            "extensions": []string{".pdf"},
            "runtime":    "go",
            "idle_timeout_seconds": map[string]any{
                "default": 60,
                "verbs":   map[string]int{"export": 120},
            },
        })
        return
    }

    // Parse arguments: export <source> --out <target>
    source := os.Args[2]
    var target string
    for i, a := range os.Args {
        if a == "--out" && i+1 < len(os.Args) {
            target = os.Args[i+1]
        }
    }

    // Start heartbeat on stderr
    go func() {
        for {
            fmt.Fprintln(os.Stderr, `{"heartbeat":true}`)
            time.Sleep(20 * time.Second)
        }
    }()

    cmd := exec.Command("soffice", "--headless", "--convert-to", "pdf", 
        "--outdir", "/tmp/officecli-pdf", source)
    if err := cmd.Run(); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(3) // Exit code 3 indicates unsupported feature
    }
    // Move produced PDF to target location...
}

```

### Format-Handler Session Management

For long-lived sessions, implement a loop reading from stdin and responding to protocol messages:

```csharp
var stdin  = new StreamReader(Console.OpenStandardInput(), new UTF8Encoding(false));
var stdout = new StreamWriter(Console.OpenStandardOutput(), new UTF8Encoding(false))
{
    NewLine = "\n",
    AutoFlush = true,
};

while (true)
{
    var line = stdin.ReadLine();
    if (line == null) break;
    var msg = JsonNode.Parse(line)!;
    
    switch ((string)msg["msg_type"]!)
    {
        case "open":
            // Load file and reply with capabilities
            stdout.WriteLine(JsonSerializer.Serialize(new {
                protocol = 1,
                msg_type = "ok",
                result = new {
                    capabilities = new {
                        commands = new[] { "get", "set", "save" },
                        features = Array.Empty<string>()
                    },
                    vocabulary = new {}
                }
            }));
            break;
            
        case "save":
            // Persist changes
            stdout.WriteLine("""{"protocol":1,"msg_type":"ok","result":null}""");
            break;
            
        case "close":
            stdout.WriteLine("""{"protocol":1,"msg_type":"ok","result":null}""");
            return;
    }
}

```

## Installing Custom Plugins for OfficeCLI

Once developed, plugins must be placed in discoverable locations or registered in the public registry to function with the main binary.

### Manual Installation

Place the compiled binary in the user-local plugins directory following the structure `~/.officecli/plugins/<kind>/<ext>/plugin`:

```bash

# Make executable

chmod +x officecli-doc-minimal

# Create directory structure

mkdir -p ~/.officecli/plugins/dump-reader/.doc

# Move to discovery location

mv officecli-doc-minimal ~/.officecli/plugins/dump-reader/.doc/plugin

# Verify installation

officecli plugins list

```

### Using the Built-in Installer

For plugins published to the registry, use the CLI installer which validates SHA-256 hashes and handles placement automatically:

```bash
officecli plugins install officecli-doc-minimal

```

The installer contacts `https://officecli.ai/plugins/registry.json`, downloads the binary, and places it in `~/.officecli/plugins/`. The [`install.sh`](https://github.com/iOfficeAI/OfficeCLI/blob/main/install.sh) script and [`npm/officecli.js`](https://github.com/iOfficeAI/OfficeCLI/blob/main/npm/officecli.js) wrapper in the repository demonstrate how the binary locates and manages these plugin directories.

## Summary

- OfficeCLI uses a **plugin protocol** defined in [`plugins/plugin-protocol.md`](https://github.com/iOfficeAI/OfficeCLI/blob/main/plugins/plugin-protocol.md) where standalone executables communicate via JSON-L over stdout
- Plugins must implement `--info` to return a manifest declaring protocol version 1, supported kinds, and timeout budgets
- Three plugin kinds exist: **dump-reader**, **exporter**, and **format-handler**, each with specific lifecycle contracts
- Discovery follows a hierarchy: environment variables → `~/.officecli/plugins` → bundled `plugins/` → PATH
- Installation methods include manual placement in the user directory or using `officecli plugins install` for registry-hosted plugins
- Heartbeat messages on stderr prevent the idle-watchdog from terminating long-running operations

## Frequently Asked Questions

### What programming languages can I use to develop OfficeCLI plugins?

You can use any language capable of producing a standalone executable and handling stdin/stdout. The protocol only requires that your binary emit JSON-L lines and respond to `--info`. The repository includes examples in C# and Go, but Python, Rust, or Node.js binaries work equally well as long as they adhere to the IPC contract.

### Where does OfficeCLI search for custom plugins?

The main binary searches environment variables first, then the user-local `~/.officecli/plugins` directory, followed by bundled `plugins/` directories, and finally the system PATH using the naming convention `officecli-<kind>-<ext>`. This discovery sequence is documented in [`plugins/plugin-protocol.md`](https://github.com/iOfficeAI/OfficeCLI/blob/main/plugins/plugin-protocol.md) under the plugin discovery section.

### How do I prevent my plugin from being killed during long operations?

Emit heartbeat JSON objects on stderr at regular intervals shorter than your declared `idle_timeout_seconds`. The watchdog monitors both stdout activity and stderr heartbeats; if neither occurs within the timeout window, the plugin process terminates. The Go exporter example above demonstrates this pattern using a goroutine that emits `{"heartbeat":true}` every 20 seconds.

### Can I distribute plugins through the public OfficeCLI registry?

Yes. Submit your plugin to the registry at `https://officecli.ai/plugins/registry.json` including SHA-256 hashes for verification. Once published, users can install via `officecli plugins install <name>`. The registry schema and validation logic are referenced in the main binary source code under `src/officecli/`.