How to Use OfficeCLI Python and Node.js SDKs for Resident-Pipe Communication

The OfficeCLI SDKs maintain a persistent subprocess connection to format-handler plugins via JSONL streams over stdin/stdout, automatically managing the open handshake, command serialization, idle-timeout watchdogs, and graceful shutdown.

The iOfficeAI/OfficeCLI repository provides official Python and Node.js SDKs that wrap the resident-pipe protocol, enabling stateful, low-latency interaction with Office documents without spawning a new process for each operation. By abstracting the subprocess lifecycle and JSONL message framing, these libraries allow developers to send structured commands like add, get, and save while the heavy native plugin remains resident in memory.

Understanding the Resident-Pipe Architecture

The resident-pipe architecture relies on format-handler plugins that implement a JSONL request/response protocol over standard streams. When you instantiate an SDK client, it spawns the plugin as a child process—using subprocess.Popen in Python or child_process.spawn in Node.js—and maintains that connection for the duration of the session.

Key protocol elements include:

  • Open Handshake: The client sends {"msg_type":"open",...} to establish capabilities and vocabulary, and the plugin replies with supported features.
  • Message Framing: All communication uses one JSON object per line, UTF-8 encoded, without BOM, ensuring deterministic request/response ordering.
  • Idle-Timeout Watchdog: A watchdog monitors activity and terminates inactive plugins; long operations must emit {"heartbeat":true} on stderr to stay alive.

These mechanisms are defined in the [plugins/plugin-protocol.md](https://github.com/iOfficeAI/OfficeCLI/blob/main/plugins/plugin-protocol.md) specification.

Python SDK Integration with ResidentPipeClient

The Python implementation resides in [sdk/python/officecli.py](https://github.com/iOfficeAI/OfficeCLI/blob/main/sdk/python/officecli.py), exposing the ResidentPipeClient class to manage the persistent subprocess.

Initializing the Client and Opening a Document

Instantiate ResidentPipeClient with the plugin_cmd array and optional idle_timeout override (in seconds). The constructor automatically performs the open handshake and stores the plugin's capabilities.

from officecli import ResidentPipeClient

# Spawn the format-handler and perform the open handshake

client = ResidentPipeClient(
    plugin_cmd=["officecli-format-handler", "open", "report.docx"],
    idle_timeout=120,  # Overrides the manifest idle_timeout_seconds

)

Sending Commands and Managing Responses

Use the command() method to send operations. It accepts command, args, and optional props parameters, serializes them to JSONL, and returns the parsed response. For long-running tasks, call send_heartbeat() to prevent the watchdog from terminating the plugin.


# Add a paragraph to the document body

resp = client.command(
    command="add",
    args={"parent": "/body", "type": "paragraph"},
    props={"text": "Automated content from Python"},
)
print(f"Added node: {resp}")

# Retrieve an existing node

node = client.command(
    command="get",
    args={"path": "/body/paragraph[1]"},
)

# Persist changes and close the session

client.command(command="save", args={})
client.command(command="close", args={})  # Terminates the plugin process

Errors raise PluginError with code and message attributes corresponding to protocol error responses such as invalid_argument or plugin_idle_timeout.

Node.js SDK Integration with ResidentPipe

The Node.js equivalent is implemented in [sdk/node/index.js](https://github.com/iOfficeAI/OfficeCLI/blob/main/sdk/node/index.js) and exports the ResidentPipe class.

Creating a Persistent Pipe Connection

Configure the pipe with the cmd array and idleTimeout option. The constructor queues commands internally to guarantee strict request-response ordering as required by the protocol.

const { ResidentPipe } = require("@officecli/sdk");

const pipe = new ResidentPipe({
  cmd: ["officecli-format-handler", "open", "presentation.pptx"],
  idleTimeout: 180,  // Seconds; relaxes the watchdog for heavy exports
});

Executing Commands and Lifecycle Management

The command() method returns a Promise that resolves with the JSON response. Always send save before close to ensure changes are flushed to disk.

// Add a slide to the presentation
await pipe.command({
  command: "add",
  args: { parent: "/slides", type: "slide" },
  props: { title: "Q4 Results" },
});

// Query the first slide
const slide = await pipe.command({
  command: "get",
  args: { path: "/slides/slide[1]" },
});

// Save and terminate
await pipe.command({ command: "save", args: {} });
await pipe.command({ command: "close", args: {} });

Call sendHeartbeat() during intensive operations to emit the required heartbeat on stderr and reset the idle timer.

Protocol Implementation Details

Open Handshake and Capabilities

As implemented in the SDKs, the open handshake exchanges capability metadata. You can inspect the returned vocabulary to determine which commands and properties the specific plugin supports for the opened file format.

Idle-Timeout and Heartbeat Mechanics

Both SDKs respect the idle_timeout_seconds field from the plugin manifest, which can be overridden via constructor options. The watchdog resets when:

  • Any byte is received on stdout (a response), or
  • A line containing {"heartbeat":true} is received on stderr

The SDKs provide send_heartbeat() (Python) and sendHeartbeat() (Node) to emit these heartbeats manually during long computations.

Error Handling

When a plugin returns an error response, the SDKs throw PluginError (Python) or reject with a PluginError instance (Node). These objects expose:

  • code: Protocol error string (e.g., node_not_found, save_failed)
  • message: Human-readable description

Handle these by restarting the client if the plugin process terminates unexpectedly.

Summary

Frequently Asked Questions

What distinguishes a format-handler plugin from other plugin kinds?

A format-handler is a long-lived plugin kind that maintains stateful document models in memory, enabling rapid incremental updates via the resident-pipe protocol. Other kinds, such as exporters or importers, are typically short-lived and execute a single task before exiting, whereas format-handlers persist until explicitly closed.

How do I prevent the plugin from timing out during long exports?

Set the idle_timeout (Python) or idleTimeout (Node) constructor option to a value exceeding your longest expected operation, or manually invoke send_heartbeat() / sendHeartbeat() periodically. This writes {"heartbeat":true} to the plugin's stderr, resetting the watchdog timer.

Can I use the resident-pipe SDKs for read-only document inspection?

Yes. Open the document in read-only mode (if the plugin supports it) or simply avoid sending save commands. The command() method supports read operations like get without modifying the underlying file, though you must still send close to terminate the plugin process cleanly.

What error codes does PluginError expose, and how should I handle them?

PluginError surfaces protocol-level codes such as invalid_argument, node_not_found, plugin_idle_timeout, and save_failed. Handle plugin_idle_timeout by recreating the client, handle invalid_argument by validating inputs against the capabilities returned during the open handshake, and treat save_failed as a critical error requiring user intervention or disk space checks.

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 →