How the OfficeCLI Plugin System Works: A Complete Guide to Extending Office CLI Functionality
The OfficeCLI plugin system is a language-agnostic extension framework that uses JSON-L communication over stdin/stdout to add support for non-native formats through three plugin kinds: dump-reader, exporter, and format-handler.
Office CLI provides native support for .docx, .xlsx, and .pptx files out of the box. For every other format—from legacy .doc files to proprietary formats—the tool relies on an external plugin system that discovers, launches, and communicates with independent executables. This article explains how the OfficeCLI plugin system works and how you can build your own extensions.
Understanding the Three Plugin Kinds
The OfficeCLI plugin architecture defines three distinct plugin types, each with different lifecycles and IPC patterns. The official specification lives in [plugins/plugin-protocol.md](https://github.com/iOfficeAI/OfficeCLI/blob/main/plugins/plugin-protocol.md#L25), starting at line 25.
| Kind | Purpose | Lifecycle | IPC Pattern |
|---|---|---|---|
| dump-reader | Parses foreign source files (e.g., .doc) and streams OfficeCLI commands to build native documents |
Short-lived, one-shot | Streams JSONL to stdout; no request/response |
| exporter | Renders native OfficeCLI documents into foreign targets (e.g., PDF) | Short-lived | Plain CLI invocation; diagnostics on stderr |
| format-handler | Owns non-native formats for entire sessions (e.g., .hwpx) |
Long-lived, session-duration | Request/response JSON messages via stdin/stdout |
Choosing the right kind depends on your workflow: dump-reader for import conversions, exporter for export rendering, and format-handler for full read/write session support.
How OfficeCLI Discovers Plugins
Plugin discovery follows a strict priority order implemented in [src/officecli/Core/Plugins/PluginRegistry.cs](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/Core/Plugins/PluginRegistry.cs#L16) (line 16). When OfficeCLI needs a plugin for a specific (kind, ext) pair, it searches:
- Environment variable:
OFFICECLI_PLUGIN_<KIND>_<EXT>(absolute path to executable) - User directory:
~/.officecli/plugins/<kind>/<ext>/plugin(.exe) - Bundled directory:
<officecli-exe-dir>/plugins/<kind>/<ext>/plugin(.exe) - PATH lookup: Executable named
officecli-<kind>-<ext>orofficecli-<ext>in$PATH
The first match wins. This hierarchy lets users override bundled plugins with custom versions or environment-specific configurations.
The Plugin Manifest: Required Metadata
Every OfficeCLI plugin must respond to <plugin> --info with a JSON manifest describing its capabilities. The schema is enforced in [src/officecli/Core/Plugins/PluginManifest.cs](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/Core/Plugins/PluginManifest.cs#L10) (line 10).
Required manifest fields include:
name,version,protocol(must be 1 for current spec)kinds(array of declared plugin kinds)extensions(handled file extensions with leading dot)- Kind-specific fields:
targetfor dump-reader,vocabularyfor format-handler runtime(diagnostic tag)idle_timeout_seconds(watchdog budget for heartbeat monitoring)
This self-describing approach lets OfficeCLI validate compatibility before attempting invocation.
Plugin Invocation Patterns
Each plugin kind uses a distinct invocation pattern, implemented in dedicated invoker classes.
Dump-Reader Invocation
Launched as: <plugin> dump <source> [--media-dir <dir>]
Emits one JSONL command per line to stdout. Example command structure:
{ "command":"add", "parent":"/body", "type":"paragraph", "props":{ "text":"..." } }
The invoker code resides in [DumpReaderInvoker.cs](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/Core/Plugins/DumpReaderInvoker.cs#L10) (line 10).
Exporter Invocation
Launched as: <plugin> export <source> --out <target> [--options <json>]
Reads the native file read-only, writes the foreign file, and exits. Diagnostics go to stderr. See [ExporterInvoker.cs](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/Core/Plugins/ExporterInvoker.cs#L7) (line 7).
Format-Handler Invocation
Launched as: <plugin> open <file>
Engages in a request/response JSON envelope protocol (see §5.3 of the protocol specification). The first request is an open handshake returning capabilities and vocabulary snapshot. Long-lived session management is handled by [FormatHandlerSession.cs](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/Core/Plugins/FormatHandlerSession.cs) and [FormatHandlerProxy.cs](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/Core/Plugins/FormatHandlerProxy.cs).
Managing Plugins with the CLI
OfficeCLI includes a plugins subcommand for plugin management, implemented in [CommandBuilder.Plugins.cs](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/CommandBuilder.Plugins.cs#L17) (line 17).
# List all discoverable plugins
officecli plugins list
# Display detailed manifest for a specific plugin
officecli plugins info officecli-doc
# Validate a dump-reader plugin against the protocol
officecli plugins lint officecli-doc
These commands help troubleshoot discovery issues and verify plugin compliance before deployment.
Building a Minimal Dump-Reader Plugin (C#)
Here's a complete minimal dump-reader implementation, adapted from lines 84-100 of the protocol specification:
using System;
using System.Text.Json;
// Build as: dotnet new console -n officecli-doc-minimal
class Program
{
static int Main(string[] args)
{
// Manifest request
if (args.Length > 0 && args[0] == "--info")
{
var manifest = 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 }
};
Console.WriteLine(JsonSerializer.Serialize(manifest));
return 0;
}
// Dump invocation: args[0] == "dump", args[1] == source path
string sourcePath = args[1];
// Parse your .doc file here...
// Emit JSONL commands to build the native document
Console.WriteLine(JsonSerializer.Serialize(new
{
command = "add",
parent = "/body",
type = "paragraph",
props = new { text = "Hello from .doc" }
}));
Console.Out.Flush(); // Critical: flush each line immediately
return 0;
}
}
Install to ~/.officecli/plugins/dump-reader/.doc/officecli-doc-minimal and run officecli plugins list to verify discovery.
Building a Minimal Exporter Plugin (Go)
This Go exporter example, from lines 21-48 of the protocol specification, converts to PDF using LibreOffice:
package main
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"time"
)
func main() {
// Manifest request
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: 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]
}
}
// Heartbeat to prevent host timeout
go func() {
for {
fmt.Fprintln(os.Stderr, `{"heartbeat":true}`)
time.Sleep(20 * time.Second)
}
}()
// Perform conversion
cmd := exec.Command("soffice", "--headless", "--convert-to",
"pdf", "--outdir", "/tmp", source)
if err := cmd.Run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(3) // Exit code 3 = unsupported feature
}
// Move /tmp output to target...
}
Building a Minimal Format-Handler Plugin (C#)
For session-based formats, implement the request/response protocol from lines 76-118 of the specification:
using System;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
// args: open <file>
string filePath = args[1];
using var stdin = new StreamReader(
Console.OpenStandardInput(),
new UTF8Encoding(false));
using 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 msgType = (string)msg["msg_type"]!;
switch (msgType)
{
case "open":
// Handshake: return capabilities and vocabulary
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 { /* format-specific schema */ }
}
}));
break;
case "save":
// Persist changes
File.WriteAllBytes(filePath, currentBytes);
stdout.WriteLine("""{"protocol":1,"msg_type":"ok","result":null}""");
break;
case "close":
stdout.WriteLine("""{"protocol":1,"msg_type":"ok","result":null}""");
return 0;
// Handle get, set, and other commands...
}
}
Using Your Plugin from OfficeCLI
Once installed in a discovery location, plugins activate automatically:
# Verify installation
officecli plugins list
# Dump-reader automatically handles .doc files
officecli dump legacy.doc
# Exporter renders to target format
officecli view report.docx pdf --out report.pdf
# Format-handler provides transparent session support
officecli edit document.hwpx
Key Source Files for Plugin Development
| File | Purpose | Link |
|---|---|---|
plugins/plugin-protocol.md |
Complete protocol specification | view |
PluginRegistry.cs |
Discovery order and caching | view |
PluginManifest.cs |
Manifest parsing and validation | view |
DumpReaderInvoker.cs |
Dump-reader execution | view |
ExporterInvoker.cs |
Exporter execution | view |
FormatHandlerSession.cs |
Long-lived session management | view |
FormatHandlerProxy.cs |
Command translation layer | view |
CommandBuilder.Plugins.cs |
CLI plugin commands | view |
Summary
- The OfficeCLI plugin system extends native format support through three kinds: dump-reader (import), exporter (export), and format-handler (full session).
- Discovery follows a 4-tier priority: environment variables, user directory, bundled directory, then PATH.
- Every plugin must provide a manifest via
--infowith protocol version 1, declared kinds, and handled extensions. - IPC is language-agnostic: JSON-L over stdin/stdout with specific patterns per kind—streaming for dump-reader, plain CLI for exporter, request/response for format-handler.
- The protocol requires line-flushed output, heartbeat messages for long-lived plugins, and idle timeout compliance.
- CLI integration via
officecli plugins list|info|lintstreamlines development and debugging.
Frequently Asked Questions
What programming languages can I use for OfficeCLI plugins?
Any language that produces an executable and can read/write JSON to stdin/stdout. The protocol is intentionally language-agnostic—examples in the official repository include C#, Go, Python, and Rust implementations. The only requirements are proper JSON serialization, line-terminated messages, and respect for the heartbeat/timeout rules in §5.6 of the protocol.
Why does my format-handler plugin get killed after 30 seconds?
The idle_timeout_seconds field in your manifest controls this. According to [PluginManifest.cs](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/Core/Plugins/PluginManifest.cs), you must either increase the default timeout or emit heartbeat messages to stderr: {"heartbeat":true}. The protocol allows per-verb timeout overrides in the verbs sub-object for long-running operations like export.
How do I debug a plugin that OfficeCLI doesn't discover?
Run officecli plugins list to see what OfficeCLI detects. If missing, check: (1) file permissions are executable, (2) directory structure matches ~/.officecli/plugins/<kind>/<ext>/plugin, (3) the --info output returns valid JSON with protocol: 1, and (4) your environment variable names use uppercase with underscores: OFFICECLI_PLUGIN_DUMP_READER_DOC. Use officecli plugins lint <name> to validate protocol compliance.
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 →