pty-auto 0.3.6

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 (143) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +515 -0
  3. package/dist/index.d.ts +3 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +3 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/src/plugin/constants.d.ts +5 -0
  8. package/dist/src/plugin/constants.d.ts.map +1 -0
  9. package/dist/src/plugin/constants.js +7 -0
  10. package/dist/src/plugin/constants.js.map +1 -0
  11. package/dist/src/plugin/pty/buffer.d.ts +19 -0
  12. package/dist/src/plugin/pty/buffer.d.ts.map +1 -0
  13. package/dist/src/plugin/pty/buffer.js +61 -0
  14. package/dist/src/plugin/pty/buffer.js.map +1 -0
  15. package/dist/src/plugin/pty/formatters.d.ts +4 -0
  16. package/dist/src/plugin/pty/formatters.d.ts.map +1 -0
  17. package/dist/src/plugin/pty/formatters.js +22 -0
  18. package/dist/src/plugin/pty/formatters.js.map +1 -0
  19. package/dist/src/plugin/pty/manager.d.ts +33 -0
  20. package/dist/src/plugin/pty/manager.d.ts.map +1 -0
  21. package/dist/src/plugin/pty/manager.js +108 -0
  22. package/dist/src/plugin/pty/manager.js.map +1 -0
  23. package/dist/src/plugin/pty/notification-manager.d.ts +9 -0
  24. package/dist/src/plugin/pty/notification-manager.d.ts.map +1 -0
  25. package/dist/src/plugin/pty/notification-manager.js +69 -0
  26. package/dist/src/plugin/pty/notification-manager.js.map +1 -0
  27. package/dist/src/plugin/pty/output-manager.d.ts +7 -0
  28. package/dist/src/plugin/pty/output-manager.d.ts.map +1 -0
  29. package/dist/src/plugin/pty/output-manager.js +26 -0
  30. package/dist/src/plugin/pty/output-manager.js.map +1 -0
  31. package/dist/src/plugin/pty/permissions.d.ts +27 -0
  32. package/dist/src/plugin/pty/permissions.d.ts.map +1 -0
  33. package/dist/src/plugin/pty/permissions.js +54 -0
  34. package/dist/src/plugin/pty/permissions.js.map +1 -0
  35. package/dist/src/plugin/pty/session-lifecycle.d.ts +20 -0
  36. package/dist/src/plugin/pty/session-lifecycle.d.ts.map +1 -0
  37. package/dist/src/plugin/pty/session-lifecycle.js +177 -0
  38. package/dist/src/plugin/pty/session-lifecycle.js.map +1 -0
  39. package/dist/src/plugin/pty/tools/kill.d.ts +12 -0
  40. package/dist/src/plugin/pty/tools/kill.d.ts.map +1 -0
  41. package/dist/src/plugin/pty/tools/kill.js +37 -0
  42. package/dist/src/plugin/pty/tools/kill.js.map +1 -0
  43. package/dist/src/plugin/pty/tools/kill.txt +25 -0
  44. package/dist/src/plugin/pty/tools/list.d.ts +6 -0
  45. package/dist/src/plugin/pty/tools/list.d.ts.map +1 -0
  46. package/dist/src/plugin/pty/tools/list.js +22 -0
  47. package/dist/src/plugin/pty/tools/list.js.map +1 -0
  48. package/dist/src/plugin/pty/tools/list.txt +22 -0
  49. package/dist/src/plugin/pty/tools/read.d.ts +18 -0
  50. package/dist/src/plugin/pty/tools/read.d.ts.map +1 -0
  51. package/dist/src/plugin/pty/tools/read.js +165 -0
  52. package/dist/src/plugin/pty/tools/read.js.map +1 -0
  53. package/dist/src/plugin/pty/tools/read.txt +39 -0
  54. package/dist/src/plugin/pty/tools/spawn.d.ts +24 -0
  55. package/dist/src/plugin/pty/tools/spawn.d.ts.map +1 -0
  56. package/dist/src/plugin/pty/tools/spawn.js +75 -0
  57. package/dist/src/plugin/pty/tools/spawn.js.map +1 -0
  58. package/dist/src/plugin/pty/tools/spawn.txt +52 -0
  59. package/dist/src/plugin/pty/tools/write.d.ts +12 -0
  60. package/dist/src/plugin/pty/tools/write.d.ts.map +1 -0
  61. package/dist/src/plugin/pty/tools/write.js +62 -0
  62. package/dist/src/plugin/pty/tools/write.js.map +1 -0
  63. package/dist/src/plugin/pty/tools/write.txt +28 -0
  64. package/dist/src/plugin/pty/types.d.ts +70 -0
  65. package/dist/src/plugin/pty/types.d.ts.map +1 -0
  66. package/dist/src/plugin/pty/types.js +1 -0
  67. package/dist/src/plugin/pty/types.js.map +1 -0
  68. package/dist/src/plugin/pty/utils.d.ts +12 -0
  69. package/dist/src/plugin/pty/utils.d.ts.map +1 -0
  70. package/dist/src/plugin/pty/utils.js +17 -0
  71. package/dist/src/plugin/pty/utils.js.map +1 -0
  72. package/dist/src/plugin/pty/wildcard.d.ts +7 -0
  73. package/dist/src/plugin/pty/wildcard.d.ts.map +1 -0
  74. package/dist/src/plugin/pty/wildcard.js +51 -0
  75. package/dist/src/plugin/pty/wildcard.js.map +1 -0
  76. package/dist/src/plugin/types.d.ts +5 -0
  77. package/dist/src/plugin/types.d.ts.map +1 -0
  78. package/dist/src/plugin/types.js +1 -0
  79. package/dist/src/plugin/types.js.map +1 -0
  80. package/dist/src/plugin.d.ts +3 -0
  81. package/dist/src/plugin.d.ts.map +1 -0
  82. package/dist/src/plugin.js +71 -0
  83. package/dist/src/plugin.js.map +1 -0
  84. package/dist/src/shared/constants.d.ts +5 -0
  85. package/dist/src/shared/constants.d.ts.map +1 -0
  86. package/dist/src/shared/constants.js +7 -0
  87. package/dist/src/shared/constants.js.map +1 -0
  88. package/dist/src/web/server/callback-manager.d.ts +8 -0
  89. package/dist/src/web/server/callback-manager.d.ts.map +1 -0
  90. package/dist/src/web/server/callback-manager.js +23 -0
  91. package/dist/src/web/server/callback-manager.js.map +1 -0
  92. package/dist/src/web/server/handlers/health.d.ts +3 -0
  93. package/dist/src/web/server/handlers/health.d.ts.map +1 -0
  94. package/dist/src/web/server/handlers/health.js +33 -0
  95. package/dist/src/web/server/handlers/health.js.map +1 -0
  96. package/dist/src/web/server/handlers/responses.d.ts +10 -0
  97. package/dist/src/web/server/handlers/responses.d.ts.map +1 -0
  98. package/dist/src/web/server/handlers/responses.js +26 -0
  99. package/dist/src/web/server/handlers/responses.js.map +1 -0
  100. package/dist/src/web/server/handlers/sessions.d.ts +13 -0
  101. package/dist/src/web/server/handlers/sessions.d.ts.map +1 -0
  102. package/dist/src/web/server/handlers/sessions.js +96 -0
  103. package/dist/src/web/server/handlers/sessions.js.map +1 -0
  104. package/dist/src/web/server/handlers/static.d.ts +2 -0
  105. package/dist/src/web/server/handlers/static.d.ts.map +1 -0
  106. package/dist/src/web/server/handlers/static.js +38 -0
  107. package/dist/src/web/server/handlers/static.js.map +1 -0
  108. package/dist/src/web/server/handlers/upgrade.d.ts +2 -0
  109. package/dist/src/web/server/handlers/upgrade.d.ts.map +1 -0
  110. package/dist/src/web/server/handlers/upgrade.js +11 -0
  111. package/dist/src/web/server/handlers/upgrade.js.map +1 -0
  112. package/dist/src/web/server/handlers/websocket.d.ts +3 -0
  113. package/dist/src/web/server/handlers/websocket.d.ts.map +1 -0
  114. package/dist/src/web/server/handlers/websocket.js +119 -0
  115. package/dist/src/web/server/handlers/websocket.js.map +1 -0
  116. package/dist/src/web/server/server.d.ts +12 -0
  117. package/dist/src/web/server/server.d.ts.map +1 -0
  118. package/dist/src/web/server/server.js +73 -0
  119. package/dist/src/web/server/server.js.map +1 -0
  120. package/dist/src/web/shared/api-client.d.ts +132 -0
  121. package/dist/src/web/shared/api-client.d.ts.map +1 -0
  122. package/dist/src/web/shared/api-client.js +79 -0
  123. package/dist/src/web/shared/api-client.js.map +1 -0
  124. package/dist/src/web/shared/constants.d.ts +11 -0
  125. package/dist/src/web/shared/constants.d.ts.map +1 -0
  126. package/dist/src/web/shared/constants.js +23 -0
  127. package/dist/src/web/shared/constants.js.map +1 -0
  128. package/dist/src/web/shared/route-builder.d.ts +30 -0
  129. package/dist/src/web/shared/route-builder.d.ts.map +1 -0
  130. package/dist/src/web/shared/route-builder.js +42 -0
  131. package/dist/src/web/shared/route-builder.js.map +1 -0
  132. package/dist/src/web/shared/routes.d.ts +37 -0
  133. package/dist/src/web/shared/routes.d.ts.map +1 -0
  134. package/dist/src/web/shared/routes.js +39 -0
  135. package/dist/src/web/shared/routes.js.map +1 -0
  136. package/dist/src/web/shared/types.d.ts +87 -0
  137. package/dist/src/web/shared/types.d.ts.map +1 -0
  138. package/dist/src/web/shared/types.js +15 -0
  139. package/dist/src/web/shared/types.js.map +1 -0
  140. package/dist/web/assets/index-CYWtqpTG.js +24 -0
  141. package/dist/web/assets/index-DDGTF8rc.css +1 -0
  142. package/dist/web/index.html +204 -0
  143. package/package.json +107 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Jens
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,515 @@
1
+ # pty-auto
2
+
3
+ A plugin for [OpenCode](https://opencode.ai) that provides interactive PTY (pseudo-terminal) management with mandatory spawn timeouts and bash-independent tool calls, enabling the AI agent to run background processes, send interactive input, and read output on demand.
4
+
5
+ ## Why?
6
+
7
+ OpenCode's built-in `bash` tool runs commands synchronously—the agent waits for completion. This works for quick commands, but not for:
8
+
9
+ - **Dev servers** (`npm run dev`, `cargo watch`)
10
+ - **Watch modes** (`npm test -- --watch`)
11
+ - **Long-running processes** (database servers, tunnels)
12
+ - **Interactive programs** (REPLs, prompts)
13
+
14
+ This plugin gives the agent full control over multiple terminal sessions, like tabs in a terminal app.
15
+
16
+ ## Features
17
+
18
+ - **Background Execution**: Spawn processes that run independently
19
+ - **Multiple Sessions**: Manage multiple PTYs simultaneously
20
+ - **Interactive Input**: Send keystrokes, Ctrl+C, arrow keys, etc.
21
+ - **Output Buffer**: Read output anytime with pagination (offset/limit)
22
+ - **Pattern Filtering**: Search output using regex (like `grep`)
23
+ - **Exit Notifications**: Get notified when processes finish (eliminates polling)
24
+ - **Bash-Independent Tool Calls**: Uses PTY-specific tool calls instead of OpenCode's built-in `bash` permission path
25
+ - **Session Lifecycle**: Sessions persist until explicitly killed
26
+ - **Auto-cleanup**: PTYs are cleaned up when OpenCode sessions end
27
+ - **Web UI**: Modern React-based interface for session management
28
+ - **Real-time Streaming**: WebSocket-based live output updates
29
+
30
+ ## Setup
31
+
32
+ Add the plugin to your [OpenCode config](https://opencode.ai/docs/config/):
33
+
34
+ ```json
35
+ {
36
+ "$schema": "https://opencode.ai/config.json",
37
+ "plugin": ["pty-auto"]
38
+ }
39
+ ```
40
+
41
+ That's it. OpenCode will automatically install the plugin on next run.
42
+
43
+ ## Updating
44
+
45
+ OpenCode automatically checks for and installs plugin updates on startup. You don't need to do anything manually!
46
+
47
+ If you ever need to force a clean reinstall, you can clear the cache:
48
+
49
+ ```bash
50
+ rm -rf ~/.cache/opencode/node_modules/pty-auto
51
+ opencode
52
+ ```
53
+
54
+ ## Tools Provided
55
+
56
+ | Tool | Description |
57
+ | ----------- | --------------------------------------------------------------------------- |
58
+ | `pty_spawn` | Create a new PTY session with a required `timeoutSeconds` budget (command, args, workdir, env, title, notifyOnExit) |
59
+ | `pty_write` | Send input to a PTY (text, escape sequences like `\x03` for Ctrl+C) |
60
+ | `pty_read` | Read output buffer with pagination and optional regex filtering |
61
+ | `pty_list` | List all PTY sessions with status, PID, line count |
62
+ | `pty_kill` | Terminate a PTY, optionally cleanup the buffer |
63
+
64
+ ## Slash Commands
65
+
66
+ This plugin provides slash commands that can be used in OpenCode chat:
67
+
68
+ | Command | Description |
69
+ | -------------------------- | -------------------------------------------------- |
70
+ | `/pty-open-background-spy` | Open the PTY web server interface in the browser |
71
+ | `/pty-show-server-url` | Show the URL of the running PTY web server instance |
72
+
73
+ ## Web UI
74
+
75
+ This plugin includes a modern React-based web interface for monitoring and interacting with PTY sessions.
76
+
77
+ [![pty-auto Web UI Demo](https://img.youtube.com/vi/wPqmTPnzvVY/0.jpg)](https://youtu.be/wPqmTPnzvVY)
78
+
79
+ If you instruct the coding agent to run something in background, you have to name it "session",
80
+ i.e. "run xy as a background SESSION".
81
+ If you name it "task" or "process" or anything else, the agent will sometimes run it as background subprocess using `&`.
82
+
83
+ ### Starting the Web UI
84
+
85
+ 1. Run opencode with the plugin.
86
+ 2. Run slash command `/pty-open-background-spy`.
87
+
88
+ This will start the background sessions observer cockpit server and launch the browser with web UI.
89
+
90
+ ### Features
91
+
92
+ - **Session List**: View all active PTY sessions with status indicators
93
+ - **Real-time Output**: Live streaming of process output via WebSocket
94
+ - **Interactive Input**: Send commands and input to running processes
95
+ - **Session Management**: Kill sessions directly from the UI
96
+ - **Connection Status**: Visual indicator of WebSocket connection status
97
+
98
+ ### REST API
99
+
100
+ The web server provides a REST API for session management:
101
+
102
+ | Method | Endpoint | Description |
103
+ | -------- | -------------------------------- | --------------------------------------------------------------------------- |
104
+ | `GET` | `/api/sessions` | List all PTY sessions |
105
+ | `POST` | `/api/sessions` | Create a new PTY session |
106
+ | `GET` | `/api/sessions/:id` | Get session details |
107
+ | `POST` | `/api/sessions/:id/input` | Send input to a session |
108
+ | `DELETE` | `/api/sessions/:id` | Kill a session (without cleanup) |
109
+ | `DELETE` | `/api/sessions/:id/cleanup` | Kill and cleanup a session |
110
+ | `GET` | `/api/sessions/:id/buffer/plain` | Get session output buffer (returns `{ plain: string, byteLength: number }`) |
111
+ | `GET` | `/api/sessions/:id/buffer/raw` | Get session output buffer (raw data) |
112
+ | `DELETE` | `/api/sessions` | Clear all sessions |
113
+ | `GET` | `/health` | Server health check with metrics |
114
+
115
+ #### Session Creation
116
+
117
+ ```bash
118
+ curl -X POST http://localhost:[PORT]/api/sessions \
119
+ -H "Content-Type: application/json" \
120
+ -d '{
121
+ "command": "bash",
122
+ "args": ["-c", "echo hello && sleep 10"],
123
+ "description": "Test session",
124
+ "timeoutSeconds": 5
125
+ }'
126
+ ```
127
+
128
+ Replace `[PORT]` with the actual port number shown in the server console output.
129
+
130
+ #### WebSocket Streaming
131
+
132
+ Connect to `/ws` for real-time updates:
133
+
134
+ ```javascript
135
+ const ws = new WebSocket('ws://localhost:[PORT]/ws')
136
+
137
+ ws.onmessage = (event) => {
138
+ const data = JSON.parse(event.data)
139
+ if (data.type === 'raw_data') {
140
+ console.log('New output:', data.rawData)
141
+ } else if (data.type === 'session_list') {
142
+ console.log('Session list:', data.sessions)
143
+ }
144
+ }
145
+ ```
146
+
147
+ Replace `[PORT]` with the actual port number shown in the browser when running the slash command output.
148
+
149
+ ### Development
150
+
151
+ Future implementation will include:
152
+
153
+ #### App
154
+
155
+ - A startup script that runs the server (in the same process).
156
+ - The startup script will run `bun vite` with an environment variable set to the server URL
157
+ - The client will use this environment variable for WebSocket and HTTP requests
158
+
159
+ This will ease the development on the client.
160
+
161
+ ## Usage Examples
162
+
163
+ ### Start a dev server
164
+
165
+ ```
166
+ pty_spawn: command="npm", args=["run", "dev"], title="Dev Server", timeoutSeconds=600
167
+ → Returns: pty_a1b2c3d4
168
+ ```
169
+
170
+ ### Start a timed session
171
+
172
+ ```
173
+ pty_spawn: command="npm", args=["run", "dev"], title="Dev Server", timeoutSeconds=600
174
+ → Returns: pty_a1b2c3d4
175
+ ```
176
+
177
+ ### Check server output
178
+
179
+ ```
180
+ pty_read: id="pty_a1b2c3d4", limit=50
181
+ → Shows last 50 lines of output
182
+ ```
183
+
184
+ ### Filter for errors
185
+
186
+ ```
187
+ pty_read: id="pty_a1b2c3d4", pattern="error|ERROR", ignoreCase=true
188
+ → Shows only lines matching the pattern
189
+ ```
190
+
191
+ ### Send Ctrl+C to stop
192
+
193
+ ```
194
+ pty_write: id="pty_a1b2c3d4", data="\x03"
195
+ → Sends interrupt signal
196
+ ```
197
+
198
+ ### Kill and cleanup
199
+
200
+ ```
201
+ pty_kill: id="pty_a1b2c3d4", cleanup=true
202
+ → Terminates process and frees buffer
203
+ ```
204
+
205
+ ### Run with exit notification
206
+
207
+ ```
208
+ pty_spawn: command="npm", args=["run", "build"], title="Build", notifyOnExit=true, timeoutSeconds=900
209
+ → Returns: pty_a1b2c3d4
210
+ ```
211
+
212
+ The AI agent will receive a notification when the build completes:
213
+
214
+ ```xml
215
+ <pty_exited>
216
+ ID: pty_a1b2c3d4
217
+ Title: Build
218
+ Exit Code: 0
219
+ Output Lines: 42
220
+ Last Line: Build completed successfully.
221
+ </pty_exited>
222
+
223
+ Use pty_read to check the full output.
224
+ ```
225
+
226
+ This eliminates the need for polling—perfect for long-running processes like builds, tests, or deployment scripts. If the process fails (non-zero exit code), the notification will suggest using `pty_read` with the `pattern` parameter to search for errors.
227
+
228
+ ## Configuration
229
+
230
+ ### Environment Variables
231
+
232
+ | Variable | Default | Description |
233
+ | ---------------------- | ---------- | -------------------------------------------------- |
234
+ | `PTY_MAX_BUFFER_LINES` | `50000` | Maximum lines to keep in output buffer per session |
235
+ | `PTY_WEB_HOSTNAME` | `::1` | Hostname for the web server to bind to (IPv6 loopback by default) |
236
+ | `PTY_WEB_PORT` | `0` (random) | Port for the web server (0 = random port) |
237
+
238
+ ### Permissions
239
+
240
+ `pty-auto` deliberately does **not** use OpenCode's `permission.bash` setting to approve or deny PTY commands. This lets you disable the built-in `bash` tool globally while still allowing agents to use `pty_spawn` as a separate tool call.
241
+
242
+ Every `pty_spawn` call must include a positive integer `timeoutSeconds` value. The timeout is the primary safety boundary for long-running and interactive work.
243
+
244
+ `permission.external_directory` is still respected for PTY working directories outside the current project. Set it to `deny` if you want to block out-of-project PTY sessions.
245
+
246
+ ```json
247
+ {
248
+ "$schema": "https://opencode.ai/config.json",
249
+ "plugin": ["pty-auto"],
250
+ "permission": {
251
+ "bash": "deny",
252
+ "external_directory": "deny"
253
+ }
254
+ }
255
+ ```
256
+
257
+ ## How It Works
258
+
259
+ 1. **Spawn**: Creates a PTY using [bun-pty](https://github.com/nicksrandall/bun-pty), runs command in background with a required positive `timeoutSeconds` budget
260
+ 2. **Buffer**: Output is captured into a rolling line buffer (ring buffer)
261
+ 3. **Read**: Agent can read buffer anytime with offset/limit pagination
262
+ 4. **Filter**: Optional regex pattern filters lines before pagination
263
+ 5. **Write**: Agent can send any input including escape sequences
264
+ 6. **Lifecycle**: Sessions track status (running/exited/killed), persist until cleanup
265
+ 7. **Notify**: When `notifyOnExit` is true, sends a message to the session when the process exits
266
+ 8. **Web UI**: React frontend connects via WebSocket for real-time updates
267
+
268
+ ## Session Lifecycle
269
+
270
+ ```
271
+ spawn → running → [exited | killed]
272
+
273
+ (stays in list until cleanup=true)
274
+ ```
275
+
276
+ Sessions remain in the list after exit so the agent can:
277
+
278
+ - Read final output
279
+ - Check exit code
280
+ - Compare logs between runs
281
+
282
+ Use `pty_kill` with `cleanup=true` to remove completely.
283
+
284
+ ## Local Development
285
+
286
+ ```bash
287
+ git clone https://github.com/zjm54321/pty-auto.git
288
+ cd pty-auto
289
+ bun ci # install packages from bun.lock
290
+ bun lint # Runs Biome linting checks
291
+ bun format # Runs Biome formatting checks
292
+ bun typecheck # Runs TypeScript type checking
293
+ bun build:dev # Build the React app for development
294
+ bun unittest # Runs the unit tests
295
+ bun test:e2e # Runs the e2e tests
296
+ ```
297
+
298
+ To load a local checkout in OpenCode:
299
+
300
+ ```json
301
+ {
302
+ "$schema": "https://opencode.ai/config.json",
303
+ "plugin": ["file:///absolute/path/to/pty-auto/index.ts"]
304
+ }
305
+ ```
306
+
307
+ ## Diagrams
308
+
309
+ ### Sequence
310
+
311
+ #### Use Case 1 – Opening the PTY Monitor Web UI
312
+
313
+ ```mermaid
314
+ sequenceDiagram
315
+ participant User as Human User
316
+ participant Chat as OpenCode Chat
317
+ participant Core as OpenCode Core
318
+ participant Plugin as PTY Plugin
319
+ participant Manager as PTY Manager (in Plugin)
320
+ participant WS as WebSocket Server (in Plugin)
321
+ participant Browser as Web Browser
322
+
323
+ Note over Plugin,WS: Plugin starts/owns the WS server and Manager
324
+
325
+ User->>Chat: Types /pty-open-background-spy
326
+ Chat->>Core: Slash command received
327
+ Core->>Plugin: Dispatches to registered command handler
328
+ Plugin->>Browser: open(server.url.origin)
329
+ activate Browser
330
+ Browser->>WS: Connects → ws://.../ws
331
+ WS->>Manager: Queries for current sessions (manager.list())
332
+ Manager-->>WS: Returns session data
333
+ WS-->>Browser: Sends session_list + subscribes to updates
334
+ Browser-->>User: PTY monitor UI appears (sessions + terminals)
335
+ ```
336
+
337
+ #### Use Case 2 – Starting a Long-Running Background Process
338
+
339
+ ```mermaid
340
+ sequenceDiagram
341
+ participant User
342
+ participant Chat as OpenCode Chat
343
+ participant Agent as AI Agent
344
+ participant Plugin as PTY Plugin
345
+ participant Manager as PTY Manager
346
+ participant PTY as bun-pty Process
347
+ participant WS as WebSocket Server
348
+ participant UI as PTY Web UI (optional)
349
+
350
+ User->>Chat: "start vite dev server in background"
351
+ Chat->>Agent: User message
352
+ Agent->>Plugin: Calls pty_spawn(command="vite", args=["dev"], ...)
353
+ Plugin->>Manager: spawn(options)
354
+ Manager->>PTY: Launches real process
355
+ activate PTY
356
+ PTY-->>Manager: stdout/stderr chunks
357
+ Manager->>Manager: Appends to RingBuffer
358
+ Manager->>WS: Publishes raw_data + session_update
359
+ alt UI already open
360
+ WS-->>UI: Real-time terminal output
361
+ UI-->>User: Live xterm.js view
362
+ end
363
+ Plugin-->>Agent: Returns session info
364
+ Agent-->>Chat: "Dev server started (ID: pty_abc123)"
365
+ Chat-->>User: Confirmation message
366
+ ```
367
+
368
+ #### Use Case 3 – Sending Interactive Input to a Running Session
369
+
370
+ ```mermaid
371
+ sequenceDiagram
372
+ participant User
373
+ participant UI as PTY Web UI
374
+ participant WS as WebSocket Server
375
+ participant Manager as PTY Manager
376
+ participant PTY as bun-pty Process
377
+
378
+ %% Variant A: Human typing in browser (most common)
379
+ User->>UI: Types "rs<Enter>" or pastes text
380
+ UI->>WS: Sends {type:"input", sessionId, data:"rs\n"}
381
+ WS->>Manager: write(sessionId, data)
382
+ Manager->>PTY: process.write(data)
383
+ PTY-->>Manager: New output (restart message, etc.)
384
+ Manager->>WS: Publishes raw_data
385
+ WS-->>UI: Updates xterm.js live
386
+
387
+ %% Variant B: AI sending input
388
+ Note over User,UI: Alternative path – AI controlled
389
+ Agent->>Plugin: pty_write(id, "\x03") // e.g. Ctrl+C
390
+ Plugin->>Manager: write(id, data)
391
+ Manager->>PTY: process.write("\x03")
392
+ ```
393
+
394
+ #### Use Case 4 – Reading Output / Logs On Demand
395
+
396
+ ```mermaid
397
+ sequenceDiagram
398
+ participant User
399
+ participant Chat
400
+ participant Agent as AI Agent
401
+ participant Plugin as PTY Plugin
402
+ participant Manager as PTY Manager
403
+ participant Buffer as RingBuffer
404
+
405
+ User->>Chat: "show me the last 200 lines of the dev server"
406
+ Chat->>Agent: User question
407
+ Agent->>Plugin: pty_read(id, offset?, limit=200, pattern?)
408
+ Plugin->>Manager: read / search request
409
+ Manager->>Buffer: read(offset, limit) or search(pattern)
410
+ Buffer-->>Manager: Matching / paginated lines
411
+ Manager-->>Plugin: Raw or formatted lines
412
+ Plugin-->>Agent: Text response
413
+ Agent-->>Chat: "Here are the logs:\n\n1 | [vite] ... \n..."
414
+ Chat-->>User: Logs displayed in chat
415
+ ```
416
+
417
+ #### Use Case 5A – Killing / Cleaning Up a Session via Web UI
418
+
419
+ ```mermaid
420
+ sequenceDiagram
421
+ participant User
422
+ participant UI as PTY Web UI
423
+ participant HTTP as HTTP Server
424
+ participant Manager as PTY Manager
425
+ participant PTY as bun-pty Process
426
+ participant WS as WebSocket Server
427
+
428
+ Note over PTY: Assuming PTY is active (running process)<br>notifyOnExit = false (no chat notification,<br>but WS/UI always gets status updates)
429
+
430
+ activate PTY
431
+
432
+ User->>UI: Clicks "Kill" / "×" on session
433
+ UI->>HTTP: DELETE /api/sessions/:id (or /cleanup)
434
+ HTTP->>Manager: kill(id, cleanup?)
435
+ Manager->>PTY: Sends SIGTERM (if running)
436
+ PTY-->>Manager: onExit event (code, signal)
437
+ deactivate PTY
438
+ Manager->>WS: Publishes session_update (status: killed/exited)
439
+ WS-->>UI: UI updates → shows "exited" or removes entry
440
+ ```
441
+
442
+ #### Use Case 5B – Killing / Cleaning Up a Session via Agent
443
+
444
+ ```mermaid
445
+ sequenceDiagram
446
+ participant User
447
+ participant Chat as OpenCode Chat
448
+ participant Agent as AI Agent
449
+ participant Plugin as PTY Plugin
450
+ participant Manager as PTY Manager
451
+ participant PTY as bun-pty Process
452
+ participant WS as WebSocket Server
453
+ participant UI as PTY Web UI (optional)
454
+
455
+ Note over PTY: Assuming PTY is active (running process)<br>notifyOnExit = false (no chat notification,<br>but WS/UI always gets status updates)
456
+
457
+ activate PTY
458
+
459
+ User->>Chat: "kill the dev server"
460
+ Chat->>Agent: User request
461
+ Agent->>Plugin: pty_kill(id, cleanup=true)
462
+ Plugin->>Manager: kill(id, true)
463
+ Manager->>PTY: SIGTERM + remove from list (if cleanup)
464
+ PTY-->>Manager: onExit event (code, signal)
465
+ deactivate PTY
466
+ Manager->>WS: Broadcast session_update (status: killed/exited)
467
+ alt UI open
468
+ WS-->>UI: UI updates → shows "exited" or removes entry
469
+ end
470
+ Plugin-->>Agent: Success response
471
+ Agent-->>Chat: "Session killed"
472
+ Chat-->>User: Confirmation in chat
473
+ ```
474
+
475
+ #### Use Case 6 – Automatic Exit Notification
476
+
477
+ ```mermaid
478
+ sequenceDiagram
479
+ participant PTY as bun-pty Process
480
+ participant Manager as PTY Manager
481
+ participant Plugin as PTY Plugin
482
+ participant Chat as OpenCode Chat
483
+ participant Agent as AI Agent
484
+ participant User
485
+
486
+ %%{init: {'sequence': {'messageAlign': 'left'}}}%%
487
+
488
+ activate PTY
489
+ Note over PTY: Long-running process (dev server, tests, etc.)
490
+ PTY-->>Manager: Process exits → exitCode
491
+ deactivate PTY
492
+ alt notifyOnExit was true when spawned
493
+ Manager->>Plugin: Triggers exit notification
494
+ Plugin->>Chat: Sends formatted message via SDK<br><pty_exited><br>ID: pty_abc123<br>Exit: 0<br>Lines: 342<br>Last: Server running at http://localhost:5173<br></pty_exited>
495
+ Chat-->>User: Notification appears in chat
496
+ Chat->>Agent: Triggers agent with exit message
497
+ end
498
+ Manager->>WS: Publishes final session_update (status: exited)
499
+ alt UI open
500
+ WS-->>UI: UI shows red "exited" badge / stops live output
501
+ end
502
+ ```
503
+
504
+ ## License
505
+
506
+ MIT
507
+
508
+ ## Contributing
509
+
510
+ Contributions are welcome! Please open an issue or submit a PR.
511
+
512
+ ## Credits
513
+
514
+ - [OpenCode](https://opencode.ai) - The AI coding assistant this plugin extends
515
+ - [bun-pty](https://github.com/nicksrandall/bun-pty) - Cross-platform PTY for Bun
@@ -0,0 +1,3 @@
1
+ export { PTYPlugin } from './src/plugin.ts';
2
+ export { PTYPlugin as server } from './src/plugin.ts';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAC3C,OAAO,EAAE,SAAS,IAAI,MAAM,EAAE,MAAM,iBAAiB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { PTYPlugin } from "./src/plugin.js";
2
+ export { PTYPlugin as server } from "./src/plugin.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAC3C,OAAO,EAAE,SAAS,IAAI,MAAM,EAAE,MAAM,iBAAiB,CAAA"}
@@ -0,0 +1,5 @@
1
+ export declare const DEFAULT_TERMINAL_COLS = 120;
2
+ export declare const DEFAULT_TERMINAL_ROWS = 40;
3
+ export declare const NOTIFICATION_LINE_TRUNCATE = 250;
4
+ export declare const NOTIFICATION_TITLE_TRUNCATE = 64;
5
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/plugin/constants.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,qBAAqB,MAAM,CAAA;AACxC,eAAO,MAAM,qBAAqB,KAAK,CAAA;AACvC,eAAO,MAAM,0BAA0B,MAAM,CAAA;AAC7C,eAAO,MAAM,2BAA2B,KAAK,CAAA"}
@@ -0,0 +1,7 @@
1
+ // Shared constants for the PTY plugin
2
+ // PTY terminal and UI constants
3
+ export const DEFAULT_TERMINAL_COLS = 120;
4
+ export const DEFAULT_TERMINAL_ROWS = 40;
5
+ export const NOTIFICATION_LINE_TRUNCATE = 250;
6
+ export const NOTIFICATION_TITLE_TRUNCATE = 64;
7
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../../../src/plugin/constants.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC,gCAAgC;AAChC,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAG,CAAA;AACxC,MAAM,CAAC,MAAM,qBAAqB,GAAG,EAAE,CAAA;AACvC,MAAM,CAAC,MAAM,0BAA0B,GAAG,GAAG,CAAA;AAC7C,MAAM,CAAC,MAAM,2BAA2B,GAAG,EAAE,CAAA"}
@@ -0,0 +1,19 @@
1
+ export interface SearchMatch {
2
+ lineNumber: number;
3
+ text: string;
4
+ }
5
+ export declare class RingBuffer {
6
+ private buffer;
7
+ private maxSize;
8
+ constructor(maxSize?: number);
9
+ append(data: string): void;
10
+ private splitBufferLines;
11
+ read(offset?: number, limit?: number): string[];
12
+ readRaw(): string;
13
+ search(pattern: RegExp): SearchMatch[];
14
+ get length(): number;
15
+ get byteLength(): number;
16
+ flush(): void;
17
+ clear(): void;
18
+ }
19
+ //# sourceMappingURL=buffer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buffer.d.ts","sourceRoot":"","sources":["../../../../src/plugin/pty/buffer.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,MAAM,CAAA;CACb;AAED,qBAAa,UAAU;IACrB,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,OAAO,CAAQ;gBAEX,OAAO,GAAE,MAAgC;IAIrD,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAO1B,OAAO,CAAC,gBAAgB;IASxB,IAAI,CAAC,MAAM,GAAE,MAAU,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE;IAQlD,OAAO,IAAI,MAAM;IAIjB,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,EAAE;IAatC,IAAI,MAAM,IAAI,MAAM,CAInB;IAED,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED,KAAK,IAAI,IAAI;IAIb,KAAK,IAAI,IAAI;CAGd"}
@@ -0,0 +1,61 @@
1
+ // Default buffer size in characters (approximately 1MB)
2
+ const DEFAULT_MAX_BUFFER_SIZE = parseInt(process.env.PTY_MAX_BUFFER_SIZE || '1000000', 10);
3
+ export class RingBuffer {
4
+ buffer = '';
5
+ maxSize;
6
+ constructor(maxSize = DEFAULT_MAX_BUFFER_SIZE) {
7
+ this.maxSize = maxSize;
8
+ }
9
+ append(data) {
10
+ this.buffer += data;
11
+ if (this.buffer.length > this.maxSize) {
12
+ this.buffer = this.buffer.slice(-this.maxSize);
13
+ }
14
+ }
15
+ splitBufferLines() {
16
+ const lines = this.buffer.split('\n');
17
+ // Remove empty string at end if buffer doesn't end with newline
18
+ if (lines.length && lines[lines.length - 1] === '') {
19
+ lines.pop();
20
+ }
21
+ return lines;
22
+ }
23
+ read(offset = 0, limit) {
24
+ if (this.buffer === '')
25
+ return [];
26
+ const lines = this.splitBufferLines();
27
+ const start = Math.max(0, offset);
28
+ const end = limit !== undefined ? start + limit : lines.length;
29
+ return lines.slice(start, end);
30
+ }
31
+ readRaw() {
32
+ return this.buffer;
33
+ }
34
+ search(pattern) {
35
+ const matches = [];
36
+ const lines = this.splitBufferLines();
37
+ for (let i = 0; i < lines.length; i++) {
38
+ const line = lines[i];
39
+ if (line && pattern.test(line)) {
40
+ matches.push({ lineNumber: i + 1, text: line });
41
+ }
42
+ }
43
+ return matches;
44
+ }
45
+ get length() {
46
+ if (this.buffer === '')
47
+ return 0;
48
+ const lines = this.splitBufferLines();
49
+ return lines.length;
50
+ }
51
+ get byteLength() {
52
+ return this.buffer.length;
53
+ }
54
+ flush() {
55
+ // No-op in new implementation
56
+ }
57
+ clear() {
58
+ this.buffer = '';
59
+ }
60
+ }
61
+ //# sourceMappingURL=buffer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buffer.js","sourceRoot":"","sources":["../../../../src/plugin/pty/buffer.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,MAAM,uBAAuB,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,SAAS,EAAE,EAAE,CAAC,CAAA;AAO1F,MAAM,OAAO,UAAU;IACb,MAAM,GAAW,EAAE,CAAA;IACnB,OAAO,CAAQ;IAEvB,YAAY,UAAkB,uBAAuB;QACnD,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;IACxB,CAAC;IAED,MAAM,CAAC,IAAY;QACjB,IAAI,CAAC,MAAM,IAAI,IAAI,CAAA;QACnB,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;YACtC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QAChD,CAAC;IACH,CAAC;IAEO,gBAAgB;QACtB,MAAM,KAAK,GAAa,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QAC/C,gEAAgE;QAChE,IAAI,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC;YACnD,KAAK,CAAC,GAAG,EAAE,CAAA;QACb,CAAC;QACD,OAAO,KAAK,CAAA;IACd,CAAC;IAED,IAAI,CAAC,SAAiB,CAAC,EAAE,KAAc;QACrC,IAAI,IAAI,CAAC,MAAM,KAAK,EAAE;YAAE,OAAO,EAAE,CAAA;QACjC,MAAM,KAAK,GAAa,IAAI,CAAC,gBAAgB,EAAE,CAAA;QAC/C,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,CAAA;QACjC,MAAM,GAAG,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAA;QAC9D,OAAO,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;IAChC,CAAC;IAED,OAAO;QACL,OAAO,IAAI,CAAC,MAAM,CAAA;IACpB,CAAC;IAED,MAAM,CAAC,OAAe;QACpB,MAAM,OAAO,GAAkB,EAAE,CAAA;QACjC,MAAM,KAAK,GAAa,IAAI,CAAC,gBAAgB,EAAE,CAAA;QAE/C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACtC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;YACrB,IAAI,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC/B,OAAO,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;YACjD,CAAC;QACH,CAAC;QACD,OAAO,OAAO,CAAA;IAChB,CAAC;IAED,IAAI,MAAM;QACR,IAAI,IAAI,CAAC,MAAM,KAAK,EAAE;YAAE,OAAO,CAAC,CAAA;QAChC,MAAM,KAAK,GAAG,IAAI,CAAC,gBAAgB,EAAE,CAAA;QACrC,OAAO,KAAK,CAAC,MAAM,CAAA;IACrB,CAAC;IAED,IAAI,UAAU;QACZ,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAA;IAC3B,CAAC;IAED,KAAK;QACH,8BAA8B;IAChC,CAAC;IAED,KAAK;QACH,IAAI,CAAC,MAAM,GAAG,EAAE,CAAA;IAClB,CAAC;CACF"}
@@ -0,0 +1,4 @@
1
+ import type { PTYSessionInfo } from './types.ts';
2
+ export declare function formatSessionInfo(session: PTYSessionInfo): string[];
3
+ export declare function formatLine(line: string, lineNum: number, maxLength?: number): string;
4
+ //# sourceMappingURL=formatters.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formatters.d.ts","sourceRoot":"","sources":["../../../../src/plugin/pty/formatters.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAEhD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,EAAE,CAgBnE;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,GAAE,MAAa,GAAG,MAAM,CAI1F"}