remote-access-mcp 2.1.2 → 2.2.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/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +566 -359
- package/dist/cli/main.js.map +1 -1
- package/dist/core/config.d.ts +7 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +37 -2
- package/dist/core/config.js.map +1 -1
- package/dist/core/platform.d.ts +49 -0
- package/dist/core/platform.d.ts.map +1 -0
- package/dist/core/platform.js +123 -0
- package/dist/core/platform.js.map +1 -0
- package/dist/core/policy.d.ts +2 -0
- package/dist/core/policy.d.ts.map +1 -1
- package/dist/core/policy.js +14 -5
- package/dist/core/policy.js.map +1 -1
- package/dist/core/tunnel.d.ts +23 -0
- package/dist/core/tunnel.d.ts.map +1 -0
- package/dist/core/tunnel.js +141 -0
- package/dist/core/tunnel.js.map +1 -0
- package/dist/server/app.d.ts +2 -0
- package/dist/server/app.d.ts.map +1 -1
- package/dist/server/app.js +104 -38
- package/dist/server/app.js.map +1 -1
- package/dist/server/run.d.ts +1 -0
- package/dist/server/run.d.ts.map +1 -1
- package/dist/server/run.js +48 -14
- package/dist/server/run.js.map +1 -1
- package/dist/server/sessions.d.ts +38 -0
- package/dist/server/sessions.d.ts.map +1 -0
- package/dist/server/sessions.js +62 -0
- package/dist/server/sessions.js.map +1 -0
- package/dist/tools/logs.d.ts.map +1 -1
- package/dist/tools/logs.js +21 -4
- package/dist/tools/logs.js.map +1 -1
- package/dist/tools/packages.d.ts.map +1 -1
- package/dist/tools/packages.js +82 -26
- package/dist/tools/packages.js.map +1 -1
- package/dist/tools/planning.d.ts.map +1 -1
- package/dist/tools/planning.js +3 -3
- package/dist/tools/planning.js.map +1 -1
- package/dist/tools/schedule.d.ts.map +1 -1
- package/dist/tools/schedule.js +2 -2
- package/dist/tools/schedule.js.map +1 -1
- package/dist/tools/security.d.ts.map +1 -1
- package/dist/tools/security.js +15 -2
- package/dist/tools/security.js.map +1 -1
- package/dist/tools/services.d.ts.map +1 -1
- package/dist/tools/services.js +45 -14
- package/dist/tools/services.js.map +1 -1
- package/dist/tools/shell.d.ts.map +1 -1
- package/dist/tools/shell.js +32 -12
- package/dist/tools/shell.js.map +1 -1
- package/dist/tools/system.d.ts.map +1 -1
- package/dist/tools/system.js +18 -9
- package/dist/tools/system.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/main.js
CHANGED
|
@@ -1,78 +1,83 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import net from 'node:net';
|
|
4
5
|
import { execFileSync } from 'node:child_process';
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import net from 'node:net';
|
|
7
7
|
import { loadConfig, saveConfig, generateToken, newTokenRecord, configPath, configDir, primaryToken, } from '../core/config.js';
|
|
8
|
-
import { resolveReal } from '../core/policy.js';
|
|
8
|
+
import { resolveReal, TOOL_SCOPES } from '../core/policy.js';
|
|
9
9
|
import { AuditLog } from '../core/audit.js';
|
|
10
|
+
import { platform, isWindows, isMac, isLinux, hasSystemd, platformLabel, } from '../core/platform.js';
|
|
11
|
+
import { resolveCloudflared } from '../core/tunnel.js';
|
|
10
12
|
const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
11
13
|
const PKG = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8'));
|
|
12
14
|
// ---------------------------------------------------------------------------
|
|
13
15
|
// help
|
|
14
16
|
// ---------------------------------------------------------------------------
|
|
15
|
-
const HELP = `${PKG.name} v${PKG.version} — turn any
|
|
17
|
+
const HELP = `${PKG.name} v${PKG.version} — turn any machine into an AI-agent-accessible endpoint
|
|
18
|
+
Runs on Linux, macOS, and Windows.
|
|
16
19
|
|
|
17
20
|
Usage:
|
|
18
21
|
ramcp <command> [options]
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
init
|
|
22
|
-
start [--
|
|
23
|
-
|
|
24
|
-
url [
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
Getting started:
|
|
24
|
+
init [--paths a,b] Create config + first token
|
|
25
|
+
start [--tunnel] Run the gateway (--tunnel = public https URL)
|
|
26
|
+
tunnel Start gateway + public URL, print connector link
|
|
27
|
+
url [token] Print the connector URL for a chatbot
|
|
28
|
+
doctor Diagnose everything in one pass
|
|
29
|
+
status Config + service summary
|
|
30
|
+
upgrade [--dry-run] Self-update from npm
|
|
31
|
+
version
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
token
|
|
35
|
-
|
|
36
|
-
--paths a,b,c allowed paths (comma-separated)
|
|
37
|
-
--deny a,b denied paths
|
|
38
|
-
--scopes group,group tool groups (empty = all)
|
|
39
|
-
--read-only refuse mutating tools
|
|
40
|
-
--rpm N max requests per minute
|
|
41
|
-
--expires YYYY-MM-DD expiry date
|
|
33
|
+
Paths the AI may touch (per token):
|
|
34
|
+
policy [--token N] Show policy
|
|
35
|
+
policy allow <path...> [--token N] Allow one or more directories
|
|
36
|
+
policy deny <path...> [--token N] Deny directories
|
|
37
|
+
policy shell on|off [--token N] Toggle shell execution
|
|
38
|
+
policy scopes <groups> [--token N] Limit to tool groups (or 'all')
|
|
39
|
+
policy readonly on|off Global read-only kill-switch
|
|
42
40
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
Tokens:
|
|
42
|
+
token list [--json]
|
|
43
|
+
token add --name N [--paths a,b] [--shell] [--scopes g,g]
|
|
44
|
+
[--read-only] [--rpm N] [--expires YYYY-MM-DD]
|
|
45
|
+
token show [N] [--full] | token rotate [N] | token revoke N
|
|
48
46
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
audit chain
|
|
47
|
+
Audit:
|
|
48
|
+
audit [--tool T] [--since ISO] [--limit N] [--json]
|
|
49
|
+
audit chain Verify tamper-evident hash chain
|
|
52
50
|
|
|
53
|
-
|
|
54
|
-
service
|
|
55
|
-
service logs [-f]
|
|
56
|
-
service status Detailed service check
|
|
51
|
+
Service (autostart):
|
|
52
|
+
service install [--domain D] systemd (Linux) / launchd (macOS) / schtasks (Windows)
|
|
53
|
+
service uninstall | logs [-f] | status
|
|
57
54
|
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
Other:
|
|
56
|
+
schedule list [--json]
|
|
57
|
+
scopes List available tool groups
|
|
60
58
|
|
|
61
59
|
Options:
|
|
62
60
|
-h, --help Show this help
|
|
63
|
-
--json Machine-readable output
|
|
61
|
+
--json Machine-readable output where supported
|
|
64
62
|
`;
|
|
65
63
|
function parseArgs(argv) {
|
|
66
64
|
const [command = '', ...rest] = argv;
|
|
67
65
|
const sub = [];
|
|
68
66
|
const flags = new Set();
|
|
69
67
|
const values = new Map();
|
|
68
|
+
// 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']);
|
|
70
70
|
for (let i = 0; i < rest.length; i++) {
|
|
71
71
|
const a = rest[i];
|
|
72
72
|
if (a.startsWith('--')) {
|
|
73
73
|
const key = a.slice(2);
|
|
74
|
+
const eq = key.indexOf('=');
|
|
75
|
+
if (eq > 0) {
|
|
76
|
+
values.set(key.slice(0, eq), key.slice(eq + 1));
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
74
79
|
const next = rest[i + 1];
|
|
75
|
-
if (next !== undefined && !next.startsWith('
|
|
80
|
+
if (valueFlags.has(key) && next !== undefined && !next.startsWith('--')) {
|
|
76
81
|
values.set(key, next);
|
|
77
82
|
i++;
|
|
78
83
|
}
|
|
@@ -80,7 +85,7 @@ function parseArgs(argv) {
|
|
|
80
85
|
flags.add(key);
|
|
81
86
|
}
|
|
82
87
|
}
|
|
83
|
-
else if (a.startsWith('-') && a.length > 1) {
|
|
88
|
+
else if (a.startsWith('-') && a.length > 1 && !/^-\d/.test(a)) {
|
|
84
89
|
for (const c of a.slice(1))
|
|
85
90
|
flags.add(c);
|
|
86
91
|
}
|
|
@@ -91,8 +96,9 @@ function parseArgs(argv) {
|
|
|
91
96
|
return { command, sub, flags, values };
|
|
92
97
|
}
|
|
93
98
|
const jsonOut = (args) => args.flags.has('json');
|
|
99
|
+
const splitList = (s) => (s || '').split(',').map(x => x.trim()).filter(Boolean);
|
|
94
100
|
// ---------------------------------------------------------------------------
|
|
95
|
-
//
|
|
101
|
+
// helpers
|
|
96
102
|
// ---------------------------------------------------------------------------
|
|
97
103
|
function findToken(cfg, idOrName) {
|
|
98
104
|
if (!idOrName)
|
|
@@ -104,8 +110,23 @@ function findToken(cfg, idOrName) {
|
|
|
104
110
|
}
|
|
105
111
|
return t;
|
|
106
112
|
}
|
|
107
|
-
|
|
108
|
-
|
|
113
|
+
const mask = (t) => t.slice(0, 6) + '…' + t.slice(-4);
|
|
114
|
+
function run(cmd, args) {
|
|
115
|
+
try {
|
|
116
|
+
return execFileSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return '';
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function banner() {
|
|
123
|
+
return `
|
|
124
|
+
___ _____
|
|
125
|
+
| _ \\__ _ __ _ ___ _ _ |_ _|__ _ _ _ __ _ ___
|
|
126
|
+
| / _\` / _\` / -_) '_| | |/ - \\ '_| '_/ _\` / -_)
|
|
127
|
+
|_|_\\__,_\\__, \\___|_| |_|\\___/_| |_| \\__,_\\___|
|
|
128
|
+
|___/ ${PKG.name} v${PKG.version}
|
|
129
|
+
`;
|
|
109
130
|
}
|
|
110
131
|
// ---------------------------------------------------------------------------
|
|
111
132
|
// init
|
|
@@ -113,9 +134,20 @@ function maskToken(t) {
|
|
|
113
134
|
function cmdInit(args) {
|
|
114
135
|
const existing = fs.existsSync(configPath());
|
|
115
136
|
const cfg = existing ? loadConfig() : null;
|
|
137
|
+
const extraPaths = [...splitList(args.values.get('paths')), ...args.sub].map(resolveReal);
|
|
116
138
|
if (cfg && cfg.tokens.length) {
|
|
117
|
-
|
|
118
|
-
|
|
139
|
+
if (extraPaths.length) {
|
|
140
|
+
const t = primaryToken(cfg);
|
|
141
|
+
for (const p of extraPaths)
|
|
142
|
+
if (!t.allowed_paths.includes(p))
|
|
143
|
+
t.allowed_paths.push(p);
|
|
144
|
+
saveConfig(cfg);
|
|
145
|
+
console.log(`Added ${extraPaths.length} path(s) to token "${t.name}".`);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
console.log(`Config already exists with ${cfg.tokens.length} token(s) — nothing to do.`);
|
|
149
|
+
console.log('Add paths: ramcp policy allow <dir>');
|
|
150
|
+
console.log('Add tokens: ramcp token add --name <name>');
|
|
119
151
|
return;
|
|
120
152
|
}
|
|
121
153
|
const fresh = cfg || {
|
|
@@ -128,29 +160,24 @@ function cmdInit(args) {
|
|
|
128
160
|
read_only: false,
|
|
129
161
|
tokens: [],
|
|
130
162
|
};
|
|
131
|
-
const rec = newTokenRecord({ name: 'default' });
|
|
163
|
+
const rec = newTokenRecord({ name: 'default', allowed_paths: extraPaths });
|
|
132
164
|
fresh.tokens.push(rec);
|
|
133
165
|
saveConfig(fresh);
|
|
166
|
+
console.log(banner());
|
|
167
|
+
console.log(`platform: ${platformLabel()}`);
|
|
168
|
+
console.log(`config: ${configPath()}`);
|
|
169
|
+
console.log(`token: ${mask(rec.token)} (full: ramcp token show --full)`);
|
|
170
|
+
console.log(`paths: ${extraPaths.length ? extraPaths.join(', ') : '(none — add with `ramcp policy allow <dir>`)'}`);
|
|
134
171
|
console.log(`
|
|
135
|
-
___ _____
|
|
136
|
-
| _ \\__ _ __ _ ___ _ _ |_ _|__ _ _ _ __ _ ___
|
|
137
|
-
| / _\` / _\` / -_) '_| | |/ - \\ '_| '_/ _\` / -_)
|
|
138
|
-
|_|_\\__,_\\__, \\___|_| |_|\\___/_| |_| \\__,_\\___|
|
|
139
|
-
|___/ ${PKG.name} v${PKG.version}
|
|
140
|
-
|
|
141
|
-
${existing ? '✔ Existing config migrated to v2' : '✔ Config written to ' + configPath()}
|
|
142
|
-
✔ Token "default": ${maskToken(rec.token)} (full: ramcp token show default --full)
|
|
143
|
-
✔ Audit log: ${fresh.audit.enabled ? 'enabled' : 'disabled'} (${fresh.audit.db_path})
|
|
144
|
-
|
|
145
172
|
Next steps:
|
|
146
|
-
1. ramcp
|
|
147
|
-
2. ramcp
|
|
148
|
-
3. ramcp
|
|
149
|
-
|
|
173
|
+
1. ramcp policy allow ~/projects # what the AI may read/write
|
|
174
|
+
2. ramcp policy shell on # optional: allow commands
|
|
175
|
+
3. ramcp tunnel # public https URL (laptops, no port forward)
|
|
176
|
+
or: ramcp service install # autostart on a server with a domain
|
|
150
177
|
`);
|
|
151
178
|
}
|
|
152
179
|
// ---------------------------------------------------------------------------
|
|
153
|
-
// start
|
|
180
|
+
// start / tunnel
|
|
154
181
|
// ---------------------------------------------------------------------------
|
|
155
182
|
async function cmdStart(args) {
|
|
156
183
|
const { runServer } = await import('../server/run.js');
|
|
@@ -158,6 +185,16 @@ async function cmdStart(args) {
|
|
|
158
185
|
host: args.values.get('host'),
|
|
159
186
|
port: args.values.has('port') ? parseInt(args.values.get('port'), 10) : undefined,
|
|
160
187
|
readOnly: args.flags.has('read-only'),
|
|
188
|
+
tunnel: args.flags.has('tunnel'),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
async function cmdTunnel(args) {
|
|
192
|
+
const { runServer } = await import('../server/run.js');
|
|
193
|
+
await runServer({
|
|
194
|
+
host: args.values.get('host'),
|
|
195
|
+
port: args.values.has('port') ? parseInt(args.values.get('port'), 10) : undefined,
|
|
196
|
+
readOnly: args.flags.has('read-only'),
|
|
197
|
+
tunnel: true,
|
|
161
198
|
});
|
|
162
199
|
}
|
|
163
200
|
// ---------------------------------------------------------------------------
|
|
@@ -165,33 +202,47 @@ async function cmdStart(args) {
|
|
|
165
202
|
// ---------------------------------------------------------------------------
|
|
166
203
|
function cmdUrl(args) {
|
|
167
204
|
const cfg = loadConfig();
|
|
168
|
-
const t = findToken(cfg, args.sub[0]);
|
|
205
|
+
const t = findToken(cfg, args.sub[0] || args.values.get('token'));
|
|
169
206
|
const host = cfg.public_host || `${cfg.host}:${cfg.port}`;
|
|
170
207
|
const scheme = cfg.public_host ? 'https' : 'http';
|
|
171
208
|
console.log(`${scheme}://${host}/${t.token}${cfg.mcp_path}`);
|
|
209
|
+
if (!cfg.public_host) {
|
|
210
|
+
console.log('\n(local URL — run `ramcp tunnel` for a public https link)');
|
|
211
|
+
}
|
|
172
212
|
}
|
|
173
213
|
// ---------------------------------------------------------------------------
|
|
174
|
-
// token
|
|
214
|
+
// token
|
|
175
215
|
// ---------------------------------------------------------------------------
|
|
176
216
|
function cmdToken(args) {
|
|
177
217
|
const cfg = loadConfig();
|
|
178
218
|
const sub = args.sub[0];
|
|
179
|
-
if (sub === 'list') {
|
|
219
|
+
if (sub === 'list' || !sub) {
|
|
180
220
|
if (jsonOut(args)) {
|
|
181
221
|
console.log(JSON.stringify(cfg.tokens.map(t => ({
|
|
182
222
|
id: t.id, name: t.name, created: t.created, expires: t.expires || null,
|
|
183
223
|
scopes: t.scopes, read_only: !!t.read_only, shell: t.shell_enabled,
|
|
224
|
+
allowed_paths: t.allowed_paths, denied_paths: t.denied_paths,
|
|
225
|
+
rpm: t.max_requests_per_minute || null,
|
|
184
226
|
fingerprint: AuditLog.fingerprint(t.token),
|
|
185
227
|
})), null, 2));
|
|
186
228
|
return;
|
|
187
229
|
}
|
|
188
230
|
for (const t of cfg.tokens) {
|
|
189
|
-
|
|
231
|
+
const bits = [
|
|
232
|
+
t.id,
|
|
233
|
+
t.name.padEnd(16),
|
|
234
|
+
`shell:${t.shell_enabled ? 'on ' : 'off'}`,
|
|
235
|
+
`ro:${t.read_only ? 'yes' : 'no '}`,
|
|
236
|
+
`paths:${t.allowed_paths.length}`,
|
|
237
|
+
t.expires ? `expires:${t.expires.slice(0, 10)}` : '',
|
|
238
|
+
t.scopes.length ? `scopes:${t.scopes.join('|')}` : '',
|
|
239
|
+
].filter(Boolean);
|
|
240
|
+
console.log(bits.join(' '));
|
|
190
241
|
}
|
|
191
242
|
return;
|
|
192
243
|
}
|
|
193
244
|
if (sub === 'add') {
|
|
194
|
-
const name = args.values.get('name');
|
|
245
|
+
const name = args.values.get('name') || args.sub[1];
|
|
195
246
|
if (!name) {
|
|
196
247
|
console.error('--name is required');
|
|
197
248
|
process.exit(1);
|
|
@@ -203,107 +254,145 @@ function cmdToken(args) {
|
|
|
203
254
|
const rec = newTokenRecord({
|
|
204
255
|
name,
|
|
205
256
|
shell_enabled: args.flags.has('shell') ? true : args.flags.has('no-shell') ? false : undefined,
|
|
206
|
-
allowed_paths: args.values.get('paths')
|
|
207
|
-
denied_paths: args.values.get('deny')
|
|
208
|
-
scopes: args.values.get('scopes')
|
|
257
|
+
allowed_paths: splitList(args.values.get('paths')).map(resolveReal),
|
|
258
|
+
denied_paths: splitList(args.values.get('deny')).map(resolveReal),
|
|
259
|
+
scopes: splitList(args.values.get('scopes')),
|
|
209
260
|
read_only: args.flags.has('read-only'),
|
|
210
261
|
max_requests_per_minute: args.values.has('rpm') ? parseInt(args.values.get('rpm'), 10) : undefined,
|
|
211
262
|
expires: args.values.get('expires'),
|
|
212
263
|
});
|
|
213
|
-
// resolve allowed paths to real paths
|
|
214
|
-
rec.allowed_paths = rec.allowed_paths.map(resolveReal);
|
|
215
|
-
rec.denied_paths = rec.denied_paths.map(resolveReal);
|
|
216
264
|
cfg.tokens.push(rec);
|
|
217
265
|
saveConfig(cfg);
|
|
266
|
+
const host = cfg.public_host || `${cfg.host}:${cfg.port}`;
|
|
267
|
+
const scheme = cfg.public_host ? 'https' : 'http';
|
|
218
268
|
console.log(`Token "${name}" created (${rec.id})`);
|
|
219
|
-
console.log(`URL:
|
|
220
|
-
restartHint();
|
|
269
|
+
console.log(`URL: ${scheme}://${host}/${rec.token}${cfg.mcp_path}`);
|
|
221
270
|
return;
|
|
222
271
|
}
|
|
223
272
|
if (sub === 'show') {
|
|
224
|
-
const t = findToken(cfg, args.sub[1]);
|
|
225
|
-
|
|
226
|
-
console.log(t.token);
|
|
227
|
-
else
|
|
228
|
-
console.log(`${t.name}: ${maskToken(t.token)} (use --full for the complete token)`);
|
|
273
|
+
const t = findToken(cfg, args.sub[1] || args.values.get('token'));
|
|
274
|
+
console.log(args.flags.has('full') ? t.token : `${t.name}: ${mask(t.token)} (--full for the complete token)`);
|
|
229
275
|
return;
|
|
230
276
|
}
|
|
231
277
|
if (sub === 'rotate') {
|
|
232
|
-
const t = findToken(cfg, args.sub[1]);
|
|
278
|
+
const t = findToken(cfg, args.sub[1] || args.values.get('token'));
|
|
233
279
|
t.token = generateToken();
|
|
234
280
|
saveConfig(cfg);
|
|
235
281
|
console.log(`Token "${t.name}" rotated: ${t.token}`);
|
|
236
|
-
console.log('Old token is dead. Update your chatbot connector
|
|
237
|
-
restartHint();
|
|
282
|
+
console.log('Old token is dead. Update your chatbot connector.');
|
|
238
283
|
return;
|
|
239
284
|
}
|
|
240
285
|
if (sub === 'revoke') {
|
|
241
|
-
const t = findToken(cfg, args.sub[1]);
|
|
242
|
-
|
|
243
|
-
if (!cfg.tokens.length) {
|
|
286
|
+
const t = findToken(cfg, args.sub[1] || args.values.get('token'));
|
|
287
|
+
if (cfg.tokens.length === 1) {
|
|
244
288
|
console.error('Cannot revoke the last token.');
|
|
245
289
|
process.exit(1);
|
|
246
290
|
}
|
|
291
|
+
cfg.tokens = cfg.tokens.filter(x => x !== t);
|
|
247
292
|
saveConfig(cfg);
|
|
248
293
|
console.log(`Token "${t.name}" (${t.id}) revoked.`);
|
|
249
|
-
restartHint();
|
|
250
294
|
return;
|
|
251
295
|
}
|
|
252
296
|
console.log('Usage: ramcp token <list|add|show|rotate|revoke> …');
|
|
253
297
|
process.exit(1);
|
|
254
298
|
}
|
|
255
299
|
// ---------------------------------------------------------------------------
|
|
256
|
-
// policy
|
|
300
|
+
// policy — multi-path, token-selectable
|
|
257
301
|
// ---------------------------------------------------------------------------
|
|
258
302
|
function cmdPolicy(args) {
|
|
259
303
|
const cfg = loadConfig();
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
304
|
+
const action = args.sub[0] || 'show';
|
|
305
|
+
// Global switch (no token selector)
|
|
306
|
+
if (action === 'readonly') {
|
|
307
|
+
const val = args.sub[1];
|
|
308
|
+
if (val !== 'on' && val !== 'off') {
|
|
309
|
+
console.error('Usage: ramcp policy readonly on|off');
|
|
310
|
+
process.exit(1);
|
|
311
|
+
}
|
|
312
|
+
cfg.read_only = val === 'on';
|
|
263
313
|
saveConfig(cfg);
|
|
264
|
-
console.log(`Global read-only: ${cfg.read_only ? 'ON — mutating tools refused' : 'off'}`);
|
|
265
|
-
restartHint();
|
|
314
|
+
console.log(`Global read-only: ${cfg.read_only ? 'ON — all mutating tools refused' : 'off'}`);
|
|
266
315
|
return;
|
|
267
316
|
}
|
|
268
|
-
const t = findToken(cfg, args.
|
|
269
|
-
const
|
|
270
|
-
if (
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
317
|
+
const t = findToken(cfg, args.values.get('token'));
|
|
318
|
+
const rest = args.sub.slice(1);
|
|
319
|
+
if (action === 'allow' || action === 'deny') {
|
|
320
|
+
const targets = [...rest, ...splitList(args.values.get('paths'))].map(resolveReal);
|
|
321
|
+
if (!targets.length) {
|
|
322
|
+
console.error(`Usage: ramcp policy ${action} <path> [path...] [--token N]`);
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
for (const p of targets) {
|
|
326
|
+
if (action === 'allow') {
|
|
327
|
+
t.denied_paths = t.denied_paths.filter(d => d !== p);
|
|
328
|
+
if (!t.allowed_paths.includes(p))
|
|
329
|
+
t.allowed_paths.push(p);
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
t.allowed_paths = t.allowed_paths.filter(a => a !== p);
|
|
333
|
+
if (!t.denied_paths.includes(p))
|
|
334
|
+
t.denied_paths.push(p);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
281
337
|
saveConfig(cfg);
|
|
282
|
-
console.log(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
338
|
+
console.log(`${action === 'allow' ? 'Allowed' : 'Denied'} for "${t.name}":`);
|
|
339
|
+
for (const p of targets)
|
|
340
|
+
console.log(` ${p}`);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (action === 'shell') {
|
|
344
|
+
const val = rest[0];
|
|
345
|
+
if (val !== 'on' && val !== 'off') {
|
|
346
|
+
console.error('Usage: ramcp policy shell on|off [--token N]');
|
|
347
|
+
process.exit(1);
|
|
348
|
+
}
|
|
349
|
+
t.shell_enabled = val === 'on';
|
|
290
350
|
saveConfig(cfg);
|
|
291
|
-
console.log(`
|
|
292
|
-
|
|
351
|
+
console.log(`Shell for "${t.name}": ${t.shell_enabled ? 'enabled' : 'disabled'}`);
|
|
352
|
+
return;
|
|
293
353
|
}
|
|
294
|
-
|
|
295
|
-
|
|
354
|
+
if (action === 'scopes') {
|
|
355
|
+
const raw = rest.join(',') || args.values.get('scopes') || '';
|
|
356
|
+
if (!raw) {
|
|
357
|
+
console.error('Usage: ramcp policy scopes <group,group|all> [--token N]');
|
|
358
|
+
process.exit(1);
|
|
359
|
+
}
|
|
360
|
+
if (raw === 'all') {
|
|
361
|
+
t.scopes = [];
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
const groups = splitList(raw);
|
|
365
|
+
const unknown = groups.filter(g => !(g in TOOL_SCOPES));
|
|
366
|
+
if (unknown.length) {
|
|
367
|
+
console.error(`Unknown scope group(s): ${unknown.join(', ')}`);
|
|
368
|
+
console.error(`Available: ${Object.keys(TOOL_SCOPES).join(', ')}`);
|
|
369
|
+
process.exit(1);
|
|
370
|
+
}
|
|
371
|
+
t.scopes = groups;
|
|
372
|
+
}
|
|
296
373
|
saveConfig(cfg);
|
|
297
|
-
console.log(`
|
|
298
|
-
|
|
374
|
+
console.log(`Scopes for "${t.name}": ${t.scopes.length ? t.scopes.join(', ') : '(all tools)'}`);
|
|
375
|
+
return;
|
|
299
376
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
console.log(
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
377
|
+
// show
|
|
378
|
+
if (jsonOut(args)) {
|
|
379
|
+
console.log(JSON.stringify({
|
|
380
|
+
token: t.name, allowed_paths: t.allowed_paths, denied_paths: t.denied_paths,
|
|
381
|
+
shell: t.shell_enabled, read_only: !!t.read_only, scopes: t.scopes,
|
|
382
|
+
global_read_only: cfg.read_only,
|
|
383
|
+
}, null, 2));
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
console.log(`token: ${t.name} (${t.id})`);
|
|
387
|
+
console.log(`allowed: ${t.allowed_paths.length ? '\n ' + t.allowed_paths.join('\n ') : '(none — the AI can read nothing)'}`);
|
|
388
|
+
console.log(`denied: ${t.denied_paths.length ? '\n ' + t.denied_paths.join('\n ') : '(none)'}`);
|
|
389
|
+
console.log(`shell: ${t.shell_enabled ? 'enabled' : 'disabled'}`);
|
|
390
|
+
console.log(`read_only: ${t.read_only ? 'yes' : 'no'}${cfg.read_only ? ' (global read-only is ON)' : ''}`);
|
|
391
|
+
console.log(`scopes: ${t.scopes.length ? t.scopes.join(', ') : '(all tools)'}`);
|
|
392
|
+
}
|
|
393
|
+
function cmdScopes() {
|
|
394
|
+
for (const [group, tools] of Object.entries(TOOL_SCOPES)) {
|
|
395
|
+
console.log(`${group.padEnd(12)} ${tools.join(', ')}`);
|
|
307
396
|
}
|
|
308
397
|
}
|
|
309
398
|
// ---------------------------------------------------------------------------
|
|
@@ -312,14 +401,14 @@ function cmdPolicy(args) {
|
|
|
312
401
|
function cmdAudit(args) {
|
|
313
402
|
const cfg = loadConfig();
|
|
314
403
|
if (!cfg.audit.enabled) {
|
|
315
|
-
console.log('Audit log is disabled
|
|
404
|
+
console.log('Audit log is disabled.');
|
|
316
405
|
return;
|
|
317
406
|
}
|
|
318
407
|
const audit = new AuditLog(cfg.audit.db_path);
|
|
319
408
|
if (args.sub[0] === 'chain' || args.flags.has('verify')) {
|
|
320
|
-
const
|
|
321
|
-
console.log(
|
|
322
|
-
process.exit(
|
|
409
|
+
const bad = audit.verify();
|
|
410
|
+
console.log(bad === null ? '✔ hash chain intact' : `✖ TAMPERED — first bad entry: line ${bad}`);
|
|
411
|
+
process.exit(bad === null ? 0 : 2);
|
|
323
412
|
}
|
|
324
413
|
const rows = audit.query({
|
|
325
414
|
tool: args.values.get('tool'),
|
|
@@ -328,181 +417,278 @@ function cmdAudit(args) {
|
|
|
328
417
|
});
|
|
329
418
|
if (jsonOut(args)) {
|
|
330
419
|
console.log(JSON.stringify(rows, null, 2));
|
|
420
|
+
return;
|
|
331
421
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const d = new Date(r.ts).toISOString().slice(0, 19);
|
|
339
|
-
console.log(`${d} fp:${r.token_fingerprint} ${r.tool.padEnd(20)} ${r.is_error ? 'ERR ' : 'ok '} ${r.duration_ms}ms`);
|
|
340
|
-
}
|
|
422
|
+
if (!rows.length) {
|
|
423
|
+
console.log('(no audit entries)');
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
for (const r of rows) {
|
|
427
|
+
console.log(`${new Date(r.ts).toISOString().slice(0, 19)} fp:${r.token_fingerprint} ${r.tool.padEnd(22)} ${r.is_error ? 'ERR' : 'ok '} ${r.duration_ms}ms`);
|
|
341
428
|
}
|
|
342
|
-
audit.close();
|
|
343
429
|
}
|
|
344
430
|
// ---------------------------------------------------------------------------
|
|
345
431
|
// doctor
|
|
346
432
|
// ---------------------------------------------------------------------------
|
|
347
433
|
async function cmdDoctor(args) {
|
|
348
434
|
const results = [];
|
|
349
|
-
const ok = (
|
|
350
|
-
const bad = (
|
|
351
|
-
const warn = (
|
|
435
|
+
const ok = (n, m) => results.push([`✔ ${n}`, m]);
|
|
436
|
+
const bad = (n, m) => results.push([`✖ ${n}`, m]);
|
|
437
|
+
const warn = (n, m) => results.push([`⚠ ${n}`, m]);
|
|
352
438
|
const cfg = loadConfig();
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
439
|
+
ok('platform', platformLabel());
|
|
440
|
+
ok('config', configPath());
|
|
441
|
+
if (cfg.tokens.length) {
|
|
442
|
+
const withPaths = cfg.tokens.filter(t => t.allowed_paths.length).length;
|
|
443
|
+
ok('tokens', `${cfg.tokens.length} configured, ${withPaths} with path access`);
|
|
444
|
+
if (!withPaths)
|
|
445
|
+
warn('paths', 'no token has any allowed path — add with `ramcp policy allow <dir>`');
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
357
448
|
bad('tokens', 'none — run `ramcp init`');
|
|
358
|
-
|
|
449
|
+
}
|
|
450
|
+
// port / gateway
|
|
359
451
|
const inUse = await new Promise((resolve) => {
|
|
360
452
|
const srv = net.createServer();
|
|
361
453
|
srv.once('error', () => resolve(true));
|
|
362
|
-
srv.once('listening', () =>
|
|
454
|
+
srv.once('listening', () => srv.close(() => resolve(false)));
|
|
363
455
|
srv.listen(cfg.port, cfg.host);
|
|
364
456
|
});
|
|
365
|
-
const
|
|
457
|
+
const svcActive = serviceIsActive();
|
|
366
458
|
if (inUse) {
|
|
367
|
-
serviceActive ? ok('port', `${cfg.host}:${cfg.port} in use by the running service`) : warn('port', `${cfg.host}:${cfg.port} in use by ANOTHER process`);
|
|
368
|
-
}
|
|
369
|
-
else {
|
|
370
|
-
serviceActive ? warn('port', `port free but service claims active — crashed?`) : warn('port', `not listening — start with \`ramcp service install\` or \`ramcp start\``);
|
|
371
|
-
}
|
|
372
|
-
// 3. gateway answers?
|
|
373
|
-
if (!inUse || serviceActive) {
|
|
374
459
|
try {
|
|
375
|
-
const resp = await fetch(`http://${cfg.host}:${cfg.port}/health
|
|
460
|
+
const resp = await fetch(`http://${cfg.host}:${cfg.port}/health`, { signal: AbortSignal.timeout(4000) });
|
|
376
461
|
const body = (await resp.json());
|
|
377
|
-
|
|
378
|
-
ok('gateway', `healthy (v${body.version})`)
|
|
379
|
-
|
|
380
|
-
bad('gateway', `unexpected: ${JSON.stringify(body)}`);
|
|
462
|
+
body.status === 'ok'
|
|
463
|
+
? ok('gateway', `healthy on ${cfg.host}:${cfg.port} (v${body.version})`)
|
|
464
|
+
: bad('gateway', `unexpected reply: ${JSON.stringify(body)}`);
|
|
381
465
|
}
|
|
382
466
|
catch {
|
|
383
|
-
|
|
467
|
+
warn('gateway', `port ${cfg.port} is in use but /health did not answer — another app?`);
|
|
384
468
|
}
|
|
385
469
|
}
|
|
386
|
-
|
|
470
|
+
else {
|
|
471
|
+
warn('gateway', `not running — start with \`ramcp tunnel\` or \`ramcp start\``);
|
|
472
|
+
}
|
|
473
|
+
if (svcActive)
|
|
474
|
+
ok('service', `${serviceLabel()} active`);
|
|
475
|
+
else if (serviceIsInstalled())
|
|
476
|
+
warn('service', `${serviceLabel()} installed but not active`);
|
|
477
|
+
// tunnel readiness
|
|
478
|
+
const cf = resolveCloudflared();
|
|
479
|
+
cf ? ok('tunnel', `cloudflared ready (${cf})`) : warn('tunnel', 'cloudflared not installed yet — `ramcp tunnel` downloads it automatically');
|
|
480
|
+
// public reachability
|
|
387
481
|
if (cfg.public_host) {
|
|
388
|
-
const vhost = `/etc/nginx/sites-enabled/${cfg.public_host}`;
|
|
389
|
-
if (fs.existsSync(vhost)) {
|
|
390
|
-
const conf = fs.readFileSync(vhost, 'utf8');
|
|
391
|
-
if (conf.includes(`proxy_pass http://${cfg.host}:${cfg.port}`)) {
|
|
392
|
-
ok('nginx', `vhost for ${cfg.public_host} → ${cfg.host}:${cfg.port}`);
|
|
393
|
-
}
|
|
394
|
-
else {
|
|
395
|
-
warn('nginx', `${vhost} exists but does not proxy to ${cfg.host}:${cfg.port}`);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
else {
|
|
399
|
-
warn('nginx', `no vhost at ${vhost} — run \`ramcp service install --domain ${cfg.public_host}\``);
|
|
400
|
-
}
|
|
401
|
-
// 5. public DNS resolves + Cloudflare edge reachable?
|
|
402
482
|
try {
|
|
403
483
|
const resp = await fetch(`https://${cfg.public_host}/health`, { signal: AbortSignal.timeout(8000) });
|
|
404
484
|
const body = (await resp.json());
|
|
405
|
-
|
|
406
|
-
ok('public', `https://${cfg.public_host} healthy end-to-end`);
|
|
407
|
-
else
|
|
408
|
-
warn('public', `got status ${resp.status} from edge`);
|
|
485
|
+
body.status === 'ok' ? ok('public', `https://${cfg.public_host} reachable`) : warn('public', `edge answered ${resp.status}`);
|
|
409
486
|
}
|
|
410
487
|
catch (e) {
|
|
411
|
-
warn('public', `https://${cfg.public_host} unreachable
|
|
488
|
+
warn('public', `https://${cfg.public_host} unreachable (${e.message})`);
|
|
412
489
|
}
|
|
413
490
|
}
|
|
414
|
-
//
|
|
415
|
-
if (cfg.audit.enabled
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
console.log(`${name.padEnd(14)} ${msg}`);
|
|
426
|
-
const hasBad = results.some(r => r[0].startsWith('✖'));
|
|
427
|
-
process.exitCode = hasBad ? 1 : 0;
|
|
428
|
-
}
|
|
429
|
-
function run(cmd, args) {
|
|
430
|
-
try {
|
|
431
|
-
return execFileSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
432
|
-
}
|
|
433
|
-
catch {
|
|
434
|
-
return '';
|
|
491
|
+
// audit
|
|
492
|
+
if (cfg.audit.enabled) {
|
|
493
|
+
const file = cfg.audit.db_path.replace(/\.db$/, '') + (cfg.audit.db_path.endsWith('.jsonl') ? '' : '.jsonl');
|
|
494
|
+
if (fs.existsSync(file) || fs.existsSync(cfg.audit.db_path)) {
|
|
495
|
+
const a = new AuditLog(cfg.audit.db_path);
|
|
496
|
+
const t = a.verify();
|
|
497
|
+
t === null ? ok('audit', 'hash chain intact') : bad('audit', `tampered at line ${t}`);
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
warn('audit', 'enabled, no entries yet');
|
|
501
|
+
}
|
|
435
502
|
}
|
|
503
|
+
for (const [n, m] of results)
|
|
504
|
+
console.log(`${n.padEnd(14)} ${m}`);
|
|
505
|
+
process.exitCode = results.some(r => r[0].startsWith('✖')) ? 1 : 0;
|
|
436
506
|
}
|
|
437
507
|
// ---------------------------------------------------------------------------
|
|
438
|
-
// service
|
|
508
|
+
// service — systemd / launchd / schtasks
|
|
439
509
|
// ---------------------------------------------------------------------------
|
|
440
510
|
const SERVICE_NAME = 'remote-access-mcp';
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
511
|
+
const SYSTEMD_FILE = `/etc/systemd/system/${SERVICE_NAME}.service`;
|
|
512
|
+
const LAUNCHD_LABEL = 'ir.amiralimanzar.ramcp';
|
|
513
|
+
const launchdFile = () => path.join(os.homedir(), 'Library', 'LaunchAgents', `${LAUNCHD_LABEL}.plist`);
|
|
514
|
+
const SCHTASK_NAME = 'RemoteAccessMCP';
|
|
515
|
+
function serviceLabel() {
|
|
516
|
+
if (hasSystemd())
|
|
517
|
+
return 'systemd unit';
|
|
518
|
+
if (isMac())
|
|
519
|
+
return 'launchd agent';
|
|
520
|
+
if (isWindows())
|
|
521
|
+
return 'scheduled task';
|
|
522
|
+
return 'service';
|
|
444
523
|
}
|
|
445
|
-
function
|
|
446
|
-
if (
|
|
447
|
-
|
|
448
|
-
|
|
524
|
+
function serviceIsInstalled() {
|
|
525
|
+
if (hasSystemd())
|
|
526
|
+
return fs.existsSync(SYSTEMD_FILE);
|
|
527
|
+
if (isMac())
|
|
528
|
+
return fs.existsSync(launchdFile());
|
|
529
|
+
if (isWindows())
|
|
530
|
+
return run('schtasks', ['/query', '/tn', SCHTASK_NAME]).length > 0;
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
function serviceIsActive() {
|
|
534
|
+
if (hasSystemd())
|
|
535
|
+
return run('systemctl', ['is-active', SERVICE_NAME]).trim() === 'active';
|
|
536
|
+
if (isMac())
|
|
537
|
+
return run('launchctl', ['list']).includes(LAUNCHD_LABEL);
|
|
538
|
+
if (isWindows())
|
|
539
|
+
return /Running/i.test(run('schtasks', ['/query', '/tn', SCHTASK_NAME, '/fo', 'LIST', '/v']));
|
|
540
|
+
return false;
|
|
449
541
|
}
|
|
450
542
|
function cmdService(args) {
|
|
451
543
|
const sub = args.sub[0] || '';
|
|
452
|
-
if (sub === 'install')
|
|
453
|
-
installService(args);
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
544
|
+
if (sub === 'install')
|
|
545
|
+
return installService(args);
|
|
546
|
+
if (sub === 'uninstall')
|
|
547
|
+
return uninstallService();
|
|
548
|
+
if (sub === 'status')
|
|
549
|
+
return cmdStatus();
|
|
550
|
+
if (sub === 'logs') {
|
|
551
|
+
if (hasSystemd()) {
|
|
552
|
+
const f = args.flags.has('f') || args.flags.has('follow') ? ['-f'] : [];
|
|
553
|
+
try {
|
|
554
|
+
execFileSync('journalctl', ['-u', SERVICE_NAME, '-n', '100', ...f], { stdio: 'inherit' });
|
|
555
|
+
}
|
|
556
|
+
catch (e) {
|
|
557
|
+
console.error(e.message);
|
|
558
|
+
process.exit(1);
|
|
559
|
+
}
|
|
560
|
+
return;
|
|
462
561
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
562
|
+
const log = path.join(configDir(), 'gateway.log');
|
|
563
|
+
if (!fs.existsSync(log)) {
|
|
564
|
+
console.log(`No log file at ${log}`);
|
|
565
|
+
return;
|
|
466
566
|
}
|
|
567
|
+
if (args.flags.has('f') || args.flags.has('follow')) {
|
|
568
|
+
console.log(`(following ${log} — Ctrl+C to stop)`);
|
|
569
|
+
let size = fs.statSync(log).size;
|
|
570
|
+
setInterval(() => {
|
|
571
|
+
const s = fs.statSync(log).size;
|
|
572
|
+
if (s > size) {
|
|
573
|
+
const fd = fs.openSync(log, 'r');
|
|
574
|
+
const buf = Buffer.alloc(s - size);
|
|
575
|
+
fs.readSync(fd, buf, 0, buf.length, size);
|
|
576
|
+
fs.closeSync(fd);
|
|
577
|
+
process.stdout.write(buf.toString());
|
|
578
|
+
size = s;
|
|
579
|
+
}
|
|
580
|
+
}, 700);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
const content = fs.readFileSync(log, 'utf8').split('\n').slice(-100).join('\n');
|
|
584
|
+
console.log(content);
|
|
585
|
+
return;
|
|
467
586
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
}
|
|
471
|
-
else {
|
|
472
|
-
console.log('Usage: ramcp service <install [--domain D]|uninstall|logs [-f]|status>');
|
|
473
|
-
process.exit(1);
|
|
474
|
-
}
|
|
587
|
+
console.log('Usage: ramcp service <install [--domain D]|uninstall|logs [-f]|status>');
|
|
588
|
+
process.exit(1);
|
|
475
589
|
}
|
|
476
590
|
function installService(args) {
|
|
477
|
-
if (process.getuid?.() !== 0) {
|
|
478
|
-
console.error('service install requires root (sudo).');
|
|
479
|
-
process.exit(1);
|
|
480
|
-
}
|
|
481
|
-
const domain = args.values.get('domain') || '';
|
|
482
591
|
const cfg = loadConfig();
|
|
483
592
|
const nodeBin = process.execPath;
|
|
484
|
-
const
|
|
485
|
-
const
|
|
593
|
+
const entry = path.join(PKG_ROOT, 'dist', 'server', 'run.js');
|
|
594
|
+
const dryRun = args.flags.has('dry-run');
|
|
595
|
+
const wantTunnel = args.flags.has('tunnel');
|
|
596
|
+
const domain = args.values.get('domain') || '';
|
|
597
|
+
// ---- Linux (systemd) ----
|
|
598
|
+
if (hasSystemd()) {
|
|
599
|
+
if (process.getuid?.() !== 0) {
|
|
600
|
+
console.error('systemd install requires root (sudo).');
|
|
601
|
+
process.exit(1);
|
|
602
|
+
}
|
|
603
|
+
const unit = `[Unit]
|
|
486
604
|
Description=Remote Access MCP Gateway
|
|
487
605
|
After=network.target
|
|
488
606
|
|
|
489
607
|
[Service]
|
|
490
608
|
Type=simple
|
|
491
|
-
ExecStart=${nodeBin} ${
|
|
609
|
+
ExecStart=${nodeBin} ${entry}${wantTunnel ? ' --tunnel' : ''}
|
|
492
610
|
Restart=on-failure
|
|
493
611
|
RestartSec=5
|
|
494
612
|
Environment=NODE_ENV=production
|
|
613
|
+
Environment=HOME=${os.homedir()}
|
|
495
614
|
|
|
496
615
|
[Install]
|
|
497
616
|
WantedBy=multi-user.target
|
|
498
617
|
`;
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
618
|
+
if (dryRun) {
|
|
619
|
+
console.log(`--- ${SYSTEMD_FILE} ---\n${unit}`);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
fs.writeFileSync(SYSTEMD_FILE, unit);
|
|
623
|
+
execFileSync('systemctl', ['daemon-reload']);
|
|
624
|
+
execFileSync('systemctl', ['enable', '--now', SERVICE_NAME]);
|
|
625
|
+
console.log(`✔ systemd service installed and started: ${SERVICE_NAME}`);
|
|
626
|
+
if (domain)
|
|
627
|
+
installNginx(domain, cfg, dryRun);
|
|
628
|
+
else
|
|
629
|
+
console.log('Tip: --domain mcp.example.com also writes an nginx vhost.');
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
// ---- macOS (launchd, per-user, no sudo) ----
|
|
633
|
+
if (isMac()) {
|
|
634
|
+
const out = path.join(configDir(), 'gateway.log');
|
|
635
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
636
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
637
|
+
<plist version="1.0">
|
|
638
|
+
<dict>
|
|
639
|
+
<key>Label</key><string>${LAUNCHD_LABEL}</string>
|
|
640
|
+
<key>ProgramArguments</key>
|
|
641
|
+
<array>
|
|
642
|
+
<string>${nodeBin}</string>
|
|
643
|
+
<string>${entry}</string>${wantTunnel ? '\n <string>--tunnel</string>' : ''}
|
|
644
|
+
</array>
|
|
645
|
+
<key>RunAtLoad</key><true/>
|
|
646
|
+
<key>KeepAlive</key><true/>
|
|
647
|
+
<key>StandardOutPath</key><string>${out}</string>
|
|
648
|
+
<key>StandardErrorPath</key><string>${out}</string>
|
|
649
|
+
<key>EnvironmentVariables</key>
|
|
650
|
+
<dict><key>HOME</key><string>${os.homedir()}</string></dict>
|
|
651
|
+
</dict>
|
|
652
|
+
</plist>
|
|
653
|
+
`;
|
|
654
|
+
if (dryRun) {
|
|
655
|
+
console.log(`--- ${launchdFile()} ---\n${plist}`);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
fs.mkdirSync(path.dirname(launchdFile()), { recursive: true });
|
|
659
|
+
fs.writeFileSync(launchdFile(), plist);
|
|
660
|
+
run('launchctl', ['unload', launchdFile()]);
|
|
661
|
+
execFileSync('launchctl', ['load', '-w', launchdFile()]);
|
|
662
|
+
console.log(`✔ launchd agent installed: ${LAUNCHD_LABEL}`);
|
|
663
|
+
console.log(` logs: ${out}`);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
// ---- Windows (Scheduled Task at logon, no admin needed) ----
|
|
667
|
+
if (isWindows()) {
|
|
668
|
+
const log = path.join(configDir(), 'gateway.log');
|
|
669
|
+
const cmd = `"${nodeBin}" "${entry}"${wantTunnel ? ' --tunnel' : ''}`;
|
|
670
|
+
if (dryRun) {
|
|
671
|
+
console.log(`schtasks /create /tn ${SCHTASK_NAME} /tr ${cmd} /sc onlogon /rl limited /f`);
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
try {
|
|
675
|
+
execFileSync('schtasks', ['/create', '/tn', SCHTASK_NAME, '/tr', cmd, '/sc', 'onlogon', '/rl', 'limited', '/f'], { stdio: 'inherit' });
|
|
676
|
+
execFileSync('schtasks', ['/run', '/tn', SCHTASK_NAME], { stdio: 'inherit' });
|
|
677
|
+
console.log(`✔ scheduled task installed and started: ${SCHTASK_NAME}`);
|
|
678
|
+
console.log(` logs: ${log}`);
|
|
679
|
+
}
|
|
680
|
+
catch (e) {
|
|
681
|
+
console.error(`schtasks failed: ${e.message}`);
|
|
682
|
+
process.exit(1);
|
|
683
|
+
}
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
console.error(`No supported service manager on ${platform()}. Run \`ramcp start\` manually or use your own supervisor.`);
|
|
687
|
+
process.exit(1);
|
|
688
|
+
}
|
|
689
|
+
function installNginx(domain, cfg, dryRun) {
|
|
690
|
+
const nginxFile = `/etc/nginx/sites-available/${domain}`;
|
|
691
|
+
const vhost = `server {
|
|
506
692
|
listen 80;
|
|
507
693
|
listen [::]:80;
|
|
508
694
|
server_name ${domain};
|
|
@@ -527,75 +713,149 @@ WantedBy=multi-user.target
|
|
|
527
713
|
error_log /var/log/nginx/${domain}.error.log;
|
|
528
714
|
}
|
|
529
715
|
`;
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
fs.symlinkSync(nginxFile, link);
|
|
534
|
-
try {
|
|
535
|
-
execFileSync('nginx', ['-t']);
|
|
536
|
-
execFileSync('systemctl', ['reload', 'nginx']);
|
|
537
|
-
console.log(`✔ nginx vhost installed: ${domain} → ${cfg.host}:${cfg.port}`);
|
|
538
|
-
}
|
|
539
|
-
catch (e) {
|
|
540
|
-
console.error(`nginx config test failed:\n${e.stdout || e.message}`);
|
|
541
|
-
process.exit(1);
|
|
542
|
-
}
|
|
716
|
+
if (dryRun) {
|
|
717
|
+
console.log(`--- ${nginxFile} ---\n${vhost}`);
|
|
718
|
+
return;
|
|
543
719
|
}
|
|
544
|
-
|
|
545
|
-
|
|
720
|
+
fs.writeFileSync(nginxFile, vhost);
|
|
721
|
+
const link = `/etc/nginx/sites-enabled/${domain}`;
|
|
722
|
+
if (!fs.existsSync(link))
|
|
723
|
+
fs.symlinkSync(nginxFile, link);
|
|
724
|
+
try {
|
|
725
|
+
execFileSync('nginx', ['-t']);
|
|
726
|
+
execFileSync('systemctl', ['reload', 'nginx']);
|
|
727
|
+
console.log(`✔ nginx vhost installed: ${domain} → ${cfg.host}:${cfg.port}`);
|
|
546
728
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
if (process.getuid?.() !== 0) {
|
|
550
|
-
console.error('service uninstall requires root (sudo).');
|
|
729
|
+
catch (e) {
|
|
730
|
+
console.error(`nginx config test failed:\n${e.stdout || e.message}`);
|
|
551
731
|
process.exit(1);
|
|
552
732
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
733
|
+
}
|
|
734
|
+
function uninstallService() {
|
|
735
|
+
if (hasSystemd()) {
|
|
736
|
+
if (process.getuid?.() !== 0) {
|
|
737
|
+
console.error('uninstall requires root (sudo).');
|
|
738
|
+
process.exit(1);
|
|
556
739
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
740
|
+
if (fs.existsSync(SYSTEMD_FILE)) {
|
|
741
|
+
run('systemctl', ['disable', '--now', SERVICE_NAME]);
|
|
742
|
+
fs.unlinkSync(SYSTEMD_FILE);
|
|
743
|
+
execFileSync('systemctl', ['daemon-reload']);
|
|
744
|
+
console.log('✔ systemd service removed');
|
|
745
|
+
}
|
|
746
|
+
else
|
|
747
|
+
console.log('Service not installed.');
|
|
748
|
+
const sitesDir = '/etc/nginx/sites-enabled';
|
|
749
|
+
if (fs.existsSync(sitesDir)) {
|
|
750
|
+
const cfg = loadConfig();
|
|
751
|
+
for (const f of fs.readdirSync(sitesDir)) {
|
|
752
|
+
try {
|
|
753
|
+
if (fs.readFileSync(path.join(sitesDir, f), 'utf8').includes(`proxy_pass http://${cfg.host}:${cfg.port}`)) {
|
|
754
|
+
fs.unlinkSync(path.join(sitesDir, f));
|
|
755
|
+
console.log(`✔ nginx vhost removed: ${f}`);
|
|
756
|
+
}
|
|
574
757
|
}
|
|
758
|
+
catch { /* skip */ }
|
|
575
759
|
}
|
|
576
|
-
|
|
760
|
+
run('nginx', ['-t']) && run('systemctl', ['reload', 'nginx']);
|
|
577
761
|
}
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
if (isMac()) {
|
|
765
|
+
if (fs.existsSync(launchdFile())) {
|
|
766
|
+
run('launchctl', ['unload', '-w', launchdFile()]);
|
|
767
|
+
fs.unlinkSync(launchdFile());
|
|
768
|
+
console.log('✔ launchd agent removed');
|
|
769
|
+
}
|
|
770
|
+
else
|
|
771
|
+
console.log('Agent not installed.');
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
if (isWindows()) {
|
|
578
775
|
try {
|
|
579
|
-
execFileSync('
|
|
580
|
-
|
|
776
|
+
execFileSync('schtasks', ['/delete', '/tn', SCHTASK_NAME, '/f'], { stdio: 'inherit' });
|
|
777
|
+
console.log('✔ scheduled task removed');
|
|
778
|
+
}
|
|
779
|
+
catch {
|
|
780
|
+
console.log('Task not installed.');
|
|
581
781
|
}
|
|
582
|
-
|
|
782
|
+
return;
|
|
583
783
|
}
|
|
784
|
+
console.log(`Nothing to uninstall on ${platform()}.`);
|
|
584
785
|
}
|
|
585
786
|
// ---------------------------------------------------------------------------
|
|
586
787
|
// status
|
|
587
788
|
// ---------------------------------------------------------------------------
|
|
588
789
|
function cmdStatus() {
|
|
589
|
-
const installed = isSystemdInstalled();
|
|
590
|
-
console.log(`service installed: ${installed ? 'yes' : 'no'}`);
|
|
591
|
-
if (installed) {
|
|
592
|
-
console.log(`service status: ${run('systemctl', ['is-active', SERVICE_NAME]).trim() || 'unknown'}`);
|
|
593
|
-
}
|
|
594
790
|
const cfg = loadConfig();
|
|
595
|
-
console.log(`
|
|
596
|
-
console.log(`
|
|
597
|
-
console.log(
|
|
598
|
-
console.log(`
|
|
791
|
+
console.log(`platform: ${platformLabel()}`);
|
|
792
|
+
console.log(`config: ${configPath()}`);
|
|
793
|
+
console.log(`${serviceLabel()}:${' '.repeat(Math.max(1, 19 - serviceLabel().length - 1))}${serviceIsInstalled() ? (serviceIsActive() ? 'installed, active' : 'installed, inactive') : 'not installed'}`);
|
|
794
|
+
console.log(`listen: ${cfg.host}:${cfg.port}${cfg.mcp_path}`);
|
|
795
|
+
console.log(`public_host: ${cfg.public_host || '(none — use `ramcp tunnel`)'}`);
|
|
796
|
+
console.log(`tokens: ${cfg.tokens.length}`);
|
|
797
|
+
console.log(`allowed paths: ${cfg.tokens.reduce((n, t) => n + t.allowed_paths.length, 0)}`);
|
|
798
|
+
console.log(`audit: ${cfg.audit.enabled ? 'on' : 'off'}`);
|
|
799
|
+
console.log(`read_only: ${cfg.read_only ? 'on' : 'off'}`);
|
|
800
|
+
}
|
|
801
|
+
// ---------------------------------------------------------------------------
|
|
802
|
+
// schedule / upgrade
|
|
803
|
+
// ---------------------------------------------------------------------------
|
|
804
|
+
function cmdSchedule(args) {
|
|
805
|
+
const p = path.join(configDir(), 'schedule.json');
|
|
806
|
+
if (jsonOut(args)) {
|
|
807
|
+
console.log(fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : '[]');
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
try {
|
|
811
|
+
const tasks = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
812
|
+
if (!tasks.length) {
|
|
813
|
+
console.log('(no scheduled tasks)');
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
for (const t of tasks)
|
|
817
|
+
console.log(`${t.id} | ${t.enabled ? 'on' : 'off'} | ${t.command} | next: ${t.next_run || t.run_at || '-'}`);
|
|
818
|
+
}
|
|
819
|
+
catch {
|
|
820
|
+
console.log('(no scheduled tasks)');
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
async function cmdUpgrade(args) {
|
|
824
|
+
console.log(`current: v${PKG.version}`);
|
|
825
|
+
const latest = await fetch('https://registry.npmjs.org/remote-access-mcp/latest')
|
|
826
|
+
.then(r => r.json()).then((j) => j.version)
|
|
827
|
+
.catch(() => null);
|
|
828
|
+
if (!latest) {
|
|
829
|
+
console.error('could not reach npm registry');
|
|
830
|
+
process.exit(1);
|
|
831
|
+
}
|
|
832
|
+
if (latest === PKG.version) {
|
|
833
|
+
console.log('already up to date');
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
console.log(`latest: v${latest}`);
|
|
837
|
+
if (args.flags.has('dry-run')) {
|
|
838
|
+
console.log('(dry-run — no changes)');
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
if (isLinux() && process.getuid?.() !== 0) {
|
|
842
|
+
console.error('upgrade requires root (sudo) for a global npm install');
|
|
843
|
+
process.exit(1);
|
|
844
|
+
}
|
|
845
|
+
execFileSync('npm', ['install', '-g', `remote-access-mcp@${latest}`], { stdio: 'inherit' });
|
|
846
|
+
if (serviceIsInstalled()) {
|
|
847
|
+
if (hasSystemd())
|
|
848
|
+
run('systemctl', ['restart', SERVICE_NAME]);
|
|
849
|
+
else if (isMac()) {
|
|
850
|
+
run('launchctl', ['unload', launchdFile()]);
|
|
851
|
+
run('launchctl', ['load', '-w', launchdFile()]);
|
|
852
|
+
}
|
|
853
|
+
else if (isWindows()) {
|
|
854
|
+
run('schtasks', ['/end', '/tn', SCHTASK_NAME]);
|
|
855
|
+
run('schtasks', ['/run', '/tn', SCHTASK_NAME]);
|
|
856
|
+
}
|
|
857
|
+
console.log(`service restarted on v${latest}`);
|
|
858
|
+
}
|
|
599
859
|
}
|
|
600
860
|
// ---------------------------------------------------------------------------
|
|
601
861
|
// main
|
|
@@ -613,6 +873,9 @@ export async function main(argv) {
|
|
|
613
873
|
case 'start':
|
|
614
874
|
await cmdStart(args);
|
|
615
875
|
break;
|
|
876
|
+
case 'tunnel':
|
|
877
|
+
await cmdTunnel(args);
|
|
878
|
+
break;
|
|
616
879
|
case 'url':
|
|
617
880
|
cmdUrl(args);
|
|
618
881
|
break;
|
|
@@ -622,6 +885,9 @@ export async function main(argv) {
|
|
|
622
885
|
case 'policy':
|
|
623
886
|
cmdPolicy(args);
|
|
624
887
|
break;
|
|
888
|
+
case 'scopes':
|
|
889
|
+
cmdScopes();
|
|
890
|
+
break;
|
|
625
891
|
case 'audit':
|
|
626
892
|
cmdAudit(args);
|
|
627
893
|
break;
|
|
@@ -653,63 +919,4 @@ export async function main(argv) {
|
|
|
653
919
|
process.exit(1);
|
|
654
920
|
}
|
|
655
921
|
}
|
|
656
|
-
// ---------------------------------------------------------------------------
|
|
657
|
-
// schedule
|
|
658
|
-
// ---------------------------------------------------------------------------
|
|
659
|
-
function cmdSchedule(args) {
|
|
660
|
-
const p = path.join(os.homedir(), '.config', 'remote-access-mcp', 'schedule.json');
|
|
661
|
-
if (args.flags.has('json')) {
|
|
662
|
-
console.log(fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : '[]');
|
|
663
|
-
return;
|
|
664
|
-
}
|
|
665
|
-
try {
|
|
666
|
-
const tasks = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
667
|
-
if (!tasks.length) {
|
|
668
|
-
console.log('(no scheduled tasks)');
|
|
669
|
-
return;
|
|
670
|
-
}
|
|
671
|
-
for (const t of tasks) {
|
|
672
|
-
console.log(`${t.id} | ${t.enabled ? 'on' : 'off'} | ${t.command} | next: ${t.next_run || t.run_at || '-'}`);
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
catch {
|
|
676
|
-
console.log('(no scheduled tasks)');
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
// ---------------------------------------------------------------------------
|
|
680
|
-
// upgrade
|
|
681
|
-
// ---------------------------------------------------------------------------
|
|
682
|
-
async function cmdUpgrade(args) {
|
|
683
|
-
const dryRun = args.flags.has('dry-run');
|
|
684
|
-
console.log(`current: v${PKG.version}`);
|
|
685
|
-
// Check latest from npm registry
|
|
686
|
-
const latest = await fetch('https://registry.npmjs.org/remote-access-mcp/latest')
|
|
687
|
-
.then(r => r.json())
|
|
688
|
-
.then((j) => j.version)
|
|
689
|
-
.catch(() => null);
|
|
690
|
-
if (!latest) {
|
|
691
|
-
console.error('could not reach npm registry');
|
|
692
|
-
process.exit(1);
|
|
693
|
-
}
|
|
694
|
-
if (latest === PKG.version) {
|
|
695
|
-
console.log('already up to date');
|
|
696
|
-
return;
|
|
697
|
-
}
|
|
698
|
-
console.log(`latest: v${latest}`);
|
|
699
|
-
if (dryRun) {
|
|
700
|
-
console.log('(dry-run — no changes made)');
|
|
701
|
-
return;
|
|
702
|
-
}
|
|
703
|
-
if (process.getuid?.() !== 0) {
|
|
704
|
-
console.error('upgrade requires root (sudo)');
|
|
705
|
-
process.exit(1);
|
|
706
|
-
}
|
|
707
|
-
console.log('upgrading…');
|
|
708
|
-
const { execFileSync } = await import('node:child_process');
|
|
709
|
-
execFileSync('npm', ['install', '-g', `remote-access-mcp@${latest}`], { stdio: 'inherit' });
|
|
710
|
-
if (isSystemdInstalled()) {
|
|
711
|
-
execFileSync('systemctl', ['restart', SERVICE_NAME]);
|
|
712
|
-
console.log(`service restarted on v${latest}`);
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
922
|
//# sourceMappingURL=main.js.map
|