terminal-pilot 0.0.2 → 0.0.4
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.
- package/README.md +15 -142
- package/dist/terminal-buffer.d.ts +38 -0
- package/dist/terminal-buffer.js +445 -0
- package/dist/terminal-pilot.d.ts +1 -0
- package/dist/terminal-pilot.js +4 -4
- package/dist/terminal-session.d.ts +3 -0
- package/dist/terminal-session.js +20 -8
- package/package.json +4 -14
- package/dist/mcp-server.d.ts +0 -11
- package/dist/mcp-server.js +0 -40
- package/dist/mcp-tools.d.ts +0 -46
- package/dist/mcp-tools.js +0 -169
package/README.md
CHANGED
|
@@ -1,38 +1,23 @@
|
|
|
1
|
-
#
|
|
1
|
+
# terminal-pilot
|
|
2
2
|
|
|
3
|
-
`terminal-pilot` is a Playwright-like SDK
|
|
3
|
+
`terminal-pilot` is a Playwright-like SDK for automating interactive CLI apps through a real pseudoterminal (PTY).
|
|
4
4
|
|
|
5
5
|
Use it when plain stdio is not enough: menus, prompts, arrow-key navigation, confirmations, and terminal redraws such as `poe-code configure`.
|
|
6
6
|
|
|
7
7
|
For design rationale and scope, see `docs/plans/terminal-pilot.md`.
|
|
8
8
|
|
|
9
|
+
For the MCP server, see [terminal-pilot-mcp](../terminal-pilot-mcp).
|
|
10
|
+
|
|
9
11
|
## What it includes
|
|
10
12
|
|
|
11
13
|
- **SDK:** `TerminalPilot` → `TerminalSession` → `TerminalScreen`
|
|
12
|
-
- **MCP server:** 10 tools for AI-driven CLI automation
|
|
13
14
|
- **Real PTY execution:** works with interactive CLIs that expect a terminal
|
|
14
15
|
- **Headless by default:** optional `observe: true` mirrors PTY output for debugging
|
|
15
16
|
|
|
16
17
|
## Entry points
|
|
17
18
|
|
|
18
19
|
```ts
|
|
19
|
-
import { TerminalPilot } from "
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
```ts
|
|
23
|
-
import { createTerminalPilotMcpServer, main } from "@poe-code/terminal-pilot/mcp";
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
CLI bin:
|
|
27
|
-
|
|
28
|
-
```sh
|
|
29
|
-
terminal-pilot-mcp
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
Development entry point in this repo:
|
|
33
|
-
|
|
34
|
-
```sh
|
|
35
|
-
npx tsx packages/terminal-pilot/src/mcp-server.ts
|
|
20
|
+
import { TerminalPilot } from "terminal-pilot";
|
|
36
21
|
```
|
|
37
22
|
|
|
38
23
|
## SDK API
|
|
@@ -42,7 +27,7 @@ npx tsx packages/terminal-pilot/src/mcp-server.ts
|
|
|
42
27
|
Creates and tracks active terminal sessions.
|
|
43
28
|
|
|
44
29
|
```ts
|
|
45
|
-
import { TerminalPilot } from "
|
|
30
|
+
import { TerminalPilot } from "terminal-pilot";
|
|
46
31
|
|
|
47
32
|
const pilot = await TerminalPilot.launch();
|
|
48
33
|
|
|
@@ -76,8 +61,9 @@ type NewSessionOptions = {
|
|
|
76
61
|
class TerminalPilot {
|
|
77
62
|
static launch(): Promise<TerminalPilot>;
|
|
78
63
|
newSession(options: NewSessionOptions): Promise<TerminalSession>;
|
|
79
|
-
getSession(id: string): TerminalSession; // throws if
|
|
80
|
-
|
|
64
|
+
getSession(id: string): TerminalSession; // throws if not in map; works for exited sessions until deleteSession
|
|
65
|
+
deleteSession(id: string): void; // explicit removal from session map
|
|
66
|
+
sessions(): TerminalSession[]; // running sessions only (exitCode === null)
|
|
81
67
|
close(): Promise<void>;
|
|
82
68
|
}
|
|
83
69
|
```
|
|
@@ -137,12 +123,13 @@ class TerminalSession {
|
|
|
137
123
|
readonly pid: number;
|
|
138
124
|
exitCode: number | null;
|
|
139
125
|
|
|
140
|
-
type(text: string): Promise<void>; // character-by-character
|
|
141
|
-
fill(text: string): Promise<void>; // bulk write
|
|
126
|
+
type(text: string): Promise<void>; // character-by-character with delay
|
|
127
|
+
fill(text: string): Promise<void>; // bulk write (\n → \r)
|
|
142
128
|
press(key: TerminalKey): Promise<void>;
|
|
143
129
|
send(raw: string): Promise<void>; // raw bytes / escape sequences
|
|
144
130
|
signal(signal: string): Promise<void>;
|
|
145
131
|
waitFor(pattern: string | RegExp, options?: WaitForOptions): Promise<string>;
|
|
132
|
+
waitForExit(options?: { timeout?: number }): Promise<number>; // throws on timeout
|
|
146
133
|
waitForQuiet(ms: number): Promise<void>;
|
|
147
134
|
screen(): Promise<TerminalScreen>;
|
|
148
135
|
history(options?: HistoryOptions): Promise<string[]>;
|
|
@@ -188,125 +175,11 @@ class TerminalScreen {
|
|
|
188
175
|
}
|
|
189
176
|
```
|
|
190
177
|
|
|
191
|
-
## MCP server
|
|
192
|
-
|
|
193
|
-
The MCP server holds one in-memory `TerminalPilot` instance and exposes terminal automation over stdio.
|
|
194
|
-
|
|
195
|
-
### Run it
|
|
196
|
-
|
|
197
|
-
Development:
|
|
198
|
-
|
|
199
|
-
```sh
|
|
200
|
-
npx tsx packages/terminal-pilot/src/mcp-server.ts
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
Built / installed package:
|
|
204
|
-
|
|
205
|
-
```sh
|
|
206
|
-
terminal-pilot-mcp
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
Programmatic:
|
|
210
|
-
|
|
211
|
-
```ts
|
|
212
|
-
import { main } from "@poe-code/terminal-pilot/mcp";
|
|
213
|
-
|
|
214
|
-
await main();
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
### Connect to an MCP client
|
|
218
|
-
|
|
219
|
-
Install the package globally from a local build:
|
|
220
|
-
|
|
221
|
-
```sh
|
|
222
|
-
cd packages/terminal-pilot
|
|
223
|
-
npm pack --pack-destination /tmp && npm install -g /tmp/poe-code-terminal-pilot-*.tgz && rm /tmp/poe-code-terminal-pilot-*.tgz
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
This makes the `terminal-pilot-mcp` bin available globally.
|
|
227
|
-
|
|
228
|
-
**Claude Code** (`~/.claude.json` or project `.mcp.json`):
|
|
229
|
-
|
|
230
|
-
```json
|
|
231
|
-
{
|
|
232
|
-
"mcpServers": {
|
|
233
|
-
"terminal-pilot": {
|
|
234
|
-
"command": "terminal-pilot-mcp"
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
**Claude Desktop** (`claude_desktop_config.json`):
|
|
241
|
-
|
|
242
|
-
```json
|
|
243
|
-
{
|
|
244
|
-
"mcpServers": {
|
|
245
|
-
"terminal-pilot": {
|
|
246
|
-
"command": "terminal-pilot-mcp"
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
**Cursor** (`.cursor/mcp.json`):
|
|
253
|
-
|
|
254
|
-
```json
|
|
255
|
-
{
|
|
256
|
-
"mcpServers": {
|
|
257
|
-
"terminal-pilot": {
|
|
258
|
-
"command": "terminal-pilot-mcp"
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
### Tools
|
|
265
|
-
|
|
266
|
-
`void` means the tool returns no payload.
|
|
267
|
-
|
|
268
|
-
| Tool | Input | Output |
|
|
269
|
-
| --- | --- | --- |
|
|
270
|
-
| `terminal_create_session` | `{ command: string, args?: string[], cwd?: string, cols?: number, rows?: number, observe?: boolean }` | `{ sessionId: string, pid: number }` |
|
|
271
|
-
| `terminal_type` | `{ sessionId: string, text: string }` | `void` |
|
|
272
|
-
| `terminal_press_key` | `{ sessionId: string, key: TerminalKey }` | `void` |
|
|
273
|
-
| `terminal_send_signal` | `{ sessionId: string, signal: string }` | `void` |
|
|
274
|
-
| `terminal_wait_for` | `{ sessionId: string, pattern: string, timeout?: number }` | `{ matched: true, output: string }` |
|
|
275
|
-
| `terminal_read_screen` | `{ sessionId: string }` | `{ lines: string[], cursor: { row: number, col: number }, size: { rows: number, cols: number } }` |
|
|
276
|
-
| `terminal_read_history` | `{ sessionId: string, last?: number }` | `{ lines: string[] }` |
|
|
277
|
-
| `terminal_resize` | `{ sessionId: string, cols: number, rows: number }` | `void` |
|
|
278
|
-
| `terminal_close_session` | `{ sessionId: string }` | `{ exitCode: number }` |
|
|
279
|
-
| `terminal_list_sessions` | `{}` | `{ sessions: Array<{ id: string, command: string, pid: number }> }` |
|
|
280
|
-
|
|
281
|
-
Practical notes:
|
|
282
|
-
|
|
283
|
-
- `terminal_wait_for.pattern` is compiled as a JavaScript `RegExp` on the server.
|
|
284
|
-
- `terminal_type` maps to `session.fill(...)` for bulk text entry.
|
|
285
|
-
- `terminal_read_screen` returns the **current visible screen**, not scrollback.
|
|
286
|
-
- `terminal_read_history` returns ANSI-stripped output since session start.
|
|
287
|
-
- `terminal_list_sessions` returns **active** sessions only.
|
|
288
|
-
- `observe: true` mirrors PTY output to `stderr`, which is useful when debugging MCP-driven runs.
|
|
289
|
-
|
|
290
|
-
Minimal MCP flow:
|
|
291
|
-
|
|
292
|
-
```json
|
|
293
|
-
{"tool":"terminal_create_session","arguments":{"command":"poe-code","args":["configure"]}}
|
|
294
|
-
{"tool":"terminal_wait_for","arguments":{"sessionId":"<id>","pattern":"Pick an agent to configure:"}}
|
|
295
|
-
{"tool":"terminal_press_key","arguments":{"sessionId":"<id>","key":"Enter"}}
|
|
296
|
-
{"tool":"terminal_read_screen","arguments":{"sessionId":"<id>"}}
|
|
297
|
-
{"tool":"terminal_close_session","arguments":{"sessionId":"<id>"}}
|
|
298
|
-
```
|
|
299
|
-
|
|
300
178
|
## Environment variables
|
|
301
179
|
|
|
302
180
|
There are **no terminal-pilot-specific environment variables**.
|
|
303
181
|
|
|
304
|
-
Runtime environment is controlled per session
|
|
305
|
-
|
|
306
|
-
- SDK: `newSession({ env })`
|
|
307
|
-
- MCP server: spawned commands inherit the MCP server process environment unless you override it in your command wrapper
|
|
308
|
-
|
|
309
|
-
There are also **no package-level config files or config options** beyond the per-session options above.
|
|
182
|
+
Runtime environment is controlled per session via `newSession({ env })`. There are no package-level config files or config options beyond the per-session options.
|
|
310
183
|
|
|
311
184
|
## Testing
|
|
312
185
|
|
|
@@ -340,7 +213,7 @@ Example fixture-based test:
|
|
|
340
213
|
|
|
341
214
|
```ts
|
|
342
215
|
import path from "node:path";
|
|
343
|
-
import { TerminalPilot } from "
|
|
216
|
+
import { TerminalPilot } from "terminal-pilot";
|
|
344
217
|
|
|
345
218
|
const pilot = await TerminalPilot.launch();
|
|
346
219
|
const tsxPath = path.join(process.cwd(), "node_modules", ".bin", "tsx");
|
|
@@ -371,7 +244,7 @@ Use a temporary home directory so you do not touch your real config while testin
|
|
|
371
244
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
372
245
|
import os from "node:os";
|
|
373
246
|
import path from "node:path";
|
|
374
|
-
import { TerminalPilot } from "
|
|
247
|
+
import { TerminalPilot } from "terminal-pilot";
|
|
375
248
|
|
|
376
249
|
const tmpHome = mkdtempSync(path.join(os.tmpdir(), "poe-configure-test-"));
|
|
377
250
|
const pilot = await TerminalPilot.launch();
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
type Cell = [number, string] | null;
|
|
2
|
+
type Row = Cell[];
|
|
3
|
+
export declare class TerminalBuffer {
|
|
4
|
+
private _cols;
|
|
5
|
+
private _rows;
|
|
6
|
+
private _screen;
|
|
7
|
+
private _cursorX;
|
|
8
|
+
private _cursorY;
|
|
9
|
+
private _savedCursor;
|
|
10
|
+
private _scrollTop;
|
|
11
|
+
private _scrollBottom;
|
|
12
|
+
private _state;
|
|
13
|
+
private _csiParams;
|
|
14
|
+
private _csiPrivate;
|
|
15
|
+
readonly displayBuffer: {
|
|
16
|
+
readonly cursorX: number;
|
|
17
|
+
readonly cursorY: number;
|
|
18
|
+
readonly data: Array<Row | undefined>;
|
|
19
|
+
};
|
|
20
|
+
constructor(cols: number, rows: number);
|
|
21
|
+
write(data: string): void;
|
|
22
|
+
resize(cols: number, rows: number): void;
|
|
23
|
+
private _makeScreen;
|
|
24
|
+
private _makeRow;
|
|
25
|
+
private _clamp;
|
|
26
|
+
private _setChar;
|
|
27
|
+
private _eraseLine;
|
|
28
|
+
private _scrollUp;
|
|
29
|
+
private _scrollDown;
|
|
30
|
+
private _newline;
|
|
31
|
+
private _parseCsiParams;
|
|
32
|
+
private _execCsi;
|
|
33
|
+
private _feed;
|
|
34
|
+
private _feedNormal;
|
|
35
|
+
private _feedEscape;
|
|
36
|
+
private _feedCsi;
|
|
37
|
+
}
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
export class TerminalBuffer {
|
|
2
|
+
_cols;
|
|
3
|
+
_rows;
|
|
4
|
+
_screen;
|
|
5
|
+
_cursorX = 0;
|
|
6
|
+
_cursorY = 0;
|
|
7
|
+
_savedCursor = { x: 0, y: 0 };
|
|
8
|
+
_scrollTop = 0;
|
|
9
|
+
_scrollBottom;
|
|
10
|
+
_state = 0 /* State.Normal */;
|
|
11
|
+
_csiParams = "";
|
|
12
|
+
_csiPrivate = "";
|
|
13
|
+
displayBuffer;
|
|
14
|
+
constructor(cols, rows) {
|
|
15
|
+
this._cols = cols;
|
|
16
|
+
this._rows = rows;
|
|
17
|
+
this._scrollBottom = rows - 1;
|
|
18
|
+
this._screen = this._makeScreen(cols, rows);
|
|
19
|
+
this.displayBuffer = Object.defineProperties({}, {
|
|
20
|
+
cursorX: { get: () => this._cursorX, enumerable: true },
|
|
21
|
+
cursorY: { get: () => this._cursorY, enumerable: true },
|
|
22
|
+
data: { get: () => this._screen, enumerable: true },
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
write(data) {
|
|
26
|
+
for (const ch of data) {
|
|
27
|
+
this._feed(ch);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
resize(cols, rows) {
|
|
31
|
+
// Adjust row count
|
|
32
|
+
while (this._screen.length < rows) {
|
|
33
|
+
this._screen.push(this._makeRow(cols));
|
|
34
|
+
}
|
|
35
|
+
this._screen.length = rows;
|
|
36
|
+
// Adjust col count for each row
|
|
37
|
+
for (let y = 0; y < rows; y++) {
|
|
38
|
+
const row = this._screen[y] ?? this._makeRow(cols);
|
|
39
|
+
while (row.length < cols)
|
|
40
|
+
row.push(null);
|
|
41
|
+
row.length = cols;
|
|
42
|
+
this._screen[y] = row;
|
|
43
|
+
}
|
|
44
|
+
this._cols = cols;
|
|
45
|
+
this._rows = rows;
|
|
46
|
+
this._scrollTop = 0;
|
|
47
|
+
this._scrollBottom = rows - 1;
|
|
48
|
+
this._cursorX = this._clamp(this._cursorX, 0, cols - 1);
|
|
49
|
+
this._cursorY = this._clamp(this._cursorY, 0, rows - 1);
|
|
50
|
+
}
|
|
51
|
+
_makeScreen(cols, rows) {
|
|
52
|
+
return Array.from({ length: rows }, () => this._makeRow(cols));
|
|
53
|
+
}
|
|
54
|
+
_makeRow(cols) {
|
|
55
|
+
return Array(cols).fill(null);
|
|
56
|
+
}
|
|
57
|
+
_clamp(value, min, max) {
|
|
58
|
+
return Math.max(min, Math.min(max, value));
|
|
59
|
+
}
|
|
60
|
+
_setChar(y, x, ch) {
|
|
61
|
+
const row = this._screen[y];
|
|
62
|
+
if (row && x >= 0 && x < this._cols) {
|
|
63
|
+
row[x] = [ch.charCodeAt(0), ch];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
_eraseLine(y, fromX, toX) {
|
|
67
|
+
const row = this._screen[y];
|
|
68
|
+
if (!row)
|
|
69
|
+
return;
|
|
70
|
+
for (let x = fromX; x <= toX && x < this._cols; x++) {
|
|
71
|
+
row[x] = null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
_scrollUp(count) {
|
|
75
|
+
for (let i = 0; i < count; i++) {
|
|
76
|
+
this._screen.splice(this._scrollTop, 1);
|
|
77
|
+
this._screen.splice(this._scrollBottom, 0, this._makeRow(this._cols));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
_scrollDown(count) {
|
|
81
|
+
for (let i = 0; i < count; i++) {
|
|
82
|
+
this._screen.splice(this._scrollBottom, 1);
|
|
83
|
+
this._screen.splice(this._scrollTop, 0, this._makeRow(this._cols));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
_newline() {
|
|
87
|
+
if (this._cursorY === this._scrollBottom) {
|
|
88
|
+
this._scrollUp(1);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
this._cursorY = Math.min(this._cursorY + 1, this._rows - 1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
_parseCsiParams() {
|
|
95
|
+
if (!this._csiParams)
|
|
96
|
+
return [];
|
|
97
|
+
return this._csiParams.split(";").map((s) => (s === "" ? 0 : parseInt(s, 10)));
|
|
98
|
+
}
|
|
99
|
+
_execCsi(final) {
|
|
100
|
+
const params = this._parseCsiParams();
|
|
101
|
+
const p0 = params[0] ?? 0;
|
|
102
|
+
const p1 = params[1] ?? 0;
|
|
103
|
+
if (this._csiPrivate === "?") {
|
|
104
|
+
if (final === "h" || final === "l") {
|
|
105
|
+
// Only handle alt screen (1049) — ignore everything else
|
|
106
|
+
if (params.includes(1049)) {
|
|
107
|
+
if (final === "h") {
|
|
108
|
+
this._screen = this._makeScreen(this._cols, this._rows);
|
|
109
|
+
this._cursorX = 0;
|
|
110
|
+
this._cursorY = 0;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
this._screen = this._makeScreen(this._cols, this._rows);
|
|
114
|
+
this._cursorX = 0;
|
|
115
|
+
this._cursorY = 0;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
switch (final) {
|
|
122
|
+
case "A": // cursor up
|
|
123
|
+
this._cursorY = this._clamp(this._cursorY - Math.max(1, p0), 0, this._rows - 1);
|
|
124
|
+
break;
|
|
125
|
+
case "B": // cursor down
|
|
126
|
+
this._cursorY = this._clamp(this._cursorY + Math.max(1, p0), 0, this._rows - 1);
|
|
127
|
+
break;
|
|
128
|
+
case "C": // cursor forward
|
|
129
|
+
case "a":
|
|
130
|
+
this._cursorX = this._clamp(this._cursorX + Math.max(1, p0), 0, this._cols - 1);
|
|
131
|
+
break;
|
|
132
|
+
case "D": // cursor backward
|
|
133
|
+
this._cursorX = this._clamp(this._cursorX - Math.max(1, p0), 0, this._cols - 1);
|
|
134
|
+
break;
|
|
135
|
+
case "E": // cursor next line
|
|
136
|
+
this._cursorY = this._clamp(this._cursorY + Math.max(1, p0), 0, this._rows - 1);
|
|
137
|
+
this._cursorX = 0;
|
|
138
|
+
break;
|
|
139
|
+
case "F": // cursor preceding line
|
|
140
|
+
this._cursorY = this._clamp(this._cursorY - Math.max(1, p0), 0, this._rows - 1);
|
|
141
|
+
this._cursorX = 0;
|
|
142
|
+
break;
|
|
143
|
+
case "G": // cursor horizontal absolute
|
|
144
|
+
case "`":
|
|
145
|
+
this._cursorX = this._clamp(Math.max(1, p0) - 1, 0, this._cols - 1);
|
|
146
|
+
break;
|
|
147
|
+
case "H": // cursor position
|
|
148
|
+
case "f":
|
|
149
|
+
this._cursorY = this._clamp(Math.max(1, p0) - 1, 0, this._rows - 1);
|
|
150
|
+
this._cursorX = this._clamp(Math.max(1, p1) - 1, 0, this._cols - 1);
|
|
151
|
+
break;
|
|
152
|
+
case "I": // cursor forward tabulation
|
|
153
|
+
for (let i = 0; i < Math.max(1, p0); i++) {
|
|
154
|
+
this._cursorX = Math.min(this._cols - 1, (Math.floor(this._cursorX / 8) + 1) * 8);
|
|
155
|
+
}
|
|
156
|
+
break;
|
|
157
|
+
case "J": // erase in display
|
|
158
|
+
if (p0 === 0) {
|
|
159
|
+
this._eraseLine(this._cursorY, this._cursorX, this._cols - 1);
|
|
160
|
+
for (let y = this._cursorY + 1; y < this._rows; y++)
|
|
161
|
+
this._eraseLine(y, 0, this._cols - 1);
|
|
162
|
+
}
|
|
163
|
+
else if (p0 === 1) {
|
|
164
|
+
for (let y = 0; y < this._cursorY; y++)
|
|
165
|
+
this._eraseLine(y, 0, this._cols - 1);
|
|
166
|
+
this._eraseLine(this._cursorY, 0, this._cursorX);
|
|
167
|
+
}
|
|
168
|
+
else if (p0 === 2 || p0 === 3) {
|
|
169
|
+
for (let y = 0; y < this._rows; y++)
|
|
170
|
+
this._eraseLine(y, 0, this._cols - 1);
|
|
171
|
+
}
|
|
172
|
+
break;
|
|
173
|
+
case "K": // erase in line
|
|
174
|
+
if (p0 === 0)
|
|
175
|
+
this._eraseLine(this._cursorY, this._cursorX, this._cols - 1);
|
|
176
|
+
else if (p0 === 1)
|
|
177
|
+
this._eraseLine(this._cursorY, 0, this._cursorX);
|
|
178
|
+
else if (p0 === 2)
|
|
179
|
+
this._eraseLine(this._cursorY, 0, this._cols - 1);
|
|
180
|
+
break;
|
|
181
|
+
case "X": // erase characters (ECH)
|
|
182
|
+
this._eraseLine(this._cursorY, this._cursorX, this._cursorX + Math.max(1, p0) - 1);
|
|
183
|
+
break;
|
|
184
|
+
case "L": { // insert lines
|
|
185
|
+
const n = Math.max(1, p0);
|
|
186
|
+
for (let i = 0; i < n; i++) {
|
|
187
|
+
this._screen.splice(this._scrollBottom, 1);
|
|
188
|
+
this._screen.splice(this._cursorY, 0, this._makeRow(this._cols));
|
|
189
|
+
}
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
case "M": { // delete lines
|
|
193
|
+
const n = Math.max(1, p0);
|
|
194
|
+
for (let i = 0; i < n; i++) {
|
|
195
|
+
this._screen.splice(this._cursorY, 1);
|
|
196
|
+
this._screen.splice(this._scrollBottom, 0, this._makeRow(this._cols));
|
|
197
|
+
}
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
case "P": { // delete characters
|
|
201
|
+
const row = this._screen[this._cursorY];
|
|
202
|
+
if (row) {
|
|
203
|
+
const n = Math.max(1, p0);
|
|
204
|
+
row.splice(this._cursorX, n);
|
|
205
|
+
while (row.length < this._cols)
|
|
206
|
+
row.push(null);
|
|
207
|
+
}
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
case "@": { // insert blank characters
|
|
211
|
+
const row = this._screen[this._cursorY];
|
|
212
|
+
if (row) {
|
|
213
|
+
const n = Math.max(1, p0);
|
|
214
|
+
for (let i = 0; i < n; i++)
|
|
215
|
+
row.splice(this._cursorX, 0, null);
|
|
216
|
+
row.splice(this._cols);
|
|
217
|
+
}
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
case "S": // scroll up
|
|
221
|
+
this._scrollUp(Math.max(1, p0));
|
|
222
|
+
break;
|
|
223
|
+
case "T": // scroll down
|
|
224
|
+
if (params.length <= 1)
|
|
225
|
+
this._scrollDown(Math.max(1, p0));
|
|
226
|
+
break;
|
|
227
|
+
case "Z": { // cursor backward tabulation
|
|
228
|
+
const n = Math.max(1, p0);
|
|
229
|
+
for (let i = 0; i < n; i++) {
|
|
230
|
+
this._cursorX = Math.max(0, (Math.ceil(this._cursorX / 8) - 1) * 8);
|
|
231
|
+
}
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
case "d": // line position absolute
|
|
235
|
+
this._cursorY = this._clamp(Math.max(1, p0) - 1, 0, this._rows - 1);
|
|
236
|
+
break;
|
|
237
|
+
case "e": // vertical position relative
|
|
238
|
+
this._cursorY = this._clamp(this._cursorY + Math.max(1, p0), 0, this._rows - 1);
|
|
239
|
+
break;
|
|
240
|
+
case "r": { // set scrolling region
|
|
241
|
+
const top = this._clamp(Math.max(1, p0) - 1, 0, this._rows - 1);
|
|
242
|
+
const bottom = this._clamp((p1 === 0 ? this._rows : p1) - 1, 0, this._rows - 1);
|
|
243
|
+
if (top < bottom) {
|
|
244
|
+
this._scrollTop = top;
|
|
245
|
+
this._scrollBottom = bottom;
|
|
246
|
+
}
|
|
247
|
+
this._cursorX = 0;
|
|
248
|
+
this._cursorY = 0;
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
case "s": // save cursor
|
|
252
|
+
this._savedCursor = { x: this._cursorX, y: this._cursorY };
|
|
253
|
+
break;
|
|
254
|
+
case "u": // restore cursor
|
|
255
|
+
this._cursorX = this._clamp(this._savedCursor.x, 0, this._cols - 1);
|
|
256
|
+
this._cursorY = this._clamp(this._savedCursor.y, 0, this._rows - 1);
|
|
257
|
+
break;
|
|
258
|
+
case "m": // SGR — ignore (we don't track attributes)
|
|
259
|
+
break;
|
|
260
|
+
default:
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
_feed(ch) {
|
|
265
|
+
const code = ch.charCodeAt(0);
|
|
266
|
+
switch (this._state) {
|
|
267
|
+
case 0 /* State.Normal */:
|
|
268
|
+
this._feedNormal(ch, code);
|
|
269
|
+
break;
|
|
270
|
+
case 1 /* State.Escape */:
|
|
271
|
+
this._feedEscape(ch, code);
|
|
272
|
+
break;
|
|
273
|
+
case 2 /* State.Csi */:
|
|
274
|
+
this._feedCsi(ch, code);
|
|
275
|
+
break;
|
|
276
|
+
case 3 /* State.Osc */:
|
|
277
|
+
// consume until BEL or ESC (ESC \ = ST)
|
|
278
|
+
if (code === 0x07 || code === 0x9c) {
|
|
279
|
+
this._state = 0 /* State.Normal */;
|
|
280
|
+
}
|
|
281
|
+
else if (code === 0x1b) {
|
|
282
|
+
// next char should be `\` — just return to normal, it will be consumed
|
|
283
|
+
this._state = 0 /* State.Normal */;
|
|
284
|
+
}
|
|
285
|
+
break;
|
|
286
|
+
case 4 /* State.Str */:
|
|
287
|
+
// consume until ST (0x9c) or BEL
|
|
288
|
+
if (code === 0x9c || code === 0x07) {
|
|
289
|
+
this._state = 0 /* State.Normal */;
|
|
290
|
+
}
|
|
291
|
+
else if (code === 0x1b) {
|
|
292
|
+
this._state = 0 /* State.Normal */;
|
|
293
|
+
}
|
|
294
|
+
break;
|
|
295
|
+
case 5 /* State.EscCharset */:
|
|
296
|
+
// consume one character for charset designation
|
|
297
|
+
this._state = 0 /* State.Normal */;
|
|
298
|
+
break;
|
|
299
|
+
case 6 /* State.EscHash */:
|
|
300
|
+
// consume one character for line attributes
|
|
301
|
+
this._state = 0 /* State.Normal */;
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
_feedNormal(ch, code) {
|
|
306
|
+
if (code === 0x1b) {
|
|
307
|
+
this._state = 1 /* State.Escape */;
|
|
308
|
+
}
|
|
309
|
+
else if (code === 0x9b) {
|
|
310
|
+
// C1 CSI
|
|
311
|
+
this._csiParams = "";
|
|
312
|
+
this._csiPrivate = "";
|
|
313
|
+
this._state = 2 /* State.Csi */;
|
|
314
|
+
}
|
|
315
|
+
else if (code === 0x9d) {
|
|
316
|
+
// C1 OSC
|
|
317
|
+
this._state = 3 /* State.Osc */;
|
|
318
|
+
}
|
|
319
|
+
else if (code === 0x90 || code === 0x98 || code === 0x9e || code === 0x9f) {
|
|
320
|
+
// DCS, SOS, PM, APC
|
|
321
|
+
this._state = 4 /* State.Str */;
|
|
322
|
+
}
|
|
323
|
+
else if (code === 0x07 || code === 0x05 || code === 0x06) {
|
|
324
|
+
// BEL, ENQ, ACK — ignore
|
|
325
|
+
}
|
|
326
|
+
else if (code === 0x08) {
|
|
327
|
+
// BS
|
|
328
|
+
if (this._cursorX > 0)
|
|
329
|
+
this._cursorX--;
|
|
330
|
+
}
|
|
331
|
+
else if (code === 0x7f) {
|
|
332
|
+
// DEL
|
|
333
|
+
if (this._cursorX > 0) {
|
|
334
|
+
this._cursorX--;
|
|
335
|
+
this._setChar(this._cursorY, this._cursorX, " ");
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
else if (code === 0x09) {
|
|
339
|
+
// HT
|
|
340
|
+
this._cursorX = Math.min(this._cols - 1, (Math.floor(this._cursorX / 8) + 1) * 8);
|
|
341
|
+
}
|
|
342
|
+
else if (code === 0x0a || code === 0x0b || code === 0x0c) {
|
|
343
|
+
// LF, VT, FF
|
|
344
|
+
this._newline();
|
|
345
|
+
}
|
|
346
|
+
else if (code === 0x0d) {
|
|
347
|
+
// CR
|
|
348
|
+
this._cursorX = 0;
|
|
349
|
+
}
|
|
350
|
+
else if (code === 0x0e || code === 0x0f) {
|
|
351
|
+
// SO, SI — charset switch, ignore
|
|
352
|
+
}
|
|
353
|
+
else if (code >= 0x20 && code !== 0x7f) {
|
|
354
|
+
// Printable character (including multi-byte Unicode via code points)
|
|
355
|
+
this._setChar(this._cursorY, this._cursorX, ch);
|
|
356
|
+
this._cursorX++;
|
|
357
|
+
if (this._cursorX >= this._cols) {
|
|
358
|
+
// Auto-wrap
|
|
359
|
+
this._cursorX = 0;
|
|
360
|
+
this._newline();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
_feedEscape(ch, code) {
|
|
365
|
+
this._state = 0 /* State.Normal */;
|
|
366
|
+
if (code === 0x5b) {
|
|
367
|
+
// ESC [ = CSI
|
|
368
|
+
this._csiParams = "";
|
|
369
|
+
this._csiPrivate = "";
|
|
370
|
+
this._state = 2 /* State.Csi */;
|
|
371
|
+
}
|
|
372
|
+
else if (code === 0x5d) {
|
|
373
|
+
// ESC ] = OSC
|
|
374
|
+
this._state = 3 /* State.Osc */;
|
|
375
|
+
}
|
|
376
|
+
else if (code === 0x50 || code === 0x58 || code === 0x5e || code === 0x5f) {
|
|
377
|
+
// DCS, SOS, PM, APC
|
|
378
|
+
this._state = 4 /* State.Str */;
|
|
379
|
+
}
|
|
380
|
+
else if (code === 0x28 || code === 0x29 || code === 0x2a || code === 0x2b || code === 0x2d || code === 0x2e) {
|
|
381
|
+
// ESC ( ) * + - . = charset designation (consume next char)
|
|
382
|
+
this._state = 5 /* State.EscCharset */;
|
|
383
|
+
}
|
|
384
|
+
else if (code === 0x23) {
|
|
385
|
+
// ESC # = line attributes (consume next char)
|
|
386
|
+
this._state = 6 /* State.EscHash */;
|
|
387
|
+
}
|
|
388
|
+
else if (code === 0x37) {
|
|
389
|
+
// ESC 7 = save cursor
|
|
390
|
+
this._savedCursor = { x: this._cursorX, y: this._cursorY };
|
|
391
|
+
}
|
|
392
|
+
else if (code === 0x38) {
|
|
393
|
+
// ESC 8 = restore cursor
|
|
394
|
+
this._cursorX = this._clamp(this._savedCursor.x, 0, this._cols - 1);
|
|
395
|
+
this._cursorY = this._clamp(this._savedCursor.y, 0, this._rows - 1);
|
|
396
|
+
}
|
|
397
|
+
else if (code === 0x44) {
|
|
398
|
+
// ESC D = index (LF)
|
|
399
|
+
this._newline();
|
|
400
|
+
}
|
|
401
|
+
else if (code === 0x45) {
|
|
402
|
+
// ESC E = next line
|
|
403
|
+
this._cursorX = 0;
|
|
404
|
+
this._newline();
|
|
405
|
+
}
|
|
406
|
+
else if (code === 0x4d) {
|
|
407
|
+
// ESC M = reverse index
|
|
408
|
+
if (this._cursorY === this._scrollTop) {
|
|
409
|
+
this._scrollDown(1);
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
this._cursorY = Math.max(0, this._cursorY - 1);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
else if (code === 0x48) {
|
|
416
|
+
// ESC H = tab set (ignore)
|
|
417
|
+
}
|
|
418
|
+
else if (code === 0x63) {
|
|
419
|
+
// ESC c = full reset
|
|
420
|
+
this._screen = this._makeScreen(this._cols, this._rows);
|
|
421
|
+
this._cursorX = 0;
|
|
422
|
+
this._cursorY = 0;
|
|
423
|
+
this._savedCursor = { x: 0, y: 0 };
|
|
424
|
+
this._scrollTop = 0;
|
|
425
|
+
this._scrollBottom = this._rows - 1;
|
|
426
|
+
}
|
|
427
|
+
// All other ESC sequences: two-char, already consumed — ignore
|
|
428
|
+
}
|
|
429
|
+
_feedCsi(ch, code) {
|
|
430
|
+
if (code >= 0x40 && code <= 0x7e) {
|
|
431
|
+
// Final byte
|
|
432
|
+
this._execCsi(ch);
|
|
433
|
+
this._state = 0 /* State.Normal */;
|
|
434
|
+
}
|
|
435
|
+
else if (code === 0x3f || code === 0x21 || code === 0x3e || code === 0x20) {
|
|
436
|
+
// Private/intermediate marker (?, !, >, space)
|
|
437
|
+
this._csiPrivate = ch;
|
|
438
|
+
}
|
|
439
|
+
else if ((code >= 0x30 && code <= 0x39) || code === 0x3b) {
|
|
440
|
+
// Digit or semicolon — parameter byte
|
|
441
|
+
this._csiParams += ch;
|
|
442
|
+
}
|
|
443
|
+
// Other bytes ignored
|
|
444
|
+
}
|
|
445
|
+
}
|
package/dist/terminal-pilot.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export declare class TerminalPilot {
|
|
|
13
13
|
static launch(): Promise<TerminalPilot>;
|
|
14
14
|
newSession(opts: NewSessionOptions): Promise<TerminalSession>;
|
|
15
15
|
getSession(id: string): TerminalSession;
|
|
16
|
+
deleteSession(id: string): void;
|
|
16
17
|
sessions(): TerminalSession[];
|
|
17
18
|
close(): Promise<void>;
|
|
18
19
|
}
|
package/dist/terminal-pilot.js
CHANGED
|
@@ -18,9 +18,6 @@ export class TerminalPilot {
|
|
|
18
18
|
rows: opts.rows ?? DEFAULT_ROWS,
|
|
19
19
|
observe: opts.observe ?? false
|
|
20
20
|
});
|
|
21
|
-
session.on("exit", () => {
|
|
22
|
-
this.sessionMap.delete(session.id);
|
|
23
|
-
});
|
|
24
21
|
this.sessionMap.set(session.id, session);
|
|
25
22
|
return session;
|
|
26
23
|
}
|
|
@@ -31,8 +28,11 @@ export class TerminalPilot {
|
|
|
31
28
|
}
|
|
32
29
|
return session;
|
|
33
30
|
}
|
|
31
|
+
deleteSession(id) {
|
|
32
|
+
this.sessionMap.delete(id);
|
|
33
|
+
}
|
|
34
34
|
sessions() {
|
|
35
|
-
return [...this.sessionMap.values()];
|
|
35
|
+
return [...this.sessionMap.values()].filter((s) => s.exitCode === null);
|
|
36
36
|
}
|
|
37
37
|
async close() {
|
|
38
38
|
const sessions = [...this.sessionMap.values()];
|
|
@@ -42,6 +42,9 @@ export declare class TerminalSession {
|
|
|
42
42
|
screen(): Promise<TerminalScreen>;
|
|
43
43
|
history(opts?: HistoryOptions): Promise<string[]>;
|
|
44
44
|
resize(cols: number, rows: number): Promise<void>;
|
|
45
|
+
waitForExit(opts?: {
|
|
46
|
+
timeout?: number;
|
|
47
|
+
}): Promise<number>;
|
|
45
48
|
close(): Promise<number>;
|
|
46
49
|
on(event: "exit", cb: (code: number) => void): void;
|
|
47
50
|
}
|
package/dist/terminal-session.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { spawn as spawnChildProcess } from "node:child_process";
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
|
-
import { createRequire } from "node:module";
|
|
4
3
|
import * as nodePty from "node-pty";
|
|
5
4
|
import { stripAnsi } from "./ansi.js";
|
|
5
|
+
import { TerminalBuffer } from "./terminal-buffer.js";
|
|
6
6
|
import { keyToSequence } from "./keys.js";
|
|
7
7
|
import { TerminalScreen } from "./terminal-screen.js";
|
|
8
|
-
const require = createRequire(import.meta.url);
|
|
9
|
-
const HeadlessTerminal = require("headless-terminal");
|
|
10
8
|
const DEFAULT_COLS = 120;
|
|
11
9
|
const DEFAULT_ROWS = 40;
|
|
12
10
|
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
@@ -33,7 +31,7 @@ export class TerminalSession {
|
|
|
33
31
|
this.command = command;
|
|
34
32
|
this.currentCols = cols;
|
|
35
33
|
this.currentRows = rows;
|
|
36
|
-
this.terminal = new
|
|
34
|
+
this.terminal = new TerminalBuffer(cols, rows);
|
|
37
35
|
this.pty = createPtyProcess({ command, args, cwd, env, cols, rows });
|
|
38
36
|
this.pid = this.pty.pid;
|
|
39
37
|
const dataSubscription = this.pty.onData((chunk) => {
|
|
@@ -145,17 +143,31 @@ export class TerminalSession {
|
|
|
145
143
|
}
|
|
146
144
|
this.terminal.resize(cols, rows);
|
|
147
145
|
}
|
|
146
|
+
async waitForExit(opts) {
|
|
147
|
+
if (this.exitCode !== null) {
|
|
148
|
+
return this.exitCode;
|
|
149
|
+
}
|
|
150
|
+
if (opts?.timeout !== undefined) {
|
|
151
|
+
const result = await waitForExit(this.exitPromise, opts.timeout);
|
|
152
|
+
if (result === null) {
|
|
153
|
+
throw new Error(`Timed out waiting for process to exit after ${opts.timeout}ms`);
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
return this.exitPromise;
|
|
158
|
+
}
|
|
148
159
|
async close() {
|
|
149
160
|
if (this.exitCode !== null) {
|
|
150
161
|
return this.exitCode;
|
|
151
162
|
}
|
|
152
163
|
if (!this.closeRequested) {
|
|
153
164
|
this.closeRequested = true;
|
|
165
|
+
const gracefulExitCode = await waitForExit(this.exitPromise, CLOSE_AFTER_SIGNAL_GRACE_MS);
|
|
166
|
+
if (gracefulExitCode !== null) {
|
|
167
|
+
return gracefulExitCode;
|
|
168
|
+
}
|
|
154
169
|
if (this.signalRequested) {
|
|
155
|
-
|
|
156
|
-
if (exitCode !== null) {
|
|
157
|
-
return exitCode;
|
|
158
|
-
}
|
|
170
|
+
return this.exitPromise;
|
|
159
171
|
}
|
|
160
172
|
if (this.exitCode === null) {
|
|
161
173
|
this.pty.kill("SIGTERM");
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "terminal-pilot",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.0.4",
|
|
4
|
+
"description": "Playwright-like SDK for automating interactive CLI apps through a real PTY",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -9,19 +9,12 @@
|
|
|
9
9
|
".": {
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
11
|
"import": "./dist/index.js"
|
|
12
|
-
},
|
|
13
|
-
"./mcp": {
|
|
14
|
-
"types": "./dist/mcp-server.d.ts",
|
|
15
|
-
"import": "./dist/mcp-server.js"
|
|
16
12
|
}
|
|
17
13
|
},
|
|
18
|
-
"bin": {
|
|
19
|
-
"terminal-pilot-mcp": "dist/mcp-server.js"
|
|
20
|
-
},
|
|
21
14
|
"scripts": {
|
|
22
15
|
"build": "tsc",
|
|
23
16
|
"prepublishOnly": "tsc",
|
|
24
|
-
"install-local-package": "TMPD=$(mktemp -d) &&
|
|
17
|
+
"install-local-package": "TMPD=$(mktemp -d) && npm pack --pack-destination $TMPD && npm install -g $TMPD/terminal-pilot-*.tgz && rm -rf $TMPD"
|
|
25
18
|
},
|
|
26
19
|
"files": [
|
|
27
20
|
"dist"
|
|
@@ -37,14 +30,11 @@
|
|
|
37
30
|
"license": "MIT",
|
|
38
31
|
"keywords": [
|
|
39
32
|
"terminal",
|
|
40
|
-
"mcp",
|
|
41
33
|
"pty",
|
|
42
34
|
"headless"
|
|
43
35
|
],
|
|
44
36
|
"dependencies": {
|
|
45
|
-
"
|
|
46
|
-
"node-pty": "^1.1.0",
|
|
47
|
-
"tiny-stdio-mcp-server": "^0.1.0"
|
|
37
|
+
"node-pty": "^1.1.0"
|
|
48
38
|
},
|
|
49
39
|
"devDependencies": {}
|
|
50
40
|
}
|
package/dist/mcp-server.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { type Server } from "tiny-stdio-mcp-server";
|
|
3
|
-
import { TerminalPilot } from "./terminal-pilot.js";
|
|
4
|
-
type RuntimeProcess = Pick<typeof process, "on" | "off">;
|
|
5
|
-
export declare function createTerminalPilotMcpServer(agent: TerminalPilot): Server;
|
|
6
|
-
export declare function main({ launchAgent, createMcpServer, runtimeProcess }?: {
|
|
7
|
-
launchAgent?: typeof TerminalPilot.launch;
|
|
8
|
-
createMcpServer?: (agent: TerminalPilot) => Server;
|
|
9
|
-
runtimeProcess?: RuntimeProcess;
|
|
10
|
-
}): Promise<void>;
|
|
11
|
-
export {};
|
package/dist/mcp-server.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import packageJson from "../package.json" with { type: "json" };
|
|
3
|
-
import { createServer } from "tiny-stdio-mcp-server";
|
|
4
|
-
import { terminalPilotMcpTools } from "./mcp-tools.js";
|
|
5
|
-
import { TerminalPilot } from "./terminal-pilot.js";
|
|
6
|
-
export function createTerminalPilotMcpServer(agent) {
|
|
7
|
-
const server = createServer({
|
|
8
|
-
name: "terminal-pilot",
|
|
9
|
-
version: packageJson.version
|
|
10
|
-
});
|
|
11
|
-
for (const tool of terminalPilotMcpTools(agent)) {
|
|
12
|
-
server.tool(tool.name, tool.description, tool.inputSchema, tool.handler);
|
|
13
|
-
}
|
|
14
|
-
return server;
|
|
15
|
-
}
|
|
16
|
-
export async function main({ launchAgent = TerminalPilot.launch, createMcpServer = createTerminalPilotMcpServer, runtimeProcess = process } = {}) {
|
|
17
|
-
const agent = await launchAgent();
|
|
18
|
-
let closed = false;
|
|
19
|
-
const closeAgent = async () => {
|
|
20
|
-
if (closed) {
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
closed = true;
|
|
24
|
-
await agent.close();
|
|
25
|
-
};
|
|
26
|
-
const handleExit = () => {
|
|
27
|
-
void closeAgent();
|
|
28
|
-
};
|
|
29
|
-
runtimeProcess.on("exit", handleExit);
|
|
30
|
-
try {
|
|
31
|
-
await createMcpServer(agent).listen();
|
|
32
|
-
}
|
|
33
|
-
finally {
|
|
34
|
-
runtimeProcess.off("exit", handleExit);
|
|
35
|
-
await closeAgent();
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
39
|
-
void main();
|
|
40
|
-
}
|
package/dist/mcp-tools.d.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { type ToolDefinition } from "tiny-stdio-mcp-server";
|
|
2
|
-
import type { TerminalKey } from "./keys.js";
|
|
3
|
-
import type { TerminalPilot } from "./terminal-pilot.js";
|
|
4
|
-
export type TerminalPilotMcpTool<T = Record<string, unknown>> = ToolDefinition<T>;
|
|
5
|
-
export declare function terminalCreateSessionTool(agent: TerminalPilot): TerminalPilotMcpTool<{
|
|
6
|
-
command: string;
|
|
7
|
-
args?: string[];
|
|
8
|
-
cwd?: string;
|
|
9
|
-
cols?: number;
|
|
10
|
-
rows?: number;
|
|
11
|
-
observe?: boolean;
|
|
12
|
-
}>;
|
|
13
|
-
export declare function terminalTypeTool(agent: TerminalPilot): TerminalPilotMcpTool<{
|
|
14
|
-
sessionId: string;
|
|
15
|
-
text: string;
|
|
16
|
-
}>;
|
|
17
|
-
export declare function terminalPressKeyTool(agent: TerminalPilot): TerminalPilotMcpTool<{
|
|
18
|
-
sessionId: string;
|
|
19
|
-
key: TerminalKey;
|
|
20
|
-
}>;
|
|
21
|
-
export declare function terminalSendSignalTool(agent: TerminalPilot): TerminalPilotMcpTool<{
|
|
22
|
-
sessionId: string;
|
|
23
|
-
signal: string;
|
|
24
|
-
}>;
|
|
25
|
-
export declare function terminalWaitForTool(agent: TerminalPilot): TerminalPilotMcpTool<{
|
|
26
|
-
sessionId: string;
|
|
27
|
-
pattern: string;
|
|
28
|
-
timeout?: number;
|
|
29
|
-
}>;
|
|
30
|
-
export declare function terminalReadScreenTool(agent: TerminalPilot): TerminalPilotMcpTool<{
|
|
31
|
-
sessionId: string;
|
|
32
|
-
}>;
|
|
33
|
-
export declare function terminalReadHistoryTool(agent: TerminalPilot): TerminalPilotMcpTool<{
|
|
34
|
-
sessionId: string;
|
|
35
|
-
last?: number;
|
|
36
|
-
}>;
|
|
37
|
-
export declare function terminalResizeTool(agent: TerminalPilot): TerminalPilotMcpTool<{
|
|
38
|
-
sessionId: string;
|
|
39
|
-
cols: number;
|
|
40
|
-
rows: number;
|
|
41
|
-
}>;
|
|
42
|
-
export declare function terminalCloseSessionTool(agent: TerminalPilot): TerminalPilotMcpTool<{
|
|
43
|
-
sessionId: string;
|
|
44
|
-
}>;
|
|
45
|
-
export declare function terminalPilotMcpTools(agent: TerminalPilot): Array<TerminalPilotMcpTool<any>>;
|
|
46
|
-
export declare function terminalListSessionsTool(agent: TerminalPilot): TerminalPilotMcpTool<Record<string, never>>;
|
package/dist/mcp-tools.js
DELETED
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
import { defineSchema } from "tiny-stdio-mcp-server";
|
|
2
|
-
export function terminalCreateSessionTool(agent) {
|
|
3
|
-
return {
|
|
4
|
-
name: "terminal_create_session",
|
|
5
|
-
description: "Spawn an interactive CLI in a PTY",
|
|
6
|
-
inputSchema: defineSchema({
|
|
7
|
-
command: { type: "string", description: "Command to execute" },
|
|
8
|
-
args: { type: "array", description: "Command arguments", optional: true },
|
|
9
|
-
cwd: { type: "string", description: "Working directory", optional: true },
|
|
10
|
-
cols: { type: "number", description: "Terminal width in columns", optional: true },
|
|
11
|
-
rows: { type: "number", description: "Terminal height in rows", optional: true },
|
|
12
|
-
observe: { type: "boolean", description: "Mirror PTY output to stderr", optional: true }
|
|
13
|
-
}),
|
|
14
|
-
async handler(input) {
|
|
15
|
-
const session = await agent.newSession(input);
|
|
16
|
-
return { sessionId: session.id, pid: session.pid };
|
|
17
|
-
}
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
export function terminalTypeTool(agent) {
|
|
21
|
-
return {
|
|
22
|
-
name: "terminal_type",
|
|
23
|
-
description: "Write text to an active terminal session",
|
|
24
|
-
inputSchema: defineSchema({
|
|
25
|
-
sessionId: { type: "string", description: "Terminal session id" },
|
|
26
|
-
text: { type: "string", description: "Text to write to the session" }
|
|
27
|
-
}),
|
|
28
|
-
async handler(input) {
|
|
29
|
-
await agent.getSession(input.sessionId).fill(input.text);
|
|
30
|
-
return undefined;
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
export function terminalPressKeyTool(agent) {
|
|
35
|
-
return {
|
|
36
|
-
name: "terminal_press_key",
|
|
37
|
-
description: "Send a named key press to an active terminal session",
|
|
38
|
-
inputSchema: defineSchema({
|
|
39
|
-
sessionId: { type: "string", description: "Terminal session id" },
|
|
40
|
-
key: { type: "string", description: "Named key to press" }
|
|
41
|
-
}),
|
|
42
|
-
async handler(input) {
|
|
43
|
-
await agent.getSession(input.sessionId).press(input.key);
|
|
44
|
-
return undefined;
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
export function terminalSendSignalTool(agent) {
|
|
49
|
-
return {
|
|
50
|
-
name: "terminal_send_signal",
|
|
51
|
-
description: "Send a process signal to an active terminal session",
|
|
52
|
-
inputSchema: defineSchema({
|
|
53
|
-
sessionId: { type: "string", description: "Terminal session id" },
|
|
54
|
-
signal: { type: "string", description: "Signal to send to the session process" }
|
|
55
|
-
}),
|
|
56
|
-
async handler(input) {
|
|
57
|
-
await agent.getSession(input.sessionId).signal(input.signal);
|
|
58
|
-
return undefined;
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
export function terminalWaitForTool(agent) {
|
|
63
|
-
return {
|
|
64
|
-
name: "terminal_wait_for",
|
|
65
|
-
description: "Wait for terminal output to match a pattern",
|
|
66
|
-
inputSchema: defineSchema({
|
|
67
|
-
sessionId: { type: "string", description: "Terminal session id" },
|
|
68
|
-
pattern: { type: "string", description: "Regular expression pattern to wait for" },
|
|
69
|
-
timeout: { type: "number", description: "Maximum wait time in milliseconds", optional: true }
|
|
70
|
-
}),
|
|
71
|
-
async handler(input) {
|
|
72
|
-
const session = agent.getSession(input.sessionId);
|
|
73
|
-
const pattern = new RegExp(input.pattern);
|
|
74
|
-
const line = input.timeout === undefined
|
|
75
|
-
? await session.waitFor(pattern)
|
|
76
|
-
: await session.waitFor(pattern, { timeout: input.timeout });
|
|
77
|
-
return { matched: true, line };
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
export function terminalReadScreenTool(agent) {
|
|
82
|
-
return {
|
|
83
|
-
name: "terminal_read_screen",
|
|
84
|
-
description: "Read the current visible terminal screen",
|
|
85
|
-
inputSchema: defineSchema({
|
|
86
|
-
sessionId: { type: "string", description: "Terminal session id" }
|
|
87
|
-
}),
|
|
88
|
-
async handler(input) {
|
|
89
|
-
const screen = await agent.getSession(input.sessionId).screen();
|
|
90
|
-
return {
|
|
91
|
-
lines: [...screen.lines],
|
|
92
|
-
cursor: { ...screen.cursor },
|
|
93
|
-
size: { ...screen.size }
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
export function terminalReadHistoryTool(agent) {
|
|
99
|
-
return {
|
|
100
|
-
name: "terminal_read_history",
|
|
101
|
-
description: "Read terminal output history",
|
|
102
|
-
inputSchema: defineSchema({
|
|
103
|
-
sessionId: { type: "string", description: "Terminal session id" },
|
|
104
|
-
last: { type: "number", description: "Return only the last N lines", optional: true }
|
|
105
|
-
}),
|
|
106
|
-
async handler(input) {
|
|
107
|
-
const lines = await agent.getSession(input.sessionId).history({ last: input.last });
|
|
108
|
-
return { lines };
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
export function terminalResizeTool(agent) {
|
|
113
|
-
return {
|
|
114
|
-
name: "terminal_resize",
|
|
115
|
-
description: "Resize an active terminal session",
|
|
116
|
-
inputSchema: defineSchema({
|
|
117
|
-
sessionId: { type: "string", description: "Terminal session id" },
|
|
118
|
-
cols: { type: "number", description: "Terminal width in columns" },
|
|
119
|
-
rows: { type: "number", description: "Terminal height in rows" }
|
|
120
|
-
}),
|
|
121
|
-
async handler(input) {
|
|
122
|
-
await agent.getSession(input.sessionId).resize(input.cols, input.rows);
|
|
123
|
-
return undefined;
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
export function terminalCloseSessionTool(agent) {
|
|
128
|
-
return {
|
|
129
|
-
name: "terminal_close_session",
|
|
130
|
-
description: "Close an active terminal session",
|
|
131
|
-
inputSchema: defineSchema({
|
|
132
|
-
sessionId: { type: "string", description: "Terminal session id" }
|
|
133
|
-
}),
|
|
134
|
-
async handler(input) {
|
|
135
|
-
const exitCode = await agent.getSession(input.sessionId).close();
|
|
136
|
-
return { exitCode };
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
export function terminalPilotMcpTools(agent) {
|
|
141
|
-
return [
|
|
142
|
-
terminalCreateSessionTool(agent),
|
|
143
|
-
terminalTypeTool(agent),
|
|
144
|
-
terminalPressKeyTool(agent),
|
|
145
|
-
terminalSendSignalTool(agent),
|
|
146
|
-
terminalWaitForTool(agent),
|
|
147
|
-
terminalReadScreenTool(agent),
|
|
148
|
-
terminalReadHistoryTool(agent),
|
|
149
|
-
terminalResizeTool(agent),
|
|
150
|
-
terminalCloseSessionTool(agent),
|
|
151
|
-
terminalListSessionsTool(agent)
|
|
152
|
-
];
|
|
153
|
-
}
|
|
154
|
-
export function terminalListSessionsTool(agent) {
|
|
155
|
-
return {
|
|
156
|
-
name: "terminal_list_sessions",
|
|
157
|
-
description: "List active terminal sessions",
|
|
158
|
-
inputSchema: defineSchema({}),
|
|
159
|
-
async handler() {
|
|
160
|
-
return {
|
|
161
|
-
sessions: agent.sessions().map((session) => ({
|
|
162
|
-
id: session.id,
|
|
163
|
-
command: session.command,
|
|
164
|
-
pid: session.pid
|
|
165
|
-
}))
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
}
|