@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 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"}