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/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