@termwright/mcp 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +109 -28
  2. package/dist/bin.js +2 -1
  3. package/dist/bin.js.map +1 -1
  4. package/dist/chunk-R2N52YYH.js +629 -0
  5. package/dist/chunk-R2N52YYH.js.map +1 -0
  6. package/dist/{chunk-IPNUAUAN.js → chunk-ROMJP5D3.js} +595 -482
  7. package/dist/chunk-ROMJP5D3.js.map +1 -0
  8. package/dist/docs-85fGFORb.d.ts +13 -0
  9. package/dist/docs.d.ts +1 -0
  10. package/dist/docs.js +7 -0
  11. package/dist/docs.js.map +1 -0
  12. package/dist/index.d.ts +50 -28
  13. package/dist/index.js +9 -7
  14. package/package.json +11 -8
  15. package/dist/chunk-2J5WHI6X.js +0 -2000
  16. package/dist/chunk-2J5WHI6X.js.map +0 -1
  17. package/dist/chunk-36C7A7DW.js +0 -2685
  18. package/dist/chunk-36C7A7DW.js.map +0 -1
  19. package/dist/chunk-3PLOAM2C.js +0 -2427
  20. package/dist/chunk-3PLOAM2C.js.map +0 -1
  21. package/dist/chunk-57GYK2EF.js +0 -2991
  22. package/dist/chunk-57GYK2EF.js.map +0 -1
  23. package/dist/chunk-ABLJBL5P.js +0 -2687
  24. package/dist/chunk-ABLJBL5P.js.map +0 -1
  25. package/dist/chunk-BOOUADRN.js +0 -1938
  26. package/dist/chunk-BOOUADRN.js.map +0 -1
  27. package/dist/chunk-BPWIETN5.js +0 -2983
  28. package/dist/chunk-BPWIETN5.js.map +0 -1
  29. package/dist/chunk-CMQB5G7R.js +0 -2968
  30. package/dist/chunk-CMQB5G7R.js.map +0 -1
  31. package/dist/chunk-I4B53KZ7.js +0 -2955
  32. package/dist/chunk-I4B53KZ7.js.map +0 -1
  33. package/dist/chunk-IPNUAUAN.js.map +0 -1
  34. package/dist/chunk-KZWL2S6E.js +0 -2869
  35. package/dist/chunk-KZWL2S6E.js.map +0 -1
  36. package/dist/chunk-LB2QBYW4.js +0 -2686
  37. package/dist/chunk-LB2QBYW4.js.map +0 -1
  38. package/dist/chunk-MR3AXSXL.js +0 -1977
  39. package/dist/chunk-MR3AXSXL.js.map +0 -1
  40. package/dist/chunk-NVSZXEZU.js +0 -2688
  41. package/dist/chunk-NVSZXEZU.js.map +0 -1
  42. package/dist/chunk-PD2WKAFE.js +0 -2531
  43. package/dist/chunk-PD2WKAFE.js.map +0 -1
  44. package/dist/chunk-PGY4ZDLD.js +0 -1843
  45. package/dist/chunk-PGY4ZDLD.js.map +0 -1
  46. package/dist/chunk-QDIAASH7.js +0 -2982
  47. package/dist/chunk-QDIAASH7.js.map +0 -1
  48. package/dist/chunk-UZWFLJGG.js +0 -2873
  49. package/dist/chunk-UZWFLJGG.js.map +0 -1
  50. package/dist/chunk-VFYTROYG.js +0 -2825
  51. package/dist/chunk-VFYTROYG.js.map +0 -1
  52. package/dist/chunk-ZTHKAJKT.js +0 -2981
  53. package/dist/chunk-ZTHKAJKT.js.map +0 -1
  54. package/dist/chunk-ZZULGRRE.js +0 -2991
  55. package/dist/chunk-ZZULGRRE.js.map +0 -1
package/README.md CHANGED
@@ -7,7 +7,7 @@ snapshot, click a button by its ref, wait on a condition, and ask what changed.
7
7
  It is deliberately thin. Every tool validates its arguments with zod, calls the
8
8
  public `@termwright/driver` API, and renders the result. There is no locator
9
9
  engine, no wait loop and no matching heuristic here — a behaviour that differed
10
- between this server and the test preset would be a bug in this package.
10
+ between this server and the Native Host would be a bug in this package.
11
11
 
12
12
  ## Install
13
13
 
@@ -47,28 +47,68 @@ process.on('SIGINT', () => void running.close());
47
47
  // terminal.snapshot -> the compact format, plus refs / cursor / modes / scroll
48
48
  Terminal t1 100x30 revision 42
49
49
  semanticTree: available
50
- dialog "Permission" ref=n7@42 bounds=(8,20,40,9) modal
51
- button "Approve" ref=n8@42 bounds=(14,23,11,1) focused
50
+ dialog "Permission" ref=semantic:n7@42 bounds=(8,20,40,9) modal
51
+ button "Approve" ref=semantic:n8@42 bounds=(14,23,11,1) focused
52
+ button "Reject" ref=semantic:n9@42 bounds=(14,36,10,1)
52
53
  visible text:
53
54
 
54
55
 
55
- // terminal.click { "terminal": "t1", "ref": "n8@42" }
56
- // terminal.wait_for { "terminal": "t1", "wait": "text", "text": "Approved" }
56
+ // terminal.click { "terminal": "t1", "ref": "semantic:n9@42" }
57
+ // terminal.wait_for { "terminal": "t1", "wait": "text", "text": "Rejected" }
58
+ // terminal.wait_for { "terminal": "t1", "wait": "focused", "testId": "reject" }
57
59
  // terminal.capture_since{ "terminal": "t1", "cursor": 42 } -> changed rows + subtrees
58
60
  // terminal.close { "terminal": "t1" }
59
61
  ```
60
62
 
63
+ The text wait proves the PTY output. When the next operation requires a semantic
64
+ result, wait for that explicit state (`focused`, `checked`, `selected`, …)
65
+ before capturing. The condition must represent a change from the baseline; an
66
+ already-satisfied condition proves no new commit. Screen-only output and the
67
+ prefix of a future semantic frame are intentionally indistinguishable until the
68
+ probe publishes a causal signal.
69
+
61
70
  Streamable HTTP, for hosts that connect over a socket:
62
71
 
63
72
  ```sh
64
- termwright-mcp --http --port 7333 # endpoint: http://127.0.0.1:7333/mcp
73
+ termwright-mcp --http --port 7333 --show-auth-token
74
+ # Explicit opt-in prints the endpoint and its fresh per-launch bearer token.
75
+ ```
76
+
77
+ Every MCP-bearing HTTP request, including initialize and `DELETE`, must send
78
+ `Authorization: Bearer <token>`; an allowlisted CORS preflight is the only
79
+ bearer-free request and cannot reach MCP routing. Library callers receive the
80
+ token as `handle.authToken`; the SDK transport accepts it through
81
+ `requestInit.headers`:
82
+
83
+ ```ts
84
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
85
+ import { serveHttp } from '@termwright/mcp';
86
+
87
+ const handle = await serveHttp();
88
+ const transport = new StreamableHTTPClientTransport(
89
+ new URL(`http://127.0.0.1:${handle.port}/mcp`),
90
+ { requestInit: { headers: { authorization: `Bearer ${handle.authToken}` } } },
91
+ );
65
92
  ```
66
93
 
67
94
  Sessions are keyed by `Mcp-Session-Id` in this package's own `SessionRegistry`,
68
- not inside transport objects: each session owns its terminals, `DELETE` disposes
95
+ not inside transport objects. A session id is routing metadata, never a
96
+ credential: authentication happens before session lookup, refresh, initialize
97
+ or request-body buffering. Each session owns its terminals, `DELETE` disposes
69
98
  them, and the ceiling (16 sessions, 16 terminals each) is enforced before a
70
99
  transport exists.
71
100
 
101
+ The listener binds to loopback by default. A non-loopback `host` is refused
102
+ unless `allowNonLoopback: true` is set (CLI: `--allow-non-loopback`). That opt-in
103
+ does not add TLS: put a remotely reachable listener behind a private,
104
+ authenticated TLS boundary because the bearer otherwise crosses the network in
105
+ cleartext. Browser requests carrying `Origin` are rejected by default; an
106
+ embedding may allow exact HTTP(S) origins with `allowedOrigins`. A bounded
107
+ per-peer rate limiter protects authenticated work and accepted preflights
108
+ without letting invalid credentials or preflight traffic consume a legitimate
109
+ client's bucket; configure its window/request/client ceilings with `rateLimit`
110
+ when the deployment has a known proxy or concurrency envelope.
111
+
72
112
  Streamable HTTP gives no disconnect signal, so a session also expires after
73
113
  `idleTtlMs` without a request (10 minutes by default, `0` to disable). Every
74
114
  request naming a session refreshes it; expiry runs the full teardown — terminals
@@ -77,19 +117,57 @@ stderr. stdio has no TTL: there, EOF on the pipe is the signal.
77
117
 
78
118
  ## Tools
79
119
 
80
- `terminal.launch`, `capabilities`, `snapshot`, `capture_since`, `query`,
81
- `click`, `double_click`, `press`, `type`, `paste`, `write_raw`, `drag`, `wheel`,
82
- `resize`, `signal`, `scrollback`, `select_cells`, `copy_selection`, `wait_for`,
83
- `close` — the surface CONTRACTS.md §MCP defines — plus `trace.open`,
84
- `trace.overview`, `trace.frame_at` and `trace.diff` for recorded sessions. Every
85
- one carries an `inputSchema` and an `outputSchema` and returns
86
- `structuredContent`.
87
-
88
- Targeting, in precedence order: `ref`, `selector` (the CSS dialect
89
- `dialog button#approve:focused`), `testId`, `role` (+ `name`), `label`, `text`.
90
- Any name or text may be written as `/pattern/flags` to match as a regular
91
- expression. Locators are strict: more than one match fails with
92
- `ambiguous-locator` unless you pass `nth`.
120
+ <!-- BEGIN GENERATED MCP TOOL SURFACE -->
121
+ <!-- Generated from packages/mcp/src/registry.ts; do not edit this block by hand. -->
122
+ ### Live terminal tools
123
+
124
+ | Tool | Purpose |
125
+ | --- | --- |
126
+ | `terminal.launch` | Starts a program in a real pseudo-terminal and returns a terminal handle plus the first snapshot. The child gets a minimal environment unless envMode is "inherit"; values passed in env are never echoed back. |
127
+ | `terminal.capabilities` | What this session supports: whether a semantic tree is published, which adapter publishes it, and the terminal geometry. Call it before relying on role-based targeting. |
128
+ | `terminal.snapshot` | One typed view of the terminal: compact semantic refs, visible text, cursor, terminal modes and scroll position. variant "full" writes the complete dump (text, ANSI, HTML, semantic tree) to disk and returns only refs plus the file path. The returned revision is the cursor for terminal.capture_since. |
129
+ | `terminal.capture_since` | Incremental view: the screen rows that differ and the semantic subtrees that were added, removed or updated in the latest committed semantic tree since the given cursor. A screen change alone does not imply a future semantic commit; wait for an explicit semantic state when the caller requires one. The cursor must be a revision this server handed out earlier (snapshot or capture_since); older cursors fail with history-truncated. |
130
+ | `terminal.query` | Resolves a target to refs without acting on it. Use it to check how many nodes a locator matches before clicking, or to turn a role/name into a ref. |
131
+ | `terminal.checkpoint` | Returns the atomic session/contract/screen/semantic identity used by revision-safe actions and waits. |
132
+ | `terminal.actionability` | Runs the same ActionPlanner used by execution, but sends no input. Reports every authoritative requirement and the chosen strategy or typed rejection. |
133
+ | `terminal.click` | Sends a real click mouse report through the pseudo-terminal. Fails closed with input-mode-disabled when the required tracking mode or encoding is disabled or unobservable. |
134
+ | `terminal.double_click` | Sends a real double-click mouse report through the pseudo-terminal. Fails closed with input-mode-disabled when the required tracking mode or encoding is disabled or unobservable. |
135
+ | `terminal.hover` | Sends a real motion mouse report through the pseudo-terminal. Fails closed with input-mode-disabled when the required tracking mode or encoding is disabled or unobservable. |
136
+ | `terminal.press` | Sends key chords as real bytes, honouring the modes the program enabled (application cursor keys, keypad). Examples: "Enter", "Escape", "Control+K Control+U". With a target, the node must already be focused. |
137
+ | `terminal.type` | Types text as individual keystrokes (not a paste). With a target, the node must already be focused; use terminal.fill for focus + replacement. |
138
+ | `terminal.fill` | Ensures the semantic control receives focus through the real input path, selects its current value, and types the replacement. |
139
+ | `terminal.check` | Uses the central action planner and real terminal input to check a checkbox or radio, then verifies semantic state. |
140
+ | `terminal.uncheck` | Uses the central action planner and real terminal input to uncheck a checkbox or radio, then verifies semantic state. |
141
+ | `terminal.paste` | Pastes text, wrapped in bracketed-paste markers when the program enabled that mode. Use it for multi-line input instead of terminal.type. |
142
+ | `terminal.write_raw` | Writes bytes to the pseudo-terminal verbatim — no newline, no key encoding. The escape hatch for sequences the key encoder does not model. |
143
+ | `terminal.drag` | Drags with real mouse reports: either from one target to another (toTarget), or between two cell positions inside the source target (from/to). |
144
+ | `terminal.wheel` | Sends wheel reports over a target. Positive deltaY scrolls down. |
145
+ | `terminal.resize` | Resizes the pseudo-terminal; the child sees a real SIGWINCH. |
146
+ | `terminal.signal` | Sends INT, TERM, KILL or HUP to the child. Destructive by design: terminal.close cleans up without signalling. |
147
+ | `terminal.scrollback` | Emulator-side history: read a line range, search it, or move the viewport. The child sees nothing — no input is sent. |
148
+ | `terminal.select_cells` | Selects a rectangle in the emulator (like a mouse selection). No input is sent. |
149
+ | `terminal.copy_selection` | Returns the text of the current selection and optionally clears it. |
150
+ | `terminal.wait_for` | Revision-driven waits — never a sleep. "text"/"title" wait for content, locator states use the driver's canonical Conditions, "quiet" explicitly waits for heuristic silence, "render" for a render after a given revision, "exit" for the child to exit. |
151
+ | `terminal.close` | Bounded physical cleanup: hangs up the pseudo-terminal and forgets the handle. Send signals explicitly with terminal.signal if the child must be killed first. |
152
+
153
+ ### Trace tools
154
+
155
+ | Tool | Purpose |
156
+ | --- | --- |
157
+ | `trace.open` | Validates a .twtrace directory or zip and returns a handle plus its metadata: the recorded command, viewport, duration, exit status and whether the session published a semantic tree. Start every replay investigation here. |
158
+ | `trace.overview` | The shape of a recording: every step with its status and timing, the cast markers, the exit status, and which step failed. Use it to pick the moment worth reconstructing before calling trace.frame_at. |
159
+ | `trace.frame_at` | Rebuilds the screen at a moment — named by timeMs, stepIndex or marker — by replaying the recording into a headless emulator, and pairs it with the semantic tree of the nearest revision at or before that moment. Reads exactly like a live terminal.snapshot. |
160
+ | `trace.diff` | Reconstructs two moments of a recording and reports what moved: changed screen rows and changed semantic subtrees, in the same shape as terminal.capture_since on a live session. |
161
+
162
+ ### Targeting
163
+
164
+ Targeting precedence is `ref`, `selector`, `testId`, `role` (+`name`), `label`, `text`, `screenText`.
165
+
166
+ `semanticTree: unavailable` means the program ships no integration — target physical output with `screenText`, never semantic `text` or `role`.
167
+
168
+ Names and text accept `/pattern/flags`. Locators are strict: more than one match returns
169
+ `ambiguous-locator` unless `nth` is explicit.
170
+ <!-- END GENERATED MCP TOOL SURFACE -->
93
171
 
94
172
  ## Replaying a recorded failure
95
173
 
@@ -102,8 +180,8 @@ same vocabulary as a live session.
102
180
  // trace.frame_at { "traceId": "tr1", "stepIndex": 1 }
103
181
  Terminal tr1 40x6 revision 2
104
182
  semanticTree: available
105
- dialog "Permission" ref=n1@2 modal
106
- button "Approve" ref=n2@2 disabled
183
+ dialog "Permission" ref=semantic:n1@2 modal
184
+ button "Approve" ref=semantic:n2@2 disabled
107
185
  visible text:
108
186
 
109
187
  // trace.diff { "traceId": "tr1", "fromMs": 0, "toMs": 3000 } -> changed rows + subtrees
@@ -139,8 +217,8 @@ window; lower `screenshotScale` or resize the terminal.
139
217
 
140
218
  ## Refs and revisions
141
219
 
142
- A ref is `n8@42`: node id at semantic revision 42 (grid matches get
143
- `grid:1,2,9,1@7`). Refs go straight to `harness.locatorForRef()`, so they resolve
220
+ A ref is `semantic:n8@42`: node id at semantic revision 42 (screen matches get
221
+ `screen:1,2,9,1@7`). Refs go straight to `harness.locatorForRef()`, so they resolve
144
222
  by node *identity* — two buttons with the same name stay distinct. A producer
145
223
  which promises stable identity can resolve that node again in later revisions.
146
224
  Frame-local identities and grid refs remain revision-bound; take a fresh
@@ -148,11 +226,14 @@ snapshot when either becomes stale.
148
226
 
149
227
  `terminal.snapshot` also returns a screen `revision`; pass it back as the
150
228
  `cursor` of `terminal.capture_since` to get only the rows and semantic subtrees
151
- that changed. Cursors the server never handed out fail with `history-truncated`
152
- (the last 16 captures per terminal are retained).
229
+ that changed in the latest committed tree. A changed screen does not promise a
230
+ future semantic commit; wait for an explicit semantic condition when one is
231
+ required. Cursors the server never handed out fail with `history-truncated` (the
232
+ last 16 captures per terminal are retained).
153
233
 
154
234
  Programs without a framework probe or custom semantic producer report
155
- `semanticTree: unavailable`. There are no invented roles: target them by text.
235
+ `semanticTree: unavailable`. There are no invented roles: target physical
236
+ output with `screenText`.
156
237
 
157
238
  ## Application logs
158
239
 
@@ -216,7 +297,7 @@ are the one thing never recorded: the driver keeps their size only.
216
297
  Failures come back as tool results with `isError` set. The text content reads
217
298
 
218
299
  ```
219
- error stale-snapshot: ref n8@42 no longer exists at semantic revision 43
300
+ error stale-snapshot: ref semantic:n8@42 no longer exists at semantic revision 43
220
301
  suggestion: re-resolve the locator; the node identity is no longer present
221
302
  semanticTree: true
222
303
  ```
package/dist/bin.js CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  main
4
- } from "./chunk-IPNUAUAN.js";
4
+ } from "./chunk-R2N52YYH.js";
5
+ import "./chunk-ROMJP5D3.js";
5
6
 
6
7
  // src/bin.ts
7
8
  process.stdout.on("error", (error) => {
package/dist/bin.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/bin.ts"],"sourcesContent":["#!/usr/bin/env node\n/** The `termwright-mcp` executable. All logic lives in `cli.ts`. */\nimport { main } from './cli.js';\n\n// `termwright-mcp agent-context | head` closes the pipe early; that is a normal\n// end of output for a CLI, not a crash.\nprocess.stdout.on('error', (error: NodeJS.ErrnoException) => {\n if (error.code === 'EPIPE') process.exit(0);\n throw error;\n});\n\nawait main();\n"],"mappings":";;;;;;AAMA,QAAQ,OAAO,GAAG,SAAS,CAAC,UAAiC;AAC3D,MAAI,MAAM,SAAS,QAAS,SAAQ,KAAK,CAAC;AAC1C,QAAM;AACR,CAAC;AAED,MAAM,KAAK;","names":[]}
1
+ {"version":3,"sources":["../src/bin.ts"],"sourcesContent":["#!/usr/bin/env node\n/** The `termwright-mcp` executable. All logic lives in `cli.ts`. */\nimport { main } from './cli.js';\n\n// `termwright-mcp agent-context | head` closes the pipe early; that is a normal\n// end of output for a CLI, not a crash.\nprocess.stdout.on('error', (error: NodeJS.ErrnoException) => {\n if (error.code === 'EPIPE') process.exit(0);\n throw error;\n});\n\nawait main();\n"],"mappings":";;;;;;;AAMA,QAAQ,OAAO,GAAG,SAAS,CAAC,UAAiC;AAC3D,MAAI,MAAM,SAAS,QAAS,SAAQ,KAAK,CAAC;AAC1C,QAAM;AACR,CAAC;AAED,MAAM,KAAK;","names":[]}