sessioncast-cli 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +65 -40
  2. package/dist/agent/session-handler.d.ts +1 -0
  3. package/dist/agent/session-handler.js +42 -0
  4. package/dist/agent/tmux-executor.d.ts +66 -0
  5. package/dist/agent/tmux-executor.js +368 -0
  6. package/dist/agent/tmux.d.ts +9 -1
  7. package/dist/agent/tmux.js +52 -76
  8. package/dist/agent/websocket.d.ts +2 -8
  9. package/dist/agent/websocket.js +78 -14
  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/commands/autopilot.d.ts +30 -0
  25. package/dist/commands/autopilot.js +262 -0
  26. package/dist/commands/login.d.ts +2 -1
  27. package/dist/commands/login.js +199 -8
  28. package/dist/commands/project.d.ts +1 -1
  29. package/dist/commands/project.js +4 -13
  30. package/dist/config.d.ts +20 -0
  31. package/dist/config.js +69 -2
  32. package/dist/index.js +7 -47
  33. package/dist/project/executor.d.ts +8 -53
  34. package/dist/project/executor.js +64 -520
  35. package/dist/project/manager.d.ts +0 -13
  36. package/dist/project/manager.js +0 -107
  37. package/dist/project/relay-client.d.ts +18 -68
  38. package/dist/project/relay-client.js +134 -130
  39. package/dist/project/types.d.ts +5 -0
  40. package/dist/utils/fileUtils.d.ts +28 -0
  41. package/dist/utils/fileUtils.js +159 -0
  42. package/dist/utils/oauthServer.d.ts +18 -0
  43. package/dist/utils/oauthServer.js +244 -0
  44. package/dist/utils/pkce.d.ts +16 -0
  45. package/dist/utils/pkce.js +73 -0
  46. package/package.json +5 -14
  47. package/LICENSE +0 -21
@@ -7,45 +7,41 @@ exports.sendKeys = sendKeys;
7
7
  exports.resizeWindow = resizeWindow;
8
8
  exports.createSession = createSession;
9
9
  exports.killSession = killSession;
10
- const child_process_1 = require("child_process");
10
+ exports.isAvailable = isAvailable;
11
+ exports.getVersion = getVersion;
12
+ const tmux_executor_1 = require("./tmux-executor");
13
+ // Lazy-initialized executor (created on first use)
14
+ let executor = null;
15
+ /**
16
+ * Get or create the tmux executor for the current platform.
17
+ */
18
+ function getExecutor() {
19
+ if (!executor) {
20
+ console.log(`[tmux] Initializing on ${(0, tmux_executor_1.getPlatformName)()}`);
21
+ executor = (0, tmux_executor_1.createTmuxExecutor)();
22
+ const version = executor.getVersion();
23
+ console.log(`[tmux] Version: ${version}`);
24
+ }
25
+ return executor;
26
+ }
11
27
  /**
12
28
  * Scan for all tmux sessions
13
29
  */
14
30
  function scanSessions() {
15
- try {
16
- const output = (0, child_process_1.execSync)('tmux ls -F "#{session_name}"', {
17
- encoding: 'utf-8',
18
- stdio: ['pipe', 'pipe', 'pipe']
19
- });
20
- return output
21
- .trim()
22
- .split('\n')
23
- .filter(s => s.length > 0);
24
- }
25
- catch {
26
- // tmux not running or no sessions
27
- return [];
28
- }
31
+ return getExecutor().listSessions();
29
32
  }
30
33
  /**
31
34
  * Get detailed session info
32
35
  */
33
36
  function listSessions() {
34
37
  try {
35
- const output = (0, child_process_1.execSync)('tmux list-sessions -F "#{session_name}|#{session_windows}|#{session_created}|#{session_attached}"', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
36
- return output
37
- .trim()
38
- .split('\n')
39
- .filter(line => line.length > 0)
40
- .map(line => {
41
- const [name, windows, created, attached] = line.split('|');
42
- return {
43
- name,
44
- windows: parseInt(windows, 10) || 1,
45
- created: created || undefined,
46
- attached: attached === '1'
47
- };
48
- });
38
+ const sessions = getExecutor().listSessions();
39
+ // For now, return basic info (detailed info would require additional tmux commands)
40
+ return sessions.map(name => ({
41
+ name,
42
+ windows: 1,
43
+ attached: false
44
+ }));
49
45
  }
50
46
  catch {
51
47
  return [];
@@ -55,53 +51,39 @@ function listSessions() {
55
51
  * Capture tmux pane content with escape sequences (colors)
56
52
  */
57
53
  function capturePane(sessionName) {
58
- try {
59
- const output = (0, child_process_1.execSync)(`tmux capture-pane -t "${sessionName}" -p -e -N`, {
60
- encoding: 'utf-8',
61
- stdio: ['pipe', 'pipe', 'pipe'],
62
- maxBuffer: 10 * 1024 * 1024 // 10MB
63
- });
64
- // Normalize line endings
65
- return output.replace(/\n/g, '\r\n');
66
- }
67
- catch {
68
- return null;
69
- }
54
+ return getExecutor().capturePane(sessionName);
70
55
  }
71
56
  /**
72
57
  * Send keys to tmux session
73
58
  */
74
59
  function sendKeys(target, keys, enter = true) {
60
+ const exec = getExecutor();
75
61
  try {
76
62
  // Handle special keys
77
63
  if (keys === '\x03') {
78
64
  // Ctrl+C
79
- (0, child_process_1.execSync)(`tmux send-keys -t "${target}" C-c`, { stdio: 'pipe' });
80
- return true;
65
+ return exec.sendSpecialKey(target, 'C-c');
81
66
  }
82
67
  if (keys === '\x04') {
83
68
  // Ctrl+D
84
- (0, child_process_1.execSync)(`tmux send-keys -t "${target}" C-d`, { stdio: 'pipe' });
85
- return true;
69
+ return exec.sendSpecialKey(target, 'C-d');
86
70
  }
87
71
  // For Enter key only
88
72
  if (keys === '\n' || keys === '\r\n') {
89
- (0, child_process_1.execSync)(`tmux send-keys -t "${target}" Enter`, { stdio: 'pipe' });
90
- return true;
73
+ return exec.sendSpecialKey(target, 'Enter');
91
74
  }
92
75
  // For text with newline at end (command + enter)
93
76
  if (keys.endsWith('\n')) {
94
77
  const cmd = keys.slice(0, -1);
95
78
  if (cmd) {
96
- (0, child_process_1.execSync)(`tmux send-keys -t "${target}" -l "${escapeForShell(cmd)}"`, { stdio: 'pipe' });
79
+ exec.sendKeys(target, cmd);
97
80
  }
98
- (0, child_process_1.execSync)(`tmux send-keys -t "${target}" Enter`, { stdio: 'pipe' });
99
- return true;
81
+ return exec.sendSpecialKey(target, 'Enter');
100
82
  }
101
83
  // Regular text input
102
- (0, child_process_1.execSync)(`tmux send-keys -t "${target}" -l "${escapeForShell(keys)}"`, { stdio: 'pipe' });
84
+ exec.sendKeys(target, keys);
103
85
  if (enter) {
104
- (0, child_process_1.execSync)(`tmux send-keys -t "${target}" Enter`, { stdio: 'pipe' });
86
+ exec.sendSpecialKey(target, 'Enter');
105
87
  }
106
88
  return true;
107
89
  }
@@ -113,45 +95,39 @@ function sendKeys(target, keys, enter = true) {
113
95
  * Resize tmux window
114
96
  */
115
97
  function resizeWindow(sessionName, cols, rows) {
116
- try {
117
- (0, child_process_1.execSync)(`tmux resize-window -t "${sessionName}" -x ${cols} -y ${rows}`, { stdio: 'pipe' });
118
- return true;
119
- }
120
- catch {
121
- return false;
122
- }
98
+ return getExecutor().resizeWindow(sessionName, cols, rows);
123
99
  }
124
100
  /**
125
101
  * Create new tmux session
126
102
  */
127
- function createSession(sessionName) {
128
- try {
129
- // Sanitize session name
130
- const sanitized = sessionName.replace(/[^a-zA-Z0-9_-]/g, '_');
131
- if (!sanitized)
132
- return false;
133
- (0, child_process_1.execSync)(`tmux new-session -d -s "${sanitized}"`, { stdio: 'pipe' });
134
- return true;
135
- }
136
- catch {
137
- return false;
138
- }
103
+ function createSession(sessionName, workingDir) {
104
+ return getExecutor().createSession(sessionName, workingDir);
139
105
  }
140
106
  /**
141
107
  * Kill tmux session
142
108
  */
143
109
  function killSession(sessionName) {
110
+ return getExecutor().killSession(sessionName);
111
+ }
112
+ /**
113
+ * Check if tmux is available
114
+ */
115
+ function isAvailable() {
144
116
  try {
145
- (0, child_process_1.execSync)(`tmux kill-session -t "${sessionName}"`, { stdio: 'pipe' });
146
- return true;
117
+ return getExecutor().isAvailable();
147
118
  }
148
119
  catch {
149
120
  return false;
150
121
  }
151
122
  }
152
123
  /**
153
- * Escape string for shell
124
+ * Get tmux version
154
125
  */
155
- function escapeForShell(str) {
156
- return str.replace(/"/g, '\\"').replace(/\$/g, '\\$').replace(/`/g, '\\`');
126
+ function getVersion() {
127
+ try {
128
+ return getExecutor().getVersion();
129
+ }
130
+ catch {
131
+ return null;
132
+ }
157
133
  }
@@ -26,19 +26,13 @@ export declare class RelayWebSocketClient extends EventEmitter {
26
26
  connect(): void;
27
27
  private registerAsHost;
28
28
  private handleMessage;
29
+ private handleFileUpload;
29
30
  private handleError;
30
31
  private scheduleReconnect;
31
32
  send(message: Message): boolean;
32
33
  sendScreen(data: Buffer): boolean;
33
34
  sendScreenCompressed(data: Buffer): boolean;
34
- /**
35
- * Send file content to be displayed in the web FileViewer
36
- * @param filename - The name of the file
37
- * @param content - The file content (UTF-8 for text, base64 for images)
38
- * @param contentType - MIME type (e.g., 'text/markdown', 'text/html', 'image/png')
39
- * @param path - Optional file path
40
- */
41
- sendFileView(filename: string, content: string, contentType: string, path?: string): boolean;
35
+ sendFileView(filePath: string, content: string, language: string, error?: string): boolean;
42
36
  getConnected(): boolean;
43
37
  destroy(): void;
44
38
  }
@@ -40,6 +40,7 @@ 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 fileUtils_1 = require("../utils/fileUtils");
43
44
  const MAX_RECONNECT_ATTEMPTS = 5;
44
45
  const BASE_RECONNECT_DELAY_MS = 2000;
45
46
  const MAX_RECONNECT_DELAY_MS = 60000;
@@ -116,6 +117,10 @@ class RelayWebSocketClient extends events_1.EventEmitter {
116
117
  });
117
118
  }
118
119
  handleMessage(message) {
120
+ // Debug: log all incoming message types (except frequent ones)
121
+ if (message.type !== 'pong' && message.type !== 'keys') {
122
+ console.log(`[WS] Message received: type=${message.type}, session=${message.session}`);
123
+ }
119
124
  switch (message.type) {
120
125
  case 'keys':
121
126
  if (message.session === this.sessionId && message.payload) {
@@ -141,6 +146,20 @@ class RelayWebSocketClient extends events_1.EventEmitter {
141
146
  this.emit('killSession');
142
147
  }
143
148
  break;
149
+ case 'requestFileView':
150
+ console.log(`[WS] requestFileView received: session=${message.session}, mySession=${this.sessionId}, filePath=${message.meta?.filePath}`);
151
+ if (message.session === this.sessionId && message.meta?.filePath) {
152
+ this.emit('requestFileView', message.meta.filePath);
153
+ }
154
+ else {
155
+ console.log(`[WS] requestFileView ignored: sessionMatch=${message.session === this.sessionId}, hasFilePath=${!!message.meta?.filePath}`);
156
+ }
157
+ break;
158
+ case 'uploadFile':
159
+ if (message.session === this.sessionId && message.meta && message.payload) {
160
+ this.handleFileUpload(message);
161
+ }
162
+ break;
144
163
  case 'error':
145
164
  this.handleError(message);
146
165
  break;
@@ -148,6 +167,55 @@ class RelayWebSocketClient extends events_1.EventEmitter {
148
167
  this.emit('message', message);
149
168
  }
150
169
  }
170
+ async handleFileUpload(message) {
171
+ const meta = message.meta;
172
+ if (!meta || !message.payload)
173
+ return;
174
+ try {
175
+ const result = await (0, fileUtils_1.handleUploadChunk)(this.sessionId, {
176
+ filename: meta.filename || 'unknown',
177
+ size: meta.size || '0',
178
+ mimeType: meta.mimeType || 'application/octet-stream',
179
+ chunkIndex: meta.chunkIndex || '0',
180
+ totalChunks: meta.totalChunks || '1',
181
+ }, message.payload);
182
+ // Only send response when upload is complete (result is not null)
183
+ if (result) {
184
+ if (result.success) {
185
+ this.send({
186
+ type: 'uploadComplete',
187
+ session: this.sessionId,
188
+ meta: {
189
+ filename: meta.filename || 'unknown',
190
+ path: result.path || '',
191
+ success: 'true',
192
+ },
193
+ });
194
+ }
195
+ else {
196
+ this.send({
197
+ type: 'uploadError',
198
+ session: this.sessionId,
199
+ meta: {
200
+ filename: meta.filename || 'unknown',
201
+ error: result.error || 'Upload failed',
202
+ },
203
+ });
204
+ }
205
+ }
206
+ }
207
+ catch (e) {
208
+ console.error('[WS] File upload error:', e);
209
+ this.send({
210
+ type: 'uploadError',
211
+ session: this.sessionId,
212
+ meta: {
213
+ filename: meta.filename || 'unknown',
214
+ error: e instanceof Error ? e.message : 'Upload failed',
215
+ },
216
+ });
217
+ }
218
+ }
151
219
  handleError(message) {
152
220
  const meta = message.meta;
153
221
  if (!meta)
@@ -248,25 +316,21 @@ class RelayWebSocketClient extends events_1.EventEmitter {
248
316
  return this.sendScreen(data);
249
317
  }
250
318
  }
251
- /**
252
- * Send file content to be displayed in the web FileViewer
253
- * @param filename - The name of the file
254
- * @param content - The file content (UTF-8 for text, base64 for images)
255
- * @param contentType - MIME type (e.g., 'text/markdown', 'text/html', 'image/png')
256
- * @param path - Optional file path
257
- */
258
- sendFileView(filename, content, contentType, path) {
319
+ sendFileView(filePath, content, language, error) {
259
320
  if (!this.isConnected)
260
321
  return false;
322
+ const meta = {
323
+ filePath: filePath,
324
+ language: language
325
+ };
326
+ if (error) {
327
+ meta.error = error;
328
+ }
261
329
  return this.send({
262
330
  type: 'file_view',
263
331
  session: this.sessionId,
264
- meta: {
265
- filename,
266
- contentType,
267
- path: path || ''
268
- },
269
- payload: content
332
+ meta: meta,
333
+ payload: Buffer.from(content).toString('base64')
270
334
  });
271
335
  }
272
336
  getConnected() {
@@ -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>;