@iflow-mcp/npcnpc09-remotex 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +201 -0
- package/bin/cli.js +484 -0
- package/package.json +1 -0
- package/public/index.html +1806 -0
- package/public/status.html +689 -0
- package/public/terminal.html +454 -0
- package/src/bridge.js +1124 -0
- package/src/dashboard-server.js +799 -0
- package/src/mcp-server.js +849 -0
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# RemoteX v0.4.0 — AI-Native SSH Fleet Management for Claude Code
|
|
2
|
+
|
|
3
|
+
You have MCP tools (38) to operate on remote servers via SSH, plus an HTTP API to control the RemoteX GUI terminal.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Claude Code ──MCP (stdio)──> mcp-server.js (38 tools) ──ssh2──> Servers
|
|
9
|
+
Claude Code ──curl──> RemoteX.py HTTP API (:9876) ──paramiko──> Servers (with live GUI)
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Quick Reference: Which Tool to Use
|
|
13
|
+
|
|
14
|
+
| I want to... | Use |
|
|
15
|
+
|---|---|
|
|
16
|
+
| Run a command on 1 server | `ssh_exec` |
|
|
17
|
+
| Run a command on N servers | `ssh_exec_batch` |
|
|
18
|
+
| Read a file | `ssh_read_file` (line range support) |
|
|
19
|
+
| Edit part of a file | `ssh_edit_block` (token-efficient) |
|
|
20
|
+
| Search code | `ssh_search_code` (grep with context) |
|
|
21
|
+
| Write same file to N servers | `ssh_batch_write_file` |
|
|
22
|
+
| Find/replace across N servers | `ssh_batch_replace_in_file` |
|
|
23
|
+
| Check if configs match | `ssh_diff_files` |
|
|
24
|
+
| Monitor server health | `ssh_health_check` / `ssh_batch_health_check` |
|
|
25
|
+
| Check Docker | `ssh_docker_status` |
|
|
26
|
+
| Deploy a file to fleet | `ssh_batch_upload` or `ssh_batch_write_file` |
|
|
27
|
+
| Copy file between servers | `ssh_sync_file` |
|
|
28
|
+
| Control GUI terminal | `curl http://localhost:9876/...` |
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## All MCP Tools (38)
|
|
33
|
+
|
|
34
|
+
### Server Management
|
|
35
|
+
| Tool | Purpose |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `ssh_exec` | Execute any command on a server |
|
|
38
|
+
| `ssh_exec_batch` | Execute on multiple servers (by group or list) |
|
|
39
|
+
| `ssh_server_info` | CPU, memory, disk, uptime, ports |
|
|
40
|
+
| `ssh_list_servers` | List all configured servers |
|
|
41
|
+
| `ssh_list_groups` | List server groups |
|
|
42
|
+
| `ssh_add_server` | Add a server to config |
|
|
43
|
+
| `ssh_remove_server` | Remove a server |
|
|
44
|
+
| `ssh_import_servers` | Import from ip.txt |
|
|
45
|
+
| `ssh_clear_all_servers` | Remove all servers from config |
|
|
46
|
+
|
|
47
|
+
### File Operations
|
|
48
|
+
| Tool | Purpose |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `ssh_read_file` | Read file content (supports line ranges) |
|
|
51
|
+
| `ssh_write_file` | Write/overwrite/append file (auto-backup, auto-mkdir) |
|
|
52
|
+
| `ssh_replace_in_file` | Find and replace text in a file |
|
|
53
|
+
| `ssh_edit_block` | Edit specific text block (80-90% fewer tokens) |
|
|
54
|
+
| `ssh_search_code` | Search pattern in files with context lines |
|
|
55
|
+
| `ssh_list_dir` | List directory contents |
|
|
56
|
+
| `ssh_find_files` | Search by filename or content |
|
|
57
|
+
| `ssh_stat_file` | File metadata (size, perms, mtime) |
|
|
58
|
+
| `ssh_mkdir` | Create directory |
|
|
59
|
+
| `ssh_delete_file` | Delete file/directory (safety guards) |
|
|
60
|
+
| `ssh_move_file` | Move/rename |
|
|
61
|
+
| `ssh_copy_file` | Copy file/directory |
|
|
62
|
+
| `ssh_upload_file` | Upload local -> server (SFTP) |
|
|
63
|
+
| `ssh_download_file` | Download server -> local (SFTP) |
|
|
64
|
+
| `ssh_project_structure` | Project tree + key files overview |
|
|
65
|
+
|
|
66
|
+
### Batch Operations (Batch vi / Batch sed)
|
|
67
|
+
| Tool | Purpose |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `ssh_batch_read_file` | Read same file from N servers |
|
|
70
|
+
| `ssh_batch_write_file` | Write same file to N servers |
|
|
71
|
+
| `ssh_batch_replace_in_file` | Find/replace in file on N servers |
|
|
72
|
+
| `ssh_diff_files` | Compare file across servers (drift detection) |
|
|
73
|
+
| `ssh_batch_upload` | Upload local file to N servers |
|
|
74
|
+
| `ssh_sync_file` | Copy file between two servers |
|
|
75
|
+
|
|
76
|
+
### System Monitoring
|
|
77
|
+
| Tool | Purpose |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `ssh_health_check` | CPU, mem, disk, load, uptime (one call) |
|
|
80
|
+
| `ssh_batch_health_check` | Health check across N servers |
|
|
81
|
+
| `ssh_process_list` | Top processes (sort by cpu/mem, filter) |
|
|
82
|
+
| `ssh_docker_status` | Docker containers, images, stats |
|
|
83
|
+
| `ssh_network_info` | Interfaces, routes, connections, DNS |
|
|
84
|
+
| `ssh_service` | start/stop/restart systemd services |
|
|
85
|
+
| `ssh_ports` | List listening ports |
|
|
86
|
+
| `ssh_tail_log` | Read last N lines of a log file |
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## RemoteX.py HTTP API (GUI Terminal Control)
|
|
91
|
+
|
|
92
|
+
When RemoteX.py is running, Claude can control it via HTTP API on port 9876.
|
|
93
|
+
|
|
94
|
+
### Read (GET)
|
|
95
|
+
```bash
|
|
96
|
+
# List sessions
|
|
97
|
+
curl http://localhost:9876/sessions
|
|
98
|
+
|
|
99
|
+
# Read terminal output from a session
|
|
100
|
+
curl "http://localhost:9876/output?server=SMAPP1&lines=50"
|
|
101
|
+
|
|
102
|
+
# Read output from ALL sessions
|
|
103
|
+
curl "http://localhost:9876/output_all?lines=20"
|
|
104
|
+
|
|
105
|
+
# Execute command and wait for output
|
|
106
|
+
curl "http://localhost:9876/exec?server=SMAPP1&command=hostname&wait=2"
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Write (POST)
|
|
110
|
+
```bash
|
|
111
|
+
# Send command to specific server
|
|
112
|
+
curl -X POST http://localhost:9876/send -d '{"server":"SMAPP1","command":"ls"}'
|
|
113
|
+
|
|
114
|
+
# Broadcast to all sessions
|
|
115
|
+
curl -X POST http://localhost:9876/broadcast -d '{"command":"uptime"}'
|
|
116
|
+
|
|
117
|
+
# Send to matching servers
|
|
118
|
+
curl -X POST http://localhost:9876/send_multi -d '{"filter":"SMAPP","command":"df -h"}'
|
|
119
|
+
|
|
120
|
+
# Execute and collect output from all
|
|
121
|
+
curl -X POST http://localhost:9876/exec_all -d '{"command":"hostname","wait":3}'
|
|
122
|
+
|
|
123
|
+
# Execute and wait for output (single server)
|
|
124
|
+
curl -X POST http://localhost:9876/exec_wait -d '{"server":"SMAPP1","command":"hostname","wait":2}'
|
|
125
|
+
|
|
126
|
+
# Open connections by filter
|
|
127
|
+
curl -X POST http://localhost:9876/open -d '{"filter":"IVR"}'
|
|
128
|
+
|
|
129
|
+
# Close session
|
|
130
|
+
curl -X POST http://localhost:9876/close -d '{"server":"SMAPP1"}'
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Typical Workflows
|
|
136
|
+
|
|
137
|
+
### 1. Batch config modification (batch vi)
|
|
138
|
+
```
|
|
139
|
+
ssh_batch_read_file -> Read config from all servers
|
|
140
|
+
ssh_diff_files -> Check for drift
|
|
141
|
+
ssh_batch_replace_in_file -> Apply change
|
|
142
|
+
ssh_batch_read_file -> Verify change
|
|
143
|
+
ssh_exec_batch -> Restart services
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### 2. Fleet health monitoring
|
|
147
|
+
```
|
|
148
|
+
ssh_batch_health_check -> CPU/mem/disk overview
|
|
149
|
+
ssh_process_list -> Investigate high-load server
|
|
150
|
+
ssh_docker_status -> Check container health
|
|
151
|
+
ssh_tail_log -> Read error logs
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### 3. Deploy script to fleet
|
|
155
|
+
```
|
|
156
|
+
ssh_batch_write_file -> Write script to all servers
|
|
157
|
+
ssh_exec_batch -> chmod +x && run
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### 4. Interactive session via RemoteX GUI
|
|
161
|
+
```
|
|
162
|
+
curl POST /open -> Open connections
|
|
163
|
+
curl POST /broadcast -> Send commands
|
|
164
|
+
curl GET /output_all -> Read results
|
|
165
|
+
curl POST /exec_all -> Execute and collect output
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### 5. Import servers from ip.txt
|
|
169
|
+
```
|
|
170
|
+
ssh_import_servers file_path="ip.txt"
|
|
171
|
+
ssh_list_groups
|
|
172
|
+
ssh_batch_health_check group=production
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Safety Rules
|
|
178
|
+
- `ssh_write_file` backs up original as `.bak` by default
|
|
179
|
+
- `ssh_edit_block` errors if block not found or matches multiple times
|
|
180
|
+
- `ssh_replace_in_file` errors if old_text not found (unless replace_all)
|
|
181
|
+
- `ssh_delete_file` refuses dangerous paths (/, /etc, /usr, etc.)
|
|
182
|
+
- Always test config before restarting (e.g. `nginx -t`)
|
|
183
|
+
- Use `ssh_read_file` / `ssh_batch_read_file` to verify after writing
|
|
184
|
+
- Use `ssh_diff_files` to detect config drift before making changes
|
|
185
|
+
|
|
186
|
+
## Config
|
|
187
|
+
Stored at `~/.remotex.json`:
|
|
188
|
+
```json
|
|
189
|
+
{
|
|
190
|
+
"servers": {
|
|
191
|
+
"prod-01": { "host": "10.1.1.10", "port": 22, "username": "root", "password": "...", "group": "production" }
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## ip.txt Format
|
|
197
|
+
```
|
|
198
|
+
# name,host,port,username,password,group
|
|
199
|
+
prod-web-01,10.0.1.10,22,root,password,production
|
|
200
|
+
prod-web-02,10.0.1.11,22,root,password,production
|
|
201
|
+
```
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* remotex CLI
|
|
4
|
+
*
|
|
5
|
+
* Standalone CLI for Claude Code to manage SSH servers.
|
|
6
|
+
* Works without MCP — Claude Code can call this directly via bash.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* remotex list # List all servers
|
|
10
|
+
* remotex info <server> # Server system info
|
|
11
|
+
* remotex exec <server> <command> # Execute command
|
|
12
|
+
* remotex batch <group|server,server> <cmd> # Batch execute
|
|
13
|
+
* remotex add <id> <host> [options] # Add server
|
|
14
|
+
* remotex remove <id> # Remove server
|
|
15
|
+
* remotex groups # List groups
|
|
16
|
+
* remotex upload <server> <local> <remote> # Upload file
|
|
17
|
+
* remotex download <server> <remote> <local># Download file
|
|
18
|
+
* remotex tail <server> <logpath> [lines] # Tail log
|
|
19
|
+
* remotex service <server> <name> <action> # Manage service
|
|
20
|
+
* remotex ports <server> # List listening ports
|
|
21
|
+
* remotex status # Quick status of all servers
|
|
22
|
+
* remotex import <file> [options] # Import servers from ip.txt
|
|
23
|
+
* remotex clear-all # Remove all servers
|
|
24
|
+
* remotex batch-cat <group|s,s> <path> # Read file from multiple servers
|
|
25
|
+
* remotex batch-write <group|s,s> <path> # Write file to multiple servers (stdin)
|
|
26
|
+
* remotex batch-replace <group|s,s> <path> <old> <new> # Replace in file on multiple servers
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import * as bridge from '../src/bridge.js';
|
|
30
|
+
|
|
31
|
+
const args = process.argv.slice(2);
|
|
32
|
+
const cmd = args[0];
|
|
33
|
+
|
|
34
|
+
function resolveTarget(target) {
|
|
35
|
+
const groups = bridge.listGroups();
|
|
36
|
+
if (groups[target]) return groups[target];
|
|
37
|
+
return target.split(',').map(s => s.trim());
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function main() {
|
|
41
|
+
try {
|
|
42
|
+
switch (cmd) {
|
|
43
|
+
case 'list':
|
|
44
|
+
case 'ls': {
|
|
45
|
+
const checkAlive = args.includes('--check') || args.includes('-c');
|
|
46
|
+
const servers = await bridge.listServers(checkAlive);
|
|
47
|
+
if (!servers.length) {
|
|
48
|
+
console.log('No servers configured. Use "remotex add <id> <host>" to add one.');
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
console.log(`\n Servers (${servers.length}):\n`);
|
|
52
|
+
for (const s of servers) {
|
|
53
|
+
const status = s.status === 'online' ? '●' : s.status === 'offline' ? '○' : '?';
|
|
54
|
+
console.log(` ${status} ${s.id.padEnd(20)} ${s.host.padEnd(18)} :${s.port} @${s.username} [${s.group}]`);
|
|
55
|
+
}
|
|
56
|
+
console.log();
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
case 'info': {
|
|
61
|
+
const server = args[1];
|
|
62
|
+
if (!server) { console.error('Usage: remotex info <server>'); process.exit(1); }
|
|
63
|
+
const info = await bridge.getServerInfo(server);
|
|
64
|
+
console.log(`\n Server: ${server}\n`);
|
|
65
|
+
console.log(` Hostname: ${info.hostname || '?'}`);
|
|
66
|
+
console.log(` OS: ${info.os || '?'}`);
|
|
67
|
+
console.log(` Kernel: ${info.kernel || '?'}`);
|
|
68
|
+
console.log(` IP: ${info.ip || '?'}`);
|
|
69
|
+
console.log(` Uptime: ${info.uptime || '?'}`);
|
|
70
|
+
console.log(` CPU: ${info.cpu_cores || '?'} cores, ${info.cpu_usage || '?'}% usage`);
|
|
71
|
+
console.log(` Memory: ${info.mem_used || '?'}/${info.mem_total || '?'} MB (${info.mem_percent || '?'}%)`);
|
|
72
|
+
console.log(` Disk: ${info.disk_used || '?'}/${info.disk_total || '?'} (${info.disk_percent || '?'})`);
|
|
73
|
+
console.log(` Load: ${info.load || '?'}`);
|
|
74
|
+
console.log(` Docker: ${info.docker_running || '0'} containers`);
|
|
75
|
+
console.log(` Ports: ${info.listening_ports || 'none'}`);
|
|
76
|
+
console.log();
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
case 'exec':
|
|
81
|
+
case 'run': {
|
|
82
|
+
const server = args[1];
|
|
83
|
+
const command = args.slice(2).join(' ');
|
|
84
|
+
if (!server || !command) { console.error('Usage: remotex exec <server> <command>'); process.exit(1); }
|
|
85
|
+
const r = await bridge.execCommand(server, command);
|
|
86
|
+
if (r.stdout) process.stdout.write(r.stdout + '\n');
|
|
87
|
+
if (r.stderr) process.stderr.write(r.stderr + '\n');
|
|
88
|
+
process.exit(r.code);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
case 'batch': {
|
|
92
|
+
const target = args[1];
|
|
93
|
+
const command = args.slice(2).join(' ');
|
|
94
|
+
if (!target || !command) { console.error('Usage: remotex batch <group|s1,s2,...> <command>'); process.exit(1); }
|
|
95
|
+
|
|
96
|
+
let serverIds;
|
|
97
|
+
const groups = bridge.listGroups();
|
|
98
|
+
if (groups[target]) {
|
|
99
|
+
serverIds = groups[target];
|
|
100
|
+
} else {
|
|
101
|
+
serverIds = target.split(',');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
console.log(`\n Executing on ${serverIds.length} servers: ${command}\n`);
|
|
105
|
+
const results = await bridge.execBatch(serverIds, command);
|
|
106
|
+
|
|
107
|
+
for (const [id, r] of Object.entries(results)) {
|
|
108
|
+
const icon = r.code === 0 ? '✓' : '✗';
|
|
109
|
+
const preview = (r.stdout || r.stderr || '').split('\n')[0].substring(0, 80);
|
|
110
|
+
console.log(` ${icon} ${id.padEnd(20)} [exit ${r.code}] ${preview}`);
|
|
111
|
+
}
|
|
112
|
+
console.log();
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
case 'add': {
|
|
117
|
+
const id = args[1], host = args[2];
|
|
118
|
+
if (!id || !host) { console.error('Usage: remotex add <id> <host> [--user root] [--port 22] [--key ~/.ssh/id_rsa] [--group prod]'); process.exit(1); }
|
|
119
|
+
const opts = {};
|
|
120
|
+
for (let i = 3; i < args.length; i += 2) {
|
|
121
|
+
if (args[i] === '--user') opts.username = args[i + 1];
|
|
122
|
+
if (args[i] === '--port') opts.port = parseInt(args[i + 1]);
|
|
123
|
+
if (args[i] === '--key') opts.privateKey = args[i + 1];
|
|
124
|
+
if (args[i] === '--pass') opts.password = args[i + 1];
|
|
125
|
+
if (args[i] === '--group') opts.group = args[i + 1];
|
|
126
|
+
}
|
|
127
|
+
bridge.addServer(id, host, opts);
|
|
128
|
+
console.log(` ✓ Added server: ${id} (${host})`);
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
case 'remove':
|
|
133
|
+
case 'rm': {
|
|
134
|
+
const id = args[1];
|
|
135
|
+
if (!id) { console.error('Usage: remotex remove <id>'); process.exit(1); }
|
|
136
|
+
bridge.removeServer(id);
|
|
137
|
+
console.log(` ✓ Removed server: ${id}`);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
case 'groups': {
|
|
142
|
+
const groups = bridge.listGroups();
|
|
143
|
+
console.log('\n Groups:\n');
|
|
144
|
+
for (const [name, ids] of Object.entries(groups)) {
|
|
145
|
+
console.log(` ${name}: ${ids.join(', ')}`);
|
|
146
|
+
}
|
|
147
|
+
console.log();
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
case 'upload': {
|
|
152
|
+
const [, server, local, remote] = args;
|
|
153
|
+
if (!server || !local || !remote) { console.error('Usage: remotex upload <server> <local> <remote>'); process.exit(1); }
|
|
154
|
+
await bridge.uploadFile(server, local, remote);
|
|
155
|
+
console.log(` ✓ Uploaded ${local} → ${server}:${remote}`);
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
case 'download': {
|
|
160
|
+
const [, server, remote, local] = args;
|
|
161
|
+
if (!server || !remote || !local) { console.error('Usage: remotex download <server> <remote> <local>'); process.exit(1); }
|
|
162
|
+
await bridge.downloadFile(server, remote, local);
|
|
163
|
+
console.log(` ✓ Downloaded ${server}:${remote} → ${local}`);
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
case 'tail': {
|
|
168
|
+
const server = args[1], logPath = args[2], lines = parseInt(args[3]) || 50;
|
|
169
|
+
if (!server || !logPath) { console.error('Usage: remotex tail <server> <logpath> [lines]'); process.exit(1); }
|
|
170
|
+
const r = await bridge.tailLog(server, logPath, lines);
|
|
171
|
+
console.log(r.stdout);
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
case 'service':
|
|
176
|
+
case 'svc': {
|
|
177
|
+
const [, server, name, action] = args;
|
|
178
|
+
if (!server || !name || !action) { console.error('Usage: remotex service <server> <name> <start|stop|restart|status>'); process.exit(1); }
|
|
179
|
+
const r = await bridge.manageService(server, name, action);
|
|
180
|
+
if (r.stdout) console.log(r.stdout);
|
|
181
|
+
if (r.stderr) console.error(r.stderr);
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
case 'ports': {
|
|
186
|
+
const server = args[1];
|
|
187
|
+
if (!server) { console.error('Usage: remotex ports <server>'); process.exit(1); }
|
|
188
|
+
const ports = await bridge.getListeningPorts(server);
|
|
189
|
+
console.log(`\n Listening ports on ${server}:\n`);
|
|
190
|
+
for (const p of ports) {
|
|
191
|
+
console.log(` :${String(p.port).padEnd(6)} ${p.process.padEnd(20)} PID ${p.pid}`);
|
|
192
|
+
}
|
|
193
|
+
console.log();
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ── Remote File Operations ──
|
|
198
|
+
|
|
199
|
+
case 'cat':
|
|
200
|
+
case 'read': {
|
|
201
|
+
const server = args[1], path = args[2];
|
|
202
|
+
if (!server || !path) { console.error('Usage: remotex cat <server> <path> [startLine] [endLine]'); process.exit(1); }
|
|
203
|
+
const startLine = args[3] ? parseInt(args[3]) : undefined;
|
|
204
|
+
const endLine = args[4] ? parseInt(args[4]) : undefined;
|
|
205
|
+
const r = await bridge.readFile(server, path, { startLine, endLine });
|
|
206
|
+
if (r.warning) console.error(` ⚠ ${r.warning}`);
|
|
207
|
+
console.log(r.content);
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
case 'write': {
|
|
212
|
+
const server = args[1], path = args[2];
|
|
213
|
+
if (!server || !path) { console.error('Usage: remotex write <server> <path> (reads stdin)'); process.exit(1); }
|
|
214
|
+
// Read content from stdin
|
|
215
|
+
const chunks = [];
|
|
216
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
217
|
+
const content = Buffer.concat(chunks).toString('utf-8');
|
|
218
|
+
const noBackup = args.includes('--no-backup');
|
|
219
|
+
const r = await bridge.writeFile(server, path, content, { backup: !noBackup });
|
|
220
|
+
console.log(` ✓ Written ${path} (${content.length} bytes, backup: ${!noBackup})`);
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
case 'replace': {
|
|
225
|
+
const server = args[1], path = args[2], old = args[3], nw = args[4];
|
|
226
|
+
if (!server || !path || !old || nw === undefined) { console.error('Usage: remotex replace <server> <path> <old_text> <new_text> [--all]'); process.exit(1); }
|
|
227
|
+
const all = args.includes('--all');
|
|
228
|
+
const r = await bridge.replaceInFile(server, path, old, nw, { all });
|
|
229
|
+
console.log(` ✓ Replaced ${r.replacements} occurrence(s) in ${path}`);
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
case 'dir':
|
|
234
|
+
case 'lsdir': {
|
|
235
|
+
const server = args[1], path = args[2] || '/';
|
|
236
|
+
if (!server) { console.error('Usage: remotex dir <server> [path] [--recursive] [--hidden]'); process.exit(1); }
|
|
237
|
+
const recursive = args.includes('--recursive') || args.includes('-r');
|
|
238
|
+
const showHidden = args.includes('--hidden') || args.includes('-a');
|
|
239
|
+
const r = await bridge.listDir(server, path, { recursive, showHidden });
|
|
240
|
+
console.log(`\n ${path} (${r.entries.length} entries):\n`);
|
|
241
|
+
for (const e of r.entries) {
|
|
242
|
+
const icon = e.type === 'directory' ? '📁' : e.type === 'symlink' ? '🔗' : ' ';
|
|
243
|
+
const size = e.type === 'file' ? `${e.size}` : '';
|
|
244
|
+
const name = recursive ? e.path : e.name;
|
|
245
|
+
console.log(` ${icon} ${(name || '').padEnd(40)} ${size.padStart(10)} ${e.permissions || ''} ${e.owner || ''}`);
|
|
246
|
+
}
|
|
247
|
+
console.log();
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
case 'find':
|
|
252
|
+
case 'grep': {
|
|
253
|
+
const server = args[1], searchPath = args[2];
|
|
254
|
+
if (!server || !searchPath) { console.error('Usage: remotex find <server> <path> --name "*.py" OR --content "def main"'); process.exit(1); }
|
|
255
|
+
const nameIdx = args.indexOf('--name');
|
|
256
|
+
const contentIdx = args.indexOf('--content');
|
|
257
|
+
const typeIdx = args.indexOf('--type');
|
|
258
|
+
const opts = {};
|
|
259
|
+
if (nameIdx > 0) opts.namePattern = args[nameIdx + 1];
|
|
260
|
+
if (contentIdx > 0) opts.contentPattern = args[contentIdx + 1];
|
|
261
|
+
if (typeIdx > 0) opts.fileType = args[typeIdx + 1];
|
|
262
|
+
if (!opts.namePattern && !opts.contentPattern) { console.error('Specify --name or --content'); process.exit(1); }
|
|
263
|
+
|
|
264
|
+
const r = await bridge.findFiles(server, searchPath, opts);
|
|
265
|
+
if (r.type === 'name') {
|
|
266
|
+
console.log(`\n Found ${r.count} files matching "${r.query}":\n`);
|
|
267
|
+
for (const f of r.files) console.log(` ${f}`);
|
|
268
|
+
} else {
|
|
269
|
+
console.log(`\n Found "${r.query}" in ${r.fileCount} files:\n`);
|
|
270
|
+
for (const m of r.matches) {
|
|
271
|
+
console.log(` ${m.file}:${m.line} ${m.content}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
console.log();
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
case 'stat': {
|
|
279
|
+
const server = args[1], path = args[2];
|
|
280
|
+
if (!server || !path) { console.error('Usage: remotex stat <server> <path>'); process.exit(1); }
|
|
281
|
+
const r = await bridge.statFile(server, path);
|
|
282
|
+
console.log(JSON.stringify(r, null, 2));
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
case 'mkdir': {
|
|
287
|
+
const server = args[1], path = args[2];
|
|
288
|
+
if (!server || !path) { console.error('Usage: remotex mkdir <server> <path>'); process.exit(1); }
|
|
289
|
+
await bridge.mkDir(server, path);
|
|
290
|
+
console.log(` ✓ Created ${path}`);
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
case 'rm': {
|
|
295
|
+
const server = args[1], path = args[2];
|
|
296
|
+
if (!server || !path) { console.error('Usage: remotex rm <server> <path> [-r]'); process.exit(1); }
|
|
297
|
+
const recursive = args.includes('-r') || args.includes('--recursive');
|
|
298
|
+
await bridge.deleteFile(server, path, { recursive });
|
|
299
|
+
console.log(` ✓ Deleted ${path}`);
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
case 'mv': {
|
|
304
|
+
const server = args[1], from = args[2], to = args[3];
|
|
305
|
+
if (!server || !from || !to) { console.error('Usage: remotex mv <server> <from> <to>'); process.exit(1); }
|
|
306
|
+
await bridge.moveFile(server, from, to);
|
|
307
|
+
console.log(` ✓ Moved ${from} → ${to}`);
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
case 'cp': {
|
|
312
|
+
const server = args[1], from = args[2], to = args[3];
|
|
313
|
+
if (!server || !from || !to) { console.error('Usage: remotex cp <server> <from> <to> [-r]'); process.exit(1); }
|
|
314
|
+
const recursive = args.includes('-r');
|
|
315
|
+
await bridge.copyFile(server, from, to, { recursive });
|
|
316
|
+
console.log(` ✓ Copied ${from} → ${to}`);
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
case 'tree':
|
|
321
|
+
case 'project': {
|
|
322
|
+
const server = args[1], path = args[2] || '.';
|
|
323
|
+
if (!server) { console.error('Usage: remotex tree <server> [path]'); process.exit(1); }
|
|
324
|
+
const r = await bridge.getProjectStructure(server, path);
|
|
325
|
+
console.log(`\n Project: ${r.root}`);
|
|
326
|
+
console.log(` ${r.totalFiles} files, ${r.totalDirs} dirs, ${r.totalSizeKB} KB\n`);
|
|
327
|
+
if (r.keyFiles.length) {
|
|
328
|
+
console.log(` Key files: ${r.keyFiles.join(', ')}\n`);
|
|
329
|
+
}
|
|
330
|
+
for (const e of r.tree.slice(0, 80)) {
|
|
331
|
+
const indent = ' '.repeat(e.depth);
|
|
332
|
+
const icon = e.type === 'dir' ? '📁' : ' ';
|
|
333
|
+
console.log(` ${indent}${icon} ${e.path.split('/').pop()}`);
|
|
334
|
+
}
|
|
335
|
+
if (r.tree.length > 80) console.log(` ... and ${r.tree.length - 80} more`);
|
|
336
|
+
console.log();
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ── Batch Import / Batch File Ops ──
|
|
341
|
+
|
|
342
|
+
case 'import': {
|
|
343
|
+
const filePath = args[1];
|
|
344
|
+
if (!filePath) { console.error('Usage: remotex import <file> [--overwrite] [--group name]'); process.exit(1); }
|
|
345
|
+
const overwrite = args.includes('--overwrite');
|
|
346
|
+
const groupIdx = args.indexOf('--group');
|
|
347
|
+
const defaultGroup = groupIdx > 0 ? args[groupIdx + 1] : 'imported';
|
|
348
|
+
const r = bridge.importServersFromFile(filePath, { overwrite, defaultGroup });
|
|
349
|
+
console.log(`\n ✓ Imported: ${r.imported}, Skipped: ${r.skipped}, Total: ${r.total}`);
|
|
350
|
+
if (r.errors.length) {
|
|
351
|
+
console.log(` Errors:`);
|
|
352
|
+
for (const e of r.errors) console.log(` - ${e}`);
|
|
353
|
+
}
|
|
354
|
+
console.log();
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
case 'clear-all': {
|
|
359
|
+
const r = bridge.clearAllServers();
|
|
360
|
+
console.log(` ✓ Cleared ${r.cleared} servers`);
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
case 'batch-cat':
|
|
365
|
+
case 'batch-read': {
|
|
366
|
+
const target = args[1], path = args[2];
|
|
367
|
+
if (!target || !path) { console.error('Usage: remotex batch-cat <group|s1,s2,...> <path>'); process.exit(1); }
|
|
368
|
+
const serverIds = resolveTarget(target);
|
|
369
|
+
console.log(`\n Reading ${path} from ${serverIds.length} servers...\n`);
|
|
370
|
+
const results = await bridge.batchReadFile(serverIds, path);
|
|
371
|
+
for (const [id, r] of Object.entries(results)) {
|
|
372
|
+
if (r.error) {
|
|
373
|
+
console.log(` ✗ ${id}: ${r.error}`);
|
|
374
|
+
} else {
|
|
375
|
+
const preview = (r.content || '').split('\n').slice(0, 3).join(' | ').substring(0, 100);
|
|
376
|
+
console.log(` ✓ ${id.padEnd(25)} ${(r.content || '').split('\n').length} lines ${preview}...`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
console.log();
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
case 'batch-write': {
|
|
384
|
+
const target = args[1], path = args[2];
|
|
385
|
+
if (!target || !path) { console.error('Usage: remotex batch-write <group|s1,s2,...> <path> (reads stdin)'); process.exit(1); }
|
|
386
|
+
const serverIds = resolveTarget(target);
|
|
387
|
+
const chunks = [];
|
|
388
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
389
|
+
const content = Buffer.concat(chunks).toString('utf-8');
|
|
390
|
+
const noBackup = args.includes('--no-backup');
|
|
391
|
+
console.log(`\n Writing ${path} to ${serverIds.length} servers (${content.length} bytes)...\n`);
|
|
392
|
+
const results = await bridge.batchWriteFile(serverIds, path, content, { backup: !noBackup });
|
|
393
|
+
for (const [id, r] of Object.entries(results)) {
|
|
394
|
+
const icon = r.error ? '✗' : '✓';
|
|
395
|
+
const msg = r.error || `ok (backup: ${r.backedUp})`;
|
|
396
|
+
console.log(` ${icon} ${id.padEnd(25)} ${msg}`);
|
|
397
|
+
}
|
|
398
|
+
console.log();
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
case 'batch-replace': {
|
|
403
|
+
const target = args[1], path = args[2], old = args[3], nw = args[4];
|
|
404
|
+
if (!target || !path || !old || nw === undefined) {
|
|
405
|
+
console.error('Usage: remotex batch-replace <group|s1,s2,...> <path> <old_text> <new_text> [--all]');
|
|
406
|
+
process.exit(1);
|
|
407
|
+
}
|
|
408
|
+
const serverIds = resolveTarget(target);
|
|
409
|
+
const all = args.includes('--all');
|
|
410
|
+
console.log(`\n Replacing in ${path} on ${serverIds.length} servers...\n`);
|
|
411
|
+
const results = await bridge.batchReplaceInFile(serverIds, path, old, nw, { all });
|
|
412
|
+
for (const [id, r] of Object.entries(results)) {
|
|
413
|
+
const icon = r.error ? '✗' : '✓';
|
|
414
|
+
const msg = r.error || `${r.replacements} replacement(s)`;
|
|
415
|
+
console.log(` ${icon} ${id.padEnd(25)} ${msg}`);
|
|
416
|
+
}
|
|
417
|
+
console.log();
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
case 'status': {
|
|
422
|
+
console.log('\n Checking all servers...\n');
|
|
423
|
+
const servers = await bridge.listServers(true);
|
|
424
|
+
const online = servers.filter(s => s.status === 'online').length;
|
|
425
|
+
for (const s of servers) {
|
|
426
|
+
const icon = s.status === 'online' ? '\x1b[32m●\x1b[0m' : '\x1b[31m○\x1b[0m';
|
|
427
|
+
console.log(` ${icon} ${s.id.padEnd(20)} ${s.host}`);
|
|
428
|
+
}
|
|
429
|
+
console.log(`\n ${online}/${servers.length} online\n`);
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
case 'help':
|
|
434
|
+
case '--help':
|
|
435
|
+
case '-h':
|
|
436
|
+
case undefined: {
|
|
437
|
+
console.log(`
|
|
438
|
+
remotex — Claude Code SSH Server Manager
|
|
439
|
+
|
|
440
|
+
Commands:
|
|
441
|
+
list [-c] List servers (add -c to check connectivity)
|
|
442
|
+
status Check all servers connectivity
|
|
443
|
+
info <server> Detailed system info
|
|
444
|
+
exec <server> <command> Execute command on server
|
|
445
|
+
batch <group|s1,s2> <command> Execute on multiple servers
|
|
446
|
+
add <id> <host> [options] Add server (--user --port --key --group)
|
|
447
|
+
remove <id> Remove server
|
|
448
|
+
groups List server groups
|
|
449
|
+
upload <server> <local> <remote> Upload file via SFTP
|
|
450
|
+
download <server> <remote> <local> Download file via SFTP
|
|
451
|
+
tail <server> <logpath> [lines] Tail remote log file
|
|
452
|
+
service <server> <name> <action> Manage systemd service
|
|
453
|
+
ports <server> List listening ports
|
|
454
|
+
|
|
455
|
+
Batch Import:
|
|
456
|
+
import <file> [--overwrite] [--group name] Import from ip.txt format
|
|
457
|
+
clear-all Remove all servers
|
|
458
|
+
|
|
459
|
+
Batch File Ops (批量 vi):
|
|
460
|
+
batch-cat <group|s1,s2> <path> Read file from multiple servers
|
|
461
|
+
batch-write <group|s1,s2> <path> Write file to servers (stdin)
|
|
462
|
+
batch-replace <group|s1,s2> <path> <old> <new> [--all] Replace text
|
|
463
|
+
|
|
464
|
+
MCP Server:
|
|
465
|
+
claude mcp add remotex node ${process.argv[1].replace('/bin/cli.js', '/src/mcp-server.js')}
|
|
466
|
+
|
|
467
|
+
Config: ~/.remotex.json
|
|
468
|
+
`);
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
default:
|
|
473
|
+
console.error(`Unknown command: ${cmd}. Run "remotex help" for usage.`);
|
|
474
|
+
process.exit(1);
|
|
475
|
+
}
|
|
476
|
+
} catch (err) {
|
|
477
|
+
console.error(`Error: ${err.message}`);
|
|
478
|
+
process.exit(1);
|
|
479
|
+
} finally {
|
|
480
|
+
bridge.disconnectAll();
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"name":"@iflow-mcp/npcnpc09-remotex","version":"0.4.0","description":"RemoteX MCP Server — 30+ SSH tools for Claude Code, AI-native fleet management","type":"module","main":"src/mcp-server.js","bin":{"iflow-mcp_npcnpc09-remotex":"bin/cli.js"},"scripts":{"start":"node src/mcp-server.js","dashboard":"node src/dashboard-server.js","cli":"node bin/cli.js"},"dependencies":{"@modelcontextprotocol/sdk":"^1.29.0","node-fetch":"^3.3.2","ssh2":"^1.16.0","ws":"^8.18.0"},"keywords":["remotex","claude-code","mcp","ssh","ai-agent","server-management","fleet"],"license":"MIT"}
|