sessioncast-cli 2.0.4 → 2.0.6

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 CHANGED
@@ -1,25 +1,11 @@
1
1
  # SessionCast CLI
2
2
 
3
- Node.js agent and CLI for [SessionCast](https://sessioncast.io) - a real-time terminal sharing platform.
3
+ [![npm version](https://img.shields.io/npm/v/sessioncast-cli)](https://www.npmjs.com/package/sessioncast-cli)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
4
5
 
5
- ## Features
6
-
7
- ### Agent
8
- - **Auto-discovery**: Automatically detects and connects tmux sessions
9
- - **Real-time screen capture**: Streams terminal output with gzip compression
10
- - **Circuit breaker**: Prevents reconnection storms with exponential backoff
11
- - **Interactive control**: Supports keyboard input, resize, and session management
12
- - **File viewer**: Cmd+Click on file paths to view files in browser
6
+ Node.js agent and CLI for [SessionCast](https://sessioncast.io) — a real-time terminal sharing and AI agent orchestration platform.
13
7
 
14
- ### CLI Commands
15
- - `sessioncast login` - Browser-based OAuth login (recommended)
16
- - `sessioncast login <api-key>` - Authenticate with API key or agent token
17
- - `sessioncast logout` - Clear stored credentials
18
- - `sessioncast status` - Check authentication status
19
- - `sessioncast agents` - List registered agents
20
- - `sessioncast list [agent]` - List tmux sessions
21
- - `sessioncast send <target> <keys>` - Send keys to a session
22
- - `sessioncast agent` - Start the agent
8
+ **SessionCast enables any AI coding agent (Claude Code, Gemini CLI, Codex, Cursor, Aider) to control other agents across machines through tmux sessions.**
23
9
 
24
10
  ## Installation
25
11
 
@@ -27,15 +13,12 @@ Node.js agent and CLI for [SessionCast](https://sessioncast.io) - a real-time te
27
13
  npm install -g sessioncast-cli
28
14
  ```
29
15
 
30
- ### Requirements
31
- - Node.js 18+
32
- - tmux (Linux/macOS) or [itmux](https://github.com/itefixnet/itmux) (Windows)
16
+ **Requirements:** Node.js 18+, tmux (Linux/macOS) or [itmux](https://github.com/itefixnet/itmux) (Windows)
33
17
 
34
18
  ### Windows Setup
35
19
 
36
- **Quick Install (PowerShell - Recommended)**:
37
20
  ```powershell
38
- # Download and extract itmux to C:\itmux
21
+ # Install itmux
39
22
  Invoke-WebRequest -Uri "https://github.com/itefixnet/itmux/releases/download/v1.1.0/itmux_1.1.0_x64_free.zip" -OutFile "$env:TEMP\itmux.zip"
40
23
  Expand-Archive -Path "$env:TEMP\itmux.zip" -DestinationPath "C:\itmux" -Force
41
24
 
@@ -43,20 +26,21 @@ Expand-Archive -Path "$env:TEMP\itmux.zip" -DestinationPath "C:\itmux" -Force
43
26
  npm install -g sessioncast-cli
44
27
  ```
45
28
 
46
- **Manual Installation**:
47
- 1. Download [itmux v1.1.0](https://github.com/itefixnet/itmux/releases/download/v1.1.0/itmux_1.1.0_x64_free.zip)
48
- 2. Extract to one of these locations:
49
- - `C:\itmux` (recommended)
50
- - `%USERPROFILE%\itmux`
51
- - Or set `ITMUX_HOME` environment variable
52
- 3. Verify: `C:\itmux\bin\bash.exe` should exist
53
- 4. Install CLI: `npm install -g sessioncast-cli`
54
-
55
29
  ## Quick Start
56
30
 
57
- 1. **Get your agent token** from [app.sessioncast.io](https://app.sessioncast.io)
31
+ ### 1. Login
58
32
 
59
- 2. **Create config file** `~/.sessioncast.yml`:
33
+ ```bash
34
+ # Browser-based OAuth (recommended)
35
+ sessioncast login
36
+
37
+ # Or with agent token
38
+ sessioncast login agt_your_token_here
39
+ ```
40
+
41
+ ### 2. Configure the agent
42
+
43
+ Create `~/.sessioncast.yml`:
60
44
 
61
45
  ```yaml
62
46
  machineId: my-machine
@@ -64,19 +48,168 @@ relay: wss://relay.sessioncast.io/ws
64
48
  token: agt_your_agent_token_here
65
49
  ```
66
50
 
67
- 3. **Start the agent**:
51
+ ### 3. Start the agent
68
52
 
69
53
  ```bash
54
+ # Background (recommended)
55
+ nohup sessioncast agent > /tmp/sessioncast-agent.log 2>&1 &
56
+
57
+ # Foreground (for debugging)
70
58
  sessioncast agent
71
59
  ```
72
60
 
73
- 4. **View your sessions** at [app.sessioncast.io](https://app.sessioncast.io)
61
+ ### 4. View your sessions
62
+
63
+ Open [app.sessioncast.io](https://app.sessioncast.io) to see your terminal sessions in real-time.
64
+
65
+ ## CLI Commands
66
+
67
+ ### Authentication
68
+
69
+ ```bash
70
+ sessioncast login # Browser-based OAuth login
71
+ sessioncast login agt_xxx # Login with agent token
72
+ sessioncast login sk-xxx # Login with API key
73
+ sessioncast logout # Clear stored credentials
74
+ sessioncast status # Check authentication status
75
+ ```
76
+
77
+ ### Session Discovery
78
+
79
+ ```bash
80
+ sessioncast list # List all sessions across machines
81
+ sessioncast list dev-server # Filter by machine name
82
+ sessioncast agents # List registered agents
83
+ ```
84
+
85
+ Example output:
86
+ ```
87
+ Sessions:
88
+
89
+ AGENT SESSION STATUS TARGET
90
+ dev-macbook workspace online dev-macbook/workspace
91
+ dev-macbook worker1 online dev-macbook/worker1
92
+ staging-server main online staging-server/main
93
+ ```
94
+
95
+ ### Send Keys
96
+
97
+ Send keystrokes to any remote tmux session:
98
+
99
+ ```bash
100
+ # Send a command (Enter is pressed automatically)
101
+ sessioncast send workspace "npm run build"
102
+
103
+ # Target a specific machine
104
+ sessioncast send dev-macbook/workspace "git status"
105
+ sessioncast send dev-macbook:workspace "git status" # colon works too
106
+
107
+ # Send without pressing Enter (for special keys)
108
+ sessioncast send workspace "C-c" --no-enter # Ctrl+C
109
+ sessioncast send workspace "C-l" --no-enter # Ctrl+L (clear)
110
+ ```
111
+
112
+ ### Agent
113
+
114
+ ```bash
115
+ sessioncast agent # Start agent
116
+ sessioncast agent -c /path/to/config.yml # Custom config path
117
+ ```
118
+
119
+ ## AI Agent Orchestration
120
+
121
+ SessionCast's key capability is enabling AI-to-AI orchestration. Any terminal-based AI agent can create tmux sessions, launch other AI agents, and send them tasks.
122
+
123
+ ### Launch an AI Worker
124
+
125
+ ```bash
126
+ # Create a tmux session and launch an AI agent
127
+ tmux new-session -d -s worker1
128
+ sessioncast send worker1 "claude" # or: gemini, codex, aider, cursor
129
+ sleep 5
130
+
131
+ # Send a task
132
+ sessioncast send worker1 "Implement input validation for the signup form in src/validators/signup.ts"
133
+ ```
134
+
135
+ ### Supported AI Agents
136
+
137
+ | Agent | Launch Command | Best For |
138
+ |-------|---------------|----------|
139
+ | Claude Code | `claude` | Multi-file tasks, architecture |
140
+ | Gemini CLI | `gemini` | Code review, analysis |
141
+ | OpenAI Codex | `codex` | Code generation, testing |
142
+ | Cursor CLI | `cursor` | Interactive editing |
143
+ | Aider | `aider` | Git-aware pair programming |
144
+ | GitHub Copilot | `gh copilot` | GitHub-integrated tasks |
145
+
146
+ ### Dev → QA → Deploy Pipeline
147
+
148
+ ```bash
149
+ # Setup sessions with different AI agents
150
+ tmux new-session -d -s dev
151
+ tmux new-session -d -s qa
152
+ tmux new-session -d -s deploy
153
+ sessioncast send dev "claude"
154
+ sessioncast send qa "codex"
155
+ sessioncast send deploy "gemini"
156
+ sleep 10
157
+
158
+ # Stage 1: Development
159
+ sessioncast send dev "Implement GET /api/users/:id endpoint in src/routes/users.ts"
160
+
161
+ # Stage 2: QA (after dev completes)
162
+ sessioncast send qa "Run tests: npm test -- --grep 'user profile'. Report any failures."
163
+
164
+ # Stage 3: Deploy (after QA passes)
165
+ sessioncast send deploy "Deploy to staging: git pull && npm run build && pm2 restart api"
166
+ ```
167
+
168
+ ### Dev ↔ QA Feedback Loop
169
+
170
+ ```bash
171
+ # QA finds issues → send back to dev
172
+ sessioncast send dev "QA found: GET /api/users/:id returns 500 when user not found. Should return 404."
173
+
174
+ # Dev fixes → QA re-tests
175
+ sessioncast send qa "Re-run user profile tests: npm test src/__tests__/users.test.ts"
176
+ ```
177
+
178
+ ### Multi-Agent Fan-Out
179
+
180
+ ```bash
181
+ # Parallel development across multiple areas
182
+ tmux new-session -d -s frontend
183
+ tmux new-session -d -s backend
184
+ tmux new-session -d -s tests
185
+
186
+ sessioncast send frontend "claude"
187
+ sessioncast send backend "gemini"
188
+ sessioncast send tests "codex"
189
+ sleep 10
190
+
191
+ sessioncast send frontend "Build Settings page at src/pages/Settings.tsx with theme toggle and language selector"
192
+ sessioncast send backend "Create REST endpoints: GET/PUT /api/settings in src/routes/settings.ts"
193
+ sessioncast send tests "Write integration tests for the settings API in src/__tests__/settings.test.ts"
194
+ ```
195
+
196
+ ### Cross-Machine Control
197
+
198
+ ```bash
199
+ # Control agents on different servers
200
+ sessioncast send dev-macbook/workspace "npm run build"
201
+ sessioncast send staging-server/main "git pull && npm test"
202
+ sessioncast send prod-server/deploy "pm2 reload api"
203
+ ```
204
+
205
+ For more orchestration patterns (code review pipeline, incident response, monorepo development), see [docs/claude-to-claude.md](docs/claude-to-claude.md).
74
206
 
75
207
  ## Configuration
76
208
 
77
- Create `~/.sessioncast.yml` or `~/.tmux-remote.yml`:
209
+ ### Agent Config (`~/.sessioncast.yml`)
78
210
 
79
211
  ```yaml
212
+ # Required
80
213
  machineId: my-machine
81
214
  relay: wss://relay.sessioncast.io/ws
82
215
  token: agt_your_agent_token_here
@@ -85,79 +218,110 @@ token: agt_your_agent_token_here
85
218
  api:
86
219
  enabled: true
87
220
  agentId: "your-agent-uuid"
88
-
89
221
  exec:
90
222
  enabled: true
91
223
  shell: /bin/bash
92
224
  workingDir: /home/user
93
225
  defaultTimeout: 30000
94
-
95
226
  llm:
96
227
  enabled: false
97
228
  ```
98
229
 
99
230
  ### Environment Variables
100
231
 
101
- - `SESSIONCAST_CONFIG` - Custom config file path
102
- - `TMUX_REMOTE_CONFIG` - Alternative config file path
232
+ | Variable | Description |
233
+ |----------|-------------|
234
+ | `SESSIONCAST_CONFIG` | Custom config file path |
235
+ | `TMUX_REMOTE_CONFIG` | Alternative config file path |
103
236
 
104
- ## Usage
237
+ ### CLI Config
105
238
 
106
- ### Start the Agent
239
+ After `sessioncast login`, credentials are stored automatically in the system config directory (`~/.config/sessioncast/`). No manual config needed for `send`, `list`, and other CLI commands.
107
240
 
108
- ```bash
109
- # Run agent (foreground)
110
- sessioncast agent
241
+ ## Features
111
242
 
112
- # Run agent (background)
113
- nohup sessioncast agent > /tmp/sessioncast-agent.log 2>&1 &
114
- ```
243
+ ### Agent Capabilities
244
+ - **Auto-discovery**: Detects and connects all tmux sessions automatically
245
+ - **Real-time streaming**: Captures terminal output with gzip compression
246
+ - **Multi-pane support**: Detects tmux panes and streams each independently
247
+ - **File viewer**: Cmd+Click on file paths to view files in browser
248
+ - **Circuit breaker**: Prevents reconnection storms with exponential backoff (max 5 retries, 30s max delay, 2min cooldown)
115
249
 
116
- ### Send Keys to Session
250
+ ### Web Viewer (app.sessioncast.io)
251
+ - Real-time terminal rendering with xterm.js
252
+ - Multi-pane layout view
253
+ - Interactive keyboard input
254
+ - File viewer panel
255
+ - Session sharing via links
256
+ - Dark/light theme
117
257
 
118
- ```bash
119
- # Send text to a session
120
- sessioncast send my-machine/dev "ls -la"
258
+ ## Architecture
121
259
 
122
- # Send special keys
123
- sessioncast send my-machine/dev "Enter"
260
+ ```
261
+ ┌──────────────┐ WebSocket ┌──────────────┐ WebSocket ┌──────────────┐
262
+ │ Agent │ ◄────────────────► │ Relay │ ◄───────────────► │ Viewer │
263
+ │ (sessioncast │ screen/keys │ (relay. │ screen/keys │ (app. │
264
+ │ agent) │ paneLayout │ sessioncast │ paneLayout │ sessioncast │
265
+ │ │ │ .io) │ │ .io) │
266
+ └──────┬───────┘ └──────────────┘ └──────────────┘
267
+
268
+ │ tmux capture-pane / send-keys
269
+
270
+ ┌──────────────┐
271
+ │ tmux │
272
+ │ sessions │
273
+ │ (AI agents) │
274
+ └──────────────┘
124
275
  ```
125
276
 
126
- ## Architecture
277
+ **Message flow:**
278
+ 1. Agent captures tmux screen → sends `screen`/`screenGz` via WebSocket
279
+ 2. Relay forwards to all connected viewers
280
+ 3. Viewer sends `keys` → Relay forwards to agent → Agent runs `tmux send-keys`
281
+ 4. For multi-pane: Agent sends `paneLayout` + per-pane `screen` with `meta.pane`
127
282
 
128
- ```
129
- ┌─────────────┐ WebSocket ┌─────────────┐ WebSocket ┌─────────────┐
130
- │ Agent │ ◄─────────────────► │ Relay │ ◄────────────────► │ Viewer │
131
- │ (Node.js) │ screen/keys │ (Server) │ screen/keys │ (Web) │
132
- └─────────────┘ └─────────────┘ └─────────────┘
133
-
134
- │ tmux
135
-
136
- ┌─────────────┐
137
- │ tmux │
138
- │ sessions │
139
- └─────────────┘
283
+ ## Troubleshooting
284
+
285
+ ### Session not found
286
+ ```bash
287
+ # Verify available sessions
288
+ sessioncast list
289
+
290
+ # Use the exact target from the TARGET column
291
+ sessioncast send my-machine/worker1 "hello"
140
292
  ```
141
293
 
142
- ## Circuit Breaker
294
+ ### Agent not connecting
295
+ ```bash
296
+ # Check tmux is running
297
+ tmux ls
143
298
 
144
- The agent implements a circuit breaker pattern to prevent reconnection storms:
299
+ # Check agent logs
300
+ tail -f /tmp/sessioncast-agent.log
145
301
 
146
- - **Max reconnect attempts**: 5
147
- - **Base delay**: 1 second
148
- - **Max delay**: 30 seconds (with exponential backoff + jitter)
149
- - **Circuit breaker duration**: 2 minutes cooldown after max attempts
302
+ # Restart agent
303
+ pkill -f "sessioncast agent"
304
+ nohup sessioncast agent > /tmp/sessioncast-agent.log 2>&1 &
305
+ ```
150
306
 
151
- ## Requirements
307
+ ### Authentication issues
308
+ ```bash
309
+ # Re-login
310
+ sessioncast logout
311
+ sessioncast login
152
312
 
153
- - Node.js >= 18
154
- - tmux installed on the host machine
313
+ # Verify
314
+ sessioncast status
315
+ ```
155
316
 
156
317
  ## License
157
318
 
158
319
  MIT License - see [LICENSE](LICENSE) for details.
159
320
 
160
- ## Support
321
+ ## Links
161
322
 
162
- - Homepage: https://sessioncast.io
163
- - Email: devload@sessioncast.io
323
+ - **Homepage**: [sessioncast.io](https://sessioncast.io)
324
+ - **Web App**: [app.sessioncast.io](https://app.sessioncast.io)
325
+ - **npm**: [sessioncast-cli](https://www.npmjs.com/package/sessioncast-cli)
326
+ - **GitHub**: [sessioncast/sessioncast-cli](https://github.com/sessioncast/sessioncast-cli)
327
+ - **Email**: devload@sessioncast.io
@@ -96,9 +96,19 @@ class TmuxSessionHandler {
96
96
  this.wsClient.on('keys', (keys, paneId) => {
97
97
  this.handleKeys(keys, paneId);
98
98
  });
99
- this.wsClient.on('resize', ({ cols, rows }) => {
100
- console.log(`[${this.tmuxSession}] Resize: ${cols}x${rows}`);
101
- tmux.resizeWindow(this.tmuxSession, cols, rows);
99
+ this.wsClient.on('resize', ({ cols, rows, pane }) => {
100
+ if (cols < 10 || rows < 4) {
101
+ console.log(`[${this.tmuxSession}] Ignoring resize with too-small dimensions: ${cols}x${rows}`);
102
+ return;
103
+ }
104
+ if (pane) {
105
+ console.log(`[${this.tmuxSession}] Resize pane ${pane}: ${cols}x${rows}`);
106
+ tmux.resizePane(pane, cols, rows);
107
+ }
108
+ else {
109
+ console.log(`[${this.tmuxSession}] Resize: ${cols}x${rows}`);
110
+ tmux.resizeWindow(this.tmuxSession, cols, rows);
111
+ }
102
112
  });
103
113
  this.wsClient.on('createSession', (name) => {
104
114
  console.log(`[${this.tmuxSession}] Create session request: ${name}`);
@@ -20,6 +20,7 @@ export interface TmuxExecutor {
20
20
  sendKeys(session: string, keys: string): boolean;
21
21
  sendSpecialKey(session: string, key: string): boolean;
22
22
  resizeWindow(session: string, cols: number, rows: number): boolean;
23
+ resizePane(paneId: string, cols: number, rows: number): boolean;
23
24
  killSession(session: string): boolean;
24
25
  createSession(session: string, workingDir?: string): boolean;
25
26
  isAvailable(): boolean;
@@ -38,6 +39,7 @@ export declare class UnixTmuxExecutor implements TmuxExecutor {
38
39
  sendKeys(session: string, keys: string): boolean;
39
40
  sendSpecialKey(session: string, key: string): boolean;
40
41
  resizeWindow(session: string, cols: number, rows: number): boolean;
42
+ resizePane(paneId: string, cols: number, rows: number): boolean;
41
43
  killSession(session: string): boolean;
42
44
  createSession(session: string, workingDir?: string): boolean;
43
45
  isAvailable(): boolean;
@@ -61,6 +63,7 @@ export declare class WindowsTmuxExecutor implements TmuxExecutor {
61
63
  sendKeys(session: string, keys: string): boolean;
62
64
  sendSpecialKey(session: string, key: string): boolean;
63
65
  resizeWindow(session: string, cols: number, rows: number): boolean;
66
+ resizePane(paneId: string, cols: number, rows: number): boolean;
64
67
  killSession(session: string): boolean;
65
68
  createSession(session: string, workingDir?: string): boolean;
66
69
  isAvailable(): boolean;
@@ -130,6 +130,15 @@ class UnixTmuxExecutor {
130
130
  return false;
131
131
  }
132
132
  }
133
+ resizePane(paneId, cols, rows) {
134
+ try {
135
+ (0, child_process_1.execSync)(`tmux resize-pane -t "${paneId}" -x ${cols} -y ${rows}`, { stdio: 'pipe' });
136
+ return true;
137
+ }
138
+ catch {
139
+ return false;
140
+ }
141
+ }
133
142
  killSession(session) {
134
143
  try {
135
144
  (0, child_process_1.execSync)(`tmux kill-session -t "${session}"`, { stdio: 'pipe' });
@@ -326,6 +335,16 @@ class WindowsTmuxExecutor {
326
335
  return false;
327
336
  }
328
337
  }
338
+ resizePane(paneId, cols, rows) {
339
+ try {
340
+ const escaped = this.escapeSession(paneId);
341
+ this.executeCommand(`tmux resize-pane -t '${escaped}' -x ${cols} -y ${rows}`);
342
+ return true;
343
+ }
344
+ catch {
345
+ return false;
346
+ }
347
+ }
329
348
  killSession(session) {
330
349
  try {
331
350
  const escaped = this.escapeSession(session);
@@ -20,6 +20,10 @@ export declare function sendKeys(target: string, keys: string, enter?: boolean):
20
20
  * Resize tmux window
21
21
  */
22
22
  export declare function resizeWindow(sessionName: string, cols: number, rows: number): boolean;
23
+ /**
24
+ * Resize a specific tmux pane by its ID (e.g., %0, %1)
25
+ */
26
+ export declare function resizePane(paneId: string, cols: number, rows: number): boolean;
23
27
  /**
24
28
  * Create new tmux session
25
29
  */
@@ -5,6 +5,7 @@ exports.listSessions = listSessions;
5
5
  exports.capturePane = capturePane;
6
6
  exports.sendKeys = sendKeys;
7
7
  exports.resizeWindow = resizeWindow;
8
+ exports.resizePane = resizePane;
8
9
  exports.createSession = createSession;
9
10
  exports.killSession = killSession;
10
11
  exports.isAvailable = isAvailable;
@@ -101,6 +102,12 @@ function sendKeys(target, keys, enter = true) {
101
102
  function resizeWindow(sessionName, cols, rows) {
102
103
  return getExecutor().resizeWindow(sessionName, cols, rows);
103
104
  }
105
+ /**
106
+ * Resize a specific tmux pane by its ID (e.g., %0, %1)
107
+ */
108
+ function resizePane(paneId, cols, rows) {
109
+ return getExecutor().resizePane(paneId, cols, rows);
110
+ }
104
111
  /**
105
112
  * Create new tmux session
106
113
  */
@@ -127,7 +127,7 @@ class RelayWebSocketClient extends events_1.EventEmitter {
127
127
  const cols = parseInt(message.meta.cols, 10);
128
128
  const rows = parseInt(message.meta.rows, 10);
129
129
  if (!isNaN(cols) && !isNaN(rows)) {
130
- this.emit('resize', { cols, rows });
130
+ this.emit('resize', { cols, rows, pane: message.meta.pane });
131
131
  }
132
132
  }
133
133
  break;
package/dist/api.js CHANGED
@@ -8,12 +8,12 @@ const node_fetch_1 = __importDefault(require("node-fetch"));
8
8
  const config_1 = require("./config");
9
9
  class ApiClient {
10
10
  getHeaders() {
11
- const apiKey = (0, config_1.getApiKey)();
12
- if (!apiKey) {
11
+ const token = (0, config_1.getApiKey)() || (0, config_1.getAccessToken)() || (0, config_1.getAgentToken)();
12
+ if (!token) {
13
13
  throw new Error('Not logged in. Run: sessioncast login');
14
14
  }
15
15
  return {
16
- 'Authorization': `Bearer ${apiKey}`,
16
+ 'Authorization': `Bearer ${token}`,
17
17
  'Content-Type': 'application/json'
18
18
  };
19
19
  }
@@ -6,56 +6,29 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.sendKeys = sendKeys;
7
7
  const chalk_1 = __importDefault(require("chalk"));
8
8
  const ora_1 = __importDefault(require("ora"));
9
- const api_1 = require("../api");
9
+ const ws_1 = __importDefault(require("ws"));
10
10
  const config_1 = require("../config");
11
11
  async function sendKeys(target, keys, options) {
12
12
  if (!(0, config_1.isLoggedIn)()) {
13
- console.log(chalk_1.default.red('Not logged in. Run: sessioncast login <api-key>'));
13
+ console.log(chalk_1.default.red('Not logged in. Run: sessioncast login'));
14
14
  process.exit(1);
15
15
  }
16
- // Parse target: "agent:session" or "agent:session:window"
17
- const parts = target.split(':');
18
- if (parts.length < 2) {
19
- console.log(chalk_1.default.red('Invalid target format.'));
20
- console.log(chalk_1.default.gray('Expected: <agent>:<session> or <agent>:<session>:<window>'));
21
- console.log(chalk_1.default.gray('Example: macbook:dev or server:main:0'));
16
+ // Parse target: "session" or "machineId/session"
17
+ // Accept both "machineId/session" and "machineId:session" formats
18
+ const normalizedTarget = target.replace(':', '/');
19
+ const token = (0, config_1.getAccessToken)() || (0, config_1.getAgentToken)() || (0, config_1.getApiKey)();
20
+ if (!token) {
21
+ console.log(chalk_1.default.red('No auth token found. Run: sessioncast login'));
22
22
  process.exit(1);
23
23
  }
24
- const agentName = parts[0];
25
- const sessionTarget = parts.slice(1).join(':'); // session:window or just session
26
- const spinner = (0, ora_1.default)('Finding agent...').start();
24
+ const relayUrl = (0, config_1.getRelayUrl)();
25
+ const spinner = (0, ora_1.default)('Connecting to relay...').start();
27
26
  try {
28
- // Find agent by name/label/machineId
29
- const agent = await api_1.api.findAgentByName(agentName);
30
- if (!agent) {
31
- spinner.stop();
32
- console.log(chalk_1.default.red(`Agent not found: ${agentName}`));
33
- console.log(chalk_1.default.gray('Run: sessioncast agents'));
34
- process.exit(1);
35
- }
36
- if (!agent.isActive) {
37
- spinner.stop();
38
- console.log(chalk_1.default.red(`Agent is offline: ${agentName}`));
39
- process.exit(1);
40
- }
41
- if (!agent.apiEnabled) {
42
- spinner.stop();
43
- console.log(chalk_1.default.red(`API is not enabled for agent: ${agentName}`));
44
- console.log(chalk_1.default.gray('Enable API in agent settings at https://account.sessioncast.io'));
45
- process.exit(1);
46
- }
47
- spinner.text = 'Sending keys...';
48
- const result = await api_1.api.sendKeys(agent.id, sessionTarget, keys, !options.noEnter);
27
+ const sessionId = await sendKeysViaRelay(relayUrl, token, normalizedTarget, keys, !options.noEnter, spinner);
49
28
  spinner.stop();
50
- if (result.success) {
51
- console.log(chalk_1.default.green(`✓ Keys sent to ${target}`));
52
- if (!options.noEnter) {
53
- console.log(chalk_1.default.gray('(Enter key was pressed)'));
54
- }
55
- }
56
- else {
57
- console.log(chalk_1.default.red(`Failed to send keys: ${result.error || 'Unknown error'}`));
58
- process.exit(1);
29
+ console.log(chalk_1.default.green(`✓ Keys sent to ${sessionId}`));
30
+ if (!options.noEnter) {
31
+ console.log(chalk_1.default.gray('(Enter key was pressed)'));
59
32
  }
60
33
  }
61
34
  catch (error) {
@@ -64,3 +37,114 @@ async function sendKeys(target, keys, options) {
64
37
  process.exit(1);
65
38
  }
66
39
  }
40
+ function sendKeysViaRelay(relayUrl, token, target, keys, enter, spinner) {
41
+ return new Promise((resolve, reject) => {
42
+ const wsUrl = `${relayUrl}?token=${encodeURIComponent(token)}`;
43
+ const ws = new ws_1.default(wsUrl);
44
+ let resolved = false;
45
+ let sessionList = [];
46
+ const timeout = setTimeout(() => {
47
+ if (!resolved) {
48
+ resolved = true;
49
+ ws.close();
50
+ reject(new Error('Timeout waiting for relay response'));
51
+ }
52
+ }, 10000);
53
+ ws.on('open', () => {
54
+ // Request session list to find the target
55
+ ws.send(JSON.stringify({ type: 'listSessions' }));
56
+ });
57
+ ws.on('message', (data) => {
58
+ try {
59
+ const message = JSON.parse(data.toString());
60
+ if (message.type === 'sessionList' && message.sessions) {
61
+ sessionList = message.sessions;
62
+ spinner.text = `Found ${sessionList.length} sessions, finding target...`;
63
+ // Find matching session
64
+ const matched = findSession(sessionList, target);
65
+ if (!matched) {
66
+ clearTimeout(timeout);
67
+ resolved = true;
68
+ ws.close();
69
+ const available = sessionList.map(s => ` ${s.id} (${s.label || 'no label'}) [${s.status}]`).join('\n');
70
+ reject(new Error(`Session not found: ${target}\n\nAvailable sessions:\n${available || ' (none)'}`));
71
+ return;
72
+ }
73
+ if (matched.status !== 'online') {
74
+ clearTimeout(timeout);
75
+ resolved = true;
76
+ ws.close();
77
+ reject(new Error(`Session is offline: ${matched.id}`));
78
+ return;
79
+ }
80
+ // Register as viewer first
81
+ ws.send(JSON.stringify({
82
+ type: 'register',
83
+ role: 'viewer',
84
+ session: matched.id,
85
+ }));
86
+ // Send keys
87
+ const payload = enter ? keys + '\n' : keys;
88
+ ws.send(JSON.stringify({
89
+ type: 'keys',
90
+ session: matched.id,
91
+ payload,
92
+ }));
93
+ // Small delay to ensure delivery, then close
94
+ setTimeout(() => {
95
+ if (!resolved) {
96
+ clearTimeout(timeout);
97
+ resolved = true;
98
+ ws.close();
99
+ resolve(matched.id);
100
+ }
101
+ }, 300);
102
+ }
103
+ }
104
+ catch {
105
+ // ignore parse errors
106
+ }
107
+ });
108
+ ws.on('error', (err) => {
109
+ if (!resolved) {
110
+ clearTimeout(timeout);
111
+ resolved = true;
112
+ reject(new Error(`WebSocket error: ${err.message}`));
113
+ }
114
+ });
115
+ ws.on('close', () => {
116
+ if (!resolved) {
117
+ clearTimeout(timeout);
118
+ resolved = true;
119
+ reject(new Error('Connection closed unexpectedly'));
120
+ }
121
+ });
122
+ });
123
+ }
124
+ function findSession(sessions, target) {
125
+ // Exact match on session id
126
+ const exact = sessions.find(s => s.id === target);
127
+ if (exact)
128
+ return exact;
129
+ // Match by label (session name part)
130
+ const byLabel = sessions.find(s => s.label === target);
131
+ if (byLabel)
132
+ return byLabel;
133
+ // Match by partial: "machineId/session" where target might be partial machineId
134
+ const parts = target.split('/');
135
+ if (parts.length >= 2) {
136
+ const [machineHint, ...sessionParts] = parts;
137
+ const sessionName = sessionParts.join('/');
138
+ const match = sessions.find(s => s.id.includes(machineHint) && s.id.endsWith('/' + sessionName));
139
+ if (match)
140
+ return match;
141
+ }
142
+ // Match just session name (last part of id after /)
143
+ const bySessionName = sessions.find(s => {
144
+ const name = s.id.split('/').pop();
145
+ return name === target;
146
+ });
147
+ if (bySessionName)
148
+ return bySessionName;
149
+ return null;
150
+ }
@@ -6,75 +6,68 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.listSessions = listSessions;
7
7
  const chalk_1 = __importDefault(require("chalk"));
8
8
  const ora_1 = __importDefault(require("ora"));
9
- const api_1 = require("../api");
9
+ const ws_1 = __importDefault(require("ws"));
10
10
  const config_1 = require("../config");
11
11
  async function listSessions(agentName) {
12
12
  if (!(0, config_1.isLoggedIn)()) {
13
- console.log(chalk_1.default.red('Not logged in. Run: sessioncast login <api-key>'));
13
+ console.log(chalk_1.default.red('Not logged in. Run: sessioncast login'));
14
14
  process.exit(1);
15
15
  }
16
- const spinner = (0, ora_1.default)('Fetching sessions...').start();
16
+ const token = (0, config_1.getAccessToken)() || (0, config_1.getAgentToken)() || (0, config_1.getApiKey)();
17
+ if (!token) {
18
+ console.log(chalk_1.default.red('No auth token found. Run: sessioncast login'));
19
+ process.exit(1);
20
+ }
21
+ const relayUrl = (0, config_1.getRelayUrl)();
22
+ const spinner = (0, ora_1.default)('Fetching sessions from relay...').start();
17
23
  try {
18
- const agents = await api_1.api.listAgents();
19
- const onlineAgents = agents.filter(a => a.isActive && a.apiEnabled);
20
- if (onlineAgents.length === 0) {
21
- spinner.stop();
22
- console.log(chalk_1.default.yellow('No online agents with API enabled.'));
24
+ const sessions = await fetchSessionsFromRelay(relayUrl, token);
25
+ spinner.stop();
26
+ if (sessions.length === 0) {
27
+ console.log(chalk_1.default.yellow('No sessions found.'));
23
28
  return;
24
29
  }
30
+ // Group by machineId
31
+ const grouped = sessions.reduce((acc, s) => {
32
+ const machine = s.machineId || 'unknown';
33
+ if (!acc[machine])
34
+ acc[machine] = [];
35
+ acc[machine].push(s);
36
+ return acc;
37
+ }, {});
25
38
  // Filter by agent name if provided
26
- let targetAgents = onlineAgents;
27
39
  if (agentName) {
28
- const found = onlineAgents.find(a => a.label?.toLowerCase() === agentName.toLowerCase() ||
29
- a.machineId?.toLowerCase() === agentName.toLowerCase() ||
30
- a.id.startsWith(agentName));
31
- if (!found) {
32
- spinner.stop();
33
- console.log(chalk_1.default.red(`Agent not found: ${agentName}`));
34
- console.log(chalk_1.default.gray('Run: sessioncast agents'));
40
+ const matchedKey = Object.keys(grouped).find(k => k.toLowerCase().includes(agentName.toLowerCase()));
41
+ if (!matchedKey) {
42
+ console.log(chalk_1.default.red(`No agent matching: ${agentName}`));
43
+ console.log(chalk_1.default.gray('Available agents: ' + Object.keys(grouped).join(', ')));
35
44
  process.exit(1);
36
45
  }
37
- targetAgents = [found];
38
- }
39
- // Fetch sessions from all target agents
40
- const allSessions = [];
41
- for (const agent of targetAgents) {
42
- try {
43
- const sessions = await api_1.api.listSessions(agent.id);
44
- allSessions.push({ agent, sessions });
45
- }
46
- catch (error) {
47
- // Skip failed agents
48
- }
46
+ const filtered = {};
47
+ filtered[matchedKey] = grouped[matchedKey];
48
+ Object.keys(grouped).forEach(k => { if (k !== matchedKey)
49
+ delete grouped[k]; });
50
+ Object.assign(grouped, filtered);
49
51
  }
50
- spinner.stop();
51
- if (allSessions.every(as => as.sessions.length === 0)) {
52
- console.log(chalk_1.default.yellow('No tmux sessions found.'));
53
- return;
54
- }
55
- console.log(chalk_1.default.bold('\nTmux Sessions:\n'));
56
- // Table header
57
- console.log(chalk_1.default.gray(padRight('AGENT', 16) +
58
- padRight('SESSION', 16) +
59
- padRight('WINDOWS', 10) +
60
- padRight('ATTACHED', 10) +
52
+ console.log(chalk_1.default.bold('\nSessions:\n'));
53
+ console.log(chalk_1.default.gray(padRight('AGENT', 30) +
54
+ padRight('SESSION', 20) +
55
+ padRight('STATUS', 10) +
61
56
  'TARGET'));
62
- console.log(chalk_1.default.gray('─'.repeat(70)));
63
- // Table rows
64
- for (const { agent, sessions } of allSessions) {
65
- const agentName = agent.label || agent.machineId || agent.id.substring(0, 8);
66
- for (const session of sessions) {
67
- const attached = session.attached ? chalk_1.default.green('yes') : chalk_1.default.gray('no');
68
- const target = `${agentName}:${session.name}`;
69
- console.log(padRight(agentName, 16) +
70
- padRight(session.name, 16) +
71
- padRight(String(session.windows), 10) +
72
- padRight(attached, 10) +
57
+ console.log(chalk_1.default.gray('─'.repeat(80)));
58
+ for (const [machineId, machineSessions] of Object.entries(grouped)) {
59
+ for (const session of machineSessions) {
60
+ const label = session.label || session.id.split('/').pop() || session.id;
61
+ const statusColor = session.status === 'online' ? chalk_1.default.green : chalk_1.default.red;
62
+ const target = session.id;
63
+ console.log(padRight(machineId, 30) +
64
+ padRight(label, 20) +
65
+ padRight(statusColor(session.status), 10) +
73
66
  chalk_1.default.cyan(target));
74
67
  }
75
68
  }
76
69
  console.log();
77
- console.log(chalk_1.default.gray('Use: sessioncast send <target> "command"'));
70
+ console.log(chalk_1.default.gray('Use: sessioncast send <session-label> "command"'));
78
71
  }
79
72
  catch (error) {
80
73
  spinner.stop();
@@ -82,6 +75,39 @@ async function listSessions(agentName) {
82
75
  process.exit(1);
83
76
  }
84
77
  }
78
+ function fetchSessionsFromRelay(relayUrl, token) {
79
+ return new Promise((resolve, reject) => {
80
+ const wsUrl = `${relayUrl}?token=${encodeURIComponent(token)}`;
81
+ const ws = new ws_1.default(wsUrl);
82
+ const timeout = setTimeout(() => {
83
+ ws.close();
84
+ reject(new Error('Timeout waiting for session list'));
85
+ }, 10000);
86
+ ws.on('open', () => {
87
+ ws.send(JSON.stringify({ type: 'listSessions' }));
88
+ });
89
+ ws.on('message', (data) => {
90
+ try {
91
+ const message = JSON.parse(data.toString());
92
+ if (message.type === 'sessionList' && message.sessions) {
93
+ clearTimeout(timeout);
94
+ ws.close();
95
+ resolve(message.sessions);
96
+ }
97
+ }
98
+ catch {
99
+ // ignore
100
+ }
101
+ });
102
+ ws.on('error', (err) => {
103
+ clearTimeout(timeout);
104
+ reject(new Error(`WebSocket error: ${err.message}`));
105
+ });
106
+ ws.on('close', () => {
107
+ clearTimeout(timeout);
108
+ });
109
+ });
110
+ }
85
111
  function padRight(str, len) {
86
112
  const plainStr = str.replace(/\x1b\[[0-9;]*m/g, '');
87
113
  const padding = Math.max(0, len - plainStr.length);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sessioncast-cli",
3
- "version": "2.0.4",
3
+ "version": "2.0.6",
4
4
  "description": "SessionCast CLI - Control your agents from anywhere",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -17,10 +17,26 @@
17
17
  "tmux",
18
18
  "cli",
19
19
  "terminal",
20
- "remote"
20
+ "remote",
21
+ "ai-agent",
22
+ "orchestration",
23
+ "claude",
24
+ "gemini",
25
+ "codex",
26
+ "websocket",
27
+ "screen-sharing",
28
+ "multi-pane"
21
29
  ],
22
30
  "author": "SessionCast",
23
31
  "license": "MIT",
32
+ "homepage": "https://sessioncast.io",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/sessioncast/sessioncast-cli.git"
36
+ },
37
+ "bugs": {
38
+ "url": "https://github.com/sessioncast/sessioncast-cli/issues"
39
+ },
24
40
  "type": "commonjs",
25
41
  "files": [
26
42
  "dist"