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 +244 -80
- package/dist/agent/session-handler.js +13 -3
- package/dist/agent/tmux-executor.d.ts +3 -0
- package/dist/agent/tmux-executor.js +19 -0
- package/dist/agent/tmux.d.ts +4 -0
- package/dist/agent/tmux.js +7 -0
- package/dist/agent/websocket.js +1 -1
- package/dist/api.js +3 -3
- package/dist/commands/sendkeys.js +125 -41
- package/dist/commands/sessions.js +77 -51
- package/package.json +18 -2
package/README.md
CHANGED
|
@@ -1,25 +1,11 @@
|
|
|
1
1
|
# SessionCast CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/sessioncast-cli)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
5
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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.
|
|
31
|
+
### 1. Login
|
|
58
32
|
|
|
59
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
102
|
-
|
|
232
|
+
| Variable | Description |
|
|
233
|
+
|----------|-------------|
|
|
234
|
+
| `SESSIONCAST_CONFIG` | Custom config file path |
|
|
235
|
+
| `TMUX_REMOTE_CONFIG` | Alternative config file path |
|
|
103
236
|
|
|
104
|
-
|
|
237
|
+
### CLI Config
|
|
105
238
|
|
|
106
|
-
|
|
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
|
-
|
|
109
|
-
# Run agent (foreground)
|
|
110
|
-
sessioncast agent
|
|
241
|
+
## Features
|
|
111
242
|
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
119
|
-
# Send text to a session
|
|
120
|
-
sessioncast send my-machine/dev "ls -la"
|
|
258
|
+
## Architecture
|
|
121
259
|
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
294
|
+
### Agent not connecting
|
|
295
|
+
```bash
|
|
296
|
+
# Check tmux is running
|
|
297
|
+
tmux ls
|
|
143
298
|
|
|
144
|
-
|
|
299
|
+
# Check agent logs
|
|
300
|
+
tail -f /tmp/sessioncast-agent.log
|
|
145
301
|
|
|
146
|
-
|
|
147
|
-
-
|
|
148
|
-
|
|
149
|
-
|
|
302
|
+
# Restart agent
|
|
303
|
+
pkill -f "sessioncast agent"
|
|
304
|
+
nohup sessioncast agent > /tmp/sessioncast-agent.log 2>&1 &
|
|
305
|
+
```
|
|
150
306
|
|
|
151
|
-
|
|
307
|
+
### Authentication issues
|
|
308
|
+
```bash
|
|
309
|
+
# Re-login
|
|
310
|
+
sessioncast logout
|
|
311
|
+
sessioncast login
|
|
152
312
|
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
##
|
|
321
|
+
## Links
|
|
161
322
|
|
|
162
|
-
- Homepage
|
|
163
|
-
-
|
|
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
|
-
|
|
101
|
-
|
|
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);
|
package/dist/agent/tmux.d.ts
CHANGED
|
@@ -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
|
*/
|
package/dist/agent/tmux.js
CHANGED
|
@@ -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
|
*/
|
package/dist/agent/websocket.js
CHANGED
|
@@ -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
|
|
12
|
-
if (!
|
|
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 ${
|
|
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
|
|
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
|
|
13
|
+
console.log(chalk_1.default.red('Not logged in. Run: sessioncast login'));
|
|
14
14
|
process.exit(1);
|
|
15
15
|
}
|
|
16
|
-
// Parse target: "
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
console.log(chalk_1.default.
|
|
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
|
|
25
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
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
|
|
13
|
+
console.log(chalk_1.default.red('Not logged in. Run: sessioncast login'));
|
|
14
14
|
process.exit(1);
|
|
15
15
|
}
|
|
16
|
-
const
|
|
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
|
|
19
|
-
|
|
20
|
-
if (
|
|
21
|
-
|
|
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
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
padRight(session.
|
|
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 <
|
|
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.
|
|
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"
|