Extending OfficeCLI with a Plugin System: A Complete Developer's Guide

OfficeCLI supports a three-layer plugin architecture that enables developers to add custom format handlers, dump-readers, and exporters without modifying the core binary. The system uses language-agnostic external executables that communicate via JSON-L over standard streams, discovered at runtime through a deterministic lookup order.

OfficeCLI is a lightweight command-line tool for manipulating Word (.docx), Excel (.xlsx), and PowerPoint (.pptx) documents natively. When you need to work with additional formats—whether legacy .doc files, proprietary formats like .hwpx, or custom export pipelines—the OfficeCLI Plugin Protocol (v1) provides a clean extension point that keeps the core binary single-file and secure.

Understanding the Plugin Architecture

OfficeCLI's design separates native document operations from extension logic across four conceptual layers:

Layer Responsibility Plugin Integration
L1 – Read High-level format-agnostic views (view, text, html) Not involved—works directly on native OOXML
L2 – DOM Structured element operations (get, set, add, remove) Not involved—core binary manipulates native documents
L3 – Raw XML Direct XPath access for edge cases Not involved
Plugin Layer New format capabilities and offline exporters Full integration via executable spawning and JSON-L IPC

The host never loads plugin code into memory. Instead, it launches external executables and forwards standard streams, making the system safe for sandboxed environments and compatible with any programming language.

Three Plugin Kinds

Every plugin declares one or more kinds that determine its lifecycle and communication pattern:

  • dump-reader — Converts foreign formats into native OfficeCLI targets (e.g., .doc.docx). Short-lived, one-shot execution. Writes JSON-L batch commands to stdout.

  • exporter — Renders native documents to foreign formats (e.g., .docx.pdf). Short-lived, one-shot execution with simple CLI invocation.

  • format-handler — Completely owns a foreign format (e.g., .hwpx). Long-lived, session-wide with full request/response JSON-L over stdin/stdout.

Source: OfficeCLI Plugin Protocol – Motivation & Kinds

Plugin Discovery Mechanism

When OfficeCLI needs a plugin for a (kind, ext) pair, it searches in this deterministic order—first match wins:

  1. Environment variableOFFICECLI_PLUGIN_<KIND>_<EXT> (absolute path)
  2. User plugins directory~/.officecli/plugins/<kind>/<ext>/plugin(.exe)
  3. Bundled plugins directory<binary-dir>/plugins/<kind>/<ext>/plugin(.exe)
  4. PATH lookup — executable named officecli-<kind>-<ext> or officecli-<ext>

Discovery results are cached for the process lifetime, so newly installed plugins require a fresh command invocation.

Source: Plugin Discovery section

Required Manifest and --info Protocol

Every plugin must respond to --info with a single JSON object describing its capabilities. Required fields include:

Field Description
name Plugin identifier
version SemVer string
protocol Protocol version (currently 1)
kinds Array of supported kinds
extensions Array of handled file extensions
target For dump-reader: the native format produced

Optional fields advertise runtime environment, idle-timeout budgets, and vocabulary (for format-handler plugins).

{
  "name": "officecli-pdf",
  "version": "0.1.0",
  "protocol": 1,
  "kinds": ["exporter"],
  "extensions": [".pdf"],
  "runtime": "dotnet",
  "idle_timeout_seconds": { "default": 60, "verbs": { "export": 120 } },
  "supports": ["from:docx", "from:xlsx", "from:pptx"]
}

Source: Manifest examples

Idle-Timeout and Heartbeat System

Long-running plugins must periodically emit heartbeats to prevent termination:

  • Emit {"heartbeat":true} on stderr to reset the watchdog
  • Default timeouts from manifest; override globally with OFFICECLI_PLUGIN_IDLE_TIMEOUT_SECONDS
  • Per-verb timeouts supported (e.g., longer timeout for export operations)

Source: Idle-timeout watchdog

Error Handling Conventions

Plugins report structured errors via JSON on stdout ("msg_type":"error"). Exit codes convey failure categories:

Exit Code Meaning
5 Protocol mismatch
6 Idle timeout exceeded

The host translates these into user-friendly messages with corrective suggestions.

Source: Exit codes & error codes

Building Plugins: Complete Examples

Dump-Reader Plugin in C#

A minimal converter that ingests .doc files and emits native document construction commands:

// officecli-doc-minimal.cs
using System.Text.Json;

if (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];

// ...parse the .doc file (library omitted)...

// Emit one JSONL command per line; flush each line.
var out = Console.Out;
out.WriteLine(JsonSerializer.Serialize(new {
    command = "add",
    parent = "/body",
    type = "paragraph",
    props = new { text = "Hello from .doc" }
}));
out.Flush();
return 0;

Source: Dump-reader example

Exporter Plugin in Go

A PDF exporter with concurrent heartbeat generation:

// officecli-pdf-min.go
package main

import (
    "encoding/json"
    "fmt"
    "os"
    "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": 30,
                "verbs":   map[string]int{"export": 120},
            },
        })
        return
    }

    // args: export <source> --out <target>
    src := 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]
        }
    }

    // Periodic heartbeat to keep host alive.
    go func() {
        for {
            fmt.Fprintln(os.Stderr, `{"heartbeat":true}`)
            time.Sleep(20 * time.Second)
        }
    }()

    // Delegate to actual conversion library...
    // Exit 0 on success, 3 for unsupported features, etc.
}

Source: Exporter example

Using a Format-Handler Plugin

Format-handlers enable full document lifecycle management for non-native formats:


# Register custom handler via environment variable

export OFFICECLI_PLUGIN_FORMAT_HANDLER_HWPX=$HOME/.officecli/plugins/format-handler/hwpx/plugin

# Open triggers handler spawning

officecli open report.hwpx

# Operations forwarded to handler process

officecli get report.hwpx /body/p[1] --json
officecli set report.hwpx /body/p[1] --prop text="새로운 텍스트"
officecli save report.hwpx    # Forces flush to handler

officecli close report.hwpx   # Terminates handler session

Source: Format-handler flow

Managing Plugins with CLI Commands

OfficeCLI provides built-in plugin introspection:


# List discovered plugins with their kinds

officecli plugins list

# Validate manifest and protocol compliance

officecli plugins lint officecli-pdf

Source: Plugins command reference

Key Reference Files

File Purpose Link
plugins/plugin-protocol.md Complete v1 protocol specification GitHub
README.md (Plugins section) User-facing officecli plugins documentation Plugins command
sdk/python/README.md Python SDK with automatic plugin management Python SDK
examples/ Real-world plugin invocation samples Examples

Summary

Extending OfficeCLI with custom plugins follows a straightforward but precise protocol:

  • Three plugin kinds (dump-reader, exporter, format-handler) cover conversion, export, and full format ownership scenarios
  • Language-agnostic design through standard stream IPC keeps the core secure and lightweight
  • Four-tier discovery (environment, user directory, bundled directory, PATH) provides flexible deployment
  • Mandatory --info manifest with optional heartbeat and timeout declarations enables reliable long-running operations
  • Structured error reporting with documented exit codes ensures graceful failure handling

Frequently Asked Questions

What programming languages can I use for OfficeCLI plugins?

Any language that produces executable binaries and handles standard I/O. The examples in the source repository include C#, Go, and Python implementations. Since the host only spawns external processes, runtime choice has no impact on compatibility.

How do I debug a plugin that isn't being discovered?

Run officecli plugins list to see discovery results. Check that your plugin responds to --info with valid JSON, verify the executable path matches the discovery order priority, and ensure file permissions allow execution.

Can a single plugin handle multiple file extensions?

Yes. The extensions field in the manifest accepts an array. A plugin can also declare multiple kinds if it implements diverse functionality (e.g., both reading and exporting a format).

What happens if my plugin exceeds its idle timeout?

The host sends SIGTERM, then SIGKILL if shutdown doesn't complete within a grace period. Plugins should emit {"heartbeat":true} on stderr every few seconds during long operations, or request extended timeouts in their manifest's idle_timeout_seconds configuration.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →