terminal-pilot 0.0.1
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 +416 -0
- package/dist/ansi.d.ts +1 -0
- package/dist/ansi.js +71 -0
- package/dist/exports.compile-check.d.ts +1 -0
- package/dist/exports.compile-check.js +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/keys.d.ts +2 -0
- package/dist/keys.js +53 -0
- package/dist/mcp-server.d.ts +11 -0
- package/dist/mcp-server.js +40 -0
- package/dist/mcp-tools.d.ts +46 -0
- package/dist/mcp-tools.js +169 -0
- package/dist/terminal-pilot.d.ts +18 -0
- package/dist/terminal-pilot.js +46 -0
- package/dist/terminal-screen.d.ts +27 -0
- package/dist/terminal-screen.js +28 -0
- package/dist/terminal-session.d.ts +48 -0
- package/dist/terminal-session.js +320 -0
- package/dist/testing/menu-cli.d.ts +2 -0
- package/dist/testing/menu-cli.js +63 -0
- package/dist/testing/test-cli.d.ts +2 -0
- package/dist/testing/test-cli.js +22 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
# @poe-code/terminal-pilot
|
|
2
|
+
|
|
3
|
+
`terminal-pilot` is a Playwright-like SDK and MCP server for automating interactive CLI apps through a real pseudoterminal (PTY).
|
|
4
|
+
|
|
5
|
+
Use it when plain stdio is not enough: menus, prompts, arrow-key navigation, confirmations, and terminal redraws such as `poe-code configure`.
|
|
6
|
+
|
|
7
|
+
For design rationale and scope, see `docs/plans/terminal-pilot.md`.
|
|
8
|
+
|
|
9
|
+
## What it includes
|
|
10
|
+
|
|
11
|
+
- **SDK:** `TerminalPilot` → `TerminalSession` → `TerminalScreen`
|
|
12
|
+
- **MCP server:** 10 tools for AI-driven CLI automation
|
|
13
|
+
- **Real PTY execution:** works with interactive CLIs that expect a terminal
|
|
14
|
+
- **Headless by default:** optional `observe: true` mirrors PTY output for debugging
|
|
15
|
+
|
|
16
|
+
## Entry points
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { TerminalPilot } from "@poe-code/terminal-pilot";
|
|
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
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## SDK API
|
|
39
|
+
|
|
40
|
+
### `TerminalPilot`
|
|
41
|
+
|
|
42
|
+
Creates and tracks active terminal sessions.
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { TerminalPilot } from "@poe-code/terminal-pilot";
|
|
46
|
+
|
|
47
|
+
const pilot = await TerminalPilot.launch();
|
|
48
|
+
|
|
49
|
+
const session = await pilot.newSession({
|
|
50
|
+
command: "poe-code",
|
|
51
|
+
args: ["configure"],
|
|
52
|
+
cwd: process.cwd(),
|
|
53
|
+
env: process.env,
|
|
54
|
+
cols: 120,
|
|
55
|
+
rows: 40,
|
|
56
|
+
observe: false
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
console.log(session.id, session.pid);
|
|
60
|
+
console.log(pilot.sessions().map(({ id, pid }) => ({ id, pid })));
|
|
61
|
+
|
|
62
|
+
await pilot.close();
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
type NewSessionOptions = {
|
|
67
|
+
command: string;
|
|
68
|
+
args?: string[];
|
|
69
|
+
cwd?: string;
|
|
70
|
+
env?: Record<string, string>;
|
|
71
|
+
cols?: number; // default: 120
|
|
72
|
+
rows?: number; // default: 40
|
|
73
|
+
observe?: boolean; // default: false
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
class TerminalPilot {
|
|
77
|
+
static launch(): Promise<TerminalPilot>;
|
|
78
|
+
newSession(options: NewSessionOptions): Promise<TerminalSession>;
|
|
79
|
+
getSession(id: string): TerminalSession; // throws if missing
|
|
80
|
+
sessions(): TerminalSession[]; // active sessions only
|
|
81
|
+
close(): Promise<void>;
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### `TerminalSession`
|
|
86
|
+
|
|
87
|
+
Represents one PTY-backed CLI process.
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
await session.waitFor(/Pick an agent to configure:/);
|
|
91
|
+
await session.press("ArrowDown");
|
|
92
|
+
await session.press("Enter");
|
|
93
|
+
|
|
94
|
+
await session.waitFor(/Waiting for authorization|default model|configured/i);
|
|
95
|
+
|
|
96
|
+
const screen = await session.screen();
|
|
97
|
+
const history = await session.history({ last: 20 });
|
|
98
|
+
|
|
99
|
+
console.log(screen.text);
|
|
100
|
+
console.log(history.join("\n"));
|
|
101
|
+
|
|
102
|
+
await session.resize(100, 30);
|
|
103
|
+
await session.signal("SIGINT");
|
|
104
|
+
await session.close();
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
type WaitForOptions = {
|
|
109
|
+
timeout?: number; // default: 10000
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
type HistoryOptions = {
|
|
113
|
+
last?: number;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
type TerminalKey =
|
|
117
|
+
| "Enter"
|
|
118
|
+
| "Tab"
|
|
119
|
+
| "Escape"
|
|
120
|
+
| "Backspace"
|
|
121
|
+
| "Delete"
|
|
122
|
+
| "ArrowUp"
|
|
123
|
+
| "ArrowDown"
|
|
124
|
+
| "ArrowLeft"
|
|
125
|
+
| "ArrowRight"
|
|
126
|
+
| "Home"
|
|
127
|
+
| "End"
|
|
128
|
+
| "PageUp"
|
|
129
|
+
| "PageDown"
|
|
130
|
+
| "Space"
|
|
131
|
+
| `Control+${string}`
|
|
132
|
+
| `Alt+${string}`;
|
|
133
|
+
|
|
134
|
+
class TerminalSession {
|
|
135
|
+
readonly id: string;
|
|
136
|
+
readonly command: string;
|
|
137
|
+
readonly pid: number;
|
|
138
|
+
exitCode: number | null;
|
|
139
|
+
|
|
140
|
+
type(text: string): Promise<void>; // character-by-character
|
|
141
|
+
fill(text: string): Promise<void>; // bulk write
|
|
142
|
+
press(key: TerminalKey): Promise<void>;
|
|
143
|
+
send(raw: string): Promise<void>; // raw bytes / escape sequences
|
|
144
|
+
signal(signal: string): Promise<void>;
|
|
145
|
+
waitFor(pattern: string | RegExp, options?: WaitForOptions): Promise<string>;
|
|
146
|
+
waitForQuiet(ms: number): Promise<void>;
|
|
147
|
+
screen(): Promise<TerminalScreen>;
|
|
148
|
+
history(options?: HistoryOptions): Promise<string[]>;
|
|
149
|
+
resize(cols: number, rows: number): Promise<void>;
|
|
150
|
+
close(): Promise<number>;
|
|
151
|
+
on(event: "exit", cb: (code: number) => void): void;
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Common key names:
|
|
156
|
+
|
|
157
|
+
- Navigation: `ArrowUp`, `ArrowDown`, `ArrowLeft`, `ArrowRight`, `Home`, `End`
|
|
158
|
+
- Editing: `Enter`, `Tab`, `Backspace`, `Delete`, `Space`
|
|
159
|
+
- Control/meta: `Control+c`, `Control+d`, `Alt+x`
|
|
160
|
+
|
|
161
|
+
### `TerminalScreen`
|
|
162
|
+
|
|
163
|
+
Normalized visible terminal state.
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
const screen = await session.screen();
|
|
167
|
+
|
|
168
|
+
screen.lines; // ANSI-stripped visible lines
|
|
169
|
+
screen.rawLines; // raw visible lines
|
|
170
|
+
screen.cursor; // { row, col }
|
|
171
|
+
screen.size; // { rows, cols }
|
|
172
|
+
screen.text; // lines joined with \n
|
|
173
|
+
screen.contains("Configured");
|
|
174
|
+
screen.line(0);
|
|
175
|
+
screen.line(-1);
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
class TerminalScreen {
|
|
180
|
+
readonly lines: readonly string[];
|
|
181
|
+
readonly rawLines: readonly string[];
|
|
182
|
+
readonly cursor: { row: number; col: number };
|
|
183
|
+
readonly size: { rows: number; cols: number };
|
|
184
|
+
|
|
185
|
+
get text(): string;
|
|
186
|
+
contains(substring: string): boolean;
|
|
187
|
+
line(index: number): string; // negative indexes supported
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
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
|
+
## Environment variables
|
|
301
|
+
|
|
302
|
+
There are **no terminal-pilot-specific environment variables**.
|
|
303
|
+
|
|
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.
|
|
310
|
+
|
|
311
|
+
## Testing
|
|
312
|
+
|
|
313
|
+
Interactive fixtures live under `packages/terminal-pilot/src/testing/`:
|
|
314
|
+
|
|
315
|
+
- `test-cli.ts` - prompt + text entry fixture
|
|
316
|
+
- `menu-cli.ts` - arrow-key menu fixture
|
|
317
|
+
- `fixtures.test.ts` - examples of driving both fixtures
|
|
318
|
+
|
|
319
|
+
Run just the fixture tests:
|
|
320
|
+
|
|
321
|
+
```sh
|
|
322
|
+
npx vitest run packages/terminal-pilot/src/testing/fixtures.test.ts
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Run the whole package test suite:
|
|
326
|
+
|
|
327
|
+
```sh
|
|
328
|
+
npx vitest run packages/terminal-pilot/src
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Typical fixture workflow:
|
|
332
|
+
|
|
333
|
+
1. Spawn the fixture in a `TerminalSession`
|
|
334
|
+
2. `waitFor(...)` the prompt
|
|
335
|
+
3. `type(...)`, `fill(...)`, `press(...)`, or `signal(...)`
|
|
336
|
+
4. Assert with `screen()` or `history()`
|
|
337
|
+
5. `close()` the session
|
|
338
|
+
|
|
339
|
+
Example fixture-based test:
|
|
340
|
+
|
|
341
|
+
```ts
|
|
342
|
+
import path from "node:path";
|
|
343
|
+
import { TerminalPilot } from "@poe-code/terminal-pilot";
|
|
344
|
+
|
|
345
|
+
const pilot = await TerminalPilot.launch();
|
|
346
|
+
const tsxPath = path.join(process.cwd(), "node_modules", ".bin", "tsx");
|
|
347
|
+
const fixturePath = path.join(process.cwd(), "packages/terminal-pilot/src/testing/menu-cli.ts");
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
const session = await pilot.newSession({
|
|
351
|
+
command: tsxPath,
|
|
352
|
+
args: [fixturePath]
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
await session.waitFor("Select an option:");
|
|
356
|
+
await session.press("ArrowDown");
|
|
357
|
+
await session.press("ArrowDown");
|
|
358
|
+
await session.press("Enter");
|
|
359
|
+
|
|
360
|
+
await session.waitFor("You selected: Option 3");
|
|
361
|
+
} finally {
|
|
362
|
+
await pilot.close();
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## Example: test `poe-code configure`
|
|
367
|
+
|
|
368
|
+
Use a temporary home directory so you do not touch your real config while testing the interactive flow.
|
|
369
|
+
|
|
370
|
+
```ts
|
|
371
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
372
|
+
import os from "node:os";
|
|
373
|
+
import path from "node:path";
|
|
374
|
+
import { TerminalPilot } from "@poe-code/terminal-pilot";
|
|
375
|
+
|
|
376
|
+
const tmpHome = mkdtempSync(path.join(os.tmpdir(), "poe-configure-test-"));
|
|
377
|
+
const pilot = await TerminalPilot.launch();
|
|
378
|
+
|
|
379
|
+
try {
|
|
380
|
+
const session = await pilot.newSession({
|
|
381
|
+
command: "npm",
|
|
382
|
+
args: ["run", "dev", "--silent", "--", "configure"],
|
|
383
|
+
cwd: process.cwd(),
|
|
384
|
+
env: {
|
|
385
|
+
...process.env,
|
|
386
|
+
HOME: tmpHome,
|
|
387
|
+
XDG_CONFIG_HOME: path.join(tmpHome, ".config"),
|
|
388
|
+
XDG_DATA_HOME: path.join(tmpHome, ".local", "share")
|
|
389
|
+
},
|
|
390
|
+
cols: 120,
|
|
391
|
+
rows: 40
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
await session.waitFor(/Pick an agent to configure:/);
|
|
395
|
+
await session.press("ArrowDown"); // choose Codex, Kimi, etc.
|
|
396
|
+
await session.press("Enter");
|
|
397
|
+
|
|
398
|
+
await session.waitFor(/Waiting for authorization|default model|configured/i, {
|
|
399
|
+
timeout: 15000
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
const screen = await session.screen();
|
|
403
|
+
const history = await session.history({ last: 40 });
|
|
404
|
+
|
|
405
|
+
console.log(screen.text);
|
|
406
|
+
console.log(history.join("\n"));
|
|
407
|
+
|
|
408
|
+
await session.signal("SIGINT");
|
|
409
|
+
await session.close();
|
|
410
|
+
} finally {
|
|
411
|
+
await pilot.close();
|
|
412
|
+
rmSync(tmpHome, { recursive: true, force: true });
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
That gives you a real end-to-end test of the interactive `configure` handoff. If the selected provider requires OAuth or API-key input, keep the temp environment and continue the flow with additional `type(...)`, `press(...)`, and `waitFor(...)` calls.
|
package/dist/ansi.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function stripAnsi(input: string): string;
|
package/dist/ansi.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const ESC = 0x1b;
|
|
2
|
+
const BEL = 0x07;
|
|
3
|
+
const ST = 0x9c;
|
|
4
|
+
const CSI = 0x9b;
|
|
5
|
+
const OSC = 0x9d;
|
|
6
|
+
const DCS = 0x90;
|
|
7
|
+
const SOS = 0x98;
|
|
8
|
+
const PM = 0x9e;
|
|
9
|
+
const APC = 0x9f;
|
|
10
|
+
export function stripAnsi(input) {
|
|
11
|
+
let output = "";
|
|
12
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
13
|
+
const code = input.charCodeAt(index);
|
|
14
|
+
if (code === ESC) {
|
|
15
|
+
const nextCode = input.charCodeAt(index + 1);
|
|
16
|
+
if (nextCode === 0x5b) {
|
|
17
|
+
index = consumeCsi(input, index + 2);
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (nextCode === 0x5d) {
|
|
21
|
+
index = consumeTerminatedString(input, index + 2, true);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (nextCode === 0x50 || nextCode === 0x58 || nextCode === 0x5e || nextCode === 0x5f) {
|
|
25
|
+
index = consumeTerminatedString(input, index + 2, false);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (!Number.isNaN(nextCode)) {
|
|
29
|
+
index += 1;
|
|
30
|
+
}
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (code === CSI) {
|
|
34
|
+
index = consumeCsi(input, index + 1);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (code === OSC) {
|
|
38
|
+
index = consumeTerminatedString(input, index + 1, true);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (code === DCS || code === SOS || code === PM || code === APC) {
|
|
42
|
+
index = consumeTerminatedString(input, index + 1, false);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
output += input[index];
|
|
46
|
+
}
|
|
47
|
+
return output;
|
|
48
|
+
}
|
|
49
|
+
function consumeCsi(input, index) {
|
|
50
|
+
while (index < input.length) {
|
|
51
|
+
const code = input.charCodeAt(index);
|
|
52
|
+
if (code >= 0x40 && code <= 0x7e) {
|
|
53
|
+
return index;
|
|
54
|
+
}
|
|
55
|
+
index += 1;
|
|
56
|
+
}
|
|
57
|
+
return input.length;
|
|
58
|
+
}
|
|
59
|
+
function consumeTerminatedString(input, index, allowBellTerminator) {
|
|
60
|
+
while (index < input.length) {
|
|
61
|
+
const code = input.charCodeAt(index);
|
|
62
|
+
if (code === ST || (allowBellTerminator && code === BEL)) {
|
|
63
|
+
return index;
|
|
64
|
+
}
|
|
65
|
+
if (code === ESC && input.charCodeAt(index + 1) === 0x5c) {
|
|
66
|
+
return index + 1;
|
|
67
|
+
}
|
|
68
|
+
index += 1;
|
|
69
|
+
}
|
|
70
|
+
return input.length;
|
|
71
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { stripAnsi } from "./ansi.js";
|
|
2
|
+
export { keyToSequence, type TerminalKey } from "./keys.js";
|
|
3
|
+
export { TerminalScreen } from "./terminal-screen.js";
|
|
4
|
+
export { TerminalSession, type HistoryOptions, type WaitForOptions } from "./terminal-session.js";
|
|
5
|
+
export { TerminalPilot, type NewSessionOptions } from "./terminal-pilot.js";
|
package/dist/index.js
ADDED
package/dist/keys.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export type TerminalKey = "Enter" | "Tab" | "Escape" | "Backspace" | "Delete" | "ArrowUp" | "ArrowDown" | "ArrowLeft" | "ArrowRight" | "Home" | "End" | "PageUp" | "PageDown" | "Space" | `Control+${string}` | `Alt+${string}`;
|
|
2
|
+
export declare function keyToSequence(key: TerminalKey): string;
|
package/dist/keys.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const NAMED_KEY_SEQUENCES = {
|
|
2
|
+
Enter: "\r",
|
|
3
|
+
Tab: "\t",
|
|
4
|
+
Escape: "\x1b",
|
|
5
|
+
Backspace: "\x7f",
|
|
6
|
+
Delete: "\x1b[3~",
|
|
7
|
+
ArrowUp: "\x1b[A",
|
|
8
|
+
ArrowDown: "\x1b[B",
|
|
9
|
+
ArrowRight: "\x1b[C",
|
|
10
|
+
ArrowLeft: "\x1b[D",
|
|
11
|
+
Home: "\x1b[H",
|
|
12
|
+
End: "\x1b[F",
|
|
13
|
+
PageUp: "\x1b[5~",
|
|
14
|
+
PageDown: "\x1b[6~",
|
|
15
|
+
Space: " "
|
|
16
|
+
};
|
|
17
|
+
export function keyToSequence(key) {
|
|
18
|
+
const namedSequence = NAMED_KEY_SEQUENCES[key];
|
|
19
|
+
if (namedSequence !== undefined) {
|
|
20
|
+
return namedSequence;
|
|
21
|
+
}
|
|
22
|
+
if (key.startsWith("Control+")) {
|
|
23
|
+
return controlKeyToSequence(key);
|
|
24
|
+
}
|
|
25
|
+
if (key.startsWith("Alt+")) {
|
|
26
|
+
const nestedKey = key.slice("Alt+".length);
|
|
27
|
+
if (nestedKey.length === 0) {
|
|
28
|
+
throw new Error(`Unknown terminal key: ${key}`);
|
|
29
|
+
}
|
|
30
|
+
if (nestedKey.length === 1) {
|
|
31
|
+
return "\x1b" + nestedKey;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
return "\x1b" + keyToSequence(nestedKey);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw new Error(`Unknown terminal key: ${key}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
throw new Error(`Unknown terminal key: ${key}`);
|
|
41
|
+
}
|
|
42
|
+
function controlKeyToSequence(key) {
|
|
43
|
+
const controlKey = key.slice("Control+".length);
|
|
44
|
+
if (controlKey.length !== 1) {
|
|
45
|
+
throw new Error(`Unknown terminal key: ${key}`);
|
|
46
|
+
}
|
|
47
|
+
const uppercaseLetter = controlKey.toUpperCase();
|
|
48
|
+
const charCode = uppercaseLetter.charCodeAt(0);
|
|
49
|
+
if (charCode < 65 || charCode > 90) {
|
|
50
|
+
throw new Error(`Unknown terminal key: ${key}`);
|
|
51
|
+
}
|
|
52
|
+
return String.fromCharCode(charCode - 64);
|
|
53
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
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 {};
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
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>>;
|