sessioncast-cli 2.0.0 → 2.0.2

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.
Files changed (34) hide show
  1. package/dist/agent/session-handler.d.ts +2 -1
  2. package/dist/agent/session-handler.js +79 -32
  3. package/dist/agent/tmux-executor.d.ts +33 -3
  4. package/dist/agent/tmux-executor.js +50 -3
  5. package/dist/agent/tmux.d.ts +6 -2
  6. package/dist/agent/tmux.js +9 -2
  7. package/dist/agent/types.d.ts +10 -0
  8. package/dist/agent/websocket.d.ts +21 -2
  9. package/dist/agent/websocket.js +46 -10
  10. package/dist/autopilot/index.d.ts +94 -0
  11. package/dist/autopilot/index.js +322 -0
  12. package/dist/autopilot/mission-analyzer.d.ts +27 -0
  13. package/dist/autopilot/mission-analyzer.js +232 -0
  14. package/dist/autopilot/project-detector.d.ts +12 -0
  15. package/dist/autopilot/project-detector.js +326 -0
  16. package/dist/autopilot/source-scanner.d.ts +26 -0
  17. package/dist/autopilot/source-scanner.js +285 -0
  18. package/dist/autopilot/speckit-generator.d.ts +60 -0
  19. package/dist/autopilot/speckit-generator.js +511 -0
  20. package/dist/autopilot/types.d.ts +110 -0
  21. package/dist/autopilot/types.js +6 -0
  22. package/dist/autopilot/workflow-generator.d.ts +33 -0
  23. package/dist/autopilot/workflow-generator.js +278 -0
  24. package/dist/project/executor.d.ts +73 -0
  25. package/dist/project/executor.js +437 -0
  26. package/dist/project/index.d.ts +4 -0
  27. package/dist/project/index.js +20 -0
  28. package/dist/project/manager.d.ts +66 -0
  29. package/dist/project/manager.js +290 -0
  30. package/dist/project/relay-client.d.ts +37 -0
  31. package/dist/project/relay-client.js +204 -0
  32. package/dist/project/types.d.ts +48 -0
  33. package/dist/project/types.js +3 -0
  34. package/package.json +1 -1
@@ -11,7 +11,8 @@ export declare class TmuxSessionHandler {
11
11
  private wsClient;
12
12
  private onCreateSession?;
13
13
  private running;
14
- private lastScreen;
14
+ private lastScreens;
15
+ private lastPaneLayoutJson;
15
16
  private lastForceSendTime;
16
17
  private lastChangeTime;
17
18
  private captureTimer;
@@ -39,8 +39,8 @@ const tmux = __importStar(require("./tmux"));
39
39
  const fs = __importStar(require("fs"));
40
40
  const path = __importStar(require("path"));
41
41
  // Capture intervals
42
- const CAPTURE_INTERVAL_ACTIVE_MS = 50;
43
- const CAPTURE_INTERVAL_IDLE_MS = 200;
42
+ const CAPTURE_INTERVAL_ACTIVE_MS = 16;
43
+ const CAPTURE_INTERVAL_IDLE_MS = 100;
44
44
  const ACTIVE_THRESHOLD_MS = 2000;
45
45
  const FORCE_SEND_INTERVAL_MS = 10000;
46
46
  const USE_COMPRESSION = true;
@@ -49,7 +49,8 @@ class TmuxSessionHandler {
49
49
  constructor(options) {
50
50
  this.wsClient = null;
51
51
  this.running = false;
52
- this.lastScreen = '';
52
+ this.lastScreens = new Map();
53
+ this.lastPaneLayoutJson = '';
53
54
  this.lastForceSendTime = 0;
54
55
  this.lastChangeTime = 0;
55
56
  this.captureTimer = null;
@@ -83,14 +84,16 @@ class TmuxSessionHandler {
83
84
  });
84
85
  this.wsClient.on('connected', () => {
85
86
  console.log(`[${this.tmuxSession}] Connected to relay`);
87
+ // Reset pane layout cache so it gets re-sent on reconnection
88
+ this.lastPaneLayoutJson = '';
86
89
  this.startScreenCapture();
87
90
  });
88
91
  this.wsClient.on('disconnected', ({ code, reason }) => {
89
92
  console.log(`[${this.tmuxSession}] Disconnected: code=${code}, reason=${reason}`);
90
93
  this.stopScreenCapture();
91
94
  });
92
- this.wsClient.on('keys', (keys) => {
93
- this.handleKeys(keys);
95
+ this.wsClient.on('keys', (keys, meta) => {
96
+ this.handleKeys(keys, meta);
94
97
  });
95
98
  this.wsClient.on('resize', ({ cols, rows }) => {
96
99
  console.log(`[${this.tmuxSession}] Resize: ${cols}x${rows}`);
@@ -279,8 +282,9 @@ class TmuxSessionHandler {
279
282
  };
280
283
  return types[ext] || 'application/octet-stream';
281
284
  }
282
- handleKeys(keys) {
283
- tmux.sendKeys(this.tmuxSession, keys, false);
285
+ handleKeys(keys, meta) {
286
+ const target = meta?.pane ? `${this.tmuxSession}.${meta.pane}` : this.tmuxSession;
287
+ tmux.sendKeys(target, keys, false);
284
288
  }
285
289
  startScreenCapture() {
286
290
  if (this.captureTimer)
@@ -292,43 +296,86 @@ class TmuxSessionHandler {
292
296
  return;
293
297
  }
294
298
  try {
295
- const screen = tmux.capturePane(this.tmuxSession);
296
- if (screen !== null) {
297
- const now = Date.now();
298
- const changed = screen !== this.lastScreen;
299
- const forceTime = (now - this.lastForceSendTime) >= FORCE_SEND_INTERVAL_MS;
300
- if (changed || forceTime) {
301
- this.lastScreen = screen;
302
- this.lastForceSendTime = now;
303
- if (changed) {
304
- this.lastChangeTime = now;
299
+ const panes = tmux.listPanes(this.tmuxSession);
300
+ const now = Date.now();
301
+ const isMultiPane = panes.length > 1;
302
+ // Check if pane layout changed
303
+ const paneLayoutJson = JSON.stringify(panes);
304
+ if (paneLayoutJson !== this.lastPaneLayoutJson) {
305
+ this.lastPaneLayoutJson = paneLayoutJson;
306
+ this.wsClient.sendPaneLayout(panes);
307
+ // Clean up screens for removed panes
308
+ const currentPaneIds = new Set(panes.map(p => p.id));
309
+ for (const key of this.lastScreens.keys()) {
310
+ if (!currentPaneIds.has(key)) {
311
+ this.lastScreens.delete(key);
305
312
  }
306
- // Send clear screen + content
307
- const fullOutput = '\x1b[2J\x1b[H' + screen;
308
- const data = Buffer.from(fullOutput, 'utf-8');
309
- // Compress if enabled and data is large enough
310
- if (USE_COMPRESSION && data.length > MIN_COMPRESS_SIZE) {
311
- this.wsClient.sendScreenCompressed(data);
312
- }
313
- else {
314
- this.wsClient.sendScreen(data);
313
+ }
314
+ }
315
+ let anyChanged = false;
316
+ const forceTime = (now - this.lastForceSendTime) >= FORCE_SEND_INTERVAL_MS;
317
+ if (isMultiPane) {
318
+ // Multi-pane: capture each pane individually
319
+ for (const pane of panes) {
320
+ const screen = tmux.capturePane(this.tmuxSession, pane.id);
321
+ if (screen !== null) {
322
+ const lastScreen = this.lastScreens.get(pane.id) || '';
323
+ const changed = screen !== lastScreen;
324
+ if (changed || forceTime) {
325
+ this.lastScreens.set(pane.id, screen);
326
+ if (changed)
327
+ anyChanged = true;
328
+ const fullOutput = '\x1b[2J\x1b[H' + screen;
329
+ const data = Buffer.from(fullOutput, 'utf-8');
330
+ const paneMeta = { pane: pane.id, index: pane.index };
331
+ if (USE_COMPRESSION && data.length > MIN_COMPRESS_SIZE) {
332
+ this.wsClient.sendScreenCompressed(data, paneMeta);
333
+ }
334
+ else {
335
+ this.wsClient.sendScreen(data, paneMeta);
336
+ }
337
+ }
315
338
  }
316
339
  }
317
- // Adaptive sleep: faster when active, slower when idle
318
- const isActive = (now - this.lastChangeTime) < ACTIVE_THRESHOLD_MS;
319
- const sleepMs = isActive ? CAPTURE_INTERVAL_ACTIVE_MS : CAPTURE_INTERVAL_IDLE_MS;
320
- this.captureTimer = setTimeout(capture, sleepMs);
321
340
  }
322
341
  else {
323
- this.captureTimer = setTimeout(capture, CAPTURE_INTERVAL_IDLE_MS);
342
+ // Single pane: backward compatible (no meta)
343
+ const screen = tmux.capturePane(this.tmuxSession);
344
+ if (screen !== null) {
345
+ const lastScreen = this.lastScreens.get('_single') || '';
346
+ const changed = screen !== lastScreen;
347
+ if (changed || forceTime) {
348
+ this.lastScreens.set('_single', screen);
349
+ if (changed)
350
+ anyChanged = true;
351
+ const fullOutput = '\x1b[2J\x1b[H' + screen;
352
+ const data = Buffer.from(fullOutput, 'utf-8');
353
+ if (USE_COMPRESSION && data.length > MIN_COMPRESS_SIZE) {
354
+ this.wsClient.sendScreenCompressed(data);
355
+ }
356
+ else {
357
+ this.wsClient.sendScreen(data);
358
+ }
359
+ }
360
+ }
361
+ }
362
+ if (anyChanged || forceTime) {
363
+ this.lastForceSendTime = now;
364
+ if (anyChanged) {
365
+ this.lastChangeTime = now;
366
+ }
324
367
  }
368
+ const isActive = (now - this.lastChangeTime) < ACTIVE_THRESHOLD_MS;
369
+ const sleepMs = isActive ? CAPTURE_INTERVAL_ACTIVE_MS : CAPTURE_INTERVAL_IDLE_MS;
370
+ this.captureTimer = setTimeout(capture, sleepMs);
325
371
  }
326
372
  catch (error) {
327
373
  console.error(`[${this.tmuxSession}] Screen capture error:`, error);
328
374
  this.captureTimer = setTimeout(capture, 500);
329
375
  }
330
376
  };
331
- capture();
377
+ // Small delay to ensure register message is processed by server first
378
+ setTimeout(capture, 300);
332
379
  console.log(`[${this.tmuxSession}] Screen capture started`);
333
380
  }
334
381
  stopScreenCapture() {
@@ -4,7 +4,17 @@
4
4
  */
5
5
  export interface TmuxExecutor {
6
6
  listSessions(): string[];
7
- capturePane(session: string): string | null;
7
+ listPanes(session: string): {
8
+ id: string;
9
+ index: number;
10
+ width: number;
11
+ height: number;
12
+ top: number;
13
+ left: number;
14
+ active: boolean;
15
+ title: string;
16
+ }[];
17
+ capturePane(session: string, paneId?: string): string | null;
8
18
  sendKeys(session: string, keys: string): boolean;
9
19
  sendSpecialKey(session: string, key: string): boolean;
10
20
  resizeWindow(session: string, cols: number, rows: number): boolean;
@@ -20,7 +30,17 @@ export interface TmuxExecutor {
20
30
  */
21
31
  export declare class UnixTmuxExecutor implements TmuxExecutor {
22
32
  listSessions(): string[];
23
- capturePane(session: string): string | null;
33
+ listPanes(session: string): {
34
+ id: string;
35
+ index: number;
36
+ width: number;
37
+ height: number;
38
+ top: number;
39
+ left: number;
40
+ active: boolean;
41
+ title: string;
42
+ }[];
43
+ capturePane(session: string, paneId?: string): string | null;
24
44
  sendKeys(session: string, keys: string): boolean;
25
45
  sendSpecialKey(session: string, key: string): boolean;
26
46
  resizeWindow(session: string, cols: number, rows: number): boolean;
@@ -41,7 +61,17 @@ export declare class WindowsTmuxExecutor implements TmuxExecutor {
41
61
  constructor(itmuxPath: string);
42
62
  private executeCommand;
43
63
  listSessions(): string[];
44
- capturePane(session: string): string | null;
64
+ listPanes(session: string): {
65
+ id: string;
66
+ index: number;
67
+ width: number;
68
+ height: number;
69
+ top: number;
70
+ left: number;
71
+ active: boolean;
72
+ title: string;
73
+ }[];
74
+ capturePane(session: string, paneId?: string): string | null;
45
75
  sendKeys(session: string, keys: string): boolean;
46
76
  sendSpecialKey(session: string, key: string): boolean;
47
77
  resizeWindow(session: string, cols: number, rows: number): boolean;
@@ -60,9 +60,31 @@ class UnixTmuxExecutor {
60
60
  return [];
61
61
  }
62
62
  }
63
- capturePane(session) {
63
+ listPanes(session) {
64
64
  try {
65
- const output = (0, child_process_1.execSync)(`tmux capture-pane -t "${session}" -p -e -N`, {
65
+ const output = (0, child_process_1.execSync)(`tmux list-panes -t "${session}" -F "#{pane_id}:#{pane_index}:#{pane_width}:#{pane_height}:#{pane_top}:#{pane_left}:#{pane_active}:#{pane_title}"`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
66
+ return output.trim().split('\n').filter(l => l.length > 0).map(line => {
67
+ const parts = line.split(':');
68
+ return {
69
+ id: parts[0],
70
+ index: parseInt(parts[1], 10),
71
+ width: parseInt(parts[2], 10),
72
+ height: parseInt(parts[3], 10),
73
+ top: parseInt(parts[4], 10),
74
+ left: parseInt(parts[5], 10),
75
+ active: parts[6] === '1',
76
+ title: parts.slice(7).join(':') // title may contain colons
77
+ };
78
+ });
79
+ }
80
+ catch {
81
+ return [];
82
+ }
83
+ }
84
+ capturePane(session, paneId) {
85
+ try {
86
+ const target = paneId ? `${session}.${paneId}` : session;
87
+ const output = (0, child_process_1.execSync)(`tmux capture-pane -t "${target}" -p -e -N`, {
66
88
  encoding: 'utf-8',
67
89
  stdio: ['pipe', 'pipe', 'pipe'],
68
90
  maxBuffer: 10 * 1024 * 1024
@@ -223,9 +245,34 @@ class WindowsTmuxExecutor {
223
245
  return [];
224
246
  }
225
247
  }
226
- capturePane(session) {
248
+ listPanes(session) {
227
249
  try {
228
250
  const escaped = this.escapeSession(session);
251
+ const output = this.executeCommand(`tmux list-panes -t '${escaped}' -F '#{pane_id}:#{pane_index}:#{pane_width}:#{pane_height}:#{pane_top}:#{pane_left}:#{pane_active}:#{pane_title}'`);
252
+ if (!output)
253
+ return [];
254
+ return output.split('\n').map(s => s.trim()).filter(l => l.length > 0).map(line => {
255
+ const parts = line.split(':');
256
+ return {
257
+ id: parts[0],
258
+ index: parseInt(parts[1], 10),
259
+ width: parseInt(parts[2], 10),
260
+ height: parseInt(parts[3], 10),
261
+ top: parseInt(parts[4], 10),
262
+ left: parseInt(parts[5], 10),
263
+ active: parts[6] === '1',
264
+ title: parts.slice(7).join(':')
265
+ };
266
+ });
267
+ }
268
+ catch {
269
+ return [];
270
+ }
271
+ }
272
+ capturePane(session, paneId) {
273
+ try {
274
+ const target = paneId ? `${session}.${paneId}` : session;
275
+ const escaped = this.escapeSession(target);
229
276
  const output = this.executeCommand(`tmux capture-pane -t '${escaped}' -p -e -N`);
230
277
  if (!output)
231
278
  return null;
@@ -1,4 +1,4 @@
1
- import { TmuxSession } from './types';
1
+ import { TmuxSession, PaneInfo } from './types';
2
2
  /**
3
3
  * Scan for all tmux sessions
4
4
  */
@@ -7,10 +7,14 @@ export declare function scanSessions(): string[];
7
7
  * Get detailed session info
8
8
  */
9
9
  export declare function listSessions(): TmuxSession[];
10
+ /**
11
+ * List all panes in a tmux session
12
+ */
13
+ export declare function listPanes(sessionName: string): PaneInfo[];
10
14
  /**
11
15
  * Capture tmux pane content with escape sequences (colors)
12
16
  */
13
- export declare function capturePane(sessionName: string): string | null;
17
+ export declare function capturePane(sessionName: string, paneId?: string): string | null;
14
18
  /**
15
19
  * Send keys to tmux session
16
20
  */
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.scanSessions = scanSessions;
4
4
  exports.listSessions = listSessions;
5
+ exports.listPanes = listPanes;
5
6
  exports.capturePane = capturePane;
6
7
  exports.sendKeys = sendKeys;
7
8
  exports.resizeWindow = resizeWindow;
@@ -49,11 +50,17 @@ function listSessions() {
49
50
  return [];
50
51
  }
51
52
  }
53
+ /**
54
+ * List all panes in a tmux session
55
+ */
56
+ function listPanes(sessionName) {
57
+ return getExecutor().listPanes(sessionName);
58
+ }
52
59
  /**
53
60
  * Capture tmux pane content with escape sequences (colors)
54
61
  */
55
- function capturePane(sessionName) {
56
- return getExecutor().capturePane(sessionName);
62
+ function capturePane(sessionName, paneId) {
63
+ return getExecutor().capturePane(sessionName, paneId);
57
64
  }
58
65
  /**
59
66
  * Send keys to tmux session
@@ -37,6 +37,16 @@ export interface TmuxSession {
37
37
  created?: string;
38
38
  attached: boolean;
39
39
  }
40
+ export interface PaneInfo {
41
+ id: string;
42
+ index: number;
43
+ width: number;
44
+ height: number;
45
+ top: number;
46
+ left: number;
47
+ active: boolean;
48
+ title: string;
49
+ }
40
50
  export interface ExecResult {
41
51
  exitCode: number;
42
52
  stdout: string;
@@ -21,16 +21,35 @@ export declare class RelayWebSocketClient extends EventEmitter {
21
21
  private circuitBreakerOpen;
22
22
  private circuitBreakerResetTime;
23
23
  private reconnectTimer;
24
+ private pingTimer;
24
25
  private destroyed;
25
26
  constructor(options: WebSocketClientOptions);
26
27
  connect(): void;
27
28
  private registerAsHost;
28
29
  private handleMessage;
29
30
  private handleError;
31
+ private startPing;
32
+ private stopPing;
30
33
  private scheduleReconnect;
31
34
  send(message: Message): boolean;
32
- sendScreen(data: Buffer): boolean;
33
- sendScreenCompressed(data: Buffer): boolean;
35
+ sendScreen(data: Buffer, paneMeta?: {
36
+ pane: string;
37
+ index: number;
38
+ }): boolean;
39
+ sendScreenCompressed(data: Buffer, paneMeta?: {
40
+ pane: string;
41
+ index: number;
42
+ }): boolean;
43
+ sendPaneLayout(panes: {
44
+ id: string;
45
+ index: number;
46
+ width: number;
47
+ height: number;
48
+ top: number;
49
+ left: number;
50
+ active: boolean;
51
+ title: string;
52
+ }[]): boolean;
34
53
  /**
35
54
  * Send file content to be displayed in the web FileViewer
36
55
  * @param filename - The name of the file
@@ -40,10 +40,11 @@ exports.RelayWebSocketClient = void 0;
40
40
  const ws_1 = __importDefault(require("ws"));
41
41
  const events_1 = require("events");
42
42
  const zlib = __importStar(require("zlib"));
43
- const MAX_RECONNECT_ATTEMPTS = 5;
43
+ const MAX_RECONNECT_ATTEMPTS = 50;
44
44
  const BASE_RECONNECT_DELAY_MS = 2000;
45
45
  const MAX_RECONNECT_DELAY_MS = 60000;
46
46
  const CIRCUIT_BREAKER_DURATION_MS = 120000;
47
+ const PING_INTERVAL_MS = 30000;
47
48
  class RelayWebSocketClient extends events_1.EventEmitter {
48
49
  constructor(options) {
49
50
  super();
@@ -53,6 +54,7 @@ class RelayWebSocketClient extends events_1.EventEmitter {
53
54
  this.circuitBreakerOpen = false;
54
55
  this.circuitBreakerResetTime = 0;
55
56
  this.reconnectTimer = null;
57
+ this.pingTimer = null;
56
58
  this.destroyed = false;
57
59
  this.url = options.url;
58
60
  this.sessionId = options.sessionId;
@@ -70,8 +72,9 @@ class RelayWebSocketClient extends events_1.EventEmitter {
70
72
  this.isConnected = true;
71
73
  this.reconnectAttempts = 0;
72
74
  this.circuitBreakerOpen = false;
73
- this.emit('connected');
74
75
  this.registerAsHost();
76
+ this.startPing();
77
+ this.emit('connected');
75
78
  });
76
79
  this.ws.on('message', (data) => {
77
80
  try {
@@ -84,6 +87,7 @@ class RelayWebSocketClient extends events_1.EventEmitter {
84
87
  });
85
88
  this.ws.on('close', (code, reason) => {
86
89
  this.isConnected = false;
90
+ this.stopPing();
87
91
  this.emit('disconnected', { code, reason: reason.toString() });
88
92
  if (this.autoReconnect && !this.destroyed) {
89
93
  this.scheduleReconnect();
@@ -119,7 +123,7 @@ class RelayWebSocketClient extends events_1.EventEmitter {
119
123
  switch (message.type) {
120
124
  case 'keys':
121
125
  if (message.session === this.sessionId && message.payload) {
122
- this.emit('keys', message.payload);
126
+ this.emit('keys', message.payload, message.meta);
123
127
  }
124
128
  break;
125
129
  case 'resize':
@@ -188,6 +192,20 @@ class RelayWebSocketClient extends events_1.EventEmitter {
188
192
  console.error(`Error: code=${meta.code}, message=${meta.messageEn}`);
189
193
  }
190
194
  }
195
+ startPing() {
196
+ this.stopPing();
197
+ this.pingTimer = setInterval(() => {
198
+ if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
199
+ this.ws.ping();
200
+ }
201
+ }, PING_INTERVAL_MS);
202
+ }
203
+ stopPing() {
204
+ if (this.pingTimer) {
205
+ clearInterval(this.pingTimer);
206
+ this.pingTimer = null;
207
+ }
208
+ }
191
209
  scheduleReconnect() {
192
210
  if (this.destroyed)
193
211
  return;
@@ -239,32 +257,49 @@ class RelayWebSocketClient extends events_1.EventEmitter {
239
257
  return false;
240
258
  }
241
259
  }
242
- sendScreen(data) {
260
+ sendScreen(data, paneMeta) {
243
261
  if (!this.isConnected)
244
262
  return false;
245
263
  const base64Data = data.toString('base64');
246
- return this.send({
264
+ const msg = {
247
265
  type: 'screen',
248
266
  session: this.sessionId,
249
267
  payload: base64Data
250
- });
268
+ };
269
+ if (paneMeta) {
270
+ msg.meta = { pane: paneMeta.pane, index: String(paneMeta.index) };
271
+ }
272
+ return this.send(msg);
251
273
  }
252
- sendScreenCompressed(data) {
274
+ sendScreenCompressed(data, paneMeta) {
253
275
  if (!this.isConnected)
254
276
  return false;
255
277
  try {
256
278
  const compressed = zlib.gzipSync(data);
257
279
  const base64Data = compressed.toString('base64');
258
- return this.send({
280
+ const msg = {
259
281
  type: 'screenGz',
260
282
  session: this.sessionId,
261
283
  payload: base64Data
262
- });
284
+ };
285
+ if (paneMeta) {
286
+ msg.meta = { pane: paneMeta.pane, index: String(paneMeta.index) };
287
+ }
288
+ return this.send(msg);
263
289
  }
264
290
  catch {
265
- return this.sendScreen(data);
291
+ return this.sendScreen(data, paneMeta);
266
292
  }
267
293
  }
294
+ sendPaneLayout(panes) {
295
+ if (!this.isConnected)
296
+ return false;
297
+ return this.send({
298
+ type: 'paneLayout',
299
+ session: this.sessionId,
300
+ payload: JSON.stringify(panes)
301
+ });
302
+ }
268
303
  /**
269
304
  * Send file content to be displayed in the web FileViewer
270
305
  * @param filename - The name of the file
@@ -323,6 +358,7 @@ class RelayWebSocketClient extends events_1.EventEmitter {
323
358
  destroy() {
324
359
  this.destroyed = true;
325
360
  this.autoReconnect = false;
361
+ this.stopPing();
326
362
  if (this.reconnectTimer) {
327
363
  clearTimeout(this.reconnectTimer);
328
364
  this.reconnectTimer = null;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * AutoPilot - Single-prompt execution layer for SessionCast
3
+ *
4
+ * Enables opencode-style experience: one prompt → auto-detect → auto-analyze → auto-execute
5
+ */
6
+ import { EventEmitter } from 'events';
7
+ import { AutoPilotContext, AutoPilotOptions, GeneratedWorkflow } from './types';
8
+ import { toExecutableWorkflow } from './workflow-generator';
9
+ import { generateSpeckit } from './speckit-generator';
10
+ export * from './types';
11
+ export { generateSpeckit, generateQuickSpeckit, saveSpeckit } from './speckit-generator';
12
+ export type { SpeckitOutput } from './speckit-generator';
13
+ export declare class AutoPilot extends EventEmitter {
14
+ private options;
15
+ private context;
16
+ private llmClient?;
17
+ constructor(options: AutoPilotOptions);
18
+ /**
19
+ * Create initial context
20
+ */
21
+ private createInitialContext;
22
+ /**
23
+ * Set LLM client for mission analysis
24
+ */
25
+ setLlmClient(client: {
26
+ chat: (messages: {
27
+ role: string;
28
+ content: string;
29
+ }[]) => Promise<string>;
30
+ }): void;
31
+ /**
32
+ * Get current context
33
+ */
34
+ getContext(): AutoPilotContext;
35
+ /**
36
+ * Main entry point: execute a single prompt
37
+ */
38
+ execute(prompt: string): Promise<GeneratedWorkflow>;
39
+ /**
40
+ * Quick execute without LLM analysis
41
+ */
42
+ quickExecute(prompt: string): Promise<GeneratedWorkflow>;
43
+ /**
44
+ * Phase 1: Detect project type
45
+ */
46
+ private detectProject;
47
+ /**
48
+ * Phase 2: Scan sources
49
+ */
50
+ private scanProject;
51
+ /**
52
+ * Phase 3: Analyze mission
53
+ */
54
+ private analyzeMission;
55
+ /**
56
+ * Phase 4: Generate workflow
57
+ */
58
+ private generateWorkflow;
59
+ /**
60
+ * Convert to executable workflow format (compatible with existing ProjectManager)
61
+ */
62
+ toExecutableFormat(): ReturnType<typeof toExecutableWorkflow> | null;
63
+ /**
64
+ * Convert to Speckit format (plan.md + tasks.md)
65
+ */
66
+ toSpeckit(): ReturnType<typeof generateSpeckit>;
67
+ /**
68
+ * Generate and save Speckit files
69
+ */
70
+ saveSpeckit(outputDir?: string): {
71
+ planPath: string;
72
+ tasksPath: string;
73
+ };
74
+ /**
75
+ * Update status and emit event
76
+ */
77
+ private updateStatus;
78
+ /**
79
+ * Get a summary of the analysis for display
80
+ */
81
+ getSummary(): string;
82
+ /**
83
+ * Save workflow to file
84
+ */
85
+ saveWorkflow(outputPath?: string): Promise<string>;
86
+ }
87
+ /**
88
+ * Convenience function for one-shot execution
89
+ */
90
+ export declare function autoPilot(prompt: string, options: AutoPilotOptions): Promise<GeneratedWorkflow>;
91
+ /**
92
+ * Quick version without LLM
93
+ */
94
+ export declare function autoPilotQuick(prompt: string, options: AutoPilotOptions): Promise<GeneratedWorkflow>;