tmux-manager 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +521 -0
- package/dist/index.d.mts +669 -0
- package/dist/index.d.ts +669 -0
- package/dist/index.js +1990 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1951 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +63 -0
package/README.md
ADDED
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
# tmux-manager
|
|
2
|
+
|
|
3
|
+
Tmux-based session manager with lifecycle management, pluggable adapters, and blocking prompt detection. Drop-in alternative to [pty-manager](../pty-manager) — no native addons required.
|
|
4
|
+
|
|
5
|
+
## Why tmux-manager?
|
|
6
|
+
|
|
7
|
+
| | pty-manager | tmux-manager |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| **Backend** | node-pty (native C++ addon) | tmux CLI (system binary) |
|
|
10
|
+
| **Native compilation** | Required (node-gyp, build tools) | None |
|
|
11
|
+
| **Runtime support** | Node.js + Bun (with compat shim) | Any JS runtime |
|
|
12
|
+
| **Session persistence** | Dies with parent process | Survives crashes |
|
|
13
|
+
| **Crash recovery** | Not possible | `reconnect()` to existing sessions |
|
|
14
|
+
| **Output latency** | ~0ms (event-driven) | ~50-100ms (polling) |
|
|
15
|
+
| **Windows** | Supported (ConPTY) | Not supported (WSL only) |
|
|
16
|
+
|
|
17
|
+
Choose **tmux-manager** when you need crash-resilient sessions, simpler installs (CI/CD, Docker, serverless), or cross-runtime support. Choose **pty-manager** when you need sub-millisecond output latency, Windows support, or byte-level PTY streaming.
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- **Multi-session management** — Spawn and manage multiple tmux sessions concurrently
|
|
22
|
+
- **Pluggable adapters** — Built-in shell adapter, easy to create custom adapters for any CLI tool
|
|
23
|
+
- **Crash recovery** — Reconnect to orphaned tmux sessions after parent process crashes
|
|
24
|
+
- **Blocking prompt detection** — Detect login prompts, confirmations, and interactive prompts
|
|
25
|
+
- **Auto-response rules** — Automatically respond to known prompts with text or key sequences
|
|
26
|
+
- **Stall detection** — Content-based stall detection with pluggable external classifiers and exponential backoff
|
|
27
|
+
- **Task completion detection** — Settle-based fast-path that short-circuits the LLM stall classifier when the CLI returns to its idle prompt
|
|
28
|
+
- **Special key support** — Send Ctrl, Alt, Shift, and function key combinations via `sendKeys()`
|
|
29
|
+
- **Session inspection** — `tmux attach` to any managed session from another terminal
|
|
30
|
+
- **Orphan management** — List and clean up sessions from crashed processes
|
|
31
|
+
- **Zero native dependencies** — No node-gyp, no build tools, no platform-specific compilation
|
|
32
|
+
- **Event-driven** — Rich event system for session lifecycle
|
|
33
|
+
- **TypeScript-first** — Full type definitions included
|
|
34
|
+
|
|
35
|
+
## Prerequisites
|
|
36
|
+
|
|
37
|
+
tmux must be installed on the system:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# macOS
|
|
41
|
+
brew install tmux
|
|
42
|
+
|
|
43
|
+
# Ubuntu/Debian
|
|
44
|
+
sudo apt-get install tmux
|
|
45
|
+
|
|
46
|
+
# Fedora/RHEL
|
|
47
|
+
sudo dnf install tmux
|
|
48
|
+
|
|
49
|
+
# Alpine
|
|
50
|
+
apk add tmux
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Requires tmux 3.0+ and Node.js 18+.
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npm install tmux-manager
|
|
59
|
+
# or
|
|
60
|
+
pnpm add tmux-manager
|
|
61
|
+
# or
|
|
62
|
+
yarn add tmux-manager
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Quick Start
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
import { TmuxManager, ShellAdapter } from 'tmux-manager';
|
|
69
|
+
|
|
70
|
+
// Create manager
|
|
71
|
+
const manager = new TmuxManager();
|
|
72
|
+
|
|
73
|
+
// Register adapters
|
|
74
|
+
manager.registerAdapter(new ShellAdapter());
|
|
75
|
+
|
|
76
|
+
// Spawn a session
|
|
77
|
+
const handle = await manager.spawn({
|
|
78
|
+
name: 'my-shell',
|
|
79
|
+
type: 'shell',
|
|
80
|
+
workdir: '/path/to/project',
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Listen for events
|
|
84
|
+
manager.on('session_ready', ({ id }) => {
|
|
85
|
+
console.log(`Session ${id} is ready`);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Send commands
|
|
89
|
+
manager.send(handle.id, 'echo "Hello, World!"');
|
|
90
|
+
|
|
91
|
+
// Stop session
|
|
92
|
+
await manager.stop(handle.id);
|
|
93
|
+
|
|
94
|
+
// Shut down all sessions
|
|
95
|
+
await manager.shutdown();
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Crash Recovery
|
|
99
|
+
|
|
100
|
+
tmux sessions survive parent process crashes. Use `reconnect()` to reattach:
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
const manager = new TmuxManager({ sessionPrefix: 'my-app' });
|
|
104
|
+
manager.registerAdapter(new ShellAdapter());
|
|
105
|
+
|
|
106
|
+
// Find orphaned sessions from a previous crash
|
|
107
|
+
const orphans = manager.listOrphanedSessions();
|
|
108
|
+
console.log(`Found ${orphans.length} orphaned sessions`);
|
|
109
|
+
|
|
110
|
+
// Clean them up
|
|
111
|
+
manager.cleanupOrphanedSessions();
|
|
112
|
+
|
|
113
|
+
// Or reconnect to a specific session
|
|
114
|
+
const session = await manager.spawn({ name: 'recovered', type: 'shell' });
|
|
115
|
+
// session.reconnect('my-app-previous-session-id');
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
You can also inspect any managed session from another terminal:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# List all managed sessions
|
|
122
|
+
tmux list-sessions | grep my-app
|
|
123
|
+
|
|
124
|
+
# Attach to watch a session live
|
|
125
|
+
tmux attach -t my-app-session-id
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Creating Custom Adapters
|
|
129
|
+
|
|
130
|
+
### Using the Factory
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import { createAdapter } from 'tmux-manager';
|
|
134
|
+
|
|
135
|
+
const myCliAdapter = createAdapter({
|
|
136
|
+
command: 'my-cli',
|
|
137
|
+
args: ['--interactive'],
|
|
138
|
+
|
|
139
|
+
loginDetection: {
|
|
140
|
+
patterns: [/please log in/i, /auth required/i],
|
|
141
|
+
extractUrl: (output) => output.match(/https:\/\/[^\s]+/)?.[0] || null,
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
blockingPrompts: [
|
|
145
|
+
{ pattern: /\[Y\/n\]/i, type: 'config', autoResponse: 'Y' },
|
|
146
|
+
{ pattern: /continue\?/i, type: 'config', autoResponse: 'yes' },
|
|
147
|
+
],
|
|
148
|
+
|
|
149
|
+
readyIndicators: [/\$ $/, /ready>/i],
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
manager.registerAdapter(myCliAdapter);
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Extending BaseCLIAdapter
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
import { BaseCLIAdapter } from 'tmux-manager';
|
|
159
|
+
|
|
160
|
+
class MyCLIAdapter extends BaseCLIAdapter {
|
|
161
|
+
readonly adapterType = 'my-cli';
|
|
162
|
+
readonly displayName = 'My CLI Tool';
|
|
163
|
+
|
|
164
|
+
getCommand() {
|
|
165
|
+
return 'my-cli';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
getArgs(config) {
|
|
169
|
+
return ['--mode', 'interactive'];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
getEnv(config) {
|
|
173
|
+
return { MY_CLI_CONFIG: config.name };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
detectLogin(output) {
|
|
177
|
+
if (/login required/i.test(output)) {
|
|
178
|
+
return { required: true, type: 'browser' };
|
|
179
|
+
}
|
|
180
|
+
return { required: false };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
detectReady(output) {
|
|
184
|
+
return /ready>/.test(output);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
detectTaskComplete(output) {
|
|
188
|
+
return /done in \d+s/.test(output) && /ready>/.test(output);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
detectLoading(output) {
|
|
192
|
+
return /processing|loading/i.test(output);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
parseOutput(output) {
|
|
196
|
+
return {
|
|
197
|
+
type: 'response',
|
|
198
|
+
content: output.trim(),
|
|
199
|
+
isComplete: true,
|
|
200
|
+
isQuestion: output.includes('?'),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
getPromptPattern() {
|
|
205
|
+
return /my-cli>/;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## API Reference
|
|
211
|
+
|
|
212
|
+
### TmuxManager
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
class TmuxManager extends EventEmitter {
|
|
216
|
+
constructor(config?: TmuxManagerConfig);
|
|
217
|
+
|
|
218
|
+
// Adapter management
|
|
219
|
+
registerAdapter(adapter: CLIAdapter): void;
|
|
220
|
+
readonly adapters: AdapterRegistry;
|
|
221
|
+
|
|
222
|
+
// Session lifecycle
|
|
223
|
+
spawn(config: SpawnConfig): Promise<SessionHandle>;
|
|
224
|
+
stop(id: string, options?: StopOptions): Promise<void>;
|
|
225
|
+
stopAll(options?: StopOptions): Promise<void>;
|
|
226
|
+
shutdown(): Promise<void>;
|
|
227
|
+
|
|
228
|
+
// Session operations
|
|
229
|
+
get(id: string): SessionHandle | null;
|
|
230
|
+
list(filter?: SessionFilter): SessionHandle[];
|
|
231
|
+
send(id: string, message: string): SessionMessage;
|
|
232
|
+
logs(id: string, options?: LogOptions): AsyncIterable<string>;
|
|
233
|
+
metrics(id: string): { uptime?: number } | null;
|
|
234
|
+
has(id: string): boolean;
|
|
235
|
+
getStatusCounts(): Record<SessionStatus, number>;
|
|
236
|
+
|
|
237
|
+
// Crash recovery
|
|
238
|
+
listOrphanedSessions(): Array<{ name: string; created: string; attached: boolean }>;
|
|
239
|
+
cleanupOrphanedSessions(): void;
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### TmuxManagerConfig
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
interface TmuxManagerConfig {
|
|
247
|
+
logger?: Logger;
|
|
248
|
+
maxLogLines?: number; // Default: 1000
|
|
249
|
+
stallDetectionEnabled?: boolean; // Default: false
|
|
250
|
+
stallTimeoutMs?: number; // Default: 8000
|
|
251
|
+
onStallClassify?: (sessionId: string, recentOutput: string, stallDurationMs: number)
|
|
252
|
+
=> Promise<StallClassification | null>;
|
|
253
|
+
historyLimit?: number; // Tmux scrollback lines (default: 50000)
|
|
254
|
+
sessionPrefix?: string; // Tmux session name prefix (default: 'parallax')
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### TmuxTransport
|
|
259
|
+
|
|
260
|
+
Low-level tmux CLI wrapper. Used internally by TmuxSession but available for direct use.
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
class TmuxTransport {
|
|
264
|
+
spawn(sessionName: string, options: TmuxSpawnOptions): void;
|
|
265
|
+
isAlive(sessionName: string): boolean;
|
|
266
|
+
kill(sessionName: string): void;
|
|
267
|
+
signal(sessionName: string, sig: string): void;
|
|
268
|
+
sendText(sessionName: string, text: string): void;
|
|
269
|
+
sendKey(sessionName: string, key: string): void;
|
|
270
|
+
capturePane(sessionName: string, options?: TmuxCaptureOptions): string;
|
|
271
|
+
startOutputStreaming(sessionName: string, callback: (data: string) => void, pollIntervalMs?: number): void;
|
|
272
|
+
stopOutputStreaming(sessionName: string): void;
|
|
273
|
+
getPanePid(sessionName: string): number | undefined;
|
|
274
|
+
getPaneDimensions(sessionName: string): { cols: number; rows: number };
|
|
275
|
+
resize(sessionName: string, cols: number, rows: number): void;
|
|
276
|
+
isPaneAlive(sessionName: string): boolean;
|
|
277
|
+
getPaneExitStatus(sessionName: string): number | undefined;
|
|
278
|
+
destroy(): void;
|
|
279
|
+
|
|
280
|
+
static listSessions(prefix?: string): Array<{ name: string; created: string; attached: boolean }>;
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Events
|
|
285
|
+
|
|
286
|
+
| Event | Payload | Description |
|
|
287
|
+
|-------|---------|-------------|
|
|
288
|
+
| `session_started` | `SessionHandle` | Session spawn initiated |
|
|
289
|
+
| `session_ready` | `SessionHandle` | Session ready for input (after settle delay) |
|
|
290
|
+
| `session_stopped` | `SessionHandle, reason` | Session terminated |
|
|
291
|
+
| `session_error` | `SessionHandle, error` | Error occurred |
|
|
292
|
+
| `login_required` | `SessionHandle, instructions?, url?` | Auth required |
|
|
293
|
+
| `blocking_prompt` | `SessionHandle, promptInfo, autoResponded` | Prompt detected |
|
|
294
|
+
| `message` | `SessionMessage` | Parsed message received |
|
|
295
|
+
| `question` | `SessionHandle, question` | Question detected |
|
|
296
|
+
| `stall_detected` | `SessionHandle, recentOutput, stallDurationMs` | Output stalled, needs classification |
|
|
297
|
+
| `task_complete` | `SessionHandle` | Agent finished task, returned to idle |
|
|
298
|
+
|
|
299
|
+
### SpawnConfig
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
interface SpawnConfig {
|
|
303
|
+
id?: string; // Auto-generated if not provided
|
|
304
|
+
name: string; // Human-readable name
|
|
305
|
+
type: string; // Adapter type
|
|
306
|
+
workdir?: string; // Working directory
|
|
307
|
+
env?: Record<string, string>; // Environment variables
|
|
308
|
+
cols?: number; // Terminal columns (default: 120)
|
|
309
|
+
rows?: number; // Terminal rows (default: 40)
|
|
310
|
+
timeout?: number; // Session timeout in ms
|
|
311
|
+
readySettleMs?: number; // Override adapter's ready settle delay
|
|
312
|
+
stallTimeoutMs?: number; // Override stall timeout for this session
|
|
313
|
+
traceTaskCompletion?: boolean; // Verbose completion trace logs (default: false)
|
|
314
|
+
inheritProcessEnv?: boolean; // Inherit parent process env (default: true)
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### SessionHandle
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
interface SessionHandle {
|
|
322
|
+
id: string;
|
|
323
|
+
name: string;
|
|
324
|
+
type: string;
|
|
325
|
+
status: SessionStatus;
|
|
326
|
+
pid?: number;
|
|
327
|
+
startedAt?: Date;
|
|
328
|
+
lastActivityAt?: Date;
|
|
329
|
+
tmuxSessionName?: string; // For reconnection / tmux attach
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
type SessionStatus =
|
|
333
|
+
| 'pending'
|
|
334
|
+
| 'starting'
|
|
335
|
+
| 'authenticating'
|
|
336
|
+
| 'ready'
|
|
337
|
+
| 'busy'
|
|
338
|
+
| 'stopping'
|
|
339
|
+
| 'stopped'
|
|
340
|
+
| 'error';
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## Special Keys
|
|
344
|
+
|
|
345
|
+
Send special key sequences to sessions. Keys are mapped to tmux key names internally.
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
// Send single key
|
|
349
|
+
session.sendKeys('ctrl+c'); // Interrupt
|
|
350
|
+
session.sendKeys('ctrl+d'); // EOF
|
|
351
|
+
|
|
352
|
+
// Send multiple keys
|
|
353
|
+
session.sendKeys(['up', 'up', 'enter']); // Navigate history
|
|
354
|
+
|
|
355
|
+
// Navigation
|
|
356
|
+
session.sendKeys('home'); // Start of line
|
|
357
|
+
session.sendKeys('end'); // End of line
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
**Supported keys:**
|
|
361
|
+
|
|
362
|
+
| Category | Examples |
|
|
363
|
+
|----------|----------|
|
|
364
|
+
| Ctrl+letter | `ctrl+a` through `ctrl+z` |
|
|
365
|
+
| Navigation | `up`, `down`, `left`, `right`, `home`, `end`, `pageup`, `pagedown` |
|
|
366
|
+
| Shift+Nav | `shift+up`, `shift+down`, `shift+left`, `shift+right`, `shift+tab` |
|
|
367
|
+
| Editing | `enter`, `tab`, `backspace`, `delete`, `insert`, `escape`, `space` |
|
|
368
|
+
| Function | `f1` through `f12` |
|
|
369
|
+
|
|
370
|
+
### TMUX_KEY_MAP
|
|
371
|
+
|
|
372
|
+
Access the full key mapping for reference:
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
import { TMUX_KEY_MAP } from 'tmux-manager';
|
|
376
|
+
|
|
377
|
+
console.log(TMUX_KEY_MAP['ctrl+c']); // 'C-c'
|
|
378
|
+
console.log(TMUX_KEY_MAP['enter']); // 'Enter'
|
|
379
|
+
console.log(TMUX_KEY_MAP['up']); // 'Up'
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
## Auto-Response Rules
|
|
383
|
+
|
|
384
|
+
Automatically handle known prompts without human intervention.
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
interface AutoResponseRule {
|
|
388
|
+
pattern: RegExp;
|
|
389
|
+
type: BlockingPromptType;
|
|
390
|
+
response: string;
|
|
391
|
+
responseType?: 'text' | 'keys';
|
|
392
|
+
keys?: string[];
|
|
393
|
+
description: string;
|
|
394
|
+
safe?: boolean;
|
|
395
|
+
once?: boolean;
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
**Text response** — sends text + Enter:
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
{ pattern: /create new file\?/i, type: 'permission', response: 'y', description: 'Allow file creation' }
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
**Key sequence response** — sends key presses for TUI menus:
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
{ pattern: /update available/i, type: 'config', response: '', responseType: 'keys', keys: ['down', 'enter'], description: 'Skip update', once: true }
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
## Ready Detection
|
|
412
|
+
|
|
413
|
+
When `detectReady()` first matches during startup, the session waits for output to settle (default: 100ms) before emitting `session_ready`. This prevents sending input while a TUI is still rendering.
|
|
414
|
+
|
|
415
|
+
```typescript
|
|
416
|
+
class MyCLIAdapter extends BaseCLIAdapter {
|
|
417
|
+
readonly readySettleMs = 500; // Slower TUI — wait longer
|
|
418
|
+
// ...
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Or override per-spawn
|
|
422
|
+
const handle = await manager.spawn({
|
|
423
|
+
name: 'agent',
|
|
424
|
+
type: 'my-cli',
|
|
425
|
+
readySettleMs: 1000,
|
|
426
|
+
});
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
## Stall Detection & Task Completion
|
|
430
|
+
|
|
431
|
+
### Stall Detection
|
|
432
|
+
|
|
433
|
+
Content-based stall detection monitors sessions for output that stops changing. When a stall is detected, the `stall_detected` event fires for external classification.
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
const manager = new TmuxManager({
|
|
437
|
+
stallDetectionEnabled: true,
|
|
438
|
+
stallTimeoutMs: 15000,
|
|
439
|
+
onStallClassify: async (sessionId, output, stallDurationMs) => {
|
|
440
|
+
return {
|
|
441
|
+
type: 'blocking_prompt',
|
|
442
|
+
confidence: 0.9,
|
|
443
|
+
suggestedResponse: 'keys:enter',
|
|
444
|
+
reasoning: 'Dialog detected',
|
|
445
|
+
};
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
Stall backoff doubles exponentially (8s -> 16s -> 30s cap) when the classifier returns `still_working`, and resets when new content arrives.
|
|
451
|
+
|
|
452
|
+
### Task Completion Fast-Path
|
|
453
|
+
|
|
454
|
+
Adapters can implement `detectTaskComplete()` to recognize completion patterns without invoking an LLM classifier:
|
|
455
|
+
|
|
456
|
+
```typescript
|
|
457
|
+
class MyCLIAdapter extends BaseCLIAdapter {
|
|
458
|
+
detectTaskComplete(output: string): boolean {
|
|
459
|
+
return /completed in \d+s/.test(output) && /my-cli>/.test(output);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
### Loading Pattern Suppression
|
|
465
|
+
|
|
466
|
+
Adapters can implement `detectLoading()` to suppress stall detection during active work:
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
class MyCLIAdapter extends BaseCLIAdapter {
|
|
470
|
+
detectLoading(output: string): boolean {
|
|
471
|
+
return /thinking|reading files/i.test(output);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
## Built-in Adapters
|
|
477
|
+
|
|
478
|
+
### ShellAdapter
|
|
479
|
+
|
|
480
|
+
```typescript
|
|
481
|
+
import { ShellAdapter } from 'tmux-manager';
|
|
482
|
+
|
|
483
|
+
const adapter = new ShellAdapter({
|
|
484
|
+
shell: '/bin/zsh', // Default: $SHELL or /bin/bash
|
|
485
|
+
prompt: 'pty> ', // Default: 'pty> '
|
|
486
|
+
});
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
The shell adapter automatically configures the shell to use the specified prompt (sets both `PS1` and `PROMPT` for bash/zsh compatibility, and passes `--norc`/`-f` flags to prevent rc files from overriding it).
|
|
490
|
+
|
|
491
|
+
## Architecture
|
|
492
|
+
|
|
493
|
+
```
|
|
494
|
+
TmuxManager — Multi-session orchestration, events, orphan management
|
|
495
|
+
└─ TmuxSession — State machine, detection logic, I/O
|
|
496
|
+
└─ TmuxTransport — Low-level tmux CLI wrapper (execSync)
|
|
497
|
+
└─ tmux — System binary (new-session, send-keys, capture-pane)
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
**Output streaming** uses `capture-pane` polling at 100ms intervals. Each poll captures the current pane content, diffs against the previous capture, and emits only new data. This is more reliable than `pipe-pane` and works consistently across platforms.
|
|
501
|
+
|
|
502
|
+
**Exit detection** uses `remain-on-exit` with polling of `#{pane_dead}` at 1-second intervals.
|
|
503
|
+
|
|
504
|
+
**Key mapping** translates key names (e.g., `ctrl+c`, `enter`, `up`) to tmux key names (e.g., `C-c`, `Enter`, `Up`) via `TMUX_KEY_MAP`.
|
|
505
|
+
|
|
506
|
+
## Blocking Prompt Types
|
|
507
|
+
|
|
508
|
+
| Type | Description |
|
|
509
|
+
|------|-------------|
|
|
510
|
+
| `login` | Authentication required |
|
|
511
|
+
| `update` | Update/upgrade available |
|
|
512
|
+
| `config` | Configuration choice needed |
|
|
513
|
+
| `tos` | Terms of service acceptance |
|
|
514
|
+
| `model_select` | Model/version selection |
|
|
515
|
+
| `project_select` | Project/workspace selection |
|
|
516
|
+
| `permission` | Permission request |
|
|
517
|
+
| `unknown` | Unrecognized prompt |
|
|
518
|
+
|
|
519
|
+
## License
|
|
520
|
+
|
|
521
|
+
MIT
|