remote-access-mcp 2.3.1 → 2.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/README.fa.md +34 -0
- package/README.md +56 -2
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +282 -2
- package/dist/cli/main.js.map +1 -1
- package/dist/core/config.d.ts +10 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +5 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/fleet.d.ts +71 -0
- package/dist/core/fleet.d.ts.map +1 -0
- package/dist/core/fleet.js +148 -0
- package/dist/core/fleet.js.map +1 -0
- package/dist/core/policy.d.ts.map +1 -1
- package/dist/core/policy.js +1 -0
- package/dist/core/policy.js.map +1 -1
- package/dist/core/tunnel.d.ts.map +1 -1
- package/dist/core/tunnel.js +4 -0
- package/dist/core/tunnel.js.map +1 -1
- package/dist/core/webhooks.d.ts +18 -0
- package/dist/core/webhooks.d.ts.map +1 -0
- package/dist/core/webhooks.js +81 -0
- package/dist/core/webhooks.js.map +1 -0
- package/dist/server/app.d.ts.map +1 -1
- package/dist/server/app.js +7 -0
- package/dist/server/app.js.map +1 -1
- package/dist/server/run.d.ts.map +1 -1
- package/dist/server/run.js +40 -8
- package/dist/server/run.js.map +1 -1
- package/dist/tools/filesystem.d.ts.map +1 -1
- package/dist/tools/filesystem.js +59 -12
- package/dist/tools/filesystem.js.map +1 -1
- package/dist/tools/fleet.d.ts +4 -0
- package/dist/tools/fleet.d.ts.map +1 -0
- package/dist/tools/fleet.js +39 -0
- package/dist/tools/fleet.js.map +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +2 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/logs.d.ts.map +1 -1
- package/dist/tools/logs.js +24 -3
- package/dist/tools/logs.js.map +1 -1
- package/dist/tools/packages.d.ts.map +1 -1
- package/dist/tools/packages.js +11 -1
- package/dist/tools/packages.js.map +1 -1
- package/dist/tools/services.d.ts.map +1 -1
- package/dist/tools/services.js +21 -4
- package/dist/tools/services.js.map +1 -1
- package/dist/tools/shell.d.ts +4 -0
- package/dist/tools/shell.d.ts.map +1 -1
- package/dist/tools/shell.js +34 -6
- package/dist/tools/shell.js.map +1 -1
- package/package.json +1 -1
package/README.fa.md
CHANGED
|
@@ -24,6 +24,8 @@ ramcp init
|
|
|
24
24
|
|
|
25
25
|
## شروع سریع
|
|
26
26
|
|
|
27
|
+
روی **سرور** با دامنه:
|
|
28
|
+
|
|
27
29
|
```bash
|
|
28
30
|
ramcp init # کانفیگ + اولین توکن
|
|
29
31
|
ramcp policy allow /srv/myapp # چه مسیرهایی رو AI ببینه
|
|
@@ -33,6 +35,17 @@ ramcp doctor # بررسی سلامت همهچیز
|
|
|
33
35
|
ramcp url # URL کانکتور برای چتبات
|
|
34
36
|
```
|
|
35
37
|
|
|
38
|
+
روی **لپتاپ / دسکتاپ** (بدون دامنه و بدون پورتفوروارد):
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
ramcp tunnel
|
|
42
|
+
# → دفعه اول cloudflared رو خودکار دانلود میکنه (بدون نیاز به اکانت)
|
|
43
|
+
# یه URL عمومی https میده مثل https://random-words.trycloudflare.com
|
|
44
|
+
# دستور `ramcp url` توی ترمینال دیگه، لینک زنده کانکتور رو نشون میده.
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
روی ویندوز، مک و لینوکس یکسانه — PowerShell/cmd روی ویندوز، launchd روی مک، systemd روی لینوکس برای سرویس خودکار.
|
|
48
|
+
|
|
36
49
|
## توکنهای چندگانه — کمترین دسترسی بهصورت پیشفرض
|
|
37
50
|
|
|
38
51
|
```bash
|
|
@@ -49,6 +62,27 @@ ramcp token add --name deploy --paths /srv/app --scopes filesystem,git,shell --s
|
|
|
49
62
|
|
|
50
63
|
فایلسیستم (۷)، شل (۳)، سیستم (۳)، HTTP با محافظ SSRF (۳)، گیت با whitelist فعلها (۱)، SQLite تک-دستوره (۲)، لاگ/journalctl (۳)، سرویسهای systemd (۲)، پکیجها apt/npm (۳)، زمانبند (۳)، اسکن امنیتی (۲)، تحلیل پروژه (۲)، پلنینگ + اسنپشات/rollback (۴)، مدیریت پالیسی (۴).
|
|
51
64
|
|
|
65
|
+
## Fleet: کنترل چند سرور با SSH
|
|
66
|
+
|
|
67
|
+
گیتوی روی یک ماشین میمونه؛ ابزارهای remote-capable پارامتر `host` میگیرن و از طریق SSH روی ماشین دیگه اجرا میشن. روی سرورهای ریموت هیچ نرمافزاری نصب نمیشه — فقط یه SSH server و کلید شما روی گیتوی.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
ramcp fleet add --name web1 --host deploy@10.0.0.5 --tools shell,fs --note "app server"
|
|
71
|
+
ramcp fleet test # تست دسترسی همه هاستها
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
بعد از داخل چتبات: «لاگ nginx روی web1 رو ببین» → `journal(unit: "nginx.service", host: "web1")`.
|
|
75
|
+
|
|
76
|
+
هر هاست allowlist ابزار خودش رو داره (`shell fs logs services packages`) — هاستی که گروهی رو نگرفته با خطای واضح رد میشه، قبل از هر تلاش SSH. نوشتن فایل ریموت از طریق stdin (base64) انجام میشه — بدون فایل موقت. SSH با BatchMode اجرا میشه — هیچ پرامپت پسوردی نیست.
|
|
77
|
+
|
|
78
|
+
## Webhook و بکاپ
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
ramcp webhook add --url https://hooks.example.com/ramcp --events tool.error
|
|
82
|
+
ramcp config export --out backup.json # اسنپشات کامل — توکنهای زنده داره!
|
|
83
|
+
ramcp config import backup.json --merge # ادغام با حفظ هویت محلی
|
|
84
|
+
```
|
|
85
|
+
|
|
52
86
|
## مدل امنیتی
|
|
53
87
|
|
|
54
88
|
- **فقط loopback** — سرور روی `127.0.0.1` گوش میده
|
package/README.md
CHANGED
|
@@ -23,7 +23,14 @@ The server binds to `127.0.0.1` only. You put it behind nginx (with Cloudflare o
|
|
|
23
23
|
|
|
24
24
|
## Install
|
|
25
25
|
|
|
26
|
-
###
|
|
26
|
+
### Any machine with Node 18+ — Linux, macOS, or Windows
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install -g remote-access-mcp
|
|
30
|
+
ramcp init
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### One-liner (Ubuntu/Debian servers)
|
|
27
34
|
|
|
28
35
|
```bash
|
|
29
36
|
curl -fsSL https://raw.githubusercontent.com/AmirAliManzar/remote-access-mcp/main/install.sh | bash
|
|
@@ -39,6 +46,8 @@ ramcp init
|
|
|
39
46
|
|
|
40
47
|
## Quick start
|
|
41
48
|
|
|
49
|
+
On a **server** with a domain:
|
|
50
|
+
|
|
42
51
|
```bash
|
|
43
52
|
ramcp init # config + first token
|
|
44
53
|
ramcp policy allow /srv/myapp # what the AI may touch
|
|
@@ -48,6 +57,18 @@ ramcp doctor # verify everything end-to-end
|
|
|
48
57
|
ramcp url # connector URL for your chatbot
|
|
49
58
|
```
|
|
50
59
|
|
|
60
|
+
On a **laptop or desktop** (no domain, no port forwarding):
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
ramcp tunnel
|
|
64
|
+
# → downloads cloudflared on first run (no account needed),
|
|
65
|
+
# prints a public https URL like https://random-words.trycloudflare.com
|
|
66
|
+
# `ramcp url` in another terminal shows the live connector link.
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Works the same on Windows, macOS, and Linux — PowerShell/cmd on Windows,
|
|
70
|
+
launchd on macOS, systemd on Linux for the autostart service.
|
|
71
|
+
|
|
51
72
|
## Commands
|
|
52
73
|
|
|
53
74
|
| Command | Description |
|
|
@@ -131,6 +152,39 @@ Endpoint `https://your-domain.com/mcp` + header `Authorization: Bearer <token>`
|
|
|
131
152
|
|
|
132
153
|
**Policy** (4) `list_allowed_paths` `allow_path` `deny_path` `shell_enabled` — each token manages only its own sandbox
|
|
133
154
|
|
|
155
|
+
## Fleet: drive other machines over SSH
|
|
156
|
+
|
|
157
|
+
The gateway itself stays on one box; every remote-capable tool gains a `host` parameter that routes over SSH. No agent software on the remotes — just an SSH server and your key on the gateway.
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
# on the gateway:
|
|
161
|
+
ramcp fleet add --name web1 --host deploy@10.0.0.5 --tools shell,fs --note "app server"
|
|
162
|
+
ramcp fleet test # reachability probe for all hosts
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Then from the chatbot: *"read the nginx log on web1"* → `journal(unit: "nginx.service", host: "web1")`.
|
|
166
|
+
|
|
167
|
+
Each host gets an explicit tool allowlist (`shell fs logs services packages`) — a host that was never granted a group refuses it with a clear error, before any SSH attempt. Remote file writes pipe content over stdin (base64), never temp files. SSH runs with `BatchMode` — no password prompts, ever.
|
|
168
|
+
|
|
169
|
+
## Webhooks
|
|
170
|
+
|
|
171
|
+
Get notified when tools run (or fail) — incident bots, Slack relays, anything that accepts a POST:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
ramcp webhook add --url https://hooks.example.com/ramcp --events tool.error
|
|
175
|
+
ramcp webhook list
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Fire-and-forget: a dead endpoint never delays a tool call (5s cap, deduped within 10s).
|
|
179
|
+
|
|
180
|
+
## Backup & restore
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
ramcp config export --out backup.json # full snapshot, 0600 perms — contains live tokens!
|
|
184
|
+
ramcp config import backup.json # replace
|
|
185
|
+
ramcp config import backup.json --merge # union: keeps local identity, adds new tokens/hosts/hooks
|
|
186
|
+
```
|
|
187
|
+
|
|
134
188
|
## Security model
|
|
135
189
|
|
|
136
190
|
- **Loopback only.** The gateway listens on `127.0.0.1` — unreachable directly from the network.
|
|
@@ -164,7 +218,7 @@ cd remote-access-mcp
|
|
|
164
218
|
npm ci && npm run build && npm test
|
|
165
219
|
```
|
|
166
220
|
|
|
167
|
-
|
|
221
|
+
122 tests: policy engine, auth matrix, transport compatibility (stateful, stateless, legacy SSE), cross-platform platform layer, tunnel wiring, CLI lifecycle, crash regressions. CI runs on Node 18/20/22.
|
|
168
222
|
|
|
169
223
|
## License
|
|
170
224
|
|
package/dist/cli/main.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":"AAmgCA,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA4BxD"}
|
package/dist/cli/main.js
CHANGED
|
@@ -7,7 +7,9 @@ import { fileURLToPath } from 'node:url';
|
|
|
7
7
|
import { loadConfig, saveConfig, generateToken, newTokenRecord, configPath, configDir, primaryToken, } from '../core/config.js';
|
|
8
8
|
import { resolveReal, TOOL_SCOPES } from '../core/policy.js';
|
|
9
9
|
import { AuditLog } from '../core/audit.js';
|
|
10
|
-
import {
|
|
10
|
+
import { fleetProbe, isFleetCapability, FLEET_CAPABILITIES } from '../core/fleet.js';
|
|
11
|
+
import { webhookStats } from '../core/webhooks.js';
|
|
12
|
+
import { platform, isWindows, isMac, isLinux, hasSystemd, platformLabel, readRuntimeState, } from '../core/platform.js';
|
|
11
13
|
import { resolveCloudflared } from '../core/tunnel.js';
|
|
12
14
|
const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
13
15
|
const PKG = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8'));
|
|
@@ -56,6 +58,18 @@ Other:
|
|
|
56
58
|
schedule list [--json]
|
|
57
59
|
scopes List available tool groups
|
|
58
60
|
|
|
61
|
+
Fleet (remote machines over SSH):
|
|
62
|
+
fleet add --name N --host user@h --tools shell,fs [--port P] [--note T]
|
|
63
|
+
fleet list | remove N | edit N [--tools …] | test | status
|
|
64
|
+
(tools accept host: run_command, read_file, write_file, … → run remotely)
|
|
65
|
+
|
|
66
|
+
Webhooks:
|
|
67
|
+
webhook add --url https://… [--events tool.error,tool.success]
|
|
68
|
+
webhook list | off URL | on URL | remove URL
|
|
69
|
+
|
|
70
|
+
Config:
|
|
71
|
+
config show | export [--out FILE] | import FILE [--merge]
|
|
72
|
+
|
|
59
73
|
Options:
|
|
60
74
|
-h, --help Show this help
|
|
61
75
|
--json Machine-readable output where supported
|
|
@@ -66,7 +80,7 @@ function parseArgs(argv) {
|
|
|
66
80
|
const flags = new Set();
|
|
67
81
|
const values = new Map();
|
|
68
82
|
// Flags that always take a value (so `--paths /a /b` still works positionally)
|
|
69
|
-
const valueFlags = new Set(['name', 'paths', 'deny', 'scopes', 'rpm', 'expires', 'token', 'host', 'port', 'domain', 'tool', 'since', 'limit']);
|
|
83
|
+
const valueFlags = new Set(['name', 'paths', 'deny', 'scopes', 'rpm', 'expires', 'token', 'host', 'port', 'domain', 'tool', 'since', 'limit', 'tools', 'url', 'events', 'out', 'note']);
|
|
70
84
|
for (let i = 0; i < rest.length; i++) {
|
|
71
85
|
const a = rest[i];
|
|
72
86
|
if (a.startsWith('--')) {
|
|
@@ -203,6 +217,15 @@ async function cmdTunnel(args) {
|
|
|
203
217
|
function cmdUrl(args) {
|
|
204
218
|
const cfg = loadConfig();
|
|
205
219
|
const t = findToken(cfg, args.sub[0] || args.values.get('token'));
|
|
220
|
+
// A live tunnel (pid-checked) wins: its URL only exists while that
|
|
221
|
+
// gateway process runs, and it is the address chatbots must use right now.
|
|
222
|
+
const rt = readRuntimeState();
|
|
223
|
+
if (rt?.tunnel_url) {
|
|
224
|
+
const base = rt.tunnel_url.replace(/\/+$/, '');
|
|
225
|
+
console.log(`${base}/${t.token}${cfg.mcp_path}`);
|
|
226
|
+
console.log(`\n(live tunnel — pid ${rt.pid}, since ${rt.started})`);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
206
229
|
const host = cfg.public_host || `${cfg.host}:${cfg.port}`;
|
|
207
230
|
const scheme = cfg.public_host ? 'https' : 'http';
|
|
208
231
|
console.log(`${scheme}://${host}/${t.token}${cfg.mcp_path}`);
|
|
@@ -477,6 +500,20 @@ async function cmdDoctor(args) {
|
|
|
477
500
|
// tunnel readiness
|
|
478
501
|
const cf = resolveCloudflared();
|
|
479
502
|
cf ? ok('tunnel', `cloudflared ready (${cf})`) : warn('tunnel', 'cloudflared not installed yet — `ramcp tunnel` downloads it automatically');
|
|
503
|
+
// live tunnel state (a running `ramcp tunnel` in another terminal)
|
|
504
|
+
const rt = readRuntimeState();
|
|
505
|
+
if (rt?.tunnel_url) {
|
|
506
|
+
try {
|
|
507
|
+
const resp = await fetch(`${rt.tunnel_url}/health`, { signal: AbortSignal.timeout(8000) });
|
|
508
|
+
const body = (await resp.json());
|
|
509
|
+
body.status === 'ok'
|
|
510
|
+
? ok('tunnel-live', `${rt.tunnel_url} (pid ${rt.pid})`)
|
|
511
|
+
: warn('tunnel-live', `edge answered ${resp.status}`);
|
|
512
|
+
}
|
|
513
|
+
catch (e) {
|
|
514
|
+
warn('tunnel-live', `runtime state says pid ${rt.pid}, but unreachable (${e.message})`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
480
517
|
// public reachability
|
|
481
518
|
if (cfg.public_host) {
|
|
482
519
|
try {
|
|
@@ -788,11 +825,14 @@ function uninstallService() {
|
|
|
788
825
|
// ---------------------------------------------------------------------------
|
|
789
826
|
function cmdStatus() {
|
|
790
827
|
const cfg = loadConfig();
|
|
828
|
+
const rt = readRuntimeState();
|
|
791
829
|
console.log(`platform: ${platformLabel()}`);
|
|
792
830
|
console.log(`config: ${configPath()}`);
|
|
793
831
|
console.log(`${serviceLabel()}:${' '.repeat(Math.max(1, 19 - serviceLabel().length - 1))}${serviceIsInstalled() ? (serviceIsActive() ? 'installed, active' : 'installed, inactive') : 'not installed'}`);
|
|
794
832
|
console.log(`listen: ${cfg.host}:${cfg.port}${cfg.mcp_path}`);
|
|
833
|
+
console.log(`gateway process: ${rt ? `running (pid ${rt.pid})` : 'not running'}`);
|
|
795
834
|
console.log(`public_host: ${cfg.public_host || '(none — use `ramcp tunnel`)'}`);
|
|
835
|
+
console.log(`live tunnel: ${rt?.tunnel_url || '(none)'}`);
|
|
796
836
|
console.log(`tokens: ${cfg.tokens.length}`);
|
|
797
837
|
console.log(`allowed paths: ${cfg.tokens.reduce((n, t) => n + t.allowed_paths.length, 0)}`);
|
|
798
838
|
console.log(`audit: ${cfg.audit.enabled ? 'on' : 'off'}`);
|
|
@@ -858,6 +898,237 @@ async function cmdUpgrade(args) {
|
|
|
858
898
|
}
|
|
859
899
|
}
|
|
860
900
|
// ---------------------------------------------------------------------------
|
|
901
|
+
// fleet — SSH remote machines
|
|
902
|
+
// ---------------------------------------------------------------------------
|
|
903
|
+
function cmdFleet(args) {
|
|
904
|
+
const cfg = loadConfig();
|
|
905
|
+
const sub = args.sub[0] || 'list';
|
|
906
|
+
const hosts = cfg.fleet.hosts;
|
|
907
|
+
if (sub === 'add') {
|
|
908
|
+
const name = args.values.get('name') || args.sub[1];
|
|
909
|
+
const host = args.values.get('host');
|
|
910
|
+
if (!name || !host) {
|
|
911
|
+
console.error('Usage: ramcp fleet add --name NAME --host user@hostname [--port N] --tools shell,fs [--note TEXT]');
|
|
912
|
+
process.exit(1);
|
|
913
|
+
}
|
|
914
|
+
if (hosts.some(h => h.name === name)) {
|
|
915
|
+
console.error(`Host "${name}" already exists (remove it first: ramcp fleet remove ${name})`);
|
|
916
|
+
process.exit(1);
|
|
917
|
+
}
|
|
918
|
+
const tools = splitList(args.values.get('tools'));
|
|
919
|
+
const bad = tools.filter(t => !isFleetCapability(t));
|
|
920
|
+
if (bad.length) {
|
|
921
|
+
console.error(`Unknown tool group(s): ${bad.join(', ')}. Available: ${FLEET_CAPABILITIES.join(', ')}`);
|
|
922
|
+
process.exit(1);
|
|
923
|
+
}
|
|
924
|
+
if (!tools.length) {
|
|
925
|
+
console.error('Refusing to add a host with no tools — grant at least one of: ' + FLEET_CAPABILITIES.join(', '));
|
|
926
|
+
process.exit(1);
|
|
927
|
+
}
|
|
928
|
+
const rec = {
|
|
929
|
+
name, host,
|
|
930
|
+
port: args.values.has('port') ? parseInt(args.values.get('port'), 10) : undefined,
|
|
931
|
+
tools, note: args.values.get('note'),
|
|
932
|
+
added: new Date().toISOString(),
|
|
933
|
+
};
|
|
934
|
+
hosts.push(rec);
|
|
935
|
+
saveConfig(cfg);
|
|
936
|
+
console.log(`✔ fleet host "${name}" added (${host}) with tools: ${tools.join(', ')}`);
|
|
937
|
+
const probe = fleetProbe(rec).then(r => {
|
|
938
|
+
console.log(r.ok ? ` SSH check: reachable` : ` SSH check: FAILED — ${r.detail}`);
|
|
939
|
+
console.log(r.ok ? ` (grant access from a chatbot with the fleet scope or all-tools tokens)` : ` (check: ssh keys loaded? BatchMode disables password prompts)`);
|
|
940
|
+
});
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
if (sub === 'remove') {
|
|
944
|
+
const name = args.sub[1];
|
|
945
|
+
if (!name) {
|
|
946
|
+
console.error('Usage: ramcp fleet remove NAME');
|
|
947
|
+
process.exit(1);
|
|
948
|
+
}
|
|
949
|
+
const i = hosts.findIndex(h => h.name === name);
|
|
950
|
+
if (i === -1) {
|
|
951
|
+
console.error(`No fleet host named "${name}".`);
|
|
952
|
+
process.exit(1);
|
|
953
|
+
}
|
|
954
|
+
hosts.splice(i, 1);
|
|
955
|
+
saveConfig(cfg);
|
|
956
|
+
console.log(`✔ removed ${name}`);
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
if (sub === 'edit') {
|
|
960
|
+
const name = args.sub[1];
|
|
961
|
+
if (!name) {
|
|
962
|
+
console.error('Usage: ramcp fleet edit NAME [--tools shell,fs] [--host user@h] [--note T]');
|
|
963
|
+
process.exit(1);
|
|
964
|
+
}
|
|
965
|
+
const h = hosts.find(h => h.name === name);
|
|
966
|
+
if (!h) {
|
|
967
|
+
console.error(`No fleet host named "${name}".`);
|
|
968
|
+
process.exit(1);
|
|
969
|
+
}
|
|
970
|
+
if (args.values.has('tools')) {
|
|
971
|
+
const tools = splitList(args.values.get('tools'));
|
|
972
|
+
const bad = tools.filter(t => !isFleetCapability(t));
|
|
973
|
+
if (bad.length) {
|
|
974
|
+
console.error(`Unknown: ${bad.join(', ')}. Available: ${FLEET_CAPABILITIES.join(', ')}`);
|
|
975
|
+
process.exit(1);
|
|
976
|
+
}
|
|
977
|
+
h.tools = tools;
|
|
978
|
+
}
|
|
979
|
+
if (args.values.has('host'))
|
|
980
|
+
h.host = args.values.get('host');
|
|
981
|
+
if (args.values.has('port'))
|
|
982
|
+
h.port = parseInt(args.values.get('port'), 10);
|
|
983
|
+
if (args.values.has('note'))
|
|
984
|
+
h.note = args.values.get('note');
|
|
985
|
+
saveConfig(cfg);
|
|
986
|
+
console.log(`✔ ${name} updated. tools: ${h.tools.join(',') || '(none)'}`);
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
if (sub === 'test') {
|
|
990
|
+
(async () => {
|
|
991
|
+
for (const h of hosts) {
|
|
992
|
+
const r = await fleetProbe(h);
|
|
993
|
+
console.log(`${r.ok ? '✔' : '✖'} ${h.name.padEnd(14)} ${h.host}${h.port ? ':' + h.port : ''} ${r.ok ? 'reachable' : 'unreachable — ' + r.detail}`);
|
|
994
|
+
}
|
|
995
|
+
if (!hosts.length)
|
|
996
|
+
console.log('(no fleet hosts)');
|
|
997
|
+
})();
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
if (sub === 'status') {
|
|
1001
|
+
const { runtime } = { runtime: null };
|
|
1002
|
+
for (const h of hosts) {
|
|
1003
|
+
console.log(`${h.name.padEnd(14)} ${h.host}${h.port ? ':' + h.port : ''} tools: ${h.tools.join(',')}${h.note ? ' — ' + h.note : ''}`);
|
|
1004
|
+
}
|
|
1005
|
+
if (!hosts.length)
|
|
1006
|
+
console.log('(no fleet hosts — add one with `ramcp fleet add`)');
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
// list / default
|
|
1010
|
+
for (const h of hosts) {
|
|
1011
|
+
console.log(`${h.name.padEnd(14)} ${h.host}${h.port ? ':' + h.port : ''} tools: ${h.tools.join(',')}${h.note ? ' — ' + h.note : ''}`);
|
|
1012
|
+
}
|
|
1013
|
+
if (!hosts.length)
|
|
1014
|
+
console.log('(no fleet hosts — add one with `ramcp fleet add --name NAME --host user@host --tools shell,fs`)');
|
|
1015
|
+
}
|
|
1016
|
+
// ---------------------------------------------------------------------------
|
|
1017
|
+
// webhook — outbound notifications
|
|
1018
|
+
// ---------------------------------------------------------------------------
|
|
1019
|
+
function cmdWebhook(args) {
|
|
1020
|
+
const cfg = loadConfig();
|
|
1021
|
+
const sub = args.sub[0] || 'list';
|
|
1022
|
+
if (sub === 'add') {
|
|
1023
|
+
const url = args.values.get('url');
|
|
1024
|
+
if (!url || !/^https?:\/\//.test(url)) {
|
|
1025
|
+
console.error('Usage: ramcp webhook add --url https://… [--events tool.error,tool.success]');
|
|
1026
|
+
process.exit(1);
|
|
1027
|
+
}
|
|
1028
|
+
const events = splitList(args.values.get('events') || 'tool.error');
|
|
1029
|
+
cfg.webhooks.push({ url, events, enabled: true });
|
|
1030
|
+
saveConfig(cfg);
|
|
1031
|
+
console.log(`✔ webhook added: ${url}`);
|
|
1032
|
+
console.log(` events: ${events.join(', ')}`);
|
|
1033
|
+
console.log(` (fires fire-and-forget after tool calls; 5s timeout, deduped within 10s)`);
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
if (sub === 'remove') {
|
|
1037
|
+
const i = cfg.webhooks.findIndex(w => w.url === args.sub[1]);
|
|
1038
|
+
if (i === -1) {
|
|
1039
|
+
console.error('No webhook with that URL.');
|
|
1040
|
+
process.exit(1);
|
|
1041
|
+
}
|
|
1042
|
+
cfg.webhooks.splice(i, 1);
|
|
1043
|
+
saveConfig(cfg);
|
|
1044
|
+
console.log('✔ removed');
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
if (sub === 'off' || sub === 'on') {
|
|
1048
|
+
const w = cfg.webhooks.find(w => w.url === args.sub[1]);
|
|
1049
|
+
if (!w) {
|
|
1050
|
+
console.error('No webhook with that URL.');
|
|
1051
|
+
process.exit(1);
|
|
1052
|
+
}
|
|
1053
|
+
w.enabled = sub === 'on';
|
|
1054
|
+
saveConfig(cfg);
|
|
1055
|
+
console.log(`✔ ${w.url} ${w.enabled ? 'enabled' : 'disabled'}`);
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
// list
|
|
1059
|
+
const stats = webhookStats();
|
|
1060
|
+
if (!cfg.webhooks.length) {
|
|
1061
|
+
console.log('(no webhooks — add with `ramcp webhook add --url …`)');
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
for (const w of cfg.webhooks) {
|
|
1065
|
+
console.log(`${w.enabled ? '✔' : '⏸'} ${w.url}`);
|
|
1066
|
+
console.log(` events: ${w.events.join(', ')}`);
|
|
1067
|
+
}
|
|
1068
|
+
console.log(`\nsession stats: sent=${stats.sent} failed=${stats.failed}`);
|
|
1069
|
+
}
|
|
1070
|
+
// ---------------------------------------------------------------------------
|
|
1071
|
+
// config — export / import / show
|
|
1072
|
+
// ---------------------------------------------------------------------------
|
|
1073
|
+
function cmdConfig(args) {
|
|
1074
|
+
const cfg = loadConfig();
|
|
1075
|
+
const sub = args.sub[0] || 'show';
|
|
1076
|
+
if (sub === 'export') {
|
|
1077
|
+
const out = args.values.get('out') || 'ramcp-export.json';
|
|
1078
|
+
const snapshot = JSON.parse(JSON.stringify(cfg));
|
|
1079
|
+
// strip nothing — export is a full backup by design; tokens are the
|
|
1080
|
+
// secrets that make the backup useful. Warn loudly instead.
|
|
1081
|
+
fs.writeFileSync(out, JSON.stringify(snapshot, null, 2), { mode: 0o600 });
|
|
1082
|
+
console.log(`✔ exported ${cfg.tokens.length} token(s), ${(cfg.fleet?.hosts || []).length} fleet host(s), ${(cfg.webhooks || []).length} webhook(s) → ${out}`);
|
|
1083
|
+
console.log('⚠ this file contains live tokens — store it encrypted and delete when done.');
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
if (sub === 'import') {
|
|
1087
|
+
const file = args.sub[1];
|
|
1088
|
+
if (!file) {
|
|
1089
|
+
console.error('Usage: ramcp config import <file> [--merge]');
|
|
1090
|
+
process.exit(1);
|
|
1091
|
+
}
|
|
1092
|
+
let incoming;
|
|
1093
|
+
try {
|
|
1094
|
+
incoming = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
1095
|
+
}
|
|
1096
|
+
catch (e) {
|
|
1097
|
+
console.error(`cannot read ${file}: ${e.message}`);
|
|
1098
|
+
process.exit(1);
|
|
1099
|
+
}
|
|
1100
|
+
if (!Array.isArray(incoming.tokens)) {
|
|
1101
|
+
console.error('not a ramcp export (no tokens array)');
|
|
1102
|
+
process.exit(1);
|
|
1103
|
+
}
|
|
1104
|
+
if (args.flags.has('merge')) {
|
|
1105
|
+
// merge: keep local identity (host/port/domain/paths), union tokens,
|
|
1106
|
+
// add fleet hosts and webhooks that are new.
|
|
1107
|
+
const existingUrls = new Set((cfg.webhooks || []).map(w => w.url));
|
|
1108
|
+
for (const w of incoming.webhooks || [])
|
|
1109
|
+
if (!existingUrls.has(w.url))
|
|
1110
|
+
cfg.webhooks.push(w);
|
|
1111
|
+
const existingHosts = new Set((cfg.fleet?.hosts || []).map(h => h.name));
|
|
1112
|
+
for (const h of incoming.fleet?.hosts || [])
|
|
1113
|
+
if (!existingHosts.has(h.name))
|
|
1114
|
+
cfg.fleet.hosts.push(h);
|
|
1115
|
+
for (const t of incoming.tokens)
|
|
1116
|
+
if (!cfg.tokens.some(x => x.token === t.token))
|
|
1117
|
+
cfg.tokens.push(t);
|
|
1118
|
+
saveConfig(cfg);
|
|
1119
|
+
console.log('✔ merged (local host/port/domain and existing entries preserved)');
|
|
1120
|
+
}
|
|
1121
|
+
else {
|
|
1122
|
+
saveConfig(incoming);
|
|
1123
|
+
console.log(`✔ imported ${incoming.tokens.length} token(s) — replaced existing config`);
|
|
1124
|
+
console.log('(restart the service or reconnect clients to apply)');
|
|
1125
|
+
}
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
// show
|
|
1129
|
+
console.log(JSON.stringify(cfg, null, 2));
|
|
1130
|
+
}
|
|
1131
|
+
// ---------------------------------------------------------------------------
|
|
861
1132
|
// main
|
|
862
1133
|
// ---------------------------------------------------------------------------
|
|
863
1134
|
export async function main(argv) {
|
|
@@ -906,6 +1177,15 @@ export async function main(argv) {
|
|
|
906
1177
|
case 'upgrade':
|
|
907
1178
|
await cmdUpgrade(args);
|
|
908
1179
|
break;
|
|
1180
|
+
case 'fleet':
|
|
1181
|
+
cmdFleet(args);
|
|
1182
|
+
break;
|
|
1183
|
+
case 'webhook':
|
|
1184
|
+
cmdWebhook(args);
|
|
1185
|
+
break;
|
|
1186
|
+
case 'config':
|
|
1187
|
+
cmdConfig(args);
|
|
1188
|
+
break;
|
|
909
1189
|
case 'version':
|
|
910
1190
|
console.log(PKG.version);
|
|
911
1191
|
break;
|