How the OfficeCLI Plugin System Extends Support for Additional File Formats
OfficeCLI uses a sidecar plugin architecture where independent executables register via a JSON manifest and communicate through stdin/stdout to handle non-native formats like .doc, .pdf, or .hwpx.
The OfficeCLI project's flexible plugin system allows the core binary to remain lightweight while supporting an unlimited range of document formats. By discovering and invoking external executables at runtime, OfficeCLI delegates format-specific work to specialized plugins without requiring changes to the main codebase.
Plugin Architecture Overview
The OfficeCLI plugin protocol defines three distinct plugin kinds, each designed for different format integration patterns. All plugins are standalone executables that declare their capabilities through a standardized manifest returned by the --info command.
The Three Plugin Kinds
| Kind | Purpose | Lifecycle | Communication Model |
|---|---|---|---|
dump-reader |
Converts foreign formats to native Office formats (.docx, .xlsx, .pptx) | Short-lived, one-shot | JSONL streamed to stdout; no input |
exporter |
Converts native files to foreign targets (PDF, EPUB, etc.) | Short-lived, one-shot | Reads native file read-only; writes foreign output |
format-handler |
Provides full read/write support for a non-native format | Long-lived session | Bidirectional JSON messages over stdin/stdout |
These definitions and their behavioral contracts are documented in [plugins/plugin-protocol.md](https://github.com/iOfficeAI/OfficeCLI/blob/main/plugins/plugin-protocol.md#L30-L44), specifically lines 30-44 for dump-readers, lines 67-78 for exporters, and lines 95-104 for format-handlers.
Plugin Discovery and Loading
When OfficeCLI needs to process a file with a given extension, it searches for a matching plugin in a deterministic priority order. The discovery algorithm is implemented in the protocol specification at lines 35-47.
Discovery Priority Order
- Environment variable:
OFFICECLI_PLUGIN_<KIND>_<EXT>— absolute path to executable - User directory:
~/.officecli/plugins/<kind>/<ext>/plugin(.exe) - Bundled directory:
<binary-dir>/plugins/<kind>/<ext>/plugin(.exe) - PATH lookup: executable named
officecli-<kind>-<ext>orofficecli-<ext>
The first match wins, and results are cached per process invocation to avoid repeated filesystem checks (caching note).
The Plugin Manifest Contract
Every plugin must respond to <plugin> --info with a JSON object defining its capabilities. This manifest enables OfficeCLI to route commands correctly and enforce resource limits.
Required Manifest Fields
{
"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 } }
}
| Field | Description |
|---|---|
kinds |
Supported plugin role(s): dump-reader, exporter, format-handler |
extensions |
File extensions handled, with leading dot |
target |
For dump-reader, the native format produced (docx/xlsx/pptx) |
idle_timeout_seconds |
Per-verb watchdog timeout; process killed if exceeded |
runtime |
Human-readable implementation tag (informational only) |
The complete manifest schema is defined in the protocol document at lines 59-71.
Runtime Invocation Patterns
Each plugin kind uses a distinct invocation pattern and IPC contract defined in the OfficeCLI plugin protocol.
Dump-Reader Invocation
Dump-readers are invoked as:
<plugin> dump <source>
They stream one JSON command per line to stdout, using commands like add, set, and batch to build a native Office document. OfficeCLI creates a blank native file, replays the command stream, and writes the result adjacent to the source file.
The JSONL schema is documented at lines 27-33.
Exporter Invocation
Exporters are called as:
<plugin> export <source> --out <target>
They read the native source file read-only and write the foreign output to the specified path. The export command syntax is specified at lines 37-43.
Format-Handler Session Protocol
Format-handlers establish a persistent session through an open handshake:
- OfficeCLI sends
{"msg_type":"open", ...}on the plugin's stdin - Plugin replies with capabilities and vocabulary on stdout
- Subsequent commands (
add,set,save,close) exchange bidirectionally
The handshake details are at lines 61-68.
Example Plugin Implementations
Minimal Dump-Reader in C#
This officecli-doc plugin converts .doc files to .docx by emitting native construction commands:
// Compile as .NET console app: 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 .doc file with preferred library...
// Stream JSONL commands to stdout (flush per line)
Console.Out.WriteLine(JsonSerializer.Serialize(new {
command = "add",
parent = "/body",
type = "paragraph",
props = new { text = "Converted from .doc" }
}));
Console.Out.Flush();
return 0;
Key protocol references: manifest at lines 57-66, dump logic at lines 68-76.
Minimal Exporter in Go
This officecli-pdf plugin wraps LibreOffice for PDF generation:
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 to satisfy idle watchdog
go func() {
for {
fmt.Fprintln(os.Stderr, `{"heartbeat":true}`)
time.Sleep(20 * time.Second)
}
}()
// LibreOffice conversion (read-only source access)
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 exit code
}
// Move result to target path (omitted)
}
Key protocol references: manifest at lines 73-84, export format at lines 37-44, heartbeat at lines 75-83.
Minimal Format-Handler in C#
This officecli-hwpx plugin provides full session-based support for the .hwpx format:
// 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 + vocabulary per protocol
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 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;
}
// Add, set, and other commands handled here
}
Key protocol references: handshake at lines 61-71, command handling at lines 73-87, save/close at lines 84-90.
Installation and Distribution
Plugins can be deployed through multiple channels:
- Manual installation: Place executable in any discovery location
- Bundled distribution: Ship with OfficeCLI releases in
<binary-dir>/plugins/ - Registry install: Use
officecli plugins install <name>to fetch from the public registry athttps://officecli.ai/plugins/registry.json
The installation section of the protocol document covers registry conventions and version resolution.
Choosing the Right Plugin Kind
When extending OfficeCLI to support a new file format, select the appropriate plugin kind based on your integration requirements:
- Use
dump-readerwhen converting legacy or niche source formats to native Office formats for one-time import - Use
exporterwhen generating static output formats (PDF, EPUB, images) from native documents - Use
format-handlerwhen the format requires full read/write/editing capabilities as a first-class citizen
Summary
- OfficeCLI's core binary only natively supports
.docx,.xlsx, and.pptx; all other formats require plugins - Plugins are independent executables discovered via environment variables, user directories, bundled directories, or PATH
- Three plugin kinds—
dump-reader,exporter, andformat-handler—cover conversion, export, and full session-based workflows - All plugins expose capabilities through a JSON manifest returned by
--info - Communication uses JSONL (dump-readers) or bidirectional JSON (format-handlers) over stdin/stdout
- The protocol specification at
plugins/plugin-protocol.mddefines all behavioral contracts and IPC schemas
Frequently Asked Questions
How does OfficeCLI find plugins for a specific file extension?
OfficeCLI searches in four locations in priority order: environment variables (OFFICECLI_PLUGIN_<KIND>_<EXT>), the user plugins directory (~/.officecli/plugins/), the bundled plugins directory next to the binary, and finally PATH for executables matching naming conventions. The first match is cached for the process lifetime.
Can plugins be written in any programming language?
Yes. Plugins are standalone executables that communicate through stdin/stdout using JSON. The runtime field in the manifest is informational only—OfficeCLI does not inspect or validate the implementation language. The examples in this article show C# and Go, but Python, Rust, Node.js, or any language producing a native executable work equally well.
What happens if a plugin exceeds its idle timeout?
OfficeCLI's main process monitors plugin output and heartbeat messages. If no activity occurs within the idle_timeout_seconds specified for the current verb, the plugin process is terminated. Plugins should emit heartbeat objects to stderr (e.g., {"heartbeat":true}) during long-running operations to prevent watchdog kills.
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 →