sessioncast-cli 2.0.0 → 2.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.
Files changed (34) hide show
  1. package/dist/agent/session-handler.d.ts +2 -1
  2. package/dist/agent/session-handler.js +77 -30
  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 +18 -2
  9. package/dist/agent/websocket.js +26 -9
  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;
@@ -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;
@@ -29,8 +29,24 @@ export declare class RelayWebSocketClient extends EventEmitter {
29
29
  private handleError;
30
30
  private scheduleReconnect;
31
31
  send(message: Message): boolean;
32
- sendScreen(data: Buffer): boolean;
33
- sendScreenCompressed(data: Buffer): boolean;
32
+ sendScreen(data: Buffer, paneMeta?: {
33
+ pane: string;
34
+ index: number;
35
+ }): boolean;
36
+ sendScreenCompressed(data: Buffer, paneMeta?: {
37
+ pane: string;
38
+ index: number;
39
+ }): boolean;
40
+ sendPaneLayout(panes: {
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
+ }[]): boolean;
34
50
  /**
35
51
  * Send file content to be displayed in the web FileViewer
36
52
  * @param filename - The name of the file
@@ -70,8 +70,8 @@ class RelayWebSocketClient extends events_1.EventEmitter {
70
70
  this.isConnected = true;
71
71
  this.reconnectAttempts = 0;
72
72
  this.circuitBreakerOpen = false;
73
- this.emit('connected');
74
73
  this.registerAsHost();
74
+ this.emit('connected');
75
75
  });
76
76
  this.ws.on('message', (data) => {
77
77
  try {
@@ -119,7 +119,7 @@ class RelayWebSocketClient extends events_1.EventEmitter {
119
119
  switch (message.type) {
120
120
  case 'keys':
121
121
  if (message.session === this.sessionId && message.payload) {
122
- this.emit('keys', message.payload);
122
+ this.emit('keys', message.payload, message.meta);
123
123
  }
124
124
  break;
125
125
  case 'resize':
@@ -239,32 +239,49 @@ class RelayWebSocketClient extends events_1.EventEmitter {
239
239
  return false;
240
240
  }
241
241
  }
242
- sendScreen(data) {
242
+ sendScreen(data, paneMeta) {
243
243
  if (!this.isConnected)
244
244
  return false;
245
245
  const base64Data = data.toString('base64');
246
- return this.send({
246
+ const msg = {
247
247
  type: 'screen',
248
248
  session: this.sessionId,
249
249
  payload: base64Data
250
- });
250
+ };
251
+ if (paneMeta) {
252
+ msg.meta = { pane: paneMeta.pane, index: String(paneMeta.index) };
253
+ }
254
+ return this.send(msg);
251
255
  }
252
- sendScreenCompressed(data) {
256
+ sendScreenCompressed(data, paneMeta) {
253
257
  if (!this.isConnected)
254
258
  return false;
255
259
  try {
256
260
  const compressed = zlib.gzipSync(data);
257
261
  const base64Data = compressed.toString('base64');
258
- return this.send({
262
+ const msg = {
259
263
  type: 'screenGz',
260
264
  session: this.sessionId,
261
265
  payload: base64Data
262
- });
266
+ };
267
+ if (paneMeta) {
268
+ msg.meta = { pane: paneMeta.pane, index: String(paneMeta.index) };
269
+ }
270
+ return this.send(msg);
263
271
  }
264
272
  catch {
265
- return this.sendScreen(data);
273
+ return this.sendScreen(data, paneMeta);
266
274
  }
267
275
  }
276
+ sendPaneLayout(panes) {
277
+ if (!this.isConnected)
278
+ return false;
279
+ return this.send({
280
+ type: 'paneLayout',
281
+ session: this.sessionId,
282
+ payload: JSON.stringify(panes)
283
+ });
284
+ }
268
285
  /**
269
286
  * Send file content to be displayed in the web FileViewer
270
287
  * @param filename - The name of the file
@@ -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>;