From 1d150e035f3c473db29425e637edebacfba8f230 Mon Sep 17 00:00:00 2001 From: Steven Obiajulu Date: Tue, 3 Mar 2026 23:05:49 -0500 Subject: [PATCH] feat(docx-mcp): proposal for CLI subcommands with full MCP tool parity Safe-docx is advertised as both an MCP server and a CLI tool, but currently only exposes `serve` and `compare` as CLI commands. This proposal adds CLI subcommands for all 20+ MCP tools with ergonomic `--flag value` syntax, a batched `edit` command for multi-operation workflows, and session persistence across sequential CLI invocations. Key design decisions (informed by user preferences): - Full CLI parity: every MCP tool maps to a kebab-case subcommand - Session-aware: filesystem-based session store persists across commands - Batched `edit` as primary interface (avoids concurrent race issues) - Schema-driven flag generation from existing Zod tool catalog - Bookmark IDs only for paragraph targeting (consistent with MCP) - JSON output to stdout (consistent with existing `compare` command) Includes proposal.md, design.md, tasks.md, and spec delta for mcp-server. Validated with `openspec validate add-cli-tool-subcommands --strict`. --- .../add-cli-tool-subcommands/design.md | 135 ++++++++++++++++++ .../add-cli-tool-subcommands/proposal.md | 25 ++++ .../specs/mcp-server/spec.md | 108 ++++++++++++++ .../changes/add-cli-tool-subcommands/tasks.md | 54 +++++++ 4 files changed, 322 insertions(+) create mode 100644 openspec/changes/add-cli-tool-subcommands/design.md create mode 100644 openspec/changes/add-cli-tool-subcommands/proposal.md create mode 100644 openspec/changes/add-cli-tool-subcommands/specs/mcp-server/spec.md create mode 100644 openspec/changes/add-cli-tool-subcommands/tasks.md diff --git a/openspec/changes/add-cli-tool-subcommands/design.md b/openspec/changes/add-cli-tool-subcommands/design.md new file mode 100644 index 0000000..adbccdc --- /dev/null +++ b/openspec/changes/add-cli-tool-subcommands/design.md @@ -0,0 +1,135 @@ +## Context + +Safe-Docx has a rich set of 20+ MCP tools but only 2 CLI commands (`serve`, `compare`). Users and scripts interacting from a terminal must spin up an MCP server and speak JSON-RPC, which is impractical for one-off edits, debugging, and shell pipelines. + +The open-agreements project demonstrates a clean `--set key=value` pattern. Safe-docx needs an equivalent that feels native to its document-editing workflow. + +## Goals / Non-Goals + +### Goals +- Full CLI parity: every MCP tool accessible as a CLI subcommand +- Session-aware: persistent sessions across CLI invocations (matches MCP behavior) +- Batched editing: primary `edit` command for multi-operation workflows +- Introspectable: generate CLI flags from existing Zod tool schemas (single source of truth) +- Consistent: JSON output to stdout, errors to stderr (matches existing `compare` command) + +### Non-Goals +- Interactive/TUI editing interface (out of scope) +- Text-search paragraph targeting (bookmark IDs only, per user decision) +- GUI or web interface +- Breaking changes to existing `serve` or `compare` commands + +## Decisions + +### 1. Session persistence via filesystem lockfile + +**Decision**: Use a `.safedocx-session.json` file in a temp directory (keyed by absolute file path hash) to persist session state across CLI invocations. + +**Why**: Simpler than a daemon process. The SessionManager already serializes/deserializes sessions. A lockfile approach: +- No background process to manage +- Survives terminal crashes (sessions have TTL-based expiry) +- `safe-docx clear-session` cleans up explicitly + +**Alternatives considered**: +- Unix socket daemon: More complex, requires lifecycle management, overkill for CLI usage +- Re-open on every invocation: Loses edit state, defeats the purpose of sessions +- In-memory only: No persistence across commands + +### 2. Schema-driven flag generation + +**Decision**: Generate CLI `--flag` definitions by introspecting the Zod schemas in `SAFE_DOCX_TOOL_CATALOG`. Each Zod field maps to a CLI flag: +- `z.string()` → `--flag ` +- `z.number()` → `--flag ` +- `z.boolean()` → `--flag` (presence = true) or `--flag true|false` +- `z.enum([...])` → `--flag ` +- `z.string().optional()` → flag is optional +- `z.array(z.string())` → repeatable `--flag ` (collected into array) + +**Why**: Single source of truth. Adding a new parameter to a tool's Zod schema automatically surfaces it in the CLI. No manual sync needed. + +**Alternatives considered**: +- Manual flag definitions per command: Duplicate maintenance, drift risk +- Codegen at build time: More complex build pipeline, but could be a future optimization + +### 3. Batched `edit` command as primary interface + +**Decision**: Introduce `safe-docx edit [--replace ...] [--insert-after ...] [-o output]` as the primary CLI editing command. Individual subcommands (`safe-docx replace-text`, `safe-docx insert-paragraph`) exist but are secondary. + +**Why**: Real editing workflows involve multiple operations. Batching avoids the concurrent race condition entirely (all operations run sequentially in one process with one session) and matches how Claude's parallel tool use works. + +**Syntax**: +```bash +# Batched edit (primary) +safe-docx edit ~/Downloads/Bylaws.docx \ + --replace _bk_721a "Section 1.1" "Article 1.1" \ + --replace _bk_3bc6 "ACME Corp" "WXY Corp" \ + --insert-after _bk_721a "Effective January 1, 2026." \ + -o ~/Downloads/Bylaws-edited.docx + +# Individual subcommands (secondary) +safe-docx replace-text ~/Downloads/Bylaws.docx \ + --para _bk_721a \ + --old "Section 1.1" \ + --new "Article 1.1" \ + --instruction "rename to article" +``` + +### 4. Command naming: kebab-case matching tool names + +**Decision**: CLI subcommand names match MCP tool names but with kebab-case: +- `read_file` → `safe-docx read-file` +- `replace_text` → `safe-docx replace-text` +- `insert_paragraph` → `safe-docx insert-paragraph` +- `get_session_status` → `safe-docx get-session-status` + +**Why**: Predictable mapping. Users familiar with MCP tool names can guess CLI command names. + +### 5. Output format + +**Decision**: JSON to stdout (matching existing `compare` command). Errors to stderr with non-zero exit code. + +```bash +# Successful read +safe-docx read-file ~/Downloads/Bylaws.docx +# → {"success": true, "session_id": "ses_...", "content": "...", ...} + +# Failed operation +safe-docx read-file /nonexistent.docx +# stderr: Error: FILE_NOT_FOUND — File not found: /nonexistent.docx +# exit code: 1 +``` + +### 6. Session context flags + +**Decision**: All session-aware commands accept: +- Positional `` as first argument (most common case) +- `--session ` flag for explicit session reference +- `--file ` as an alias for the positional argument + +```bash +# These are equivalent: +safe-docx read-file ~/Downloads/Bylaws.docx +safe-docx read-file --file ~/Downloads/Bylaws.docx + +# Explicit session: +safe-docx read-file --session ses_abc123 +``` + +## Risks / Trade-offs + +| Risk | Mitigation | +|------|------------| +| Session lockfile corruption on crash | TTL-based expiry. `clear-session` for manual cleanup. Lockfile stores session ID + timestamp. | +| Schema-driven flags may produce awkward CLI ergonomics for complex types | Manual overrides for specific commands (e.g., `edit` batched syntax). Schema-driven is the default, not the only option. | +| Large surface area (20+ subcommands) | Group under help categories (read, edit, session, meta). Prioritize `edit`, `read-file`, `save`, `grep` in docs. | +| Custom parser may not scale to 20+ subcommands | Consider adopting a lightweight parser library if manual parsing becomes unwieldy. The current custom parser works for 2 commands but may need enhancement. | + +## Open Questions + +1. Should `safe-docx edit` auto-save if `-o` is provided, or require an explicit `--save` flag? +2. Should session state persist to `$TMPDIR` (auto-cleaned by OS) or to `~/.safedocx/sessions/` (user-managed)? +3. Should we add a `safe-docx interactive` REPL mode in a future phase? +4. For the `edit` command's `--replace` flag, what's the best syntax for the 3 required values (para_id, old_string, new_string)? Options: + - `--replace _bk_id "old" "new"` (positional within the flag) + - `--replace para=_bk_id,old="old",new="new"` (key=value within the flag) + - `--replace _bk_id --old "old" --new "new"` (separate flags per replace — but then how to batch?) diff --git a/openspec/changes/add-cli-tool-subcommands/proposal.md b/openspec/changes/add-cli-tool-subcommands/proposal.md new file mode 100644 index 0000000..3ca08ef --- /dev/null +++ b/openspec/changes/add-cli-tool-subcommands/proposal.md @@ -0,0 +1,25 @@ +# Change: Add CLI subcommands for all MCP tools + +## Why + +Safe-Docx is advertised as both an MCP server and a CLI tool, but the CLI currently only exposes `serve` and `compare`. Every other operation (read, replace, insert, save, grep, comments, footnotes, etc.) requires speaking raw JSON-RPC over MCP stdio. This makes the tool unusable from a terminal without an MCP client, and makes scripting/debugging painful. + +The open-agreements project demonstrates a better pattern: `--set key=value` flags that map cleanly to tool parameters, with structured JSON output. Safe-docx should offer the same ergonomics. + +## What Changes + +- **BREAKING**: None. Existing `serve` and `compare` commands are unchanged. +- Add CLI subcommands for all 20+ MCP tools, each mapping tool schema fields to `--flag value` CLI arguments. +- Add a batched `edit` subcommand as the primary editing interface: `safe-docx edit file.docx --replace ... --replace ... --insert-after ... -o output.docx` +- CLI sessions are persistent (session-aware), matching MCP behavior. A session daemon or lockfile keeps state across sequential CLI invocations. +- Paragraph targeting uses bookmark IDs only (consistent with MCP tools, no text-search selectors). +- All CLI output is structured JSON to stdout (consistent with existing `compare` command). + +## Impact + +- Affected specs: `mcp-server` (new CLI parity requirements) +- Affected code: + - `packages/docx-mcp/src/cli/index.ts` — router, flag parser + - `packages/docx-mcp/src/cli/commands/` — new subcommand files + - `packages/docx-mcp/src/tool_catalog.ts` — introspect Zod schemas for flag generation + - `packages/docx-mcp/src/cli/session_daemon.ts` — new: persistent session layer diff --git a/openspec/changes/add-cli-tool-subcommands/specs/mcp-server/spec.md b/openspec/changes/add-cli-tool-subcommands/specs/mcp-server/spec.md new file mode 100644 index 0000000..91b34f8 --- /dev/null +++ b/openspec/changes/add-cli-tool-subcommands/specs/mcp-server/spec.md @@ -0,0 +1,108 @@ +## ADDED Requirements + +### Requirement: CLI Parity with MCP Tools + +The CLI SHALL expose every MCP tool as a CLI subcommand with kebab-case naming (e.g., `read_file` → `safe-docx read-file`). + +Each subcommand SHALL accept tool parameters as `--flag value` CLI arguments, derived from the tool's Zod schema in the tool catalog. + +All subcommands SHALL produce structured JSON output to stdout and errors to stderr, consistent with the existing `compare` command. + +#### Scenario: Read a document via CLI +- **WHEN** `safe-docx read-file ~/Downloads/Bylaws.docx` is invoked +- **THEN** the CLI opens the file, creates a session, and prints JSON with `success: true`, `session_id`, and `content` to stdout + +#### Scenario: Replace text via CLI +- **WHEN** `safe-docx replace-text ~/Downloads/Bylaws.docx --para _bk_abc123 --old "Section" --new "Article" --instruction "rename"` is invoked +- **THEN** the CLI resolves or creates a session for the file, applies the replacement, and prints JSON with `success: true` to stdout + +#### Scenario: Unknown subcommand rejected +- **WHEN** `safe-docx nonexistent-command` is invoked +- **THEN** the CLI prints an error to stderr and exits with non-zero code + +#### Scenario: Auto-generated help per subcommand +- **WHEN** `safe-docx read-file --help` is invoked +- **THEN** the CLI prints usage, description, and all available flags derived from the tool's schema + +### Requirement: CLI Session Persistence + +The CLI SHALL persist session state across sequential invocations using a filesystem-based session store. + +A session created by one CLI invocation SHALL be reusable by subsequent invocations targeting the same file path, without re-opening the document. + +Sessions SHALL expire based on the same TTL rules as MCP sessions. + +#### Scenario: Sequential CLI edits share a session +- **GIVEN** `safe-docx read-file ~/Downloads/Bylaws.docx` was invoked and created session `ses_abc` +- **WHEN** `safe-docx replace-text ~/Downloads/Bylaws.docx --para _bk_x --old "A" --new "B" --instruction "test"` is invoked +- **THEN** the CLI reuses session `ses_abc` instead of creating a new session + +#### Scenario: Expired session triggers re-open +- **GIVEN** a persisted session that has exceeded its TTL +- **WHEN** a CLI command targets the same file path +- **THEN** the CLI creates a new session transparently + +#### Scenario: Explicit session clear +- **WHEN** `safe-docx clear-session ~/Downloads/Bylaws.docx` is invoked +- **THEN** the persisted session for that file is removed + +### Requirement: Batched Edit Command + +The CLI SHALL provide an `edit` subcommand that accepts multiple `--replace` and `--insert-after`/`--insert-before` flags in a single invocation. + +The `edit` command SHALL execute all operations sequentially within a single session. + +The `edit` command SHALL accept an `-o`/`--output` flag to auto-save after all operations complete. + +#### Scenario: Multiple replacements in one command +- **WHEN** `safe-docx edit ~/Downloads/Bylaws.docx --replace _bk_a "old1" "new1" --replace _bk_b "old2" "new2" -o ~/Downloads/edited.docx` is invoked +- **THEN** both replacements are applied to the same session +- **AND** the result is saved to the output path +- **AND** the JSON output reports success with both edit results + +#### Scenario: Mixed replace and insert in one command +- **WHEN** `safe-docx edit file.docx --replace _bk_a "old" "new" --insert-after _bk_a "New paragraph text." -o out.docx` is invoked +- **THEN** the replacement is applied first +- **AND** the insertion is applied after +- **AND** both operations use the same session + +#### Scenario: Batch edit with no output flag +- **WHEN** `safe-docx edit file.docx --replace _bk_a "old" "new"` is invoked without `-o` +- **THEN** the edits are applied to the session but no file is saved +- **AND** the JSON output includes the session ID for subsequent commands + +### Requirement: Schema-Driven CLI Flag Generation + +CLI subcommand flags SHALL be generated by introspecting the Zod schemas defined in the tool catalog. + +Adding a new parameter to a tool's Zod schema SHALL automatically surface it as a CLI flag without manual CLI code changes. + +#### Scenario: String field maps to value flag +- **GIVEN** a tool schema with `target_paragraph_id: z.string()` +- **THEN** the CLI exposes `--target-paragraph-id ` (or short alias `--para`) + +#### Scenario: Boolean field maps to presence flag +- **GIVEN** a tool schema with `clean_bookmarks: z.boolean().optional()` +- **THEN** the CLI exposes `--clean-bookmarks` (presence = true) or `--clean-bookmarks false` + +#### Scenario: Enum field maps to choice flag +- **GIVEN** a tool schema with `save_format: z.enum(['clean', 'tracked', 'both'])` +- **THEN** the CLI exposes `--save-format ` + +### Requirement: CLI Session Context Flags + +All session-aware CLI subcommands SHALL accept the file path as the first positional argument. + +All session-aware CLI subcommands SHALL accept `--session ` for explicit session reference. + +#### Scenario: Positional file path +- **WHEN** `safe-docx read-file ~/Downloads/Bylaws.docx` is invoked +- **THEN** `~/Downloads/Bylaws.docx` is used as the `file_path` parameter + +#### Scenario: Explicit session flag +- **WHEN** `safe-docx read-file --session ses_abc123` is invoked +- **THEN** `ses_abc123` is used as the `session_id` parameter + +#### Scenario: Both positional and session flag +- **WHEN** `safe-docx read-file ~/Downloads/Bylaws.docx --session ses_abc123` is invoked +- **THEN** both `file_path` and `session_id` are passed to the tool (existing conflict resolution applies) diff --git a/openspec/changes/add-cli-tool-subcommands/tasks.md b/openspec/changes/add-cli-tool-subcommands/tasks.md new file mode 100644 index 0000000..789f6dc --- /dev/null +++ b/openspec/changes/add-cli-tool-subcommands/tasks.md @@ -0,0 +1,54 @@ +## 1. CLI Framework & Router + +- [ ] 1.1 Extend `cli/index.ts` router to support 20+ subcommands (evaluate if custom parser scales or if a lightweight library like `citty` is needed) +- [ ] 1.2 Add schema-driven flag parser: introspect Zod schemas from `SAFE_DOCX_TOOL_CATALOG` to generate `--flag` definitions +- [ ] 1.3 Add auto-generated `--help` per subcommand from tool description + Zod schema +- [ ] 1.4 Add command grouping in top-level `--help` (read, edit, session, meta categories) + +## 2. Session Persistence Layer + +- [ ] 2.1 Design session lockfile format (`.safedocx-session.json` with session ID, file path, timestamp, TTL) +- [ ] 2.2 Implement `cli/session_store.ts`: save/load/expire session state to filesystem +- [ ] 2.3 Integrate session store with `resolveSessionForTool()` path in CLI context +- [ ] 2.4 Add `clear-session` subcommand for explicit cleanup + +## 3. Individual Tool Subcommands + +- [ ] 3.1 `read-file` — Read document with `--format`, `--offset`, `--limit`, `--node-ids` flags +- [ ] 3.2 `grep` — Search paragraphs with `--pattern`, `--context` flags +- [ ] 3.3 `replace-text` — Replace text with `--para`, `--old`, `--new`, `--instruction` flags +- [ ] 3.4 `insert-paragraph` — Insert paragraph with `--before`/`--after`, `--text`, `--instruction` flags +- [ ] 3.5 `save` — Save with `-o`/`--output`, `--format`, `--author` flags +- [ ] 3.6 `get-session-status` — Session info +- [ ] 3.7 `has-tracked-changes` — Check for revision markup +- [ ] 3.8 `accept-changes` — Accept all tracked changes +- [ ] 3.9 `apply-plan` — Batch apply edit plan +- [ ] 3.10 `format-layout` — Apply spacing/layout controls +- [ ] 3.11 `add-comment`, `get-comments`, `delete-comment` — Comment management +- [ ] 3.12 `add-footnote`, `get-footnotes`, `update-footnote`, `delete-footnote` — Footnote management +- [ ] 3.13 `extract-revisions` — Extract tracked changes as JSON +- [ ] 3.14 `clear-formatting` — Remove run-level formatting +- [ ] 3.15 `compare-documents` — Alias/enhancement of existing `compare` (unified flag style) + +## 4. Batched `edit` Command + +- [ ] 4.1 Implement `edit` subcommand with repeatable `--replace` and `--insert-after`/`--insert-before` flags +- [ ] 4.2 Parse multi-value `--replace ` flag syntax +- [ ] 4.3 Add `--instruction` flag (applied to all operations in the batch) +- [ ] 4.4 Add `-o`/`--output` flag for auto-save after batch +- [ ] 4.5 Execute operations sequentially in a single session + +## 5. Testing + +- [ ] 5.1 Unit tests for schema-driven flag parser (Zod → CLI flags) +- [ ] 5.2 Unit tests for session persistence layer (save/load/expire/clear) +- [ ] 5.3 Integration tests for each subcommand (mock file I/O) +- [ ] 5.4 Integration tests for batched `edit` command +- [ ] 5.5 E2E test: `safe-docx read-file ... | safe-docx replace-text ... | safe-docx save ...` pipeline +- [ ] 5.6 E2E test: `safe-docx edit --replace ... --replace ... -o ...` batched workflow + +## 6. Documentation & Help + +- [ ] 6.1 Auto-generate `--help` text from Zod schemas + tool descriptions +- [ ] 6.2 Update top-level `safe-docx --help` with command categories and examples +- [ ] 6.3 Add usage examples to each subcommand's help text