diff --git a/docs/automation/native.md b/docs/automation/native.md index 3d3c9f9a7..9211f85c6 100644 --- a/docs/automation/native.md +++ b/docs/automation/native.md @@ -1,32 +1,82 @@ # Driving the native builds -Every native build exposes the same control core through two entry points: the MCP -server described in the [README](README.md) (`OpenCADStudio --mcp`), and a headless -JSON-line channel meant for scripts and CI (`OpenCADStudio --serve`). The operations, -request envelope and replies are the same; this page covers what is specific to the -native side. The browser entry is described in [web.md](web.md). +Every native build has three entry points besides the editor window: the MCP +server described in the [README](README.md) (`OpenCADStudio --mcp`), a headless +JSON-line channel for scripts and CI (`OpenCADStudio --serve`), and a one-shot +converter (`OpenCADStudio --export IN OUT`). The browser entry is described in +[web.md](web.md). ## Choosing a channel | | `--mcp` | `--serve` | |---|---|---| -| Transport | MCP over stdio | one JSON request per line on stdin, one reply per line on stdout | -| Editor | starts the desktop editor and exposes the live document | no window; works on its own document | -| Needs a display | yes — on a headless machine `xvfb-run` works, on software rendering the viewport may show the GPU warning dialog | no | -| `measure`, `ocs_capture`, `set_properties`, interactive steps | available | `measure` and `capture` are not; the remaining operations are | +| Transport | MCP (JSON-RPC over stdio) with the four tools of the README | one JSON request per line on stdin, one reply per line on stdout; `--port N` listens on `127.0.0.1:N` instead | +| Editor | attaches to a running desktop editor, or starts one when there is none (`ocs_sessions`, `launch_if_none`) | no window; a document session of its own that lives as long as the process | +| Needs a display | yes (`xvfb-run` works on a headless machine; software rendering may raise the GPU warning dialog) | no | +| Needs a writable `HOME` / `XDG_CONFIG_HOME` | yes — the session descriptors live in the user configuration directory; without one `ocs_sessions` answers `No user configuration directory` | no | +| Startup dialogs | those of the editor (below) | none | +| `capture` | `ocs_capture` | not available (`gui_required`): there is no window to capture | | Good for | an MCP client driving a real editor | batch jobs, CI, agents without a display | -`OpenCADStudio --export IN OUT` converts a drawing in one shot, without any session. It -accepts `.dwg` and `.dxf` targets only. +## `--serve` -## Requirements +The first line written is a readiness banner, before any request is read: -The process needs a writable `HOME` (or `XDG_CONFIG_HOME`) — without one, `ocs_sessions` -answers `No user configuration directory` and nothing else works. +```json +{"ok":true,"ready":true,"version":"2026.38"} +``` -## Startup dialogs +Over `--port N` the banner opens every connection; the listener serves one +client at a time and the document session persists across reconnects. -A fresh profile opens two dialogs before any drawing exists, and both of them block +Two request styles share the channel, told apart by the `protocol` field: + +| Style | Shape | Operations | +|---|---|---| +| protocol 1 | `{"protocol":1,"request_id":"…","document_id":N,"op":"…"}` — the same requests and replies as `ocs_execute` / `ocs_read`, described in the README | `new`, `open`, `save`, `activate`, `select`, `run`, `start`, `input`, `cancel`, `stop`, `action`, `property`, `set_properties`, `embed_image`, `undo`, `redo`; reads `state`, `capabilities`, `query`, `entities`, `records`, `record_schema`, `layers`, `header`, `properties`, `measure`, `history`, `commands`, `events`, `operation` | +| legacy | `{"op":"…"}` without `protocol` | `new`, `open`, `run`, `entities`, `query`, `records`, `record_schema`, `capabilities`, `layers`, `header`, `select`, `save`, `undo`, `redo` | + +The two styles differ in how a waiting command is treated. A protocol-1 `run` +or `start` while a command is still active is refused with `command_busy`: +continue it with `input` or drop it with `cancel`. A legacy `run` line ends the +waiting command and starts the new one. + +### The legacy `run` line + +`{"op":"run","cmd":"LINE 0,0 10,10"}` feeds the tokens after the command name +to the prompts in order and then presses Enter. The reply is + +```json +{"ok":true,"cmd":"CIRCLE 0,0 5 9","status":"completed","blocked_by":null,"entities":1,"added":1,"unconsumed":["9"]} +``` + +| Field | Meaning | +|---|---| +| `status` | `completed`, or `waiting_input` when the line left something open | +| `blocked_by` | what *this line* left open: `"command"`, `"text_editor"`, `"mtext_editor"`, `"modal:"`, or `null`. A surface that was already open when the line started is not attributed to it | +| `entities`, `added` | entity count after the line, and the difference to before | +| `unconsumed` | tokens no prompt asked for, in order (`["9"]` above: the radius step ended the command); `[]` when everything was consumed | + +`waiting_input` means input is still required, not that nothing happened: +`SPLINE` and `ARC` keep accepting input after the entity exists, and +`zoom_extents` reports it after the view has already moved. + +`TEXT 0,0 5 0 hello world` creates one `Text` entity holding `hello world`: the +tail of the line is typed into the in-place editor, committed, and the command +is closed so it does not stay armed for a second line. A `TEXT` line without +content answers `waiting_input` with `blocked_by: "text_editor"`. + +Known gap (#1390): `MTEXT`, `HATCH` and `POLYGON` answer `completed` with +`added: 0` on this path — they need interactive surfaces (the rich-text editor, +a pattern choice, a boundary pick) that a single line has no tokens to fill. + +## `--mcp` + +The MCP server is a client of the desktop editor: `ocs_sessions` lists the live +editor sessions found in the configuration directory and starts one when none +is running, so several MCP connections can share one editor and its state. + +A fresh profile opens two dialogs before any drawing exists, and both block document creation: | Step | `ocs_sessions` reports | @@ -35,73 +85,47 @@ document creation: | after `close_modal` | `modal: "DonationPrompt"` | | after a second `close_modal` | `modal: null` | -While a dialog is open, `{"op":"new"}` answers `ok` but no tab is created, so there is -no `document_id` to work with. Close the dialogs first: +While a dialog is open, `{"op":"new"}` answers `ok` but no tab is created. +Close the dialogs first: ```python while (info := sessions())["modal"]: execute({"op": "action", "name": "close_modal", "document_id": info["document_id"]}) ``` -The donation prompt appears once per application version per profile, so an automated -profile meets it once per version rather than on every run. See -[web.md](web.md) for the same pattern on the browser side. +The donation prompt appears once per application version per profile, so an +automated profile meets it once per version rather than on every run. -## Targeting a document +`new` makes the new drawing the active document. Requests carry `document_id`; +take it from the reply's state and pass it on the following requests, as the +README describes. -Reads and mutations target the active tab. `new` creates a document but does not move -the active tab, so pass the `document_id` from the reply explicitly — without it, reads -answer empty and it looks like nothing was created. `activate` switches the active tab. -A client connection to `--mcp` starts a new editor instance, so session state does not -carry over between connections. +## `--export` -## Feeding a whole command line - -`run` takes one complete command line, including the answers to every prompt, and then -terminates the command as if Enter were pressed. A prompt cannot be continued on a -following line — each line is parsed as a fresh command line, so a command left waiting -is replaced by the next one (send `{"op":"cancel"}` if you want to drop it explicitly). - -`waiting_input` means input is still required — not that nothing happened. `SPLINE` and -`ARC`, for example, keep accepting further input even after the entity exists, and -`zoom_extents` reports it while the view has already changed. Send `{"op":"cancel"}` to -close the command when the line is done with it. - -Two reply fields describe what happened to the line: - -| Field | Meaning | -|---|---| -| `unconsumed` | Tokens that no prompt ever asked for, in order. `CIRCLE 0,0 5 9` reports `["9"]`, because the radius step ends the command; a fully consumed line reports `[]`. | -| `blocked_by` | What *this line* left open: `"command"`, `"text_editor"`, `"mtext_editor"`, `"modal:"`, or `null`. Set whenever `status` is `waiting_input`. | - -`TEXT 0,0 5 0 hello world` therefore creates one `Text` entity that holds -`hello world`, reports `completed` with `added: 1`, and leaves nothing unconsumed. The -content step runs in the in-place editor, which the feeder commits and then closes so -that the command does not stay armed for a second line. +`OpenCADStudio --export IN OUT` loads `IN` and writes `OUT` without a window or +a session. The output format follows the extension: `.dxf` writes DXF, any other +extension writes DWG at the document's own version. The exit code is 0 on +success and 1 when the input cannot be read or the output cannot be written. ## Interactive steps -For commands whose answers cannot be written on one line, use `start` and then `input`. -`start` replies with `state.command`, whose `accepts`, `options`, `prompt` and +For commands whose answers cannot be written on one line, use `start` and then +`input`. `start` replies with `state.command`, whose `accepts`, `options` and `input_example` say what the current step wants: | Step | Request | Note | |---|---|---| -| a point | `{"op":"input","kind":"point","point":[x,y,0],"space":"wcs"}` | the point is an array; separate `x`/`y` fields are rejected | +| a point | `{"op":"input","kind":"point","point":[x,y,0],"space":"wcs"}` | the point is an array; `space` is `wcs` (default), `ucs` or `relative` | | a typed value (height, rotation) | `{"op":"input","kind":"text","text":"5"}` | digits go through `text`, not `token`; `{"op":"input","kind":"enter"}` accepts the default | | a keyword (`J`, `ST`, `C`) | `{"op":"input","kind":"token","text":"C"}` | the payload field is named `text` | -| the text of `TEXT`/`MTEXT` | `{"op":"action","name":"text_input","value":"hello"}` then `{"op":"action","name":"text_commit"}` | the command has already ended at that point and the editor owns the input | +| an object | `{"op":"input","kind":"entity","handle":"HANDLE","point":[x,y,0]}` | `kind: "structure"` for a sub-entity pick, `kind: "selection"` to hand over the current selection | +| the content of `TEXT` | `{"op":"action","name":"text_input","value":"hello"}` then `{"op":"action","name":"text_commit"}` | the command has already ended; the in-place editor owns the input | +| the content of `MTEXT` | `{"op":"action","name":"mtext_insert","value":"hello"}` then `{"op":"action","name":"mtext_commit"}` | the rich-text editor has its own actions; the `TEXT` ones answer `editor_closed` | ## Native-only operations -| Operation | Device | -|---|---| -| `measure` | `ocs_read` with `op: "measure"` and `parameters.handles` returns kernel-computed length, area, bounds and mass properties. | -| `ocs_capture` | Returns the viewport or the whole window. Either an inline base64 image, or a PNG written under the temporary directory with the reply carrying its `path`. | -| `set_properties` | Mutates record properties. It requires `collection`, and colours are the serialised enum — `{"Index": 1}` or `"ByLayer"`; a colour name such as `"Red"` is rejected with `invalid_value`. | - -## Known gaps - -On the `--serve` path, `MTEXT`, `HATCH` and `POLYGON` report `completed` with -`added: 0`: they need interactive surfaces (a pattern choice, a boundary selection) that -a single command line has no tokens to fill. `TEXT` no longer belongs to that list. +| Operation | `--serve` (protocol 1) | `--mcp` | +|---|---|---| +| measure | `{"protocol":1,"op":"measure","handles":["2A"]}` — kernel-computed length, area, bounds and mass properties; no window needed | `ocs_read` with `op: "measure"` and `parameters.handles` | +| capture | `{"protocol":1,"op":"capture","path":"out.png"}` — needs the editor window, so it answers `gui_required` here | `ocs_capture` with `scope` (`viewport` or `window`) and `max_dimension` | +| `set_properties` | requires `collection`; colours are the serialised enum — `{"Index": 1}` or `"ByLayer"`; a colour name such as `"Red"` is rejected with `invalid_value` | same request through `ocs_execute` |