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.
- package/dist/agent/session-handler.d.ts +2 -1
- package/dist/agent/session-handler.js +79 -32
- package/dist/agent/tmux-executor.d.ts +33 -3
- package/dist/agent/tmux-executor.js +50 -3
- package/dist/agent/tmux.d.ts +6 -2
- package/dist/agent/tmux.js +9 -2
- package/dist/agent/types.d.ts +10 -0
- package/dist/agent/websocket.d.ts +21 -2
- package/dist/agent/websocket.js +46 -10
- package/dist/autopilot/index.d.ts +94 -0
- package/dist/autopilot/index.js +322 -0
- package/dist/autopilot/mission-analyzer.d.ts +27 -0
- package/dist/autopilot/mission-analyzer.js +232 -0
- package/dist/autopilot/project-detector.d.ts +12 -0
- package/dist/autopilot/project-detector.js +326 -0
- package/dist/autopilot/source-scanner.d.ts +26 -0
- package/dist/autopilot/source-scanner.js +285 -0
- package/dist/autopilot/speckit-generator.d.ts +60 -0
- package/dist/autopilot/speckit-generator.js +511 -0
- package/dist/autopilot/types.d.ts +110 -0
- package/dist/autopilot/types.js +6 -0
- package/dist/autopilot/workflow-generator.d.ts +33 -0
- package/dist/autopilot/workflow-generator.js +278 -0
- package/dist/project/executor.d.ts +73 -0
- package/dist/project/executor.js +437 -0
- package/dist/project/index.d.ts +4 -0
- package/dist/project/index.js +20 -0
- package/dist/project/manager.d.ts +66 -0
- package/dist/project/manager.js +290 -0
- package/dist/project/relay-client.d.ts +37 -0
- package/dist/project/relay-client.js +204 -0
- package/dist/project/types.d.ts +48 -0
- package/dist/project/types.js +3 -0
- 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
|
|
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 =
|
|
43
|
-
const CAPTURE_INTERVAL_IDLE_MS =
|
|
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.
|
|
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
|
-
|
|
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
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
+
listPanes(session) {
|
|
64
64
|
try {
|
|
65
|
-
const output = (0, child_process_1.execSync)(`tmux
|
|
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
|
-
|
|
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;
|
package/dist/agent/tmux.d.ts
CHANGED
|
@@ -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
|
*/
|
package/dist/agent/tmux.js
CHANGED
|
@@ -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
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -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
|
|
33
|
-
|
|
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
|
package/dist/agent/websocket.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
-
|
|
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>;
|