sessioncast-cli 2.0.4 → 2.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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.5",
4
4
  "description": "SessionCast CLI - Control your agents from anywhere",
5
5
  "main": "dist/index.js",
6
6
  "bin": {