@seflless/ghosttown 1.7.0 → 1.10.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seflless/ghosttown",
3
- "version": "1.7.0",
3
+ "version": "1.10.0",
4
4
  "description": "Web-based terminal emulator using Ghostty's VT100 parser via WebAssembly",
5
5
  "type": "module",
6
6
  "main": "./dist/ghostty-web.umd.cjs",
@@ -68,6 +68,7 @@
68
68
  "build": "bun run clean && bun run build:wasm && bun run build:lib && bun run build:wasm-copy",
69
69
  "build:wasm": "./scripts/build-wasm.sh",
70
70
  "build:lib": "vite build",
71
+ "build:cli": "tsc -p tsconfig.cli.json",
71
72
  "build:wasm-copy": "cp ghostty-vt.wasm dist/",
72
73
  "clean": "rm -rf dist",
73
74
  "preview": "vite preview",
@@ -80,7 +81,7 @@
80
81
  "test:e2e": "playwright test",
81
82
  "test:e2e:headed": "playwright test --headed",
82
83
  "test:e2e:ui": "playwright test --ui",
83
- "prepublishOnly": "bun run build",
84
+ "prepublishOnly": "bun run build && bun run build:cli",
84
85
  "cli:publish": "node scripts/cli-publish.js",
85
86
  "kill:8080": "kill -9 $(lsof -ti :8080)"
86
87
  },
package/src/cli.js CHANGED
@@ -8,7 +8,7 @@
8
8
  * ghosttown [options] [command]
9
9
  *
10
10
  * Options:
11
- * -p, --port <port> Port to listen on (default: 8080, or PORT env var)
11
+ * -p, --port <port> Port to listen on (default: auto-discover available port)
12
12
  * -n, --name <name> Give the session a custom name
13
13
  * -k, --kill [session] Kill a session (current if inside one, or specify by name)
14
14
  * -ka, --kill-all Kill all ghosttown sessions
@@ -53,7 +53,7 @@ import { asciiArt } from '../bin/ascii.js';
53
53
  // Session name utilities
54
54
  import { displayNameExists, findSession, validateSessionName } from './session-utils.js';
55
55
  // Session management - custom PTY session manager (replaces tmux)
56
- import { SessionManager } from './session/session-manager.ts';
56
+ import { SessionManager } from './session/session-manager.js';
57
57
 
58
58
  // Global SessionManager instance (lazy-initialized)
59
59
  let sessionManager = null;
@@ -91,6 +91,64 @@ const SESSION_EXPIRATION_MS = 3 * 24 * 60 * 60 * 1000;
91
91
  const SESSION_FILE_DIR = path.join(homedir(), '.config', 'ghosttown');
92
92
  const SESSION_FILE_PATH = path.join(SESSION_FILE_DIR, 'sessions.json');
93
93
 
94
+ // Server config file for port discovery (allows CLI to find running server)
95
+ const SERVER_CONFIG_PATH = path.join(SESSION_FILE_DIR, 'server.json');
96
+
97
+ /**
98
+ * Write server config to disk for port discovery
99
+ */
100
+ function writeServerConfig(port) {
101
+ try {
102
+ if (!fs.existsSync(SESSION_FILE_DIR)) {
103
+ fs.mkdirSync(SESSION_FILE_DIR, { recursive: true });
104
+ }
105
+ fs.writeFileSync(
106
+ SERVER_CONFIG_PATH,
107
+ JSON.stringify({ port, pid: process.pid, startedAt: Date.now() }, null, 2)
108
+ );
109
+ } catch (err) {
110
+ console.error('Warning: Failed to write server config:', err.message);
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Remove server config file on shutdown
116
+ */
117
+ function removeServerConfig() {
118
+ try {
119
+ if (fs.existsSync(SERVER_CONFIG_PATH)) {
120
+ fs.unlinkSync(SERVER_CONFIG_PATH);
121
+ }
122
+ } catch (err) {
123
+ // Ignore errors during cleanup
124
+ }
125
+ }
126
+
127
+ // Preferred ports to try in order before falling back to auto-discovery (port 0)
128
+ // 64057 - strongest visual mapping to "GHOST"
129
+ // 6057 - subtle and clean
130
+ // 4040 - "ghost server not found" energy (404)
131
+ // 1313 - haunted house number energy
132
+ const PREFERRED_PORTS = [64057, 6057, 4040, 1313];
133
+
134
+ /**
135
+ * Find an available port from the preferred list, or return 0 for auto-discovery
136
+ */
137
+ function findPreferredPort() {
138
+ for (const port of PREFERRED_PORTS) {
139
+ try {
140
+ // Quick check if port is in use using lsof
141
+ execSync(`lsof -i:${port}`, { stdio: 'pipe' });
142
+ // If lsof succeeds, port is in use, try next
143
+ } catch (e) {
144
+ // lsof returns non-zero if port is free - use this port
145
+ return port;
146
+ }
147
+ }
148
+ // All preferred ports in use, fall back to auto-discovery
149
+ return 0;
150
+ }
151
+
94
152
  // In-memory session store: Map<token, { username, createdAt, lastActivity }>
95
153
  const authSessions = new Map();
96
154
 
@@ -636,22 +694,41 @@ function getServerStatus() {
636
694
  const isRunning = output.split('\n').includes(SERVER_SESSION_NAME);
637
695
 
638
696
  if (!isRunning) {
697
+ // Also clean up stale server.json if tmux session doesn't exist
698
+ removeServerConfig();
639
699
  return { running: false, port: null, useHttp: false };
640
700
  }
641
701
 
642
- // Get the port from tmux environment
643
- let port = 8080;
702
+ // Get the port - prefer server.json (for auto port discovery), fall back to tmux env
703
+ let port = null;
644
704
  try {
645
- const envOutput = execSync(`tmux show-environment -t ${SERVER_SESSION_NAME} GHOSTTOWN_PORT`, {
646
- encoding: 'utf8',
647
- stdio: ['pipe', 'pipe', 'pipe'],
648
- });
649
- const parsedPort = Number.parseInt(envOutput.split('=')[1], 10);
650
- if (!Number.isNaN(parsedPort)) {
651
- port = parsedPort;
705
+ if (fs.existsSync(SERVER_CONFIG_PATH)) {
706
+ const config = JSON.parse(fs.readFileSync(SERVER_CONFIG_PATH, 'utf8'));
707
+ if (config.port) {
708
+ port = config.port;
709
+ }
652
710
  }
653
711
  } catch (e) {
654
- // Use default port
712
+ // Failed to read server.json, try tmux env
713
+ }
714
+
715
+ // Fall back to tmux environment if server.json doesn't have the port
716
+ if (port === null) {
717
+ try {
718
+ const envOutput = execSync(
719
+ `tmux show-environment -t ${SERVER_SESSION_NAME} GHOSTTOWN_PORT`,
720
+ {
721
+ encoding: 'utf8',
722
+ stdio: ['pipe', 'pipe', 'pipe'],
723
+ }
724
+ );
725
+ const parsedPort = Number.parseInt(envOutput.split('=')[1], 10);
726
+ if (!Number.isNaN(parsedPort) && parsedPort > 0) {
727
+ port = parsedPort;
728
+ }
729
+ } catch (e) {
730
+ // No port info available
731
+ }
655
732
  }
656
733
 
657
734
  // Get the protocol from tmux environment (default to HTTPS if not set)
@@ -1201,7 +1278,7 @@ function killAllSessions() {
1201
1278
  * @param {number} port - Port to listen on
1202
1279
  * @param {boolean} useHttp - Use HTTP instead of HTTPS (default: false, meaning HTTPS is used)
1203
1280
  */
1204
- function startServerInBackground(port = 8080, useHttp = false) {
1281
+ async function startServerInBackground(port = 0, useHttp = false) {
1205
1282
  // Check if tmux is installed
1206
1283
  if (!checkTmuxInstalled()) {
1207
1284
  printTmuxInstallHelp();
@@ -1219,7 +1296,7 @@ function startServerInBackground(port = 8080, useHttp = false) {
1219
1296
  console.log('');
1220
1297
  console.log(` ${RED}Error:${RESET} A server is already running (port ${status.port}).`);
1221
1298
  console.log('');
1222
- if (status.port !== port) {
1299
+ if (port !== 0 && status.port !== port) {
1223
1300
  console.log(` To switch to port ${port}:`);
1224
1301
  console.log(` ${BEIGE}gt stop && gt start -p ${port}${RESET}`);
1225
1302
  } else {
@@ -1230,18 +1307,20 @@ function startServerInBackground(port = 8080, useHttp = false) {
1230
1307
  process.exit(1);
1231
1308
  }
1232
1309
 
1233
- // Check if port is already in use by another process
1234
- try {
1235
- execSync(`lsof -i:${port}`, { stdio: 'pipe' });
1236
- // If lsof succeeds, port is in use
1237
- console.log('');
1238
- console.log(` ${RED}Error:${RESET} Port ${port} is already in use.`);
1239
- console.log('');
1240
- console.log(` Try a different port: ${BEIGE}gt start -p <port>${RESET}`);
1241
- console.log('');
1242
- process.exit(1);
1243
- } catch (e) {
1244
- // lsof returns non-zero if port is free - this is what we want
1310
+ // Check if specific port is already in use by another process (skip for port 0 auto-discovery)
1311
+ if (port !== 0) {
1312
+ try {
1313
+ execSync(`lsof -i:${port}`, { stdio: 'pipe' });
1314
+ // If lsof succeeds, port is in use
1315
+ console.log('');
1316
+ console.log(` ${RED}Error:${RESET} Port ${port} is already in use.`);
1317
+ console.log('');
1318
+ console.log(` Try a different port: ${BEIGE}gt start -p <port>${RESET}`);
1319
+ console.log('');
1320
+ process.exit(1);
1321
+ } catch (e) {
1322
+ // lsof returns non-zero if port is free - this is what we want
1323
+ }
1245
1324
  }
1246
1325
 
1247
1326
  try {
@@ -1280,6 +1359,27 @@ function startServerInBackground(port = 8080, useHttp = false) {
1280
1359
  { stdio: 'pipe' }
1281
1360
  );
1282
1361
 
1362
+ // Wait for server to start and get actual port from server.json
1363
+ // The server writes to server.json once it starts listening
1364
+ let actualPort = port;
1365
+ let attempts = 0;
1366
+ const maxAttempts = 30; // 3 seconds max wait
1367
+ while (attempts < maxAttempts) {
1368
+ try {
1369
+ if (fs.existsSync(SERVER_CONFIG_PATH)) {
1370
+ const config = JSON.parse(fs.readFileSync(SERVER_CONFIG_PATH, 'utf8'));
1371
+ if (config.port) {
1372
+ actualPort = config.port;
1373
+ break;
1374
+ }
1375
+ }
1376
+ } catch (e) {
1377
+ // Ignore read errors, keep waiting
1378
+ }
1379
+ await new Promise((resolve) => setTimeout(resolve, 100));
1380
+ attempts++;
1381
+ }
1382
+
1283
1383
  // Get local IPs for display
1284
1384
  const localIPs = getLocalIPs();
1285
1385
  const protocol = useHttp ? 'http' : 'https';
@@ -1287,9 +1387,11 @@ function startServerInBackground(port = 8080, useHttp = false) {
1287
1387
  console.log('');
1288
1388
  console.log(` ${BOLD_YELLOW}Ghosttown server started!${RESET}`);
1289
1389
  console.log('');
1290
- console.log(` ${CYAN}Open:${RESET} ${BEIGE}${protocol}://localhost:${port}${RESET}`);
1390
+ console.log(` ${CYAN}Open:${RESET} ${BEIGE}${protocol}://localhost:${actualPort}${RESET}`);
1291
1391
  if (localIPs.length > 0) {
1292
- console.log(` ${CYAN}Network:${RESET} ${BEIGE}${protocol}://${localIPs[0]}:${port}${RESET}`);
1392
+ console.log(
1393
+ ` ${CYAN}Network:${RESET} ${BEIGE}${protocol}://${localIPs[0]}:${actualPort}${RESET}`
1394
+ );
1293
1395
  }
1294
1396
  console.log('');
1295
1397
  console.log(` ${CYAN}To stop:${RESET} ${BEIGE}gt stop${RESET}`);
@@ -1310,38 +1412,53 @@ function startServerInBackground(port = 8080, useHttp = false) {
1310
1412
  * Stop the background server
1311
1413
  */
1312
1414
  function stopServer() {
1313
- // Check if tmux is installed
1314
- if (!checkTmuxInstalled()) {
1315
- printTmuxInstallHelp();
1316
- }
1317
-
1318
1415
  const RESET = '\x1b[0m';
1319
1416
  const RED = '\x1b[31m';
1320
1417
  const BOLD_YELLOW = '\x1b[1;93m';
1321
1418
  const BEIGE = '\x1b[38;2;255;220;150m';
1322
1419
 
1323
- const status = getServerStatus();
1420
+ let stopped = false;
1324
1421
 
1325
- if (!status.running) {
1326
- console.log('');
1327
- console.log(` ${BEIGE}No server is currently running.${RESET}`);
1328
- console.log('');
1329
- console.log(' To start: gt start');
1330
- console.log('');
1331
- process.exit(0);
1422
+ // First, try to stop using PID from server.json (works for foreground servers)
1423
+ try {
1424
+ if (fs.existsSync(SERVER_CONFIG_PATH)) {
1425
+ const config = JSON.parse(fs.readFileSync(SERVER_CONFIG_PATH, 'utf8'));
1426
+ if (config.pid) {
1427
+ try {
1428
+ process.kill(config.pid, 'SIGTERM');
1429
+ stopped = true;
1430
+ } catch (e) {
1431
+ // Process doesn't exist or can't be killed
1432
+ }
1433
+ }
1434
+ }
1435
+ } catch (e) {
1436
+ // Ignore errors reading config
1332
1437
  }
1333
1438
 
1334
- try {
1335
- execSync(`tmux kill-session -t ${SERVER_SESSION_NAME}`, { stdio: 'pipe' });
1439
+ // Also try to kill tmux session (for background servers started with `gt start`)
1440
+ if (checkTmuxInstalled()) {
1441
+ try {
1442
+ execSync(`tmux kill-session -t ${SERVER_SESSION_NAME}`, { stdio: 'pipe' });
1443
+ stopped = true;
1444
+ } catch (err) {
1445
+ // tmux session doesn't exist or can't be killed
1446
+ }
1447
+ }
1336
1448
 
1449
+ // Clean up server.json
1450
+ removeServerConfig();
1451
+
1452
+ if (stopped) {
1337
1453
  console.log('');
1338
1454
  console.log(` ${BOLD_YELLOW}Server stopped.${RESET}`);
1339
1455
  console.log('');
1340
- } catch (err) {
1456
+ } else {
1341
1457
  console.log('');
1342
- console.log(` ${RED}Error:${RESET} Failed to stop server.`);
1458
+ console.log(` ${BEIGE}No server is currently running.${RESET}`);
1459
+ console.log('');
1460
+ console.log(' To start: gt start');
1343
1461
  console.log('');
1344
- process.exit(1);
1345
1462
  }
1346
1463
 
1347
1464
  process.exit(0);
@@ -1544,7 +1661,7 @@ function renameSession(renameArgs) {
1544
1661
  // Parse CLI arguments
1545
1662
  // ============================================================================
1546
1663
 
1547
- function parseArgs(argv) {
1664
+ async function parseArgs(argv) {
1548
1665
  const args = argv.slice(2);
1549
1666
  let port = null;
1550
1667
  let command = null;
@@ -1562,7 +1679,7 @@ function parseArgs(argv) {
1562
1679
  Usage: ghosttown [options] [command]
1563
1680
 
1564
1681
  Options:
1565
- -p, --port <port> Port to listen on (default: 8080, or PORT env var)
1682
+ -p, --port <port> Port to listen on (default: auto-discover available port)
1566
1683
  -n, --name <name> Give the session a custom name (use with a command)
1567
1684
  --http Use HTTP instead of HTTPS (default is HTTPS)
1568
1685
  --no-auth Disable authentication (binds to localhost only)
@@ -1572,7 +1689,7 @@ Options:
1572
1689
  -h, --help Show this help message
1573
1690
 
1574
1691
  Commands:
1575
- start [-p <port>] Start the server in the background (default port: 8080)
1692
+ start [-p <port>] Start the server in the background (auto-discovers available port)
1576
1693
  stop Stop the background server
1577
1694
  status Show server status and URLs
1578
1695
  list List all ghosttown tmux sessions
@@ -1680,7 +1797,7 @@ Aliases:
1680
1797
  // Handle start command
1681
1798
  else if (arg === 'start') {
1682
1799
  // Parse remaining args after 'start' for -p and --http flags
1683
- let startPort = 8080;
1800
+ let startPort = findPreferredPort(); // Try preferred ports, fall back to auto-discovery
1684
1801
  let startUseHttp = false;
1685
1802
  const remainingArgs = args.slice(i + 1);
1686
1803
  for (let j = 0; j < remainingArgs.length; j++) {
@@ -1700,7 +1817,7 @@ Aliases:
1700
1817
  }
1701
1818
  }
1702
1819
  handled = true;
1703
- startServerInBackground(startPort, startUseHttp);
1820
+ await startServerInBackground(startPort, startUseHttp);
1704
1821
  // startServerInBackground exits, so this won't be reached
1705
1822
  }
1706
1823
 
@@ -1772,7 +1889,41 @@ Aliases:
1772
1889
  // ============================================================================
1773
1890
 
1774
1891
  function startWebServer(cliArgs) {
1775
- const HTTP_PORT = cliArgs.port || process.env.PORT || 8080;
1892
+ // ANSI color codes for formatted output
1893
+ const RESET = '\x1b[0m';
1894
+ const RED = '\x1b[31m';
1895
+ const CYAN = '\x1b[36m';
1896
+ const BEIGE = '\x1b[38;2;255;220;150m';
1897
+
1898
+ // Check if a server is already running (prevents multiple instances)
1899
+ try {
1900
+ if (fs.existsSync(SERVER_CONFIG_PATH)) {
1901
+ const config = JSON.parse(fs.readFileSync(SERVER_CONFIG_PATH, 'utf8'));
1902
+ // Check if the PID is still alive
1903
+ if (config.pid) {
1904
+ try {
1905
+ process.kill(config.pid, 0); // Signal 0 = just check if process exists
1906
+ // Process exists - server already running
1907
+ console.log('');
1908
+ console.log(` ${RED}Error:${RESET} A gt server is already running.`);
1909
+ console.log('');
1910
+ console.log(` ${CYAN}To stop:${RESET} ${BEIGE}gt stop${RESET}`);
1911
+ console.log(` ${CYAN}To check:${RESET} ${BEIGE}gt status${RESET}`);
1912
+ console.log('');
1913
+ process.exit(1);
1914
+ } catch (e) {
1915
+ // Process doesn't exist - stale config, clean it up
1916
+ removeServerConfig();
1917
+ }
1918
+ }
1919
+ }
1920
+ } catch (e) {
1921
+ // Ignore errors reading config
1922
+ }
1923
+
1924
+ // Try preferred ports (64057, 6057, 4040) in order, fall back to auto-discovery (port 0)
1925
+ // if all are in use. User can still override with -p flag or PORT env var.
1926
+ const HTTP_PORT = cliArgs.port || process.env.PORT || findPreferredPort();
1776
1927
  const USE_HTTPS = !cliArgs.useHttp;
1777
1928
  const NO_AUTH = cliArgs.noAuth || false;
1778
1929
  const backgroundMode = cliArgs.serverBackgroundMode || false;
@@ -3842,7 +3993,7 @@ function startWebServer(cliArgs) {
3842
3993
  // Startup
3843
3994
  // ============================================================================
3844
3995
 
3845
- function printBanner(url, backgroundMode = false) {
3996
+ function printBanner(url, port, backgroundMode = false) {
3846
3997
  const localIPs = getLocalIPs();
3847
3998
  // ANSI color codes
3848
3999
  const RESET = '\x1b[0m';
@@ -3863,7 +4014,7 @@ function startWebServer(cliArgs) {
3863
4014
  networkCount++;
3864
4015
  const spaces = networkCount !== 1 ? ' ' : '';
3865
4016
  const protocol = USE_HTTPS ? 'https' : 'http';
3866
- network.push(`${spaces}${BEIGE}${protocol}://${ip}:${HTTP_PORT}${RESET}\n`);
4017
+ network.push(`${spaces}${BEIGE}${protocol}://${ip}:${port}${RESET}\n`);
3867
4018
  }
3868
4019
  }
3869
4020
  console.log(`\n${network.join('')} `);
@@ -3886,6 +4037,8 @@ function startWebServer(cliArgs) {
3886
4037
 
3887
4038
  process.on('SIGINT', () => {
3888
4039
  console.log('\n\nShutting down...');
4040
+ // Clean up server config file for port discovery
4041
+ removeServerConfig();
3889
4042
  for (const [ws, conn] of wsConnections.entries()) {
3890
4043
  conn.pty.kill();
3891
4044
  ws.close();
@@ -3895,6 +4048,12 @@ function startWebServer(cliArgs) {
3895
4048
  });
3896
4049
 
3897
4050
  httpServer.listen(HTTP_PORT, BIND_ADDRESS, async () => {
4051
+ // Get actual port (important when HTTP_PORT is 0 for auto-discovery)
4052
+ const actualPort = httpServer.address().port;
4053
+
4054
+ // Write server config for port discovery by CLI commands
4055
+ writeServerConfig(actualPort);
4056
+
3898
4057
  // Display ASCII art banner
3899
4058
  try {
3900
4059
  const imagePath = path.join(__dirname, '..', 'bin', 'assets', 'ghosts.png');
@@ -3911,7 +4070,7 @@ function startWebServer(cliArgs) {
3911
4070
  }
3912
4071
 
3913
4072
  const protocol = USE_HTTPS ? 'https' : 'http';
3914
- printBanner(`${protocol}://localhost:${HTTP_PORT}`, backgroundMode);
4073
+ printBanner(`${protocol}://localhost:${actualPort}`, actualPort, backgroundMode);
3915
4074
 
3916
4075
  // Show warning when auth is disabled
3917
4076
  if (NO_AUTH) {
@@ -3924,8 +4083,8 @@ function startWebServer(cliArgs) {
3924
4083
  // Main entry point
3925
4084
  // ============================================================================
3926
4085
 
3927
- export function run(argv) {
3928
- const cliArgs = parseArgs(argv);
4086
+ export async function run(argv) {
4087
+ const cliArgs = await parseArgs(argv);
3929
4088
 
3930
4089
  if (cliArgs.handled) {
3931
4090
  return;
@@ -0,0 +1,118 @@
1
+ /**
2
+ * History Replay
3
+ *
4
+ * Streams scrollback history to clients in chunks.
5
+ * Handles large histories (10,000+ lines) without blocking.
6
+ *
7
+ * Replay is throttled to prevent overwhelming the client with data
8
+ * and to show a progress indicator for long replays.
9
+ */
10
+ import { EventEmitter } from 'events';
11
+ import type { OutputChunk } from './output-recorder.js';
12
+ /**
13
+ * Configuration for HistoryReplay.
14
+ */
15
+ export interface HistoryReplayConfig {
16
+ /** Chunks to send per batch (default: 100) */
17
+ chunkSize?: number;
18
+ /** Delay between batches in milliseconds (default: 10) */
19
+ batchDelay?: number;
20
+ /** Maximum replay time in milliseconds (default: 30000 = 30s) */
21
+ maxReplayTime?: number;
22
+ }
23
+ /**
24
+ * Progress information during replay.
25
+ */
26
+ export interface ReplayProgress {
27
+ /** Number of chunks sent */
28
+ sent: number;
29
+ /** Total number of chunks */
30
+ total: number;
31
+ /** Percentage complete (0-100) */
32
+ percent: number;
33
+ /** Whether replay is complete */
34
+ complete: boolean;
35
+ }
36
+ /**
37
+ * Events emitted by HistoryReplay.
38
+ */
39
+ export interface HistoryReplayEvents {
40
+ /** Emitted for each batch of chunks */
41
+ data: (data: string) => void;
42
+ /** Emitted with progress updates */
43
+ progress: (progress: ReplayProgress) => void;
44
+ /** Emitted when replay completes */
45
+ complete: () => void;
46
+ /** Emitted on error */
47
+ error: (error: Error) => void;
48
+ }
49
+ /**
50
+ * Replays scrollback history to a client.
51
+ *
52
+ * @example
53
+ * ```typescript
54
+ * const replay = new HistoryReplay({
55
+ * chunkSize: 100,
56
+ * batchDelay: 10
57
+ * });
58
+ *
59
+ * replay.on('data', (data) => {
60
+ * // Send data to client via WebSocket
61
+ * ws.send(JSON.stringify({ type: 'scrollback', data }));
62
+ * });
63
+ *
64
+ * replay.on('progress', (progress) => {
65
+ * // Update progress indicator
66
+ * console.log(`Replay: ${progress.percent}% complete`);
67
+ * });
68
+ *
69
+ * replay.on('complete', () => {
70
+ * console.log('Replay finished');
71
+ * });
72
+ *
73
+ * // Start replay
74
+ * await replay.start(chunks);
75
+ * ```
76
+ */
77
+ export declare class HistoryReplay extends EventEmitter {
78
+ private config;
79
+ private aborted;
80
+ private startTime;
81
+ constructor(config?: HistoryReplayConfig);
82
+ /**
83
+ * Start replaying chunks.
84
+ * Emits 'data' events with concatenated output for each batch.
85
+ * Emits 'progress' events periodically.
86
+ * Emits 'complete' when done.
87
+ *
88
+ * @param chunks Array of output chunks to replay
89
+ * @returns Promise that resolves when replay completes or is aborted
90
+ */
91
+ start(chunks: OutputChunk[]): Promise<void>;
92
+ /**
93
+ * Start replay from an async generator.
94
+ * Useful for streaming from disk without loading all chunks into memory.
95
+ */
96
+ startFromGenerator(generator: AsyncGenerator<OutputChunk[], void, unknown>, total: number): Promise<void>;
97
+ /**
98
+ * Abort the replay.
99
+ */
100
+ abort(): void;
101
+ /**
102
+ * Check if replay was aborted.
103
+ */
104
+ isAborted(): boolean;
105
+ /**
106
+ * Emit progress event.
107
+ */
108
+ private emitProgress;
109
+ /**
110
+ * Delay for a specified time.
111
+ */
112
+ private delay;
113
+ }
114
+ /**
115
+ * Create an async generator that reads chunks in batches.
116
+ * Useful for memory-efficient replay of large scrollback files.
117
+ */
118
+ export declare function createChunkGenerator(chunks: OutputChunk[], batchSize: number): AsyncGenerator<OutputChunk[], void, unknown>;