How Plugins Extend OfficeCLI to Support Additional Document Formats
OfficeCLI delegates all non-native document format support to side-car plugin executables that communicate via JSON manifests and stream commands through stdin/stdout, allowing the core binary to remain lightweight while supporting unlimited file types.
The OfficeCLI project (iOfficeAI/OfficeCLI) ships with native support only for .docx, .xlsx, and .pptx. Every other format—whether converting legacy .doc files or handling modern alternatives like .hwpx—is processed through an external plugin system defined in plugins/plugin-protocol.md. These plugins are standalone executables discovered at runtime based on their declared capabilities.
Plugin Kinds and Responsibilities
The OfficeCLI plugin protocol defines three distinct roles (kinds) that determine how an extension interacts with the core binary.
Dump-Reader Plugins
A dump-reader converts foreign formats into one of the three native Office formats. When invoked as <plugin> dump <source>, it reads the source file and streams JSONL commands (one per line) to stdout, instructing OfficeCLI how to construct the equivalent native document. This kind is short-lived and one-shot, exiting after dumping the conversion commands.
Exporter Plugins
An exporter translates native Office files into foreign target formats (e.g., PDF, EPUB). Invoked as <plugin> export <source> --out <target>, it receives read-only access to the native source and writes the final output directly. Like dump-readers, exporters are short-lived processes that terminate after completing the conversion.
Format-Handler Plugins
A format-handler provides first-class, end-to-end support for a non-native format through a persistent session. When OfficeCLI opens a file like .hwpx, it launches the handler and maintains a bidirectional JSON connection over stdin/stdout. The handler receives commands (add, set, save, close) and manages the file format's full lifecycle, enabling editing capabilities beyond simple conversion.
Plugin Discovery and Loading
OfficeCLI locates plugins using a fixed priority search defined in the protocol specification. When the core binary needs to handle a file extension, it searches for a matching (kind, extension) pair in this order:
- Environment variable
OFFICECLI_PLUGIN_<KIND>_<EXT>pointing to an absolute executable path. - User directory at
~/.officecli/plugins/<kind>/<ext>/plugin(.exe). - Bundled directory adjacent to the main binary at
<dir>/plugins/<kind>/<ext>/plugin(.exe). - PATH lookup for executables named
officecli-<kind>-<ext>orofficecli-<ext>.
The first match is cached for the process lifetime and used for all subsequent operations involving that extension.
The Plugin Manifest and Runtime Contract
Every plugin must respond to the <plugin> --info command with a JSON manifest that declares its capabilities. OfficeCLI parses this output to determine compatibility before invoking the plugin for document operations.
A minimal manifest includes:
{
"name": "officecli-doc",
"version": "1.0.0",
"protocol": 1,
"kinds": ["dump-reader"],
"extensions": [".doc"],
"target": "docx",
"runtime": "dotnet",
"idle_timeout_seconds": { "default": 60, "verbs": { "dump": 30 } }
}
Key fields include:
kinds: Array containing one or more supported roles (dump-reader,exporter,format-handler).extensions: File extensions (with leading dot) the plugin handles.target: For dump-readers, the native output format (docx,xlsx, orpptx).idle_timeout_seconds: Watchdog timeout configuration per verb, after which OfficeCLI terminates unresponsive plugins.runtime: Human-readable implementation tag (e.g.,dotnet,go) for debugging purposes.
Invocation Flows by Plugin Type
Each plugin kind follows a distinct invocation pattern and IPC contract.
Dump-Reader Invocation (JSONL Streaming)
When converting a legacy file, OfficeCLI executes <plugin> dump <source>. The plugin streams one JSON command per line to stdout, using operations like add, set, or batch to describe the document structure. OfficeCLI instantiates a blank native document, replays these commands, and saves the result.
Exporter Invocation (One-Shot Conversion)
For export operations, OfficeCLI calls <plugin> export <source> --out <target>. The plugin reads the native source file read-only and writes the converted output to the specified target path. The plugin exits with code 0 on success or code 3 if the feature is unsupported.
Format-Handler Invocation (Session-Based)
Persistent handlers begin with an open handshake. OfficeCLI sends {"msg_type":"open",...} on the plugin's stdin and expects a capabilities/vocabulary response on stdout. Subsequent document commands are exchanged as individual JSON messages. The session continues until OfficeCLI sends a close command or the idle timeout expires.
Extending OfficeCLI to New Formats
To add support for a new document format, developers create a standalone executable implementing one of the three kinds:
- Legacy sources requiring conversion: Implement a
dump-readerthat emits OfficeCLI commands to reconstruct the document in.docx,.xlsx, or.pptxformat. - Export targets: Implement an
exporterthat accepts native files and produces the target format. - First-class formats: Implement a
format-handlerthat supports bidirectional editing through the persistent session protocol.
Once compiled, place the executable in any discovery location (such as ~/.officecli/plugins/dump-reader/doc/plugin) and ensure it returns a valid manifest via --info. OfficeCLI automatically routes file operations for registered extensions to the appropriate plugin without requiring changes to the core binary.
Implementation Examples
The following examples demonstrate minimal implementations for each plugin kind, including the required manifest response and command handling.
Minimal Dump-Reader in C#
// Compile as `officecli-doc`
using System.Text.Json;
if (args.Length > 0 && args[0] == "--info")
{
Console.WriteLine(JsonSerializer.Serialize(new {
name = "officecli-doc",
version = "1.0.0",
protocol = 1,
kinds = new[] { "dump-reader" },
extensions = new[] { ".doc" },
target = "docx",
runtime = "dotnet",
idle_timeout_seconds = new { @default = 60, verbs = new { dump = 30 } }
}));
return 0;
}
// args: dump <source-file>
string source = args[1];
// Parse source and emit JSONL commands
var stdout = Console.Out;
stdout.WriteLine(JsonSerializer.Serialize(new {
command = "add",
parent = "/body",
type = "paragraph",
props = new { text = "Converted from .doc" }
}));
stdout.Flush();
return 0;
Minimal Exporter in 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",
"version": "0.1.0",
"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
}
// args: 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]
}
}
// Emit heartbeat every 20s (helps the idle watchdog)
go func() {
for {
fmt.Fprintln(os.Stderr, `{"heartbeat":true}`)
time.Sleep(20 * time.Second)
}
}()
// Convert using external tool (read-only source)
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) // feature unsupported
}
// Move output to target (omitted)
}
Minimal Format-Handler in C#
// Compile as `officecli-hwpx`
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
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)!;
var type = (string)msg["msg_type"]!;
if (type == "open")
{
// Return capabilities and vocabulary
var reply = new {
protocol = 1,
msg_type = "ok",
result = new {
capabilities = new {
commands = new[] { "add","set","save","close" },
features = Array.Empty<string>()
},
vocabulary = new {
addable_types = new[] { "paragraph","run","table","image" },
settable_props = new {},
path_segments = new string[] {}
}
}
};
stdout.WriteLine(JsonSerializer.Serialize(reply));
}
else if (type == "save")
{
// Persist changes to .hwpx file (omitted)
stdout.WriteLine("""{"protocol":1,"msg_type":"ok","result":null}""");
}
else if (type == "close")
{
stdout.WriteLine("""{"protocol":1,"msg_type":"ok","result":null}""");
return 0;
}
// Handle additional commands (add, set, etc.) here
}
Summary
- OfficeCLI plugins are standalone executables that extend support beyond native
.docx,.xlsx, and.pptxformats. - Three plugin kinds exist: dump-reader (conversion to native), exporter (conversion from native), and format-handler (persistent bidirectional editing).
- Plugins are discovered via environment variables, user directories, bundled directories, or PATH lookup, and must provide a JSON manifest via the
--infoflag. - Communication uses JSONL for dump-readers, command-line arguments for exporters, and bidirectional JSON messages over stdin/stdout for format-handlers.
- The system allows developers to add support for any document format without modifying the core OfficeCLI binary.
Frequently Asked Questions
What is the difference between a dump-reader and a format-handler plugin?
A dump-reader is a short-lived process that converts a foreign format into a native Office format (like .docx) and exits immediately after streaming conversion commands. A format-handler maintains a persistent session, enabling read/write operations and editing capabilities for the foreign format through continuous bidirectional communication with the OfficeCLI core.
How does OfficeCLI discover plugins on my system?
OfficeCLI searches for plugins in a specific priority order: first checking the OFFICECLI_PLUGIN_<KIND>_<EXT> environment variable, then the user directory at ~/.officecli/plugins/<kind>/<ext>/, followed by the bundled directory next to the binary, and finally performing a PATH lookup for executables named officecli-<kind>-<ext>. The first match found is cached and used for the duration of the process.
Can I write an OfficeCLI plugin in any programming language?
Yes. Plugins communicate through standard streams (stdin/stdout) using JSON or JSONL, making them language-agnostic. The protocol only requires that your executable handle the --info flag for manifest requests and implement the appropriate IPC contract for its declared kind, whether written in C#, Go, Python, Rust, or any other language capable of producing standalone executables.
What happens if a plugin becomes unresponsive during a document operation?
OfficeCLI monitors plugins using the idle_timeout_seconds specified in the manifest. If a plugin fails to produce output or send heartbeats (via stderr logging) within the configured timeout for the current verb, the core binary terminates the plugin process and returns an error to the user, preventing hung operations from blocking the CLI indefinitely.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →