remote-cli-agent 0.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.
@@ -0,0 +1,3 @@
1
+ export declare function connect(url: string, token: string): void;
2
+ export declare function disconnect(): void;
3
+ export declare function getConnectionStatus(): boolean;
@@ -0,0 +1,116 @@
1
+ import WebSocket from 'ws';
2
+ import { getSessionList, writeToSession, resizeSession, attachSession, spawnSession, setOutputHandler, setSessionsChangeHandler } from './terminal.js';
3
+ let ws = null;
4
+ let reconnectTimer = null;
5
+ let reconnectAttempts = 0;
6
+ const MAX_RECONNECT_DELAY = 30000;
7
+ const BASE_RECONNECT_DELAY = 1000;
8
+ let serverUrl = '';
9
+ let agentToken = '';
10
+ let isConnected = false;
11
+ export function connect(url, token) {
12
+ serverUrl = url;
13
+ agentToken = token;
14
+ // Setup terminal handlers
15
+ setOutputHandler((sessionId, data) => {
16
+ sendMessage({ type: 'output', sessionId, data });
17
+ });
18
+ setSessionsChangeHandler(() => {
19
+ sendMessage({ type: 'sessions_update', sessions: getSessionList() });
20
+ });
21
+ doConnect();
22
+ }
23
+ function doConnect() {
24
+ if (ws) {
25
+ ws.removeAllListeners();
26
+ ws.close();
27
+ }
28
+ const wsUrl = `${serverUrl}/ws/agent?token=${encodeURIComponent(agentToken)}`;
29
+ console.log(`Connecting to ${serverUrl}...`);
30
+ ws = new WebSocket(wsUrl);
31
+ ws.on('open', () => {
32
+ console.log('Connected to server');
33
+ isConnected = true;
34
+ reconnectAttempts = 0;
35
+ // Register current sessions
36
+ sendMessage({ type: 'register', sessions: getSessionList() });
37
+ });
38
+ ws.on('message', (data) => {
39
+ try {
40
+ const msg = JSON.parse(data.toString());
41
+ handleMessage(msg);
42
+ }
43
+ catch (err) {
44
+ console.error('Failed to parse server message:', err);
45
+ }
46
+ });
47
+ ws.on('close', (code, reason) => {
48
+ console.log(`Disconnected from server (code: ${code}, reason: ${reason.toString() || 'unknown'})`);
49
+ isConnected = false;
50
+ scheduleReconnect();
51
+ });
52
+ ws.on('error', (err) => {
53
+ console.error('WebSocket error:', err.message);
54
+ isConnected = false;
55
+ });
56
+ }
57
+ function handleMessage(msg) {
58
+ switch (msg.type) {
59
+ case 'input': {
60
+ writeToSession(msg.sessionId, msg.data);
61
+ break;
62
+ }
63
+ case 'resize': {
64
+ resizeSession(msg.sessionId, msg.cols, msg.rows);
65
+ break;
66
+ }
67
+ case 'attach': {
68
+ const exists = attachSession(msg.sessionId);
69
+ if (!exists) {
70
+ console.warn(`Session ${msg.sessionId} not found for attach`);
71
+ }
72
+ break;
73
+ }
74
+ case 'spawn': {
75
+ const command = msg.command || undefined;
76
+ const session = spawnSession(command);
77
+ console.log(`Spawned session: ${session.name} (${session.id})`);
78
+ // Notify server of updated sessions
79
+ sendMessage({ type: 'sessions_update', sessions: getSessionList() });
80
+ break;
81
+ }
82
+ default:
83
+ console.warn('Unknown message type from server:', msg.type);
84
+ }
85
+ }
86
+ function sendMessage(msg) {
87
+ if (ws && ws.readyState === WebSocket.OPEN) {
88
+ ws.send(JSON.stringify(msg));
89
+ }
90
+ }
91
+ function scheduleReconnect() {
92
+ if (reconnectTimer)
93
+ return;
94
+ const delay = Math.min(BASE_RECONNECT_DELAY * Math.pow(2, reconnectAttempts), MAX_RECONNECT_DELAY);
95
+ reconnectAttempts++;
96
+ console.log(`Reconnecting in ${delay / 1000}s (attempt ${reconnectAttempts})...`);
97
+ reconnectTimer = setTimeout(() => {
98
+ reconnectTimer = null;
99
+ doConnect();
100
+ }, delay);
101
+ }
102
+ export function disconnect() {
103
+ if (reconnectTimer) {
104
+ clearTimeout(reconnectTimer);
105
+ reconnectTimer = null;
106
+ }
107
+ if (ws) {
108
+ ws.removeAllListeners();
109
+ ws.close(1000, 'Agent shutting down');
110
+ ws = null;
111
+ }
112
+ isConnected = false;
113
+ }
114
+ export function getConnectionStatus() {
115
+ return isConnected;
116
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { connect, disconnect } from './connection.js';
4
+ import { listTmuxSessions, attachTmuxSession, spawnSession, cleanupAll, getSessionList } from './terminal.js';
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import os from 'os';
8
+ const CONFIG_DIR = path.join(os.homedir(), '.remote-cli');
9
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
10
+ function loadConfig() {
11
+ try {
12
+ if (fs.existsSync(CONFIG_FILE)) {
13
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
14
+ }
15
+ }
16
+ catch {
17
+ // ignore
18
+ }
19
+ return null;
20
+ }
21
+ function saveConfig(config) {
22
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
23
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
24
+ fs.chmodSync(CONFIG_FILE, 0o600);
25
+ }
26
+ const program = new Command();
27
+ program
28
+ .name('remote-cli')
29
+ .description('Remote CLI agent - connects local terminal sessions to the remote server')
30
+ .version('1.0.0');
31
+ program
32
+ .command('configure')
33
+ .description('Configure the agent with server URL and token')
34
+ .requiredOption('-s, --server <url>', 'Server URL (e.g., https://remote-cli.example.com)')
35
+ .requiredOption('-t, --token <token>', 'Agent token from the web UI')
36
+ .action((opts) => {
37
+ const server = opts.server.replace(/\/$/, ''); // Remove trailing slash
38
+ saveConfig({ server, token: opts.token });
39
+ console.log(`Configuration saved to ${CONFIG_FILE}`);
40
+ console.log(`Server: ${server}`);
41
+ console.log(`Token: ${opts.token.slice(0, 8)}...`);
42
+ });
43
+ program
44
+ .command('connect')
45
+ .description('Connect to the server and register available sessions')
46
+ .option('-s, --server <url>', 'Server URL (overrides config)')
47
+ .option('-t, --token <token>', 'Agent token (overrides config)')
48
+ .option('--spawn [command]', 'Spawn a new session on connect')
49
+ .action((opts) => {
50
+ const config = loadConfig();
51
+ const server = opts.server || config?.server;
52
+ const token = opts.token || config?.token;
53
+ if (!server || !token) {
54
+ console.error('Error: Server URL and token are required.');
55
+ console.error('Run "remote-cli configure -s <url> -t <token>" first, or pass --server and --token flags.');
56
+ process.exit(1);
57
+ }
58
+ // Convert http(s) to ws(s) for WebSocket connection
59
+ const wsUrl = server.replace(/^http/, 'ws');
60
+ console.log(`Connecting to ${server}...`);
61
+ // List tmux sessions
62
+ const tmuxSessions = listTmuxSessions();
63
+ if (tmuxSessions.length > 0) {
64
+ console.log(`Found ${tmuxSessions.length} tmux session(s):`);
65
+ tmuxSessions.forEach(s => console.log(` - ${s}`));
66
+ // Auto-attach all tmux sessions
67
+ for (const sessionName of tmuxSessions) {
68
+ attachTmuxSession(sessionName);
69
+ }
70
+ }
71
+ // Spawn a session if requested
72
+ if (opts.spawn !== undefined) {
73
+ const command = typeof opts.spawn === 'string' ? opts.spawn : undefined;
74
+ spawnSession(command);
75
+ }
76
+ // If no sessions available, spawn a default one
77
+ if (getSessionList().length === 0) {
78
+ console.log('No tmux sessions found. Spawning a default shell session...');
79
+ spawnSession();
80
+ }
81
+ // Connect to server
82
+ connect(wsUrl, token);
83
+ // Handle shutdown
84
+ const shutdown = () => {
85
+ console.log('\nShutting down...');
86
+ disconnect();
87
+ cleanupAll();
88
+ process.exit(0);
89
+ };
90
+ process.on('SIGINT', shutdown);
91
+ process.on('SIGTERM', shutdown);
92
+ // Keep process alive
93
+ setInterval(() => { }, 1000);
94
+ });
95
+ program
96
+ .command('attach <session>')
97
+ .description('Attach a tmux session and connect to server')
98
+ .option('-s, --server <url>', 'Server URL')
99
+ .option('-t, --token <token>', 'Agent token')
100
+ .action((sessionName, opts) => {
101
+ const config = loadConfig();
102
+ const server = opts.server || config?.server;
103
+ const token = opts.token || config?.token;
104
+ if (!server || !token) {
105
+ console.error('Error: Server URL and token are required.');
106
+ process.exit(1);
107
+ }
108
+ const wsUrl = server.replace(/^http/, 'ws');
109
+ const session = attachTmuxSession(sessionName);
110
+ if (!session) {
111
+ console.error(`Failed to attach tmux session "${sessionName}"`);
112
+ const available = listTmuxSessions();
113
+ if (available.length > 0) {
114
+ console.log('Available tmux sessions:');
115
+ available.forEach(s => console.log(` - ${s}`));
116
+ }
117
+ else {
118
+ console.log('No tmux sessions available.');
119
+ }
120
+ process.exit(1);
121
+ }
122
+ connect(wsUrl, token);
123
+ const shutdown = () => {
124
+ console.log('\nShutting down...');
125
+ disconnect();
126
+ cleanupAll();
127
+ process.exit(0);
128
+ };
129
+ process.on('SIGINT', shutdown);
130
+ process.on('SIGTERM', shutdown);
131
+ setInterval(() => { }, 1000);
132
+ });
133
+ program
134
+ .command('spawn [command]')
135
+ .description('Spawn a new PTY session and connect to server')
136
+ .option('-s, --server <url>', 'Server URL')
137
+ .option('-t, --token <token>', 'Agent token')
138
+ .action((command, opts) => {
139
+ const config = loadConfig();
140
+ const server = opts.server || config?.server;
141
+ const token = opts.token || config?.token;
142
+ if (!server || !token) {
143
+ console.error('Error: Server URL and token are required.');
144
+ process.exit(1);
145
+ }
146
+ const wsUrl = server.replace(/^http/, 'ws');
147
+ spawnSession(command);
148
+ connect(wsUrl, token);
149
+ const shutdown = () => {
150
+ console.log('\nShutting down...');
151
+ disconnect();
152
+ cleanupAll();
153
+ process.exit(0);
154
+ };
155
+ process.on('SIGINT', shutdown);
156
+ process.on('SIGTERM', shutdown);
157
+ setInterval(() => { }, 1000);
158
+ });
159
+ program
160
+ .command('sessions')
161
+ .description('List available tmux sessions')
162
+ .action(() => {
163
+ const sessions = listTmuxSessions();
164
+ if (sessions.length === 0) {
165
+ console.log('No tmux sessions found.');
166
+ }
167
+ else {
168
+ console.log('Available tmux sessions:');
169
+ sessions.forEach(s => console.log(` - ${s}`));
170
+ }
171
+ });
172
+ program.parse();
@@ -0,0 +1,22 @@
1
+ import { IPty } from 'node-pty';
2
+ export interface TerminalSession {
3
+ id: string;
4
+ name: string;
5
+ command?: string;
6
+ pty?: IPty;
7
+ tmuxSession?: string;
8
+ }
9
+ export declare function setOutputHandler(handler: (sessionId: string, data: string) => void): void;
10
+ export declare function setSessionsChangeHandler(handler: () => void): void;
11
+ export declare function getSessionList(): {
12
+ id: string;
13
+ name: string;
14
+ command?: string;
15
+ }[];
16
+ export declare function listTmuxSessions(): string[];
17
+ export declare function attachTmuxSession(sessionName: string): TerminalSession | null;
18
+ export declare function spawnSession(command?: string): TerminalSession;
19
+ export declare function writeToSession(sessionId: string, data: string): boolean;
20
+ export declare function resizeSession(sessionId: string, cols: number, rows: number): boolean;
21
+ export declare function attachSession(sessionId: string): boolean;
22
+ export declare function cleanupAll(): void;
@@ -0,0 +1,139 @@
1
+ import { spawn as nodePtySpawn } from 'node-pty';
2
+ import { execSync } from 'child_process';
3
+ import { v4 as uuidv4 } from 'uuid';
4
+ // Active sessions
5
+ const sessions = new Map();
6
+ // Callbacks
7
+ let onOutput = null;
8
+ let onSessionsChange = null;
9
+ export function setOutputHandler(handler) {
10
+ onOutput = handler;
11
+ }
12
+ export function setSessionsChangeHandler(handler) {
13
+ onSessionsChange = handler;
14
+ }
15
+ export function getSessionList() {
16
+ return Array.from(sessions.values()).map(s => ({
17
+ id: s.id,
18
+ name: s.name,
19
+ command: s.command
20
+ }));
21
+ }
22
+ // List available tmux sessions
23
+ export function listTmuxSessions() {
24
+ try {
25
+ const output = execSync('tmux list-sessions -F "#{session_name}"', { encoding: 'utf-8' });
26
+ return output.trim().split('\n').filter(s => s.length > 0);
27
+ }
28
+ catch {
29
+ return [];
30
+ }
31
+ }
32
+ // Attach to a tmux session
33
+ export function attachTmuxSession(sessionName) {
34
+ try {
35
+ // Verify session exists
36
+ const sessions_list = listTmuxSessions();
37
+ if (!sessions_list.includes(sessionName)) {
38
+ console.error(`tmux session "${sessionName}" not found`);
39
+ return null;
40
+ }
41
+ const id = uuidv4();
42
+ const shell = process.env.SHELL || '/bin/bash';
43
+ // Spawn a PTY that attaches to the tmux session
44
+ const pty = nodePtySpawn(shell, ['-c', `tmux attach-session -t ${sessionName}`], {
45
+ name: 'xterm-256color',
46
+ cols: 80,
47
+ rows: 24,
48
+ cwd: process.env.HOME || '/',
49
+ env: process.env
50
+ });
51
+ const session = {
52
+ id,
53
+ name: `tmux: ${sessionName}`,
54
+ command: `tmux attach -t ${sessionName}`,
55
+ pty,
56
+ tmuxSession: sessionName
57
+ };
58
+ setupPtyHandlers(session);
59
+ sessions.set(id, session);
60
+ onSessionsChange?.();
61
+ console.log(`Attached to tmux session "${sessionName}" with id ${id}`);
62
+ return session;
63
+ }
64
+ catch (err) {
65
+ console.error('Failed to attach tmux session:', err);
66
+ return null;
67
+ }
68
+ }
69
+ // Spawn a new PTY session
70
+ export function spawnSession(command) {
71
+ const id = uuidv4();
72
+ const shell = process.env.SHELL || '/bin/bash';
73
+ const cmd = command || shell;
74
+ const args = command ? ['-c', command] : [];
75
+ const pty = nodePtySpawn(command ? shell : shell, args, {
76
+ name: 'xterm-256color',
77
+ cols: 80,
78
+ rows: 24,
79
+ cwd: process.env.HOME || '/',
80
+ env: process.env
81
+ });
82
+ const session = {
83
+ id,
84
+ name: command || shell,
85
+ command: cmd,
86
+ pty
87
+ };
88
+ setupPtyHandlers(session);
89
+ sessions.set(id, session);
90
+ onSessionsChange?.();
91
+ console.log(`Spawned session "${session.name}" with id ${id}`);
92
+ return session;
93
+ }
94
+ function setupPtyHandlers(session) {
95
+ if (!session.pty)
96
+ return;
97
+ session.pty.onData((data) => {
98
+ onOutput?.(session.id, data);
99
+ });
100
+ session.pty.onExit(({ exitCode }) => {
101
+ console.log(`Session "${session.name}" exited with code ${exitCode}`);
102
+ sessions.delete(session.id);
103
+ onSessionsChange?.();
104
+ });
105
+ }
106
+ // Write input to a session
107
+ export function writeToSession(sessionId, data) {
108
+ const session = sessions.get(sessionId);
109
+ if (!session?.pty)
110
+ return false;
111
+ session.pty.write(data);
112
+ return true;
113
+ }
114
+ // Resize a session
115
+ export function resizeSession(sessionId, cols, rows) {
116
+ const session = sessions.get(sessionId);
117
+ if (!session?.pty)
118
+ return false;
119
+ try {
120
+ session.pty.resize(cols, rows);
121
+ return true;
122
+ }
123
+ catch {
124
+ return false;
125
+ }
126
+ }
127
+ // Attach to session (no-op if already active, used when browser connects)
128
+ export function attachSession(sessionId) {
129
+ return sessions.has(sessionId);
130
+ }
131
+ // Cleanup all sessions
132
+ export function cleanupAll() {
133
+ for (const session of sessions.values()) {
134
+ if (session.pty) {
135
+ session.pty.kill();
136
+ }
137
+ }
138
+ sessions.clear();
139
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "remote-cli-agent",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "remote-cli": "./dist/index.js"
7
+ },
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "start": "node dist/index.js",
11
+ "dev": "tsx src/index.ts",
12
+ "prepublishOnly": "npm run build"
13
+ },
14
+ "dependencies": {
15
+ "commander": "^12.0.0",
16
+ "node-pty": "^1.0.0",
17
+ "uuid": "^9.0.0",
18
+ "ws": "^8.16.0"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^20.11.5",
22
+ "@types/uuid": "^9.0.7",
23
+ "@types/ws": "^8.5.10",
24
+ "tsx": "^4.7.0",
25
+ "typescript": "^5.3.3"
26
+ },
27
+ "description": "Remote CLI agent - access local terminal sessions from the browser",
28
+ "files": [
29
+ "dist/**/*"
30
+ ],
31
+ "keywords": [
32
+ "terminal",
33
+ "remote",
34
+ "tmux",
35
+ "pty",
36
+ "cli",
37
+ "websocket"
38
+ ],
39
+ "license": "MIT",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "https://github.com/aim-research/remote-cli.git",
43
+ "directory": "agent"
44
+ }
45
+ }