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.
- package/README.md +65 -40
- package/dist/agent/session-handler.d.ts +1 -0
- package/dist/agent/session-handler.js +42 -0
- package/dist/agent/tmux-executor.d.ts +66 -0
- package/dist/agent/tmux-executor.js +368 -0
- package/dist/agent/tmux.d.ts +9 -1
- package/dist/agent/tmux.js +52 -76
- package/dist/agent/websocket.d.ts +2 -8
- package/dist/agent/websocket.js +78 -14
- 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/commands/autopilot.d.ts +30 -0
- package/dist/commands/autopilot.js +262 -0
- package/dist/commands/login.d.ts +2 -1
- package/dist/commands/login.js +199 -8
- package/dist/commands/project.d.ts +1 -1
- package/dist/commands/project.js +4 -13
- package/dist/config.d.ts +20 -0
- package/dist/config.js +69 -2
- package/dist/index.js +7 -47
- package/dist/project/executor.d.ts +8 -53
- package/dist/project/executor.js +64 -520
- package/dist/project/manager.d.ts +0 -13
- package/dist/project/manager.js +0 -107
- package/dist/project/relay-client.d.ts +18 -68
- package/dist/project/relay-client.js +134 -130
- package/dist/project/types.d.ts +5 -0
- package/dist/utils/fileUtils.d.ts +28 -0
- package/dist/utils/fileUtils.js +159 -0
- package/dist/utils/oauthServer.d.ts +18 -0
- package/dist/utils/oauthServer.js +244 -0
- package/dist/utils/pkce.d.ts +16 -0
- package/dist/utils/pkce.js +73 -0
- package/package.json +5 -14
- package/LICENSE +0 -21
package/dist/agent/tmux.js
CHANGED
|
@@ -7,45 +7,41 @@ exports.sendKeys = sendKeys;
|
|
|
7
7
|
exports.resizeWindow = resizeWindow;
|
|
8
8
|
exports.createSession = createSession;
|
|
9
9
|
exports.killSession = killSession;
|
|
10
|
-
|
|
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
|
-
|
|
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
|
|
36
|
-
return
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
return true;
|
|
65
|
+
return exec.sendSpecialKey(target, 'C-c');
|
|
81
66
|
}
|
|
82
67
|
if (keys === '\x04') {
|
|
83
68
|
// Ctrl+D
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79
|
+
exec.sendKeys(target, cmd);
|
|
97
80
|
}
|
|
98
|
-
|
|
99
|
-
return true;
|
|
81
|
+
return exec.sendSpecialKey(target, 'Enter');
|
|
100
82
|
}
|
|
101
83
|
// Regular text input
|
|
102
|
-
|
|
84
|
+
exec.sendKeys(target, keys);
|
|
103
85
|
if (enter) {
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
(
|
|
146
|
-
return true;
|
|
117
|
+
return getExecutor().isAvailable();
|
|
147
118
|
}
|
|
148
119
|
catch {
|
|
149
120
|
return false;
|
|
150
121
|
}
|
|
151
122
|
}
|
|
152
123
|
/**
|
|
153
|
-
*
|
|
124
|
+
* Get tmux version
|
|
154
125
|
*/
|
|
155
|
-
function
|
|
156
|
-
|
|
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
|
}
|
package/dist/agent/websocket.js
CHANGED
|
@@ -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
|
-
|
|
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>;
|